COM编程技术基础

ogou421

贡献于2011-05-27

字数:0 关键词:

COM 编程技术基础 一、前言 所谓 COM(Componet Object Model,组件对象模型),是一种说明如何建立可动态互变 组件的规范,此规范提供了为保证能够互操作,客户和组件应遵循的一些二进制和网络标准。 通过这种标准将可以在任意两个组件之间进行通信而不用考虑其所处的操作环境是否相同、 使用的开发语言是否一致以及是否运行于同一台计算机。 显然,在 COM 规范下将能够以高度灵活的编程手段来开发、维护应用程序。可以将一 个单独的复杂程序划分为多个独立的模块进行开发,这 里 的 每 一个独立模块都是一个自给自 足的组件,可以采取不同的开发语言去设计每一个组件。在运行时将这些组件通过接口组装 起来以形成所需要的应用程序。构成应用程序的每一个组件都可以在不影响其他组件的前提 下被升级。这 里 所说的组件是特指在二进制级别上进行集成和重用而能够被独立生产获得和 配置的软件单元。COM 规范所描述的即是如何编写组件,遵循 COM 标准的任何一个组件 都是可以被用来组合成应用程序的。至于对组件采取的是何种编程语言则是无关紧要的,可 以 自由选取。作为一个真正意义上的组件,应具备如下特征: 1) 实现了对开发语言的封装。 2) 以二进制形式发布。 3) 能够在不妨碍已有用户的情况下被升级。 4) 在网络上的位置必须能够被透明的重新分配。 这些特征使 COM 组件具有很好的可重用性,这种可重用性与 DLL 一样都是建立在二 进制基础上的代码重用。但是 COM 在多个方面的表现均要比 DLL 的重用方式好的多。例 如,在 DLL 中存在的函数重名问题、各编译器对 C++函数名称修饰的不兼容问题、路径问 题以及与可执行程序的依赖性问题等在 COM 中通过使用虚函数表、查找注册表等手段均被 很好的解决。其实 COM 组件在发布形式上本身就包扩 DLL,只不过通过制订复杂的 COM 规范,使 COM 本身的机制改变了重用的方法,能够以一种新的方法来利用 DLL 并克服 DLL 本身所固有的一些缺陷,从而实现了更高层次的重用。 客户程序在与 COM 组件进行交互时,只需知道与哪个 COM 对象进行交互即可而不必 关心组件模块的具体名称和位置,即 COM 对象的位置对客户是透明的。客户将通过一个 128 位的全局标识符(globally unique identifier,GUID)完成对象的创建和初始化工作。对于 COM 对象,此 全局标识符也被称作 CLSID(class identifier,类标识符)。 采 用 GUID 对 COM 对象进行标识的目的是为了保证对该对象标识的全球唯一性,因此若用人工构造此 GUID 将 存在与已有 COM 对象的 GUID 发生冲突的可能。通常是采用 VC++附带的两个工具 UUIDGen.exe 和 GUIDGen.exe(如图 1 所示)来根据一定的算法产生出唯一的 GUID 值。 这两个工具可以在 Visual Studio 安装目录下的\Common\Tools\目录下找到。 如果需要在程序中通过代码来获取,也可以使用 COM 库提供的 CoCreateGuid()API 函数。每一个注册了的 COM 对象在系统注册表的 HKEY_CLASSES_ROOT\CLSID 子键下 均对应一个以 CLSID 的字符串形式命名的子键。在此子键下,通过 COM 库可以得到所需 要的信息并完成对象的创建。在 Windows 环境下,除了 CLSID 可以唯一标识一个 COM 对 象外,也支持通过组件对象名对 COM 对象的标识。此标识信息称为 ProgID(program identifier,程序标识符)。通常在以 CLSID 的字符串形式命名的子键下存在有 ProgID 子键, 而在 HKEY_CLASSES_ROOT 键下可以找到以此子键键值命名的子键,该子键下亦包含有 CLSID 子键,通过ProgID 子键的CLSID 值和 CLSID 子键的ProgID 值可以将CLSID 与 ProgID 建立起联系。在程序中也可以通过 CLSIDFromProgID()和 ProgIDFromCLSID()进行相 互转换。 图 1 使用 GUIDGen 创建 GUID 二、COM 接口与 COM 组件 COM 接口是 COM 规范中最重要的部分,COM 规范的核心内容就是对接口的定义,甚 至可以说“在 COM 中接口就是一切”。组件与组件之间、组件与客户之间都要通过接口进行 交互。接口成员函数将负责为客户或其他组件提供服务。 与标识 COM 对象的 CLSID 类似, 每一个 COM 接口也使用一个 GUID 来进行标识,该标识也被称为 IID(interface identifier, 接口标识符)。 COM 接口实际限定了组件与使用该组件的客户程序或其他组件所能进行的交互方式, 任何一个具备相同接口的组件都可对此组件进行相对于其他组件透明的替换。只要接口不发 生变化,就可以在不影响整个由组件构成的系统的情况下自由的更换组件。通 常 在程序设计 阶段需要将接口设计的尽可能完美,以减少在开发阶段对 COM 接口的更改。尽管如此,在 实际应用中是很难做到这一点的,往往需要在现有接口基础上对其做进一步的发展。与 C++ 中对类的继承有些类似,对 COM 接口的发展也可以通过接口继承来实现。但是 COM 接口 的继承只能是单继承而不允许从多个基接口进行派生,而 且派生接口只是继承了对基接口成 员函数的说明而没有继承其实现。 interface IX // IX 接口 { virtual void __stdcall Func1() = 0; virtual void __stdcall Func2() = 0; }; interface IY // IY 接口 { virtual void __stdcall Func3() = 0; virtual void __stdcall Func4() = 0; }; class CObjectA // 组件 A { public: // 抽象基类 IX 的实现 virtual void Func1() {cout<<"Func1"<QueryInterface(IID_IX, (void**)&pIX); if (SUCCEEDED(hResult)) pIX->Func1(); 由于 QueryInterface()过于灵活,为避免由此引发的冲突在 COM 规范中定义了 QueryInterface()所有实现都必须遵循的一些规则: (1)过同一对象各个接口指针所查询得到的 IUnknown 接口指针必须是指向同一个 IUnknown 接口的。即,IUnknow 接口的唯一性。 (2)如 果 某 接口曾经被成功查询过,那么此后任何时间对该接口的查询也必定会成功。 即,接口与查询时间的无关性。 (3)对于已经获取到的接口仍可对其进行再次查询,并且必定会成功。即,接口的自 反性。 (4)客户能够从任何接口查询到另外一个接口,而且能够返回到起始接口。即,接口 的对称性。 (5)如果能够从某接口获取到某特定接口,那么从任意接口都可以得到此接口。即, 接口的传递性。 IUnknown 接口的另两个成员函数 AddRef()和 Release()对对象的生存期进行了控 制。每个 COM 对象都记录有一个引用计数,该引用计数表示了当前引用了此 COM 对象的 有效指针的个数。AddRef()和 Release()实现的即是这种引用计数的内存管理技术:引 用计数初始为 0,客户每得到一个指向此对象的接口指针即通过 AddRef()将 引 用 计 数 加 1 ; 在 每 用 完 此 接口指针后,调用 Release()函数将引用计数减 1。如果引用计数减到 0,则从 内存卸载掉此 COM 对象。关于引用计数的使用,在 COM 规范中也设置了以下几条简单的 规则: (1)任何能够返回接口指针的函数(如 QreryInterface()、 CreateInstance()等)在 返回接口指针之前,必须用相应的指针调用 AddRef()函数。 (2)在使用完任何一个接口后,应及时调用该接口的 Release()函数。 (3)在进行接口指针赋值操作后,应调用 AddRef()函数。 COM 组件的创建可以通过 CoCreateInstance()函数来完成,函数原型为: HRESULT __stdcall CoCreateInstace( const CLSID& clsid, IUnknown* pIUnknownOuter, DWORD dwClsContext, const IID& iid, void** ppv ); 函数参数 clsid 是要创建组件的 CLSID,pIUnknownOuter 用于聚合组件,如果不使用可 以设置为 NULL。参数 dwClsContext 则限定了所创建组件的执行上下文。最后两个参数 iid 和 ppv 则分别为要使用接口的 IID 和返回得到的接口指针。在使用时只需将 CLSID、IID 等 作为参数传入即可创建相应的组件并从输出参数 ppv 得到所请求接口的指针。如 果 函数是直 接创建组件的,那么在函数返回时组件将创建完毕,这 样 客户将无法对组件的创建过程进行 任何干预,灵活性太差。因此,CoCreateInstance()在函数内部实现中通过调用 CoGetClassObject()函数先创建一种专门用来创建组件的组件来解决此问题。这种用途的 组件被称为类厂(class factory)。 类 厂 所 支持的用以创建组件的接口是 IClassFactory,该接口从 IUnknown 派生,并具有 两个自己的接口成员函数 CreateInstance()和 LockServer()。这两个成员函数分别用于创 建 COM 组件对象和控制组件的生存期。下面先给出 CreateInstance()的函数声明: HRESULT __stdcall CreateInstance(IUnknown* pIUnknownOuter, const IID& iid, void** ppv); 可以看出,这个用于创建组件对象的 CreateInstance()函数并未包含一个用来接受 CLSID 的参数,显然该函数将只能创建同某个 CLSID 相应的组件。对于一个类厂,由于只 能通过 CreateInstance()函数去创建组件,因此只能创建与某个特定 CLSID 相应的组件。 创建类厂的 CoGetClassObject()函数将接收一个 CLSID 作为参数并返回指向类厂对 象 IClassFactory 接口的指针。客户将可以通过此指针来创建所需要的组件并返回某接口的 指针。通过此指针,客户将可以直接调用新创建的 COM 对象接口的成员函数,从而获得 COM 对象的所有服务。 在用 CoGetClassObject()创建类厂对象时,如果 COM 对象是进程内组件(组件与客 户处于同一进程地址空间,通常多以 DLL 形式存在), CoGetClassObject()将调用 DLL 模 块的 DllGetClassObject()引出函数并把 clsid、iid 和 ppv 等参数传递进去以创建类厂,并 返回类厂对象的接口指针。 如果 COM 对象是进程外组件(拥有独立的进程地址空间,通常多以 EXE 形式存在), 则 CoGetClassObject ()将要首先启动组件进程,并一直等待到组件进程通过 CoRegisterClassObject()函数将类厂注册到 COM 后,才会返回 COM 中相应的类厂信息。 一旦组件进程退出,此注册的类厂对象也就不再有效,需调用 CoRevokeClassObject()函 数予以通知。图 4 展示了通过类厂创建组件的过程: 客户程序对 COM 组件的调用主要分对进程内组件调用和进程外调用两种情况。在 具 体 过程上却并没有什么太大的区别。为了能够使用 COM 库提供的 API 函数,首先要用 CoInitialize()初始化 COM 库。 图 4 组件的创建过程 虽然通过 CLSID 和 ProgID 都可以标识一个组件,但 ProgID 显然要比 CLSID 更易于理 解和使用,因此通常很少直接使用 CLSID,而是通过使用 CLSIDFromProgID(),根据 ProgID 得到组件的 CLSID。进而以此返回的 CLSID 作为参数去调用 CoGetClassObject()以创建 类厂对象并返回类厂接口指针。通过该指针调用类厂对象的 CreateInstance()接口成员函 数,执行结果将创建与 CLSID 相应的组件对象并返回 IUnknown 接口指针。通过此接口的 QueryInterface()成员函数将能够进一步获过程将是隐含进行的,使用更为简单。 取组件的其他接口指针,从而使用组件提供的各种服务。 最后,通过 Release()函数释放接口指针。如 果 使用的进程内组件,在 调 用 CoUninitialize ()函数释放 COM 库资源之前,应首先调用 CoFreeUnusedLibraries()将其从内存卸载。 由于在 CoCreateInstance()函数内部实现了对 CoGetClassObject()的调用并一直完成了类 厂对象接口函数对组件的创建和类厂对象的释放,因此对于客户,类厂的全部使用。 在 ActiveX 文档服务器中的 IOleDocument 接口使一个文档对象能够与其包容器进行通 信,并用其数据去创建视图,该接口也可以使一个文档对象能够枚举其视图并为包容器提供 相关信息,如是否支持多视等。 IOleDocumentView 接口则使一个包容器程序能够通过文档对象的支持而与每一个视图 进行通信。 IOleCommandTarget 接口可以使服务器对象及其包容器程序分发命令。 IPrint 接口则可以使任意的复合文档和特定的活动文档能够支持打印。在 ActiveX 文档 包容器中实现的 IOleDocumentSite 接口能够使一个已经作为文档对象实现的文档在现场激 活对象时绕过通常的激活次序,并直接指示其客户站点作为一个文档对象而将其激活。具有 这种能力的客户站点也被称为文档站点。包容器程序需要为每一个文档对象提供一个相关的 文档站点,这些站点对象为每一个活动文档的视图实现了一个独立的文档视图站点对象。 相比之下,ActiveX 控件可以说是在所有 COM 应用中使用最为广泛的一种 COM 组件。 这种 COM 组件集成了 COM 的各种应用基础,如 OLE 文档、自动化、类型库等。ActiveX 控件通常以 DLL 或 OCX 形式存在,而且只能在包容器程序中使用而不可独立运行,这与 ActiveX 文档是不一样的。 ActiveX 控件是一种实现了一系列特定接口而使其在使用和外观上更象一个控件的 COM 组件。ActiveX 控件这种技术涉及到了几乎所有的 COM 和 OLE 的技术精华,如可链 接对象、统一数据传输、OLE 文档、属性页、永久存储以及 OLE 自动化等。 ActiveX 控件作为基本的界面单元,必须拥有自己的属性和方法以适合不同特点的程序 和向包容器程序提供功能服务,其属性和方法均由自动化服务的 IDispatch 接口来支持。除 了属性和方法外,ActiveX 控件还具有区别于自动化服务的一种特性--事件。事件指的是从 控件发送给其包容程序的一种通知。与窗口控件通过发送消息通知其拥有者类似,ActiveX 控件是通过触发事件来通知其包容器的。事件的触发通常是通过控件包容器提供的 IDispatch 接口来调用自动化对象的方法来实现的。在 设计 ActiveX 控件时就应当考虑控件可能会发生 哪些事件以及包容器程序将会对其中的哪些事件感兴趣并将这些事件包含进来。 ActiveX 控件与自动化服务的另一个不同之处在于其方法、属性和事件均有自定义 (custom)和库存(stock)这两种不同的类型。自定义的方法和属性也就是是普通的自动化 方法和属性,自定义事件则是自己选取名字和 Dispatch ID 的事件。而所谓的库存方法、属 性和事件则是使用了 ActiveX 控件规定了名字和 Dispatch ID 的"标准"方法、属性和事件。 样例目标 欲给一个公司做一个信息管理系统,也就是公司中所有部门的信息可以被输入电脑,并 可进行分布式查询,即总经理可随时查询最新的订单情况和出货情况。 由于使用 COM 作为此信息管理系统的基架,可以很容易的解决分布式问题,并且由于 COM 对安全的包装,使得提供访问控制也变得容易。下面先说明 COM 提供的编程思想, 再以此编程思想设计各接口。 三、COM 编程模型 见过不少这种说法:“COM 是更加地面向对象,封装地更彻底”。这里要纠正这种错误 的思想,虽然可以说对,但是是错误的应用。这就好像牛刀可以杀鸡,但并不应该被说对。 面向对象编程思想是一种思想,指导如何设计程序架构的。其 主打思想就是将被操作数 据看成一个个对象。而所谓的对象就是具有状态,并对外提供了接口以暴露其可以提供的服 务。其是状态和功能通过语义的混合体。 其和日常生活很像,比如电视机就既提供了服务——搜台,又提供了状态——哪个频道 是哪个台。因此在使用面向对象编程思想时会从对象的概念出发来定义数据结构,这和 COM 完全不一样。 COM 叫做组件对象模型,从名字看其异常明显地表示了最开始引号中的话的正确性, 这是个误解。COM 最突出的贡献不是组件这个概念,而是接口。 接口表示功能的集合,其不是状态。与面向对象正好相反,其完全不看重对象的实现, 甚至淡化对象这个概念,极力强调接口的概念,这在各本 COM 教科书中表现地很明显—— 里面第一个讲的就是 IUnknown 接口,极力强调没有对象指针,只有接口指针。 这看起来有点混乱,如果认为面向对象强调的是状态和功能的混合,COM 强调的就是 功能集的集合。而类就是只实现了一个接口的 COM 组件(不包括 IUnknown),这从根本上 说是 COM 的退化。因此当设计中的每个 COM 组件都只实现一个接口时,此时根本不是设 计一个 COM 应用,只是在二进制代码级上应用 C++提供的编程思想而已。 由于 COM 做地并不是那么好,以至于会产生前面所说的误解。其强调功能的概念没有 体现出来,而 更表现为组件,以 至于很容易认为组件是积木,而 整 个程序就是用不同的组件 搭建的房子。这是对象级上的模块化编程。COM 不会设计到最后反而跑回老路上去。 搭积木的重点是积木,是以积木来搭建房屋。而 COM 提供的并不是积木,是积木间衔 接的形状,它主张在搭积木前先搭一个架子,不同的积木能放到架子上形成的不同的格子里, 架子搭好后再根据架子上形成的格子的形状做积木,最后将积木放到架子上。而不是先做积 木,然后根据积木搭房屋(这个比喻并不是非常准确)。 思 考这个问题:欲实现任务和任务管理器的功能,设计两个接口 ITask 和 ITaskManager, 考虑 ITask 的功能定义。其代表的是能够作用于任务上的功能,不是任务本身,因此其有如 下两个方法:TerminateTask 和 GetProcessRateOfTask 以分别终止任务和得到任务进度。但是 很明显,任务是需要启动的。如果按照面向对象的思想,在不考虑设计模式的情况下,很容 易想到将任务的发起这个动作作为 ITask 中的一个方法:StartTask,这样 ITask 的实现者就 是一个完全的任务,如 果 使用线程进行任务操作,其 也就连那个线程的操作也一起包装起来, 形成一个任务。这不是一个好的设计,ITask 是个接口,代表的是功能,不是对象。接口以 为实现它的对象就可以照其定义进行操作,因此 ITask 的实现者是可以被相当于任务一样的 操作,而不是任务这个东西。前者具有更好的可扩展性,如可以通过按遥控器来操作东西, 但那个东西不一定必须是电视机,而后者就一定要求其是电视机。 因此 COM 里重点的不是组件,而是接口,这是一种可扩展性相当好的设计思想,可以 称做面向接口编程思想。它本身是没有什么缺点的,但其实现方式由于使用对象的概念,则 一定和状态关联,这在数据量很大时是不好的。如 订单 会 很容易地就被设计成一个类,然后 提供诸如订单结帐、提货等多种服务(即成员函数)。这里的问题就是订单如此之多,如果 使用一个数组作为其容器显然性地问题严重,而 链 表更是应该判死刑。因此这里将订单设计 成一个类是很不明智的选择。对 于此,应 该 专门仔细研究如何处理大数据量的技术,并将功 能与状态拆开,然后数据变成原材料,而功能变成机器,通过流水线生产以提高效率。即面 向对象是个人主义,当数据量大时,就需要分工合作来提高效率了。对于此,Microsoft 早 已提供了 MTS 来帮助开发,其中提供的编程思想就是专门针对这种大数据量而设计的,提 倡无状态组件,即状态和功能的分离,其对于开发大数据量的应用提供了非常好的支持。 前面已经说明了 COM 提供的编程模型,下面就本样例说明如何设计接口。程序员考虑 最多的事应该是如何偷懒,并且美其名曰“代码重用”,但现在又被更好听的名词所替代 ——“具有可扩展性”。样例是一个公司的信息管理系统,里面人事部门的信息处理和营销部 门的将会千差万别,信 息有 完全不同的流程。因此是肯定需要一个一个编的。但它们能够被 称做部门,就一定有共通的地方,这 正是程序员最厉害的地方——归纳能力,然后推演出其 他东西以达到偷懒的目的。 照前面的说法,由于数据量巨大,因此决定选择数据库而不是建立对象。由于各个部门 没有什么同一性(其实还是有的,后叙), 最 后 认 为 唯 一相同的是同属一个公司旗下,故决 定提供一个基本框架界面,其提供最基本的如错误处理、日志记录等功能,欲通过在同一个 基架下以表现得各部门在同一公司旗下。 基本框架需要提供错误日志的记录以方便系统的维护和查错;需要提供界面框架以容纳 不同的部门组件的操作界面,即需要提供菜单命令的提供,也出于 Windows 界面的想法而 提供工具条和快捷键;需要提供任务管理器,因为在海量的数据中查找那么一两条信息不是 瞬间的事,因此可能总经理发起了一个人事查找后,又发起一个订单查找或客户查找,但却 由于等得不耐烦而终止了前面的人事查找;需要提供数据库系统的相关信息,以使得部门组 件可以将数据存储到统一的地方,方便备份等管理。 部门组件需要提供界面以进行信息操作(如录入、查找等),作为 Windows 界面,常规 性地需要提供菜单、工具条的维护性操作(如命令的说明字符串的提供);需要提供任务执 行进度,以提高操作者的忍耐限度。 经过上面的决定,基本框架应有 4 个接口,而部门组件应有 2 个接口。但请注意,一个 接口表示一个功能集合,如 果 一个组件实现了一个接口就表示其所有功能都实现了,但 COM 非常可惜地提供了 E_NOTIMPL 这个错误代码,因此导致了错误的接口设计——里面的方 法可以有未实现的。这个错误代码准确的说应该是为将来扩展而预留的,即方法中的某个参 数代表功能的种类,如是画圆形还是画矩形,但其可以指定为画椭圆形,这种形状暂时不支 持,但相信以后版本将会支持,这才是 E_NOTIMPL 的真正含义,却被错误的应用了,比 如: 上面提到的部门组件应该支持一个接口,其提供包含部门组件界面、菜单、工具条和快 捷键的提供。完全有可能一个部门组件不使用工具条进行操作,完全使用一个对话视搞定一 切,那么当可怜的基本框架错误地以为其需要工具条的空闲处理而调用了相应方法时却得到 一个 E_NOTIMPL 时,应该怎样?因此应该将一定会同时存在的功能归为一个接口,因此 出于上面的考虑,应该再提供三个接口:快捷键处理、菜单处理、工具条处理。由于快捷键 没有处理,只是获得即可,不像 菜 单 还需 提供菜单状态的处理等操作,所以无须快捷键处理 的接口,因此部门组件应该具有 4 个接口,其中三个是可选的。 上面的接口分工显得有些牵强,不过这只是粒度粗细的问题。如 果 愿意 粗粒度,也可以 说成基本框架只需一个接口,如果要细粒度,也可再定个工具条处理接口和菜单处理接口, 这里就是见仁见智的地方了。但还是建议至少要保证接口中的方法如果实现一个,则其他的 逻辑上也都需实现。最后其 IDL 定义文件如下: import "oaidl.idl"; import "ocidl.idl"; // 基本框架实现 IModuleSite,其提供基本的操作 [ object, uuid(1A201ABA-A669-4ac7-9DF8-2DA772E927FC), pointer_default(unique) ] interface IModuleSite : IUnknown { // 供部门组件改变当前显示模块,如点击了营销模块中的订单查找结果中的 // 办理人字段后自动跳转到人事模块中显示办理人的相关信息 HRESULT ChangeModule( [in] REFCLSID clsid, // 模块的 CLSID // 模块名字,仅用于提示 [in, string] WCHAR *pModuleName, // 模块命令,指明欲让模块执行的命令,由模块解释 [in] ULONG command, [in] ULONG param ); // 模块命令的相关参数 HRESULT GetFrameWindow( [out] HWND *pHwnd ); // 返回主框架窗口 }; // 基本框架实现 IErrorReport,其提供报告错误的功能 [ object, uuid(1A201ABA-A669-4ac7-9DF9-2DA772E927FC), pointer_default(unique) ] interface IErrorReport: IUnknown { // 报告温和型错误,相当于警告 // fileName 代表源代码文件的名字,row 代表错误所在行 HRESULT ReportSoftError( [in, string] WCHAR *fileName, [in] ULONG row, [in, string] WCHAR *errorString ); // 报告暴力型错误,相当于错误 HRESULT ReportHardError( [in, string] WCHAR *fileName, [in] ULONG row, [in, string] WCHAR *errorString ); } // 基本框架实现 ICompanyInfo,其提供数据库服务器信息 [ object, uuid(1A201ABA-A669-4ac7-9DFA-2DA772E927FC), pointer_default(unique) ] interface ICompanyInfo: IUnknown { // 返回数据库服务器的相关信息,主机 IP、服务器名字及密码 HRESULT GetDataServerInfo( [in, string] WCHAR *loaction, [in, string] WCHAR *server, [in, string] WCHAR *password ); } // 基本框架实现 ITaskManager,其提供任务的操作 interface ITask; [ object, uuid(1A201ABA-A669-4ac7-9DFB-2DA772E927FC), pointer_default(unique) ] interface ITaskManager: IUnknown { // 添加任务 HRESULT AddTask( [in, string] WCHAR *taskString, // 任务说明字符串 [in] ITask *pTask, // 任务的指针 // 返回标识一个任务的 cookie [out] DWORD* pCookie ); }; // 基本框架实现 ITaskNotify,其提供任务的通知 [ object, uuid(1A201ABA-A669-4ac7-9DFC-2DA772E927FC), pointer_default(unique) ] interface ITaskNotify: IUnknown { // 通知指定任务的进度已经变化 HRESULT ProcessRateChange( [in] DWORD cookie ); // 通知任务已经结束 HRESULT TaskOver( [in] DWORD cookie ); }; // 部门组件必须实现 IModule,其提供模块的操作 [ object, uuid(1A201ABA-A669-4ac7-9DFD-2DA772E927FC), pointer_default(unique) ] interface IModule: IUnknown { // 初始化模块,nID 为模块窗口的子窗口 ID HRESULT InitialModule( [in] IModuleSite *pSite, [in] UINT nID ); // 返回模块的图标 HRESULT GetIcon( [out] HICON *pHicon ); // 返回模块的名字 HRESULT GetName( [out, string] WCHAR **pName ); }; // 部门组件不一定实现 IModuleCommand,其提供执行模块所特有的命令 [ object, uuid(1A201ABA-A669-4ac7-9DFE-2DA772E927FC), pointer_default(unique) ] interface IModuleCommand: IUnknown { HRESULT DoCommand( [in] ULONG command, [in] DWORD param ); }; // 部门组件不一定实现 IModuleNotify,其对模块提供一个通知途径 [ object, uuid(1A201ABA-A669-4ac7-9DFF-2DA772E927FC), pointer_default(unique) ] interface IModuleNotify: IUnknown { HRESULT OnActivate(); // 模块切换时被激活 HRESULT OnDeActivate(); // 模块切换时取消激活 }; // 部门组件必须实现 IModuleUI,其提供模块界面的相关操作 [ object, uuid(1A201ABA-A669-4ac7-9E00-2DA772E927FC), pointer_default(unique) ] interface IModuleUI: IUnknown { // 返回模块的主要窗口 HRESULT GetMainWindow( [out] HWND *pHwnd ); // 翻译快捷键 HRESULT TranslateAccelerator( [in] MSG *pMsg ); }; // 部门组件不一定实现 IMenuUdpate,其提供模块界面中菜单的相关操作 [ object, uuid(1A201ABA-A669-4ac7-9E01-2DA772E927FC), pointer_default(unique) ] interface IMenuUpdate: IUnknown { HRESULT GetMenu( [out] HMENU *pHmenu ); HRESULT GetMenuItemString( [in] ULONG nID, [out, string] WCHAR **pString ); }; // 当部门组件创建了一个任务时,任务对象必须实现 ITask 以进行相应的任务管理 [ object, uuid(1A201ABA-A669-4ac7-9E02-2DA772E927FC), pointer_default(unique) ] interface ITask: IUnknown { // 返回任务的进度 HRESULT GetProcessRateOfTask( [out] float *pRate ); HRESULT TerminateTask(); // 终止任务 // 将任务和任务管理器绑定起来 HRESULT SetTaskSite( [in] ITaskManager *pManager, [in] DWORD cookie ); }; [ uuid(1A201ABA-A669-4ac7-9D00-2DA772E927FC), version(1.0), helpstring("ExampleBase 1.0 TypeLib") ] library ExampleBaseLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); interface IModuleSite; interface IErrorReport; interface ICompanyInfo; interface ITaskManager; interface IModule; interface IModuleCommand; interface IModuleNotify; interface IModuleUI; interface IMenuUpdate; interface ITask; interface ITaskNotify; }; 上面的设计有个很明显的问题就是并没有体现组件的特性,只是很简单的部门组件和基 本框架的组合,部门组件不能再有什么其他作为,是一种变相的 DLL 技术。这是样例的目 标及特点(各部门完全不一样的信息处理方式)决定的,就是一个插件接口。基本框架相当 于一个播放器,而 部 门 组件相当于一种音效处理插件。由于这只是个简单的例子,无法表现 出 COM 组件特性的优点,但就此样例给出线程模型的例子已经是足够了。 如果每个部门组件都只是信息录入、信 息 查询 和信息管理(忽略其业务流程,如 订单需 要和出货联系起来), 则 可以使用另一种功能分割方式,即信息表现的接口、录入信息的接 口、查询信息的接口和管理信息的接口(甚至还可以抽象出业务进而形成业务接口),这种 方案将体现出组件的概念,但复杂程度亦增加了不少,因为其灵活性大大高于前一种方案。 由于添加工具条的支持需要更多的代码,并且对本样例没有什么意义,故本样例中没有 提供对工具条的支持。 作为一种习惯,将工程中所有的接口定义在一个.idl 文件中,然后再专门定一个项目生 成其代理/占位组件,并导出 IID 等这类全局变量以供以后的使用,并且可以将类型信息一 起加入其中,以减少最终完成中的文件数量。 在前面的文章中我们介绍了 COM 接口及其与 COM 组件的关系,在这一节中我将向大 家介绍 COM 组件的可重用性。 四、包容与聚合 与所有面向对象的系统一样,COM 组件的可重用性是其很重要的一个特性。与 C++类 在原代码级别的重用不同,COM 组件的重用是建立在对二进制代码重用的基础上的。具体 包括包容(containment)和聚合(aggregation)两种重用模型。这两种重用机制非常相似, 其本质也都是在一个组件中对另外一个组件的使用。 在包容机制中,外部组件除了实现自己的接口外,还包含了指向内部组件所有接口的指 针,使 内部组件接口相对于外部组件的客户是不可见的,只有通过外部组件提供的接口才能 间接完成对内部组件接口的调用,并以此实现对已有组件的重用。由于包容机制为内部组件 接口提供了外部接口实现,因此可以通过在外部接口添加适当的代码以完成与被重用组件所 提供服务类似的功能。这有些类似于对 C++类虚函数的重载。 图 6 包容与聚合重用模型 聚合机制的本质其实就是包容,只不过是其一个特例而已。采用聚合机制的组件并没有 实现用于转发给内部组件接口的接口,而是直接将客户发出的对内部组件接口的请求直接传 递给内部组件的接口,使其直接暴露于外部组件的客户。但是客户在请求到此接口指针并对 其接口进行调用时,仍不会意识到被重用组件的存在。由于外部组件对内部组件的重用只是 通过传递对接口的请求而将被请求接口暴露于客户,因此只能实现与被重用组件所提供服务 完全一样的重用功能。与包容不同,并不是所有的组件都能够支持聚合。至于在重用时是采 取包容机制还是聚合机制,关键在于要实现的功能与待重用的组件所提供服务是类似还是完 全一致。 客户端使用 MFC 实现,其中的框架类 CMainFrame 实现了 IModuleSite、IErrorReport 和 ICompanyInfo,而 另 一个窗口包装类 CTaskManager 实现 ITaskManager,并由 CMainFrame 聚 合 它 以 表 现 出 CMainFrame 实现了 ITaskManager 。 由于代码较长,本篇只罗列 CMainFrame::OnCreate 和 CMainFrame 的定义,其 中 实现了获取部门组件的实例(通过 COM 的组件类别功能进行记录,而非通过注册表), 并 进行管理。 CMainFrame 的定义: #include "NewStatusBar.h" class CMainFrame : public CFrameWnd { // MFC 定义宏 DECLARE_DYNCREATE( CMainFrame ) DECLARE_MESSAGE_MAP() DECLARE_INTERFACE_MAP() // 辅助结构 private: struct TEMPSTRUCT { HWND m_hWnd; HMENU m_hMenu; IModule *m_pModule; CLSID m_CLSID; TEMPSTRUCT() : m_hWnd( NULL ), m_hMenu( NULL ), m_pModule( NULL ) { // 什么都不做 } ~TEMPSTRUCT() { SafeRelease( m_pModule ); } }; // 构造、析构 public: CMainFrame(); ~CMainFrame(); // 操作 public: void UpdateErrorState() // 更新状态条上的错误标志以表示指示最新错误的发生 { ASSERT_VALID( this ); if( m_wndStatusBar.GetSafeHwnd() ) m_wndStatusBar.Invalidate(); } // 辅助函数 protected: // 根据菜单项 ID 确定是否是基本框架的菜单命令 BOOL BeMenuOfBase( DWORD nID ) const; // 成员变量 protected: CNewStatusBar m_wndStatusBar; CToolBar m_wndToolBar; DWORD m_Selected; long m_OldViewID; IUnknown *m_pTaskManager; CTypedPtrList< CPtrList, TEMPSTRUCT* > m_ModuleList; // 重载 protected: void GetMessageString( UINT nID, CString &rMessage ) const; BOOL OnCommand( WPARAM wParam, LPARAM lParam ); // 窗口消息 protected: afx_msg int OnCreate( LPCREATESTRUCT lpCreateStruct ); afx_msg void OnActivate( UINT nState, CWnd* pWndOther, BOOL bMinimized ); afx_msg void OnSetFocus( CWnd *pOldWnd ); afx_msg void OnInitMenuPopup( CMenu *pPopupMenu, UINT nIndex, BOOL bSysMenu ); afx_msg void OnInitMenu( CMenu *pMenu ); afx_msg BOOL OnEraseBkgnd( CDC *pDC ); afx_msg void OnClose(); // 菜单消息 protected: void OnModule( UINT nID ); void OnUpdateModule( CCmdUI *pCmdUI ); // 自定消息 protected: afx_msg LRESULT OnAllTaskTerminated( WPARAM, LPARAM ); // 接口映射 public: // IModuleSite BEGIN_INTERFACE_PART( ModuleSite, IModuleSite ) INIT_INTERFACE_PART( CMainFrame, ModuleSite ) STDMETHOD(ChangeModule)( REFCLSID clsid, WCHAR *pModuleName, ULONG command, ULONG param ); STDMETHOD(GetFrameWindow)( HWND *pHwnd ); END_INTERFACE_PART_STATIC( ModuleSite ) // IErrorReport BEGIN_INTERFACE_PART( ErrorReport, IErrorReport ) INIT_INTERFACE_PART( CMainFrame, ErrorReport ) STDMETHOD(ReportSoftError)( WCHAR *fileName, ULONG row, WCHAR *errorString ); STDMETHOD(ReportHardError)( WCHAR *fileName, ULONG row, WCHAR *errorString ); END_INTERFACE_PART_STATIC( ErrorReport ) // ICompanyInfo BEGIN_INTERFACE_PART( CompanyInfo, ICompanyInfo ) INIT_INTERFACE_PART( CMainFrame, CompanyInfo ) STDMETHOD(GetDataServerInfo)( WCHAR **pLoaction, WCHAR **pServer, WCHAR **pPassword ); END_INTERFACE_PART_STATIC( CompanyInfo ) }; CMainFrame::OnCreate 代码 int CMainFrame::OnCreate( LPCREATESTRUCT lpCreateStruct ) { if( CFrameWnd::OnCreate( lpCreateStruct ) == -1 ) return -1; // 创建基本工具条及状态条 EnableDocking( CBRS_ALIGN_ANY ); if( !m_wndToolBar.CreateEx( this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC, CRect( 0, 0, 0, 0 ) ) ) WriteSoftErrorLog( __LINE__, __WFILE__,IDS_MAINFRM_CREATETOOLBAR ); else { m_wndToolBar.EnableDocking( CBRS_ALIGN_ANY ); DockControlBar( &m_wndToolBar ); } if( !m_wndStatusBar.Create( this ) ||!m_wndStatusBar.SetIndicators( indicators, sizeof( indicators ) / sizeof( UINT ) ) ) { WriteSoftErrorLog( __LINE__,__WFILE__,IDS_MAINFRM_CREATESTATUSBAR ); return -1; } else { CCmdTarget *pTarget = m_wndStatusBar.Initial(); if( !pTarget ) { WriteSoftErrorLog( __LINE__,__WFILE__,IDS_MAINFRM_CREATESTATUSBAR ); return -1; } ASSERT_VALID( pTarget ); // 设置任务管理器的指针以使得 CMainFrame 聚合 CTaskManager pTarget->m_pOuterUnknown = GetControllingUnknown(); m_pTaskManager = reinterpret_cast< IUnknown* >( &pTarget->m_xInnerUnknown ); ASSERT( m_pTaskManager ); } // 创建组件类别管理器并获得模块 CLSID 枚举器 IEnumCLSID *pEnum = NULL; ICatInformation *pCat = NULL; CATID tempCATID[1] = { CATID_Example }; if( FAILED( CoCreateInstance( CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_ICatInformation, reinterpret_cast< void** >( &pCat ) ) ) || FAILED( pCat->EnumClassesOfCategories( 1, tempCATID, 0, NULL, &pEnum ) ) ) { MessageBox( L"致命错误!系统即将退出。", g_SysCaption ); WriteHardErrorLog( __LINE__,__WFILE__,IDS_KILLINGERROR ); SafeRelease( pCat ); SafeRelease( pEnum ); return -1; } pCat->Release(); // 枚举每个模块信息 g_theApp.BeginWaitCursor(); CLSID clsid; CStringW temp; TEMPSTRUCT *pStruct = NULL; IModuleUI *pUI = NULL; IMenuUpdate *pMenuUp = NULL; long index = -1; IModuleSite *pSite = static_cast< IModuleSite* >( GetInterface( &IID_IModuleSite ) ); pSite->AddRef(); while( pEnum->Next( 1, &clsid, NULL ) == S_OK ) { ++index; pStruct = new TEMPSTRUCT; if( !pStruct ) { WriteSoftErrorLog( __LINE__,__WFILE__,IDS_OUTOFMEMORY ); continue; } // 创建模块对象 if( FAILED( CoCreateInstance( clsid,NULL,CLSCTX_INPROC_SERVER,IID_IModule, reinterpret_cast< void** >(&pStruct->m_pModule ) ) ) ) { WriteSoftErrorLog( __LINE__,__WFILE__,IDS_MAINFRM_CREATEMODULE ); delete pStruct; continue; } pStruct->m_CLSID = clsid; // 初始化模块 if(FAILED( pStruct->m_pModule->InitialModule(pSite,IDC_MODULE_VIEW +index–1 ) ) ) { WriteSoftErrorLog( __LINE__,__WFILE__,IDS_MAINFRM_CREATEMODULE ); delete pStruct; continue; } // 获取模块窗口 if( FAILED( pStruct->m_pModule->QueryInterface(IID_IModuleUI,reinterpret_cast< void** >( &pUI ) ) ) ) { WriteSoftErrorLog( __LINE__,__WFILE__,IDS_MAINFRM_GETMODULEWND ); delete pStruct; continue; } if( FAILED( pUI->GetMainWindow( &pStruct->m_hWnd ) ) || !pStruct->m_hWnd ) { WriteSoftErrorLog( __LINE__,__WFILE__,IDS_MAINFRM_GETMODULEWND ); pUI->Release(); delete pStruct; continue; } pUI->Release(); // 修改窗口风格,确保模块窗口具有 WS_EX_CLIENTEDGE 风格 if( !::SetWindowLong( pStruct->m_hWnd,GWL_EXSTYLE,::GetWindowLong( pStruct->m_hW nd,GWL_EXSTYLE ) | WS_EX_CLIENTEDGE ) ) { WriteSoftErrorLog( __LINE__,__WFILE__,IDS_SETWINDOWSTYLE ); } // 隐藏模块窗口 ::ShowWindow( pStruct->m_hWnd, SW_HIDE ); // 获取模块的菜单句柄,并插入到 MainFrame if( SUCCEEDED( pStruct->m_pModule->QueryInterface(IID_IMenuUpdate, reinterpret_cast< void** >( &pMenuUp ) ) ) ) { // 模块提供了菜单,获取菜单句柄 if( FAILED( pMenuUp->GetMenu( &pStruct->m_hMenu ) ) ) WriteSoftErrorLog( __LINE__,__WFILE__,IDS_MAINFRM_CREATEMODULEMENU ); pMenuUp->Release(); } m_ModuleList.AddTail( pStruct ); } pSite->Release(); pEnum->Release(); // 一个模块都不激活 m_Selected = static_cast< ULONG >( -1 ); ULONG count = m_ModuleList.GetCount(); if( count ) { // 获取模块的图标,并将其插入到模块工具条和菜单中 m_wndToolBar.SetSizes( CSize( 23, 22 ), CSize( 16, 15 ) ); if( !m_wndToolBar.SetButtons( NULL, count ) ) WriteSoftErrorLog( __LINE__,__WFILE__,IDS_CREATETOOLBARBUTTON ); else { // 将图标画到位图中 CBitmap bitmap; CDC *pScreen = CDC::FromHandle( ::GetDC( NULL ) ); CDC sdc; if(!bitmap.CreateCompatibleBitmap(pScreen,ICONWIDTH*count,ICONHEIGHT ) ||!sdc.CreateCompatibleDC( pScreen ) ) { WriteSoftErrorLog( __LINE__,__WFILE__,L"创建缓冲位图失败!" ); return 0; } sdc.SelectObject( &bitmap ); sdc.FillSolidRect( 0, 0, ICONWIDTH * count, ICONHEIGHT, RGB( 0xC0, 0xC0, 0xC0 ) ); HICON hIcon = NULL; ULONG i = 0; POSITION pos = m_ModuleList.GetHeadPosition(); CMenu *pMenu = GetMenu()->GetSubMenu(0)->GetSubMenu(0); ASSERT_VALID( pMenu ); // 清空原来的占位符 while( pMenu->DeleteMenu( 0, MF_BYPOSITION ) ); while( pos ) { pStruct = m_ModuleList.GetNext( pos ); ASSERT( pStruct ); // 将图标画到位图上 if( SUCCEEDED( pStruct->m_pModule->GetIcon( &hIcon ) ) ) ::DrawIconEx(sdc.GetSafeHdc(),i*ICONWIDTH,0,hIcon,ICONWIDTH, ICONHEIGHT,0,NULL,DI_NORMAL ); else WriteSoftErrorLog( __LINE__,__WFILE__,IDS_MAINFRM_GETMODULEICON ); // 添加菜单 { // 获取模块的名字 WCHAR *io = NULL; if( FAILED( pStruct->m_pModule->GetName( &io ) ) ) WriteSoftErrorLog( __LINE__,__WFILE__,IDS_MAINFRM_GETMODULENAME ); else { if( !pMenu->AppendMenu( MF_STRING, ID_MODULE + i, io ) ) WriteSoftErrorLog( __LINE__,__WFILE__,IDS_MAINFRM_CREATEMODULEMENU ); ::CoTaskMemFree( io ); } } // 设置工具条上的按钮 m_wndToolBar.SetButtonInfo( i, ID_MODULE + i, TBBS_BUTTON, i ); i++; } // 将位图设置到工具条中 if( !m_wndToolBar.SetBitmap(static_cast< HBITMAP >( bitmap.Detach() ) ) ) WriteSoftErrorLog( __LINE__,__WFILE__,IDS_SETTOOLBARBITMAP ); } } g_theApp.EndWaitCursor(); return 0; }

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享文档获得金币 ] 3 人已下载

下载文档

相关文档