ATL和MFC,用哪种框架来创建ActiveX控件:第四部分

为控件添加功能代码

MFC和ATL在功能代码的处理上是相似的。在每一个框架里,实现控件的类具有一个名为OnDraw的虚函数。你只需将你的功能代码添加到OnDraw函数里。然而,在各框架里,OnDraw函数得工作有所不同。

MFC的OnDraw在两种上下文中调用。第一个上下文发生在控件响应一个WM_PAINT消息时。此时,传递给OnDraw函数的设备上下文是实际的设备上下文。如果控件正被要求绘制它自己作为对客户调用IViewObjectEx::Draw的响应,则设备上下文或者是一个元文件设备上下文,或者是一个常规设备上下文。下面的代码说明了基于MFC的控件是怎样被绘制的:

void CMFCMsgTrafficCtrl::OnDraw(CDC* pdc, const CRect& rcBounds, 
    const CRect& rcInvalid)
{
   // TODO: 在下面加入自己的绘图代码
   pdc->FillRect(rcBounds, 
       CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));
   ShowGraph(*pdc, const_cast(rcBounds), nMessagesToShow);
}
COleControl::OnDraw的签名包括一个代表控件大小的矩形和一个代表控件无效区域的矩形。当响应WM_PAINT消息时,MFC调用控件的OnDraw函数。此时,OnDraw函数接受一个实际的设备上下文来绘图。当通过IViewObject::Draw来响应一个调用时,MFC也要调用控件的OnDraw函数。MFC的实现调用COleControl::OnDrawMetafile,并且它的缺省OnDrawMetafile调用COleControl::OnDraw。当然,这暗示了控件的实时绘制是和设计时与容器一起存储的控件图元文件表示法相同。你可以让控件的实时绘制模样与设计时的绘制模样不同,这通过重载COleControl::OnDrawMetafile来实现。通过调用你的控件的InvalidateControl方法,你可以强制进行一次重绘。

ATL的绘制机制非常类似于MFC。CComControlBase::OnPaint建立一个ATL_DRAWINFO结构,包括创建一个绘图设备上下文。然后ATL调用控件的OnDrawAdvanced函数。OnDrawAdvanced生成元文件,接着调用自己控件的OnDraw方法,它使用ATL_DRAWINFO结构中的信息来了解如何在屏幕上绘图。下面是ATL_DRAWINFO结构:

struct ATL_DRAWINFO
{
    UINT cbSize;
    DWORD dwDrawAspect;
    LONG lindex;
    DVTARGETDEVICE* ptd;
    HDC hicTargetDev;
    HDC hdcDraw;
    LPCRECTL prcBounds;  //在这个矩形中绘图
    LPCRECTL prcWBounds; //WindowOrg and Ext if metafile
    BOOL bOptimize;
    BOOL bZoomed;
    BOOL bRectInHimetric;
    SIZEL ZoomNum;       //ZoomX = ZoomNum.cx/ZoomNum.cy
    SIZEL ZoomDen;
};

ATL为你填写此结构。当你正在屏幕上绘图时,你所感兴趣的最重要的域是hdcDraw 和 prcBounds。如果你对在一个元文件里绘图感兴趣,或者你需要注意缩放因子等等,那么其它域也是重要的。下面的代码显示了基于ATL的消息流控件是怎样处理绘图的:

HRESULT CATLMsgTrafficCtl::OnDraw(ATL_DRAWINFO& di)
{
   RECT& rc = *(RECT*)di.prcBounds;
   HBRUSH hBrush = CreateSolidBrush(RGB(255, 255, 255));
   FillRect(di.hdcDraw, &rc, hBrush);
   DeleteObject(hBrush);
   Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom);
   ShowGraph(di.hdcDraw, rc, nMessagesToShow);
   return S_OK;
}

注意当你使用ATL的时候,设备和GDI句柄你都必须处理。在ATL中,你调用控件的FireViewChange函数来强制控件的一次重画。

开发一个流入接口

当开发一个基于MFC的ActiveX控件时,缺省的流入接口是一个分发接口。Visual C++ 和 MFC使得开发一个流入分发接口变得十分简单——只需使用ClassWizard来生成方法和属性。每次你使用ClassWizard添加一个新的属性或方法,它就插入一个入口到你控件的分发映射中。MFC使用分发映射来满足客户的调用请求。

MFC的缺点是在你的控件中增加一个常规的COM接口是一个枯燥无味的过程。此过程包括使用MFC的COM宏来建立实现接口的嵌套的类。

当为你的基于ATL的COM控件开发主流入接口时,类视是添加属性和方法的最好的手段。一当你为控件生成了代码,ATL ObjectWizard即添加一个缺省的流入接口。这可以是一个双接口,也可以是一个常规的自定义接口,取决于你先前设定的工程选项。

Visual Studio的类视向你提供了你的工程中包含的所有的类和接口,在类视中右击一个接口的定义时,即可添加一个属性或者方法。使用类视来定义接口非常方便,因为每次你添加一个方法或者属性的时候,类视都会更新IDL,类源代码以及头文件。

不象MFC,ATL给控件添加一个常规COM接口是非常容易的。在ATL中,你只要简单地添加新的接口样板文件的内容(一个GUID,关键字对象和关键字接口)。类视将会显示新的接口,你可以继续添加新的成员。

添加属性

ActiveX控件经常包含属性,它们是描述控件的状态的成员变量。给一个基于MFC的控件添加属性的最好的手段是利用ClassWizard。ClassWizard的自动为你添加成员变量,将它们映射到缺省的分发接口。ClassWizard给你提供了两种选择:你可以添加一个成员变量,包括一个变化通知函数,或者你可以添加一对Get/Set函数,手动添加成员变量。除了给控件添加你自己的定制属性,ClassWizard还让你象添加背景和标题一样的添加库存属性。ClassWizard甚至自动为你的类添加一个成员变量。

为一个基于ATL的控件添加属性有一点不同,你为控件中的每个属性添加单独的存取程序和变异因子函数(propget 和 propput函数)。然而,类视只是定义了接口函数。你还要手工添加数据成员到类中,然后再实现这些函数。

基于ATL的控件还支持库存属性,ATL ControlWizard预先要求你确定希望哪些库存属性包括在你的控件中。添加至少一个库存属性到控件中使得控件继承ATL的CStockPropImpl类。CStockPropImpl是IDispatch的一个实现,优化来显示ActiveX控件的库存属性,为每个标准的库存属性包含了兼容IDispatch的get 和 put函数。

ControlWizard还给控件添加代表库存属性的数据成员,例如,如果你添加了背景颜色的库存属性,ControlWizard添加一个名为m_clrBackColor的数据成员到你的类中。CStockPropImple一次性的为所有标准的库存属性的get 和 put函数添加实现。所有这些函数期望在你的类中看到合适的成员变量(象对应背景颜色的m_clrBackColor)。

编译器将在库存属性没有包括的那些get和put函数上阻塞。实现过程希望在你的类中看到成员变量。为了消除编译器错误,CComControlBase添加了一个联合结构,它包括了库存的get 和 put函数希望看到的所有成员的名字。然而,给控件添加数据成员重载了联合类型中的名字,CStopPropImpl类在它的get 和 put函数中使用控件的成员变量。

如果你忘记了使用ControlWizard预先添加库存属性,你总可以手工添加相关代码——即,从CStockPropImpl继承,然后为你想要显示的属性添加成员变量。

属性持续

MFC的属性持续机制是非常直观易懂的。从编程的观点来看,所有你要做的是填写ControlWizard已经提供的DoPropExchange函数。DoPropExchange将控件属性的状态从某些成员变量移动到持续媒体中。

MFC具有3个属性持续机制内置于COleControl:IPersistPropertyBag, IPersistStorage和 IPersistStream[Init]。所有这些持续机制都封装在MFC的CPropExchange类中,与当你需要序列化一个文档时CArchive为你包装一个文件非常相似。客户方选择使用3个接口中的一个保持对象。不管使用了哪种持续机制,执行总落在控件的DoPropExchange函数中。

下面的代码显示了MFCMsgTraffic控件是怎样将它的颜色和时间间隔属性保存起来的:

void CMFCMsgTrafficCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);

    PX_Color(pPX, "GraphLineColor", m_graphLineColor);    
    PX_Long(pPX, "GraphInterval", m_interval);
}

MFC包括了若干PX_函数在控件和存储媒体间转移数据,它们是:
PX_Short
PX_UShort
PX_Long
PX_ULong
PX_Color
PX_Bool
PX_String
PX_Currency
PX_Float
PX_Double
PX_Blob
PX_Font
PX_Picture
PX_IUnknown
PX_VBXFontConvert
PX_DataPath

在ATL中管理控件属性持续涉及到两个步骤。第一步是添加你希望客户能够使用的持续接口的ATL实现。ATL包括了类IPersistStorageImpl, IPersistStreamInitImpl, 和 IPersistPropertyBagImpl, 它们实现了三个主COM持续机制。

第二步是在控件的属性映射中插入属性。当一个客户请求保存或者加载基于ATL的控件时,ATL检查控件的属性映射表,将控件的属性输出到存储媒介,或者从存储媒介输入。属性映射表是属性名字、DISPIDs的一个表,有时还包括一个属性页面GUID。ATL遍历词表查找该持续哪个属性,并将其持续到合适的媒体。图五显示了继承持续接口实现和一个属性映射的ATLMsgTraff。

属性页

ActiveX控件经常在开发者将控件放到各类容器时提供属性页帮助。开发者将消息流控件放入一个对话框后,可能想要对控件信息进行配置,如控件的取样间隔,或者绘图线条的颜色等。

例如,当控件放在一个对话框中,你想通过右击鼠标得到控件的属性页,Visual Studio显示一个突出的对话框。这里将说明其工作过程。

Visual Studio请求控件在一个对话框框架里显示属性页(Visual Studio ISpecifyPropertyPages接口请求控件提供一个属性页的清单),属性页显示在Visual Studio中,但是通过控件提供的一个COM接口保持与控件的连接。每当你完成了属性编辑并从Visual Studio中关闭了对话框,它就会要求属性页更新控件中的属性。

当你生成一个MFC的控件时,wizard给你一个对话框模板和一个从COlePropertyPage中派生的代表此控件的缺省属性页的类。Visual Studio使得在属性页中属性与属性的连接变得容易。当你使用ControlWizard的“Automation tab”添加属性到你的基于MFC的控件中的时候,你给了属性一个外部名字。这个名字是外部客户方(包括属性页)用来识别该属性的。

你按照开发其它任何对话框的方法来开发属性页——将控件添加到对话框模板,将对话框成员变量和控件联系起来。ControlWizard添加DDX/DDV代码在对话框控件和成员变量之间交换数据。然而,当你将成员变量和对话框控件相关联时,ControlWizard给你提供了这样一个机会,你可以将外部属性名字用于对话框的成员变量。此外部名字是你给控件添加属性时键入的字符串。

当属性页需要将改变应用于控件时(例如当按下Apply按钮时),属性页使用控件的IDispatch接口以及外部名字来修改控件的属性。在MFC中,你可以通过ClassWizard来添加一个新属性,添加一个新的对话框模板到工程中,让ClassWizard创建一个类——要确保是从COlePropertyPage中派生出来的类。然后,为了使新的属性页可以被外界访问到,将它的GUID添加到控件的属性页映射中(在控件的.CPP文件中查找BEGIN_ PROPPAGEIDS 和 END_PROPPAGEIDS两个宏)。不象MFC的ActiveX ControlWizard,ATL COM App Wizard并不向DLL中添加缺省的属性页。这意味着你要自己完成此工作。幸运的是,又一个wizard可以向属性页中添加基于ATL的DLL。只要选择Insert ATL Object,然后找到属性页对象。Wizard将一个对话框模板和一个C++类与所有必要的 COM 内容细节一起添加到一个属性页中。让它们完成什么工作是你的事情。

不幸的是,ATL属性页的wizard驱动特性不如基于MFC的属性页,你得手工完成应用和显示操作。就是说要自己提供Apply 和 Show的函数实现到你的属性页类中。Apply函数只是提取对话框中控件的状态,遍历属性页拥有的指向控件的接口指针列表,使用接口指针来修改控件属性。Show函数通常提取控件的状态,然后以此来组织对话框的控件。下面的代码显示了基于ATL的属性页是怎样处理Apply函数的:

STDMETHOD(Apply)(void)
{
    long nInterval = GetDlgItemInt(IDC_EDITINTERVAL);
    ATLTRACE(_T("CMainPropPage::Apply\n"));
    for (UINT i = 0; i < m_nObjects; i++)
    {
        IATLMsgTrafficCtl* pATLMsgTrafficCtl;
        m_ppUnk[i]->QueryInterface(IID_IATLMsgTrafficCtl, 
                                   (void**)&pATLMsgTrafficCtl);
        if(pATLMsgTrafficCtl) {
            pATLMsgTrafficCtl->put_Interval(nInterval);
            pATLMsgTrafficCtl->Release();
        }
    }
    m_bDirty = FALSE;
    return S_OK;
}

为基于ATL的控件提供一个属性页的第二步是确保属性页的CLSID出现在控件的属性映射中,消息映射表明了控件的图线颜色,被标准的颜色属性页管理。控件的取样间隔由控件的主属性页来管理。

Window 消息

MFC和ATL在它们处理window消息方面有很多共同之处,都使用消息映射,都有wizards来生成代码处理window消息。在MFC中,消息映射可以添加到任何一个CCmdTarget派生的类中,然后你就可以用ClassWizard来建立你的控件的事件处理器了另外,MFC提供了处理命令和控件通知的宏。象MFC一样,ATL也通过消息映射来处理window消息,只要你的类是从CWindowImpl派生的,而且包含ATL的消息映射宏,你就可以使用类视来建立事件处理器。 ATL使用MESSAGE_HANDLER宏将标准的window消息映射到一个C++类。此宏简单地产生一个将window消息和类的成员函数关联的表。除了常规消息,消息映射还可以处理其它类型的事件。连接和事件

最后要进行的比较是MFC和ATL是怎样处理连接点和事件集的。为了管理连接点和事件集,需要一个COM类来实现IConnectionPointContainer,然后创建一种提供指向IConnectionPoint的指针给客户的方法。MFC的主控件类COleControl已经有了内置的IConnectionPointContainer,MFC通过连接映射提供了连接点。MFC已经为IPropertyNotifySink定义了连接点和控件的缺省事件集。

为了完善一个基于MFC的控件的缺省事件集,你只要简单地使用ClassWizard的“ActiveX Event”标签。在你使用ClassWizard添加事件的时候,Visual Studio更新你的控件的.ODL文件,为潜在的包容器描述外出事件。另外,Visual Studio添加一个函数到你的类中,你可以调用它反过来向包容器激发事件。基于MFC的控件的事件触发函数只是由包容器在它和控件建立连接点时提供的一个IDispatch指针的一些简单的包裹器。

在基于ATL的控件中建立事件则有所不同。在基于ATL的控件中,你从定义控件的.IDL文件中的事件开始。接着你建立了类型库的编译工程文件。 一但类型库编译通过,你就可以通过在类视中选择控件的类,在类上右击,然后选择“Implement Connection Point”,让类视来为你创建一个回调代理了。Visual Studio弹出一个对话框,列出控件类型库中所有可访问的事件接口。你选择那些你希望回调代理做的,Visual Studio就为你写一个代理。Visual Studio产生的回调代理代表了一个C++友好的函数集,被客户实现的接口所调用。

MFC的IConnectionPointContainer实现是硬分布到COleControl中,并且每个连接点是由一个连接映射处理的,而ATL的实现是用多重继承处理的。你的控件类继承IConnectionPointContainerImpl和类视生成的代理。如果你开始一个工程的时候,选择了“Supports connection points”,ObjectWizard就为你添加IConnectionPointContainerImpl。如果你忘了标记检查框,你可以写进去。此代码显示了连接点机制是怎样加入一个控件中的。类ATL_NO_VTABLE CATLMsgTrafficCtl :

class ATL_NO_VTABLE CATLMsgTrafficCtl : { ••• public IConnectionPointContainerImpl, public CProxy_DATLMsgTrafficEvents ••• { LRESULT OnTimer(UINT msg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { //••• if(nMessagesToShow > m_threshold) { Fire_ExceededThreshold(nMessagesToShow, m_threshold); } //••• } };

作为一个应用框架ATL和MFC的比较

最近,许多开发者开始对使用ATL作为框架来开发应用和控件感兴趣了。当然,MFC已经使用了很长时间了,是一个能够开发可双击的基于Windows的应用的非常成熟的框架。例如,MFC包括了这样的特性作为一个总体文档/视体系结构:Object Linking 和 Embedding支持, 以及工具条和状态栏。

然而,所有这些功能都是有代价的。一些更加普遍的抱怨是MFC的比较深的足迹(无论是在DLL中,或是在静态连接的版本中),以及自身的某种相互依赖性。例如,MFC的一种特性意味着MFC的对象连接和嵌入,也就意味着MFC的文档/视结构。也就是说,ATL是一个不依赖任何应用框架因素的原始框架。

正象你已经看到的,两个框架都提供创建控件的可行途径。然而,两者都各有千秋、各有利弊。用MFC编写控件通常更加容易——尤其是如果你不是在开发COM集中的应用并且你需要windowing和drawing支持。ATL的体系架构更加靠近COM的核心,你还会经常发现自己在编写很多的SDK类型的代码——就是说,你在回过头来用window和设备上下文句柄。ATL为更广范围的控件类型提供了很大的支持,象复合控件,基于HTML的控件,没有设计时接口的轻量级的控件等等。而MFC仅提供完全成熟的控件。

ATL中各个环节的实现是非常直接的。例如,增加一个接口通常就是添加接口到继承表,在COM映射中添加一个入口然后实现接口函数。MFC中各环节的实现通常是一种折磨。例如,添加一个接口到基于MFC的控件意味着处理所有那些接口映射宏。

最后,ATL提供了大量的调试支持,包括接口引用计数以及QueryInterface调试支持,这在MFC中是没有的。

这两种体系架构的区别是非常明显的。通常,MFC使得你很快完成你的工程并更快地运行起来,但是牺牲了灵活性。ATL没有那麽快和容易使用,但是它是COM友好的。而且,随着ATL的成熟,它将会越来越容易使用。(全文完)

你可能感兴趣的:(ATL和MFC,用哪种框架来创建ActiveX控件:第四部分)