zoukankan      html  css  js  c++  java
  • COM编程入门 第三部分(网文补充)

    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安装目录下的CommonTools目录下找到。 
    如果需要在程序中通过代码来获取,也可以使用COM库提供的CoCreateGuid()API函数。每一个注册了的COM对象在系统注册表的HKEY_CLASSES_ROOTCLSID子键下均对应一个以CLSID的字符串形式命名的子键。在此子键下,通过COM库可以得到所需要的信息并完成对象的创建。在Windows环境下,除了CLSID可以唯一标识一个COM对象外,也支持通过组件对象名对COM对象的标识。此标识信息称为ProgID(program identifier,程序标识符)。通常在以CLSID的字符串形式命名的子键下存在有ProgID子键,而在HKEY_CLASSES_ROOT键下可以找到以此子键键值命名的子键,该子键下亦包含有CLSID子键,通过ProgID子键的CLSID值和CLSID子键的ProgID值可以将CLSID与ProgID建立起联系。在程序中也可以通过CLSIDFromProgID()和ProgIDFromCLSID()进行相互转换。

    二、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"<<endl;};     
       virtual void Func2() {cout<<"Func2"<<endl;};     
    // 抽象基类IY的实现 virtual void Func3() {cout<<"Func3"<<endl;}; virtual void Func4() {cout<<"Func4"<<endl;}; };

    对于接口,通常是采用抽象基类来定义,并利用类的多重继承来实现该组件。例如,在上面这段代码中,IX和IY是用于实现接口的抽象基类。所谓的抽象基类是只包含一个或多个虚函数声明而未包括虚函数的具体实现的类。抽象基类不能被实例化,而只能用作基类使用,并要求其派生类完成其所有虚函数的实现。在上面这段代码中,CObjectA组件即继承了IX和IY这两个抽象基类,并实现了其所定义的虚函数。图2为此组件具有的这两个接口的模型展示:

    接口模型: 

    抽象基类本身由于没有实体函数与变量,所以并不分配内存。通常只是用来为派生类指定内存结构。只有在派生类实现此抽象基类时,指定的内存才会被分配。图3为此内存结构的示意:

    抽象基类定义的内存结构示意:

    图中vtable为虚拟函数表,能够为实例数据的提供一个方便保存的位置,并能够在同一类的多个实例间共享。在每个实例的内存映射中均包含一个指向该类的vtable表的指针pVtable。pVtable指针存放于所有数据成员之前,由于每个虚函数在vtable表中有唯一的索引,编译器只需根据索引从vtable表中找到函数地址即可。也就是说,客户只要获取得到了接口指针,就可以使用此COM对象的实际功能。 
    由抽象基类指定的内存结构是符合COM规范的,因此抽象基类IX可以认为是一个COM接口,但这还不是一个严格意义上的COM接口。对于一个真正意义上的COM接口,在设计时应遵循以下几个规则:

    1) 接口必须直接或间接地从IUnknown继承。 

    2) 接口必须具有唯一的标识(IID)。 
    3) 一旦分配和公布了IID,有关接口定义的任何因素都不能被改变。

     4) 接口成员函数应具有HRESULT类型的返回值。 

    5) 接口成员函数的字符串参数应采用Unicode类型。 
      这几条规则中,最基本的是第一条,如果一个对象没有至少实现一个最小程度为IUnknown的接口,那么该对象也就不是一个严格的COM对象。IUnknown接口是COM的核心接口,从上述规则可以得知,任何一个COM接口都必须从IUnknown接口继承。客户在组件之间的通信是通过接口来实现的。组件可以不提供其他接口,但是必须提供IUnknown接口以使客户能够对组件其他接口进行查询。 
      IUnknown接口提供有成员函数QueryInterface()、AddRef()和Release(),分别用于查询组件中的其他接口和进行生存期控制。由于任何COM接口都是从IUnknown接口派生,因此在所有COM接口虚拟函数表中保存的前三个成员函数指针一定是指向QueryInterface()、AddRef()和Release()的指针。这样,任何一个COM接口都可以被当作IUnknown接口来处理。在创建组件时,客户可以通过CreateInstance()函数得到IUnknown接口指针。 
      COM规范允许使用多接口,QueryInterface()成员函数可以用来查询组件是否支持某个特定的接口。如果支持,QueryInterface()将返回此接口的指针。其第一个参数为一个IID结构,指出了客户所要查询的接口,查询到的接口指针将存放在ppv所指向的变量中。函数的成功执行与否将返回S_OK或E_NOINTERFACE。但是,在使用时不能简单的将QueryInterface()返回值与其进行比较,而应使用SUCCEEDED或FAILED宏。例如: 

    IUnknow* pI = CreateInstance(); IX* pIX = NULL; 
    HRESULT hResult = pI->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库。

      虽然通过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++类虚函数的重载。

      聚合机制的本质其实就是包容,只不过是其一个特例而已。采用聚合机制的组件并没有实现用于转发给内部组件接口的接口,而是直接将客户发出的对内部组件接口的请求直接传递给内部组件的接口,使其直接暴露于外部组件的客户。但是客户在请求到此接口指针并对其接口进行调用时,仍不会意识到被重用组件的存在。由于外部组件对内部组件的重用只是通过传递对接口的请求而将被请求接口暴露于客户,因此只能实现与被重用组件所提供服务完全一样的重用功能。与包容不同,并不是所有的组件都能够支持聚合。至于在重用时是采取包容机制还是聚合机制,关键在于要实现的功能与待重用的组件所提供服务是类似还是完全一致。 
      客户端使用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_hWnd,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; }
  • 相关阅读:
    Java面试
    md5加密
    CSS3画苹果手机
    CSS3的表格布局 文字居中 圆角
    CSS3的新特性 行内盒子before和after
    DIV CSS Sprites精灵 CSS图像拼合 CSS背景贴图定位教程案例
    DAY30
    DedeCMS织梦修改数据库密码和数据库连接失败解决方法
    学习计划
    【原】雅虎前端优化的35条军规
  • 原文地址:https://www.cnblogs.com/dayw/p/3285553.html
Copyright © 2011-2022 走看看