ATL和MFC,用哪种框架来创建ActiveX控件

60 篇文章 5 订阅
5 篇文章 0 订阅
  本文假定你熟悉MFC, ATL, and COM 
摘要:目前MFC和ATL代表了两种框架,分别面向不同类型的基于Windows的开发。MFC代表了创建独立的Windows应用的一种简单、一致的方法;ATL提供了一种框架来实现创建COM客户机和服务器所必须的样板文件代码。这两种框架在用于开发ActiveX控件的道路上走到了一起。
        我们将看看这两种框架是如何适用于创建ActiveX控件的——突出其优缺点,亲自经历创建一个控件的过程——以便你能够决定何时使用一种框架,何时使用另一种。
    如果你希望用C++来写ActiveX®控件,有两个流行的框架,一个是Microsoft® Foundation Classes (MFC),另一个就是ATL。本文将将深入这两种框架,解释它们对开发ActiveX控件所提供的支持,帮助你更好地决定哪种模型最适合你的开发环境和需求。

ActiveX控件的完全形态

      ActiveX控件基于组件对象模型COM,使得ActiveX控件成为可能的COM的基本原则是:一个对象的接口和其实现能够而且应该分开对待。只要COM的对象和它的客户方代码就接口细节达成了一致,如何实现就不是问题。ActiveX控件展示了大量ActiveX控件包容器理解的接口。因为客户方代码和控件认可这些接口细节达成了一致,你可以编写一个ActiveX控件然后简单的将它放入包容器中。包容器将通过定义良好的接口来驱动控件,而这些控件将以自己的方式做出合适的响应。
    在更高的层次上,一个ActiveX控件是实现了几个主要ActiveX技术的一个COM对象,包括常规引入COM接口,OLE嵌入协议,连接点和属性页。在较低的编程层次上,ActiveX控件只是实现了某些类型接口的COM类。当某些客户方代码成功的查询到这些接口之一时,它就知道了它正在与一个ActiveX控件打交道。

    一个ActiveX控件暴露的接口主要分为三类。第一、ActiveX控件是可嵌入的对象;就是说,它们实现了大多数的OLE文档就地(in-place)激活和嵌入协议。ActiveX控件实现了如下的接口:

IOleObject, IPersistStorage, IDataObject, IOleInPlaceActiveObject, IOleInPlaceObject, IViewObject2和IRunnableObject (这一个很少用到). 第二、ActiveX控件通常都支持属性页,这样客户方就可以修改控件的属性了。最后,ActiveX控件通常都利用COM的连接点技术,实现了客户方能发现的外出接口。

    为了帮助比较ATL和MFC框架,我们来看一下在两种框架中写的相同的控件。此控件监视创建它的线程上传递的消息流。消息流控件是一个很不错的例子,因为它演示了一个ActiveX控件所有主要的方面,包括引入接口、外出接口,属性,永久性以及属性页。让我们从研究这两个框架提供的标准的COM支持开始吧。

MFC中基本的COM支持

    Microsoft建立MFC使得开发Windows®应用程序比使用SDK容易多了。有了MFC,Microsoft接着增加了对即存框架的COM支持。这意味着MFC的开发者在增加越来越多的功能时必须保持框架的完整。同时,Visual C++®编译器那时还不支持模板,因此,它们不得不借助非模板的其它手段来将COM功能掺入类中。Microsoft通过加入一些虚函数到CCmdTarget类和一些宏中解决了这个问题,使得在MFC中实现COM接口有了可能。

    MFC内部的COM支持是从CCmdTarget开始的,CCmdTarget类实现了IUnknown接口,还包括了一个用于引用计数的成员变量(m_dwRef)以及用于实现IUnknown 的6个函数:: InternalAddRef, InternalRelease, InternalQueryInterface, ExternalAddRef, ExternalRelease, 和 ExternalQueryInterface.。QueryInterface的两个版本——AddRef和Release支持COM聚合。InternalAddRef, InternalRelease和InternalQueryInterface完成引用计数和QueryInterface操作,而ExternalAddRef, ExternalRelease和 ExternalQueryInterface代理控制聚合的对象(如果此对象参与聚合的话)。

    MFC使用嵌套的类复合策略来实现COM接口。在MFC中,想实现COM 接口的类是从CCmdTarget中派生的。每个由CCmdTarget派生出的类实现的接口得到它自己的嵌套类。MFC使用宏BEGIN_INTERFACE_PART和END_INTERFACE_PART来产生嵌套类。

    最后,MFC实现了表驱动的QueryInterface。MFC的接口映射的工作机理同它的消息映射基本相同:MFC的消息映射把一个Windows消息和一个C++类中的函数相联系;MFC的接口映射把一个接口的GUID和一个表示此接口的特定的vptr的地址相联系。每个实现COM接口的基于CCmdTarget的类通过更多的宏:DECLARE_INTERFACE_MAP, BEGIN_INTERFACE_MAP, INTERFACE_PART,和 END_INTERFACE_MAP来获得增加的接口映射。

    为了理解这些宏在实际中是什么样子的,请看图一,它说明了实现ActiveX控件的MFC类 COleControl。当你细读代码时,注意COleControl带有夹在一对BEGIN_INTERFACE_PART 和 END_INTERFACE_PART宏之间的每个接口的签名,还要注意COleControl的接口映射表有22个条目。

    除了实现了IUnknown接口,MFC还包括IClassFactory的一个标准实现。MFC又一次通过若干宏提供了此支持。MFC有两个宏来提供类对象:DECLARE_OLECREATE_EX 和 IMPLEMENT_OLECREATE_EX.。在一个基于CCmdTarget的类中使用这些宏增加一个COleObjectFactory类型的静态成员到该类中。如果你看一下AFXDISP.H中 COleObjectFactory的定义,你将会看到用在COleObjectFactory 中的MFC的嵌套类宏为实现IClassFactory2定义了一个嵌套类。IClassFactory::CreateInstance的MFC版本使用MFC的动态创建机制(DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE宏打开此功能)来实例化COM类,因此有了MFC的COM支持同样意味着就有了它的动态创建机制。

    最后几个由MFC提供的在ActiveX控件内的基本COM支持是对IDispatch的支持。用Visual C++ 和 MFC实现一个分发接口几乎是微不足道的。在MFC中实现一个分发接口,只需要使用ClassWizard就可以了。ClassWizard中的“Automation”标签有一个按钮用于添加属性,另一个用于添加方法。
    在MFC中,IDispatch支持来自CCmdTarget的类。IDispatch 的MFC的实现实际上在一个叫做COleDispatchImpl 的类中,COleDispatchImpl派生自IDispatch,实现了所有4个IDispatch函数:GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, 和 Invoke.。由CCmdTarget派生的类通过调用EnableAutomation,将IDispatch vptr加入到它们的接口映射中。当客户在基于MFC的ActiveX控件上调用IDispatch 的QueryInterface时,CCmdTarget交出链接在COleDispatchImpl上的vptr。

    每次你使用ClassWizard将一个自动属性或者方法加入到一个类中时,你同时也在该类的分发映射表中加入了一项。一个分发映射表是一个将DISPIDs(用来调用分发成员的符号)和它们的供人读的名字以及和实际完成这个工作的某些C++代码联系起来的简单的表格。COleDispatchImpl的调用以及GetIDsOfNames函数通过在类的分发映射表中查找分发成员并分发DISPID相对应的函数来工作。
    MFC能为某些基于COM的高级技术如OLE文档、OLE拖放和自动操作提供非常好的支持,然而,如果你想更改框架——比如说,你想将分发接口转为双接口的——你就得大动手脚了。另一方面,ATL更加地以COM为中心.

ATL基本的COM支持

    ATL的目标是使开发者不必重写IUnknown, IDispatch, IClassFactory接口,并且在把常规的DLL和EXE变成基于COM的DLL和EXE时,也不必重写其分支部分。在这个方面,ATL是一个比MFC精简得多的框架,它设计和生成时就考虑了COM支持。它使用基于模板的方法,通过继承ATL提供的模板,开发者可以加入各种COM功能块。

    ATL的原始COM支持是从对IUnknown的支持开始的。ATL的IUnknown实现分成两个部分:CComObjectRootEx类,用来处理IUnknown部分的引用计数;CComObjectRootBase类,用来处理QueryInterface。

    CComObjectRootEx是一个基于模板的类,将线性模型作为其唯一参数。这是一个真正有趣的说明ATL怎样使用模板将算法作为模板参数传递的例子。ATL有两个处理引用计数的类,用于处理不同的线性模型: CComSingleThreadModel 和 CComMultiThreadModel。这些类每个都有一个递增和一个递减函数。它们之间的区别是CComSingleThreadModel用标准C++操作符(++和--)实现递增和递减;而CComMultiThreadModel使用线程安全的InterlockedIncrement 和 InterlockedDecrement函数来实现这两个功能。根据用来实例化CComObjectRootEx的模板参数,它能正确的运行给定的组件类型。你很快将会看到它的用法的一个例子。

    象MFC一样,ATL使用基于表的查找机制实现QueryInterface。CComObjectRootBase 通过一个接口映射处理类的QueryInterface函数。BEGIN_ COM_MAP 和 END_COM_MAP 宏定义了一个接口映射的开始和结束。然而,与MFC不同的是ATL提供了17种途径来组成一个接口映射,例如使用从ATL的基于模板的接口实现类IOleObjectImpl 带来的 vptrs。这包括了那些来自tear-off类的vptrs,或者由聚合提供的 vptrs。

    在ATL里,C++类通过继承CComObjectRootEx,指定它们想用的公寓(apartment)模型(记住,MFC的IUnknown支持是内建在CCmdTarget中的)来变成COM类。ATL的类对象(以及IClassFactory)支持也来自模板。

    而MFC的类对象支持通过COleObjectFactory和一些宏而有效。ATL的类对象支持来自CComCoClass/CComClassFactory类家族和CComCreator类家族。CComCoClass包含了类的GUID,定义了COM类的错误处理设施。CComCreator类提供了CreateInstance的实现,供CComClassFactory使用。对于MFC,你可以通过若干宏,使所有这种支持有效。ATL包括 DECLARE_CLASS_FACTORY, DECLARE_CLASS_ FACTORY2, DECLARE_CLASS_FACTORY_AUTO_THREAD, 以及 DECLARE_CLASS_FACTORY_SINGLETON等宏用来使各种具体的类工厂支持有效。

    最后,ATL 对IDispatch的支持也还是来自模板类,——其名字是IDispatchImpl.。比起MFC的IDispatch支持来,ATL对IDispatch的支持更是以COM为中心。MFC使用了一种hand-rolled 的IDispatch实现,而ATL使用更加标准的方法来加载一个接口的类型信息并代表标准的类型库编译器。

    最值得注意的一点是MFC和ATL各自是怎样引入实现一个控件所需的必要的各种接口的。MFC对标准控件接口的支持是内建在COleControl类中的。你从COleControl中派生出你的控件并且一次性继承所有的函数调用。注意ATL通过模板继承以零碎的方式逐个引入每个功能片断。这是一个非常重要的差异,因为这意味着用ATL你可以忽略一些接口实现模板

关于例子应用

    这里我将使用的例子是一个通过一个钩子(hook)程序监控消息流的ActiveX控件,它实时显示消息流图。这两个控件实际上有着相同的功能。它们都把图表送到屏幕。它们都带流入接口以便包容器能通知控件开始和停止该图表。它们都支持图表线的颜色和消息间隔长度作为可以持续存在的属性。最后,它们都支持缺省事件集,将关于在一个特定时间段里处理的消息的数量通知包容器。图三显示了这两个控件。

 

图三 监视 ActiveX 控件消息

用MFC开发一个控件

      用MFC开发一个ActiveX控件涉及到在Visual Studio®.中使用ActiveX ControlWizard。为了开始一个新的控件,从File菜单中选择New,然后从工程类型列表中选择MFC ActiveX控件Wizard。首先,ControlWizard要求你决定在DLL中包括多少个控件。接着你就可以选择你打算怎样实现你的控件。

     ControlWizard提供的第一批选项总体上适用于控件的DLL。它们包括了许可支持、源码注释和在线帮助。如果选择控件有运行时的许可使得(licensing),ControlWizard将使用BEGIN_OLEFACTORY和END_OLEFACTORY (而非DECLARE__OLECREATE)。BEGIN_OLEFACTORY 和 END_OLEFACTORY宏覆盖了VerifyUserLicense和GetLicenseKey,因而为你的控件提供许可支持。请求ControlWizard包括注释将所有的TODO注释加入代码中。最后,请求ControlWizard包括在线帮助将为DLL创建样板HELP文件源代码。

     一旦你通过了第一个对话框,ControlWizard就显示一个对话框用来配置DLL中的控件。这些配置选择包括使控件在运行时是否可见、使控件在可见时激活、对象可以被插入,是否给控件一个About对话框、是否使控件作为一个简单的容器控件。

    ControlWizard还有一个将控件实现为一个标准的Windows控件的选项,就像一个编辑框或者一个按钮。这是一个有趣的选项。例如,如果你选择将控件做成一个按钮的子类(或者说子集),控件的窗口实际上是一个按钮。此时,PreCreateWindow截获控件窗口的创建,当创建控件的窗口时使用BUTTON窗口类。

    ControlWizard使你可以选择一些高级的选项,包括无窗口的激活,使你的控件具有无剪裁的设备上下文,实现无抖动的激活,使你的控件在非激活状态也接受鼠标消息,使你的控件异步加载自己的属性。下面是在“高级(Advanced)”按钮中的每个选项如何影响ControlWizard生成代码的纲要:

无窗口的激活Windowless activation此选项覆盖COleControl::GetControlFlags,将windowlessActivate标志附加到控件标志中。一旦使此选项有效,包容器就将输入消息送交到控件的IOleInPlaceObjectWindowless接口。此接口的COleControl实现通过你控件的消息映射分发消息。你就能简单地通过添加相应的入口到消息映射表,像处理一般windows消息那样处理消息了。

无剪裁的设备上下文Unclipped device context选择了此选项覆盖COleControl::GetControlFlags并关闭clipPaintDC位,从而在COleControl的 OnPaint函数中去掉了IntersectClipRect调用。如果你确定你的控件并不需要在客户区外部绘图,这个选项就有用了,因为使得对IntersectClipRect的调用失效后,有一个明显的速度的提高。

无抖动的激活Flicker-free activation选择此选项覆盖COleControl::GetControlFlags,将缺省控件标志与noFlickerActivate逐位相或。控件在激活的时候检查此标志以阻止控件在激活和非激活状态转换时被重画。如果你的控件在激活和非激活状态外观一样,那么这个选项就特别有用。

非激活时的鼠标指针通知Mouse pointer notifications when inactive这个选项覆盖COleControl::GetControlFlags并附加了pointerInactive位。IPointerInactive接口使得一个对象大多数时间保持非激活,还是仍然要参与到与鼠标的某些交互操作中,例如拖放。

优化的绘图码Optimized drawing code这个选项覆盖COleControl::GetControlFlags,打开canOptimizeDraw位,具有优化绘图代码的控件检查这个标志(通过COleControl的IsOptimizedDraw函数)来确定控件是否需要在完成绘画后将旧的对象复原回设备上下文。

异步加载属性Load properties asynchronously此选项将库存(stock) ReadyState属性和库存(stock)ReadyStateChange事件加入到控件中去。这将使控件异步加载其属性。例如,一个加载大量数据作为其属性之一的控件会需要很长的时间来加载,而锁住了控件。这个stock属性和事件使得此控件立刻开始加载过程。包容器使用此事件和属性判断控件何时完成加载。

    当ControlWizard完成这些事情后,你就得到了编译到一个包含此控件的DLL的源代码(扩展名是.OCX)。由wizard产生的源代码包括一个从COleControlModule(它又是从CwinApp中派生的)中派生的类。这个类包含整个控件模块的初始化代码。接着,wizard为基于COleControl的表示每个控件的类生成源代码。最后,wizard生成一些ODL代码用来建立类型信息。

    一旦wizard产生了控件DLL,你就面临开发完善这个控件的任务了。这意味着要添加功能代码,开发一个流入接口(方法和属性),创建并制作属性页,暴露某些事件。但在开始所有这些工作之前,我们先来看一下使用ATL创建一个控件都需要什么步骤。

用ATL开发一个控件

    有了基于MFC的控件,你就可以用ATL COM App Wizard得到一个开发基于ATL的控件的触发器。使用ATL来创建控件可以分为两步。MFC的Control Wizard在开始要求你预先确定你希望在DLL中包含多少个控件,而 ATL COM Wizard 只简单地创建DLL——你可以以后从“Insert”菜单中使用“New ATL Object...”选项添加控件。当创建一个新的基于ATL的DLL时,你可以选择混入MFC支持。你还可以选择在控件的DLL中合并任何proxy/stub代码。这使得如果有人希望远程控制你的控件实现的接口,你只需要发布一个文件即可。

    一旦生成了基于ATL的DLL,你就可以开始添加COM类了。“Insert”=>“New ATL Object”菜单项使得这项工作变得十分容易。选择此菜单项显示一个对话框,可以创建大量COM类中的任何一个,包括无格式的COM对象、ActiveX控件以及Microsoft事务服务器(MTS)组件(Windows NT Server的一部分)。

    当添加基于ATL的控件到你的工程的时候,ATL  Object Wizard比MFC Object Wizard提供了更大范围的选项。对于新手来说,ATL使得你可以选择使用任何现有的线性模型实现你的控件。你可以将你的类标记为单线程(single threaded)或者单元(apartment threaded)线程的。ATL Object Wizard限制你创建一个自由的(free threaded)或者双线程(both threaded)的控件,因为控件一般都是有用户界面(UI)的。

    如果你创建了一个单线程(single threaded)控件,一个包容控件的客户端将总是将它加载到主单线程的单元(STA)中。结果,只有运行在客户进程空间中的单主线程才会接触到你的对象,这样就免除了并发访问时你必须保护控件状态的义务。另外,因为你的对象的所有实例将只会被一个线程接触,你就不必担心DLL中的任何全局数据。

    如果你的控件是单元线程(apartment threaded)的,你也还免去了保护控件内部状态的大部分负担。但是,你仍然必须保护DLL中的全局数据。为什么呢?首先,设想你的控件是由客户的单主线程创建的。现在假定客户试图创建该控件的另一份拷贝——从一个运行在当前进程的多线程单元中。通过将你的对象标记为单元线程的,你告诉COM你希望你的控件免遭并发访问。COM在它加载时为你的控件创建一个新的STA。现在当线程调用进入你的对象时,它们不得不通过单元边界来访问它,偏远层将同步对此对象的调用。然而,当一个特定控件的状态被保护不被并发访问,作为一个在STA中的副产品,由控件的实例所共享的数据(象在DLL中的全局数据一样)是易受攻击的。这是因为你的全局DLL数据(同时为几个对象服务,分别运行在独立的线程中)会被那些多线程同时接触到。

    虽然基于MFC的COM类总是可聚合的(内置的支持),ATL ObjectWizard可以让你指定你的控件选项:是否支持聚合,只是可聚合的,或者是独立的对象。根据你选择的聚合选项,ATL ObjectWizard使用一个宏来强行聚合策略。例如,缺省的COM类的实现是可聚合的——对象将既运行在独立的模式,又作为一个聚合的一部分。如果你让你的COM对象不可聚合,ObjectWizard把DECLARE_NOT_ AGGREGATABLE宏加到你的类定义中。如果你选择了仅是可聚合的,ObjectWizard把DECLARE_ ONLY_AGGREGATABLE宏加入到类定义中。

    这里是宏如何工作的。缺省的对象创建发生在一个名为_CreatorClass的类中。_CreatorClass当被加入到服务器范围的对象映射后(这是OBJECT_ENTRY宏所做的工作的一部分),就成为你的COM类的创建机制。_CreatorClass其实只是一个名为CComCreator2类的别名,此类将两个从CComCreator类中定制的类作为参数。此宏根据选择的聚合模式来特制CComCreator类,分别使用CComObject, CComAggObject, CComFailCreator, 或者CcomPolyObject:

      
      
  1. #define DECLARE_NOT_AGGREGATABLE(x) public: \   
  2.    typedef CComCreator2< CComCreator< CComObject< x > >, \  
  3.    CComFailCreator<CLASS_E_NOAGGREGATION> > _CreatorClass;  
  4. #define DECLARE_AGGREGATABLE(x) public: \   
  5.    typedef CComCreator2< CComCreator< CComObject< x > >, \  
  6.    CComCreator< CComAggObject< x > > > _CreatorClass;  
  7. #define DECLARE_ONLY_AGGREGATABLE(x) public: \   
  8.    typedef CComCreator2< CComFailCreator<E_FAIL>, \  
  9.    CComCreator< CComAggObject< x > > > _CreatorClass;  
  10. #define DECLARE_POLY_AGGREGATABLE(x) public: \   
  11.    typedef CComCreator< CComPolyObject< x > > _CreatorClass;  

    ATL ObjectWizard Attributes页中最后三个检查框包括对COM异常处理的支持(例如,ISupportErrorInfo接口),连接点以及自由线程(free threaded marshaler)集(FTM)。你也可以添加ISupportErrorInfo到控件的继承列表中,提供ISupportErrorInfo::InterfaceSupportsErrorInfo的一个实现。打开连接点将添加IConnectionPointImpl 模板类到控件的继承列表中。

    如果两个对象正好位于同一个进程中,请聚合你的控件到FTM以便单元间(以及Windows 2000的上下文间)的调用更为有效地发生。然而,当你使用FTM编写控件时,如果或多或少地违反了单元(以及Windows 2000的上下文)规则,就绝不要选择这个选项。

    除了你可以应用到所有COM对象的常规选项,ATL ObjectWizard还提供了几个控件创建特定的选项。首先,ATL ObjectWizard让你从一个常规控件(例如一个按钮或是一个编辑控件)中进行子类划分。你可以为你的控件指定其它几个选项使得它更加不透明,给它一个更实心的背景,在运行时不可见,或者是你的控件象一个按钮那样的工作。下面是控件属性页提供的一个选项纲要:

不透明和实心背景Opaque and solid background如果你想要确保在控件边界之后不显示任何的包容器,选择"opaque"检查框,这是控件传给它的包容器的状态信息。结果是,控件将画出它的完整矩形。选择此选项设置VIEWSTATUS_OPAQUE位以便IViewObjectExImpl::GetViewStatus向包容器指示一个不透明的控件。你可能还想选择一个实心的背景。这个选项设置VIEWSTATUS_ SOLIDBKGND 位以便GetViewStatus指示控件有一个实心的背景。

运行时不可见Invisible at runtime此选项使你的控件在运行时不可见。你可以使用不可见控件在后台完成某些操作,例如周期性的激发事件。此选项在它加入到注册表中后使得控件翻转OLEMISC_INVISIBLEATRUNTIME 位。

仿按钮Acts like button此选项使你的控件象一个按钮那样工作。此时,控件将在包容器周围属性DisplayAsDefault的基础上显示为缺省的按钮。如果控件的位置标记为缺省按钮,控件将显示为一个较厚的框架。选择此选项在它加入到注册表中后使得控件翻转OLEMISC_ACTSLIKEBUTTON 位。

仿标签Acts like label选择此选项使得你的控件取代包容器的内部标签。这使得控件在它加入到注册表中后标记OLEMISC_ACTSLIKELABEL。

在超类基础上添加控件Add control based on superclass选择此选项使得你的控件根据一种标准window类进行子类划分。下拉列表包含了Windows定义的window类。当你选择这些类名中的一个时,向导添加一个CContainedWindow成员变量到你的控件类中。CContainedWindow::Create将你指定的window类超类化。

规格化DCNormalize DC选择此选项使得你的控件在被调用来绘制自己时创建一个规格化的设备上下文。这标准化了控件的外观,但是效率降低了。此选项生成的代码覆盖了OnDrawAdvanced方法(而不是常规的OnDraw方法)。

可插入的Insertable选择此选项使得你的控件显示在象Microsoft Excel 和Word 这样的应用的“Insert Object”对话框中。你的控件就能够被插入到任何支持嵌入对象的应用中了。选择此选项在注册表项中增加了Insertable键。

仅为窗口化的Windowed only选择此选项迫使你的控件窗口化,即使在支持无窗口对象的包容器中。如果你不选择此选项,你的控件将会自动的适应包容器:在支持无窗口对象的包容器中是无窗口的,在不支持无窗口对象的包容器中是有窗口的。这将使CComControlBase::m_bWindowOnly标志设置为TRUE。ATL使用此标志来决定在控件激活过程中是否要查询包容器的IOleInPlaceSiteWindowless接口。

    ATL要求你预先在“Stock Properties”页中决定你的对象的库存(stock)属性,你可以选择Caption或者 Border Color这样的属性,或者通过点击>>按钮一次性选择所有的库存(stock)属性。这将向控件的属性映射中添加属性。

    在运行ATL COM App Wizard 和 ObjectWizard之后,你就得到了一个完整的DLL,它具有一个COM DLL所必需的所有环节。此控件暴露的众所周知的输出包括DllGetClassObject, DllCanUnloadNow, DllRegisterServer,和 DllUnregisterServer。另外,你得到了一个满足COM主要需求的对象——包括一个主流入接口和一个类对象。

    一旦你已经使用一个向导开始了一个工程,下一步就是使控件做点有趣的事情了。通常出发点是控件的功能(rendering)代码。你立刻得到一些可视化的反馈。在下一部分中,我们将揭示实现一个基于MFC的控件的功能代码的全部细节。

为控件添加功能代码

         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<CRect&>(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结构:

     
     
  1. struct ATL_DRAWINFO  
  2. {  
  3.     UINT cbSize;  
  4.     DWORD dwDrawAspect;  
  5.     LONG lindex;  
  6.     DVTARGETDEVICE* ptd;  
  7.     HDC hicTargetDev;  
  8.     HDC hdcDraw;  
  9.     LPCRECTL prcBounds;  //在这个矩形中绘图   
  10.     LPCRECTL prcWBounds; //WindowOrg and Ext if metafile   
  11.     BOOL bOptimize;  
  12.     BOOL bZoomed;  
  13.     BOOL bRectInHimetric;  
  14.     SIZEL ZoomNum;       //ZoomX = ZoomNum.cx/ZoomNum.cy   
  15.     SIZEL ZoomDen;  
  16. };  

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

     
     
  1. HRESULT CATLMsgTrafficCtl::OnDraw(ATL_DRAWINFO& di)  
  2. {  
  3.    RECT& rc = *(RECT*)di.prcBounds;  
  4.    HBRUSH hBrush = CreateSolidBrush(RGB(255, 255, 255));  
  5.    FillRect(di.hdcDraw, &rc, hBrush);  
  6.    DeleteObject(hBrush);  
  7.    Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom);  
  8.    ShowGraph(di.hdcDraw, rc, nMessagesToShow);  
  9.    return S_OK;  
  10. }  

    注意当你使用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控件是怎样将它的颜色和时间间隔属性保存起来的:

     
     
  1. void CMFCMsgTrafficCtrl::DoPropExchange(CPropExchange* pPX)  
  2. {  
  3.     ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));  
  4.     COleControl::DoPropExchange(pPX);  
  5.   
  6.     PX_Color(pPX, "GraphLineColor", m_graphLineColor);      
  7.     PX_Long(pPX, "GraphInterval", m_interval);  
  8. }  
    MFC包括了若干PX_函数在控件和存储媒体间转移数据,它们是:
     
     
  1. PX_Short  
  2. PX_UShort  
  3. PX_Long  
  4. PX_ULong  
  5. PX_Color  
  6. PX_Bool  
  7. PX_String  
  8. PX_Currency  
  9. PX_Float  
  10. PX_Double  
  11. PX_Blob  
  12. PX_Font  
  13. PX_Picture  
  14. PX_IUnknown  
  15. PX_VBXFontConvert  
  16. 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函数的:

     
     
  1. STDMETHOD(Apply)(void)  
  2. {  
  3.     long nInterval = GetDlgItemInt(IDC_EDITINTERVAL);  
  4.     ATLTRACE(_T("CMainPropPage::Apply\n"));  
  5.     for (UINT i = 0; i < m_nObjects; i++)  
  6.     {  
  7.         IATLMsgTrafficCtl* pATLMsgTrafficCtl;  
  8.         m_ppUnk[i]->QueryInterface(IID_IATLMsgTrafficCtl,   
  9.                                    (void**)&pATLMsgTrafficCtl);  
  10.         if(pATLMsgTrafficCtl) {  
  11.             pATLMsgTrafficCtl->put_Interval(nInterval);  
  12.             pATLMsgTrafficCtl->Release();  
  13.         }  
  14.     }  
  15.     m_bDirty = FALSE;  
  16.     return S_OK;  
  17. }  

    为基于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 :

  1. class ATL_NO_VTABLE CATLMsgTrafficCtl :   
  2. {  
  3. •••  
  4.    public IConnectionPointContainerImpl<CATLMsgTrafficCtl>,  
  5.    public CProxy_DATLMsgTrafficEvents<CATLMsgTrafficCtl>  
  6. •••  
  7.     {  
  8.     LRESULT OnTimer(UINT msg, WPARAM wParam,   
  9.                     LPARAM lParam,   
  10.                     BOOL& bHandled) {  
  11.     //•••       
  12.         if(nMessagesToShow > m_threshold)  
  13.         {  
  14.             Fire_ExceededThreshold(nMessagesToShow, m_threshold);  
  15.         }  
  16.     //•••   
  17.     }  
  18. };  

作为一个应用框架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的成熟,它将会越来越容易使用。(全文完)

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值