1 、 MFC 和 ATL
对于 COM 应用的开发来说,建立一套标准的、有能够不断发展的类库是非常有意义的,这样可以实现程序的重用性。 Microsoft Visual C++ 提供了两套满足这样需要的类库: MFC 库和 ATL 模板库。 MFC 不仅可用于建立 COM 应用,它更是一套 Windows 平台上各种应用开发的基本类库,而 ATL 则主要侧重于 COM 应用的开发,利用 ATL 可建立一些小巧、快捷的 COM 组件。
2 、 Win32 SDK
Win32 SDK 是开发 Windows 应用程序最基本的开发工具,也是功能最强大的开发工具,但 Win32 SDK 只提供了 C 语言的 API ,所有的接口均以 C 函数和 C 结构的形式提供,这使得开发应用程序的代码量很大。而且 Windows 应用程序的界面不易实现。
3 、 Win32 SDK 对 COM 的支持
由于 Win32 SDK 是 Windows 最基本的开发工具,而 COM 又是 Windows 系统的基本软件模型,尤其 OLE 完全建立在 COM 的基础上,所以对 COM 的支持也是 Win32 SDK 的一个重要组成部分,在 Win32 SDK 中我们也可以看到 COM 库的所有 API 函数,以及 COM 和 OLE 定义的标准接口。
Win32 SDK 提供的 C/C++ 头文件定义了 COM 和 OLE 标准接口和 API 函数的定义和声明,也提供了实现这些函数的静态连接库。头文件均按照 Microsoft C/C++ 语言标准语法对接口以及 COM 函数或者数据类型进行定义和说明,而且这些数据结构也与 MIDL 相兼容。
4 、 MFC 基础
应用类( CWinApp 的派生类)实例在 MFC 程序全局均可访问,可以通过 MFC 提供的辅助函数 AfxGetApp 得到此实例对象。 CWinApp 类有几个虚函数值得注意: InitInstance 为应用的初始化函数, ExitInstance 为应用的终结处理函数, OnIdle 为消息循环在程序接收到消息的间隙调用的空闲时间的处理函数, Run 为消息循环函数。 CWinApp 中还包含了一个窗口类( CWnd )数据成员 m_pMainWnd ,它代表了应用程序的主窗口,对于不同类型的 MFC 程序,此窗口类也有所不同。由于应用类实例是个全局对象,所以实际上主窗口对象也是个全局生存周期的对象,可以通过对种途径获取,如 AfxGetApp->GetMainWnd() 、 AfxGetApp->m_pMainwnd 或者直接调用 AfxGetMainWnd 辅助函数。
MFC 支持三种 DLL 应用:静态连接 MFC 库的正规 DLL 、动态连接 MFC 库的正规 DLL 和 MFC 扩展 DLL 。 COM 进程内组件程序应该使用前两种 DLL 应用。正规 DLL 是一种标准的 DLL 程序,可以被各种语言编写的程序调用,而 MFC 扩展 DLL 只能被 MFC 程序调用,但这种 DLL 可以直接引出整个 C++ 类,使用 MFC 的 C++ 程序员可以用这种类型的程序提供对 MFC 的扩展库。
5 、 MFC 对 COM 的支持
( 1 )支持 OLE 服务或者包容器的 SDI 应用,更进一步,可以支持 ActiveX 文档服务或者包容器。
( 2 )支持 OLE 服务或者包容器的 MDI 应用,更进一步,可以支持 ActiveX 文档服务或者包容器。
( 3 )支持自动化服务的 SDI 或者 MDI 程序。
( 4 ) ActiveX 控制应用,生成 OCX 应用程序。
我们也可以很方便地产生进程内 COM 组件程序框架,如果要生成进程外 COM 组件程序框架,可以生成一个支持自动化特性的对话框应用程序,然后去掉与对话框有关代码即可。
6 、 MFC 库结构
CObject 是 MFC 库中最基本的类,它提供了一些基本的功能,包括序列化( serialization )支持、运行时刻类信息、对象诊断输出、与集合类型相兼容。运行时刻类型信息是 MFC 对象动态创建的基础,更进一步序列化支持使得对象可以在序列化过程中被动态创建,而且,利用运行时刻类型信息可以在程序运行过程中判断 C++ 对象是否与某个类(通过 IsKingOf 成员函数)相关联。对象诊断输出特性使得应用程序在调试版本中可以获得对象的状态信息,用于检查内存泄漏、运行过程中转储对象内容等操作。
CCmdTarget 类或者其派生类的实例对象拥有自己的消息处理机制,它可以接收消息,并按照它的消息映射表调用相关的消息控制成员函数。而且 CCmdTarget 类提供了对 COM 接口的支持。
在 MFC 库的类中,集合类型也属基本类型之一,常用的集合类型包括两种:数组类型和列表类型,用 CObject 对象作为元素的集合 CObList 和 CObArray 具有一些很好的特性,如序列化、列表诊断信息转储、对象数目自动增长等。
7 、用嵌套类实现 COM 接口
在第二、三章中实现 COM 接口和对象时,一直用 C++ (多)继承的方法实现一个(或多个) COM 接口,用这种方法实现一个或少数几个接口显得非常方便,而且,在 IUnknown::QueryInterface 函数中获取新的接口指针也非常方便,直接把 this 指针转换为相应的接口基类即可,利用 C++ 的 vtable 实现 COM 的 vtable 。 MFC 没有采用这种继承机制来实现 COM 接口,而采用了 C++ 嵌套类的原理实现 COM 接口。用嵌套类实现 COM 对象和 COM 接口比较好地反映了二者之间的关系: COM 接口只是提供接口服务,而 COM 对象本身才保持对象的状态。
8 、接口映射表
MFC 对 COM 的支持从 CCmdTarget 类开始, CCmdTarget 使用了一种与消息映射表非常类似的机制来实现 COM 接口,即接口映射表。接口映射表的基本思想就是使用嵌套类,但它通过一组宏把这些细节隐藏起来了。
实现 COM 接口关键是引用计数和 QueryInterface 函数的实现。 MFC 的引用计数实现方法很简单,在 CCmdTarget 类中使用 m_dwRef 数据成员作为计数器,然后按照 COM 规范维护计数器,所有的接口共享同一个引用计数器。 QureyInterface 函数的实现取决于 COM 接口的实现机制(多继承方式和嵌套类方式)。
接口映射表记录了 CCmdTarget 类中每一个嵌套类的接口 ID 以及接口 vtable 与父类 this 指针之间的偏移量,因为嵌套类的作用域在父类作用域的内部,嵌套类不能直接访问父类的成员, CCmdTarget 利用嵌套类 vtable 与父类 this 之间的偏移量,使嵌套类可以计算出父类的 this 指针,从而访问父类的成员。此偏移量实际上是个常数,用 offsetof 宏可以给出成员与父类之间的偏移量,编译器在编译时刻计算此值。 MFC 使用的接口映射表隐藏了这些细节,使程序编写更加简捷。
宏 BEGIN_INTERFACE_MAP 、 INTERFACE_PART 、 END_INTERFACE_MAP 在 Afxdisp.h 头文件中可以找到。
宏 BEGIN_INTERFACE_PART 、 INIT_INTERFACE_PART 、 END_INTERFACE_PART_STATIC 封装了嵌套类的定义。宏 BEGIN_INTERFACE_PART 定义了接口的前三个成员函数: QueryInterface 、 AddRef 、 Release 。宏 INIT_INTERFACE_PART 中定义了记录偏移量的数据成员 m_nOffset ,并在嵌套类的构造函数中对 m_nOffset 进行初始赋值。这些宏定义在 Afxdisp.h 头文件中可以找到。
9 、接口映射表实现的几个部分
(1) 在 CCmdTarget 类和其派生类定义中使用宏 DECLARE_INTERFACE_MAP 声明接口映射表使用的一些静态成员以及两个成员函数。
(2) 在类的实现部分使用 BEGIN_INTERFACE_MAP 、 INTERFACE_PART 、 END_INTERFACE_MAP 宏定义接口映射表。
(3) 为每个接口定义嵌套类成员。
(4) 实现嵌套类。
10 、 CCmdTarget 类实现 IUnknown
IUnknown 是所有接口的基础, CCmdTarget 类提供了一种标准实现,并且支持对象被聚合的情形,所以 CCmdTarget 类实现了两个版本的 IUnknown 。在 CCmdTarget 实现的两个 IUnknown 中,称为内部 IUnknown 和外部 IUnknown ,分别对应聚合模型中的非委托 IUnknown 和委托 IUnknown 。
CCmdTarget 也可以聚合其他的 COM 对象,此时 m_xInnerUnknown 成员记录了聚合对象的非委托 IUnknown 接口指针。因此利用 CCmdTarget 类构造 COM 对象可以很方便地支持聚合和被聚合两种特性,我们可以直接利用 CCmdTarget 类提供的 IUnknown 实现。
在嵌套内部实现 IUnknown 的三个成员函数时,必须调用父类的外部 IUnknown 的相应成员函数,这是聚合模型所必须要求的。
11 、 COM 引出函数
Visual C++ 的创建向导为 COM 提供了标准的引出函数。如 DllGetClassObject 和 DllCanUnloadNow ,它们是 COM 程序所必须要求实现的;又如 DllRegisterServer 是为了实现注册而提供的可选函数; Visual C++ 没有提供 DllUnregisterServer 函数,如果需要,必须自己实现。
12 、 COM 的类厂实现
MFC 提供了一个通用的类厂类 COleObjectFactory ,值得注意的是类名中“ OleObject ”实际上是指“ ComObject ”。类厂本身也是一个 COM 对象,所以 COleObjectFactory 从 CCmdTarget 派生,并且它实现了 IClassFactory2 接口,除了包含 CreateInstance 和 LockServer 成员函数,还提供了 COM 对象许可证支持。
在 COleObjectFactory 的成员中,最主要的信息是对象的 CLSID 和对象的类型信息,类厂的 CreateInstance 成员函数利用这些类型信息在运行过程动态中创建 COM 对象。类厂的构造函数中包含了 COM 对象的 CLSID 和 COM 对象动态创建类型信息(由 RUNTIME_CLASS 提供)。客户要创建一个 COM 对象必须分两步进行:第一步,客户调用引出函数 SllGetClassObject 得到类厂对象;第二步,类厂对象再创建 COM 对象。
每个 MFC 程序( DLL 和 EXE )都有一个状态结构 AFX_MODULE_STATE ,状态结构中记录了当前程序中应用类对象、程序实例句柄、资源句柄、线程信息等许多全局信息。对于 COM 程序,状态结构也包括一个类厂表,类厂表记录了当前程序所支持的所有类厂对象。所以 DllGetClassObject 函数调用 AfxDllGetClassObject 全局函数, AfxDllGetClassObject 调用 AfxGetClassObject 函数得到全局状态结构,进而取出类厂表,然后检查类厂表中每一个类厂对象所记录的 CLSID ,如果找到客户请求的类厂对象,则返回类厂对象接口即可。在类厂对象的构造函数中,类厂对象把自己加入到状态结构的类厂表中;在析构函数中,它把自己从类厂表中删除,所以类厂表并不需要专门进行维护,其增删操作由类厂对象自己完成。
13 、 MFC 对 COM 支持小结
MFC 对 COM 的支持可以从两个方面进行讨论:单个 COM 对象的实现和类厂的支持。 CCmdTarget 类提供了 COM 对象实现的所有支持,它用接口映射表机制可实现任意多个接口,并且 CCmdTarget 实现的 IUnknown 很好地支持了对象被聚合的情形。 COleObjectFactory 类实现了通用的类厂,它从 CCmdTarget 类派生,并且与 CCmdTarget 类协作,利用 COM 对象提供的 CLSID 和运行时刻类型信息完成对象的创建工作。
进程内组件程序的几个标准引出函数通过程序状态结构中的类厂表与类厂对象联系起来,而类厂对象又与 CCmdTarget 派生类联系起来,因此客户程序和 COM 库可通过这些函数实现对组件程序的各种操作。
COleObjectFactory 也可以用作进程外组件程序的类厂, COleObjectFactory::Register 成员函数负责把类厂注册到全局对象表中,以便客户进程调用类厂对象创建 COM 对象。
14 、 VidualC++ 对 COM 应用的支持
Vidual C++ 支持各种 COM 应用,包括进程内 COM 程序和进程外 COM 程序。对于各种建立在 COM 基础上的应用技术,包括自动化对象、 ActiveX 控制、 OLE 服务程序和 OLE 包容器程序, Vidual Studio 都提供了快速的向导支持;而 ATL 工程可以建立其他一些 COM 对象,如 MTS 对象、 ASP 对象等。
15 、 ATL
ATL ( Active Template Library )是 Vidual C++ 提供的一套基于模板的 C++ 类库,利用这些模板类,可以建立小巧、快速的 COM 组件程序。所以说, ATL 主要是针对 COM 应用开发的,它内部的模板类实现了 COM 的一些基本特征,比如一些基本 COM 接口 IUnknown 、 IClassFactory 、 IDispatch 等,也支持 COM 的一些高级特性,如双接口( dual interface )、连接点( connection point )、 ActiveX 控制等,并且 ATL 建立的 COM 对象支持套间线程和自由线程两种模型。
模板是 C++ 语言的高级语法特性,它是更高层次上的抽象,在使用模板库时,我们不再通过继承的方式使用模板,而是对模板进行实例化以便得到类,可以说模板是对类的抽象,因此,使用和编写模板对程序员的要求更高,尤其要求模板本身的代码必须是类型安全的( type-safe )。
ATL 实现 COM 接口的方式与 MFC 的方式有所不同, MFC 使用嵌套类的方式实现 COM 接口,用接口映射表提供多接口支持; ATL 使用多重继承的方式实现 COM 接口。虽然 MFC 和 ATL 实现接口的原理不一致,但两者的支持形式有非常相似, MFC 采用接口映射表,定义了一组宏; ATL 采用了 COM 映射表,也定义了一组形式上很类似的宏。
ATL 提供了两个基本模板类: CComObjectRootEx 和 CComCoClass ,每一个标准的 COM 对象都必须继承与这两个模板类。 CComObjectRootEx 模板类实现了 IUnknown 成员方法,包括对引用计数的处理以及对接口请求的处理,它也支持对象被聚合的情形; CComCoClass 模板类为对象定义了缺省的类厂,并且支持聚合模型。
16 、 ATL 的 COM 映射表
COM 映射表的原理与 MFC 的接口映射表相仿,也是建立一张静态成员表,表信息记录了每一个接口 IID 所对应的 vtable 与 this 指针的偏移量,但因为 ATL 使用多继承方式,所以偏移量是指基类与派生类指针之间的位置差,并且 ATL 使用了更为灵活的机制,不仅可以指定其偏移量,还可以指定一个函数指针,通过函数间接得到接口 vtable 的地址。
17 、 ATL 的对象映射表
为了支持 COM 对象的创建操作, ATL 好定义了对象映射表。对象映射表是一张全局表,它被放在 ATL 应用的主要文件中。对象映射表中每一项指定了对象的 CLSID 及它的一些创建函数,包括注册表操作函数指针以及类厂的接口指针等信息,全局的 DllGetClassObject 函数利用对象映射表获取类厂对象。
18 、 ActiveX 技术
实际的开发过程中,一般不是对 COM 的直接开发,而是使用更高层的技术,这些技术以 COM 为基础,内容涉及到计算机应用的各个领域,从桌面程序之间的通信到 Internet 应用,从简单的数据处理应用到大型的数据库应用, Microsoft 把所有这些以 COM 为基础的技术统称为 ActiveX 技术。 Visual C++ 是开发 ActiveX 应用最重要的工具, MFC 和 ATL 分别为实现这些应用提供了全面的支持。
19 、 MFC 对自动化的支持
自动化是大多数其他 ActiveX 技术的基础,它可使解释性的宏语言(如 Visual Basic )能够在不了解应用程序实现细节的情况下控制自动化对象。自动化技术直接以 COM 为基础,所有的自动化对象都实现了标准的 IDispatch 接口,通过 IDispatch 接口暴露对象的属性和方法,以便在客户程序中使用这些属性并调用它所支持的方法。自动化对象的客户程序通过类型库( type library )获得对象运行时刻的类型信息,客户程序对对象属性和方法的引用实际上被转化为对 IDispatch::Unvoke 的调用。 MFC 提供的自动化支持分为两个方面:
( 1 ) CCmdTarget 类本身已经提供了自动化支持,其派生类就具有了 IDispatch 接口,并且可以通过 ClassWizard 添加自动化对象的属性和方法。
( 2 )在使用 MFC AppWizard ( .exe )生成 SDI 或 MDI 应用程序时,如果选择了 Automation 支持,则工程的 CDocument 或其派生类支持 IDispatch 接口。
CCmdTarget 类提供了与接口映射表类似的机制实现 IDispatch 对属性和方法的管理,称为分发映射表,分发映射表的原理和用法与接口映射表非常类似。分发映射表不必手工维护。
20 、 MFC 对 ActiveX 控制的支持
MFC 对 ActiveX 控制提供了全面的功能支持,包括基本的自动化属性和方法的支持 、永久数据属性的支持、用户界面实地编辑的支持、激发事件的支持等。 ActiveX 工程中包括一个主要的控制类,它从 COlControl 派生,而 COlControl 继承于 CWnd 和 CCmdTarget ,它继承了 CWnd 的所有窗口特性,又利用 CCmdTarget 提供的接口映射表实现了 ActiveX 控制所要求的所有接口。因此,在派生类中可以不再编写与 COM 有关的代码,而只需按一般的 MFC 窗口类进行编程。
21 、 MFC 对复合文档的支持
复合文档是 OLE 的核心技术,也是 ActiveX 技术的重要部分,但是复合文档技术非常复杂,它本身形成了一套完整的规范,包括复合文档服务程序与包容器程序之间各种交互操作,如界面实地编辑特性、菜单合并标准、数据保存和读取、激活及无效操作等。 MFC 提供了一组类,实现了复合文档服务程序与包容器程序交互过程中的所有特性,使复合文档程序的开发变得非常简单。
从 COM 技术的角度看,实际上复合文档服务程序与包容器程序之间通过一组 COM 接口进行交互,服务程序和包容器程序只需按照复合文档规范分别实现这些接口,二者就可以在运行时刻进行各种交互操作。