编译/赵湘宁
原著:Paul Dilascia
MSJ November 1999 & December 1999
关键字:Bands 对象,Desk Bands,Info/Comm Bands,Explorer Bar,Tool Bands。
本文假设你熟悉C++,COM,IE。
下载本文源代码: MyBands.zip (128KB)
TestEditSrch.zip (75KB)
第一部分:Band 对象介绍
第二部分:BandObj的类层次和MyBands服务程序的注册
第三部分:深入Band内部,揭开Band的面纱
第四部分:Band对象使用中遇到的一些问题
第五部分 建立自己的COM编程平台ComToys
概要:
可重用类就像小巧的 COM 积木一样,人们可以在不同场合以不同方式装配这些 COM 来创建更加精致的对象。但是如何创建这些 COM 呢?本文使用 MyBands 和 BandObj框架作为实验平台开发了一个可重用库:它就是 COMToys......。COMToys提供了一种用C++编写COM的方法或途径,这些方法和途径适用于任何类型的COM对象,不论你是使用MFC。还是其它的什么东西。COMToys是一种态度——它告诉人们用C++编写COM组件并不难,情况也确实是这样!
前面我们讨论了一个叫MyBands的程序,这个程序有功能之一就是可以将编辑框控制放到Windows的任务栏中。此外,MyBands实现了三种Windows的Band对象,其中就有Web搜索框,如图一所示:
图一 任务栏中的Web搜索框
为了实现MyBands程序,我编写了一个迷你型Band对象框架:BandObj,其对应的实现类是CBandObjDll,CBandObjFactory,和CBandObj。这个框架提供了编写各种Band对象所需要的支持。利用这个Band对象框架,你不必做太多的工作就能开发出满足自己需要的Band应用。
CBandObj中有许多代码实现不同的接口像IDeskBand,IOleWindow,IContextMenu等等,但只有一个接口与Band对象的处理有关,即IDeskBand接口。其它的代码都与此无关。CBandObj中IContextMenu的实现没有涉及到Band对象的任何东西;它只需要一个菜单。而IOleWindow只要一个窗口句柄。还有其它的一些实现情况也一样。CBandObj框架尽可能地抽象这些接口并将它们封装在可重用类中——像小巧的COM积木,你可以在不同场合以不同方式装配这些COM来创建更加精致的对象。那么如何创建这些小巧的COM积木呢?
本文将向你展示如何利用MyBands 和 BandObj框架作为测试平台来开发一个库,这个库的源代码本文的例子程序:COMToys。COMToys并不像ATL或者MFC那样与系统融为一体,它是一些更特别的宏、函数、类以及我认为能轻松编写BandObj的东西的集合。但COMToys提供了一种用C++编写COM的方法或途径,它适用于任何类型的COM对象,不论你是使用MFC。还是其它的什么东西。COMToys是一种态度——它告诉人们用C++编写COM组件并不难,情况也确实是这样!
COM:C++编程的困境
自从有了COM,我不得不同情可怜的使用Java语言和Visual Basic语言的C++程序员。你会看到编写"form.color = red"的程序员那得意的笑容,而C++黑客们正焦急的发出指令——QueryInterface…Get()…Set()——以及随时提醒自己不要忘了检查HRESULT!如果没有对每一个AddRef调用Release,那就叫上帝帮你吧!哈哈哈……在Visual Basic中,你不用记住要敲入分号。与Visual Basic和Java语言比起来,C++更强大,更有倾向性,但一涉及到COM,其复杂性似乎让人感到无望。这对COM是专为C++而设计是一个多么大的讽刺啊!毕竟,COM对象只是一个C++虚表(vtbl)。
问题并不是C++,也不是COM。而是大多数C++程序员对它们的了解还不够。它们从SDK的例子中剪切、粘贴代码——这些公开的长长的代码只是用于示范最原始、最无遮掩的编程方法,从来没有考虑如何面向系统设计。兄弟,那些东西不是产品代码!不知你想过没有,为什么把它们叫做例子?C++的优点是让你编写重量级的程序。但没有人能天天忍受那种编程虐待。如果你想容易,你就可以容易。只要你先花点时间建立一些工具,这些工具以后将会成倍地回报你的。Visual Basic和Java语言之所以容易是因为考虑了将基础结构内建在语言系统中。但你也可以编写自己的基础结构来使得C++易于使用啊。
前面所讨论的MyBands程序有两层结构,底层是框架BandObj,上面是应用MyBands。这两层结构是出于示范目的人为创建的。在实际编程中,我用的是三层结构来建立BandObj框架,如图十六所示。此结构的基础由COMToys提供。从图中可以看出,只有把COMToys移走之后才能聚焦在Band对象上。现在就让我们来揭示这个系统吧。
图十六 MyBands的体系结构
我在设计MyBands/BandObj/COMToys的时候,必须做出一个今天许多程序员都要面对的决定:即使用什么编程系统来编写COM?是用ATL?,MFC?,COM+?,或者以上的都不用?尽管我常常喜欢卖弄一下自己,但是从来不拒绝使用别人做好的东西,只要它能工作并且容易使用。所以我考虑ATL和MFC两者都用。
ATL口碑很好并且易于使用,但谁能搞懂所有的关于它的术语和那些尖括弧呢?你真知道ATL产生了多少代码吗?如果这些代码这么适于在模板中通用,那为什么不使用一个简单的类或子例程取而代之呢?一点都没错,模板能将很多东西参数化,这一特性当然很棒。但我只想开一个杂货店,没有必要也不需要借助航天飞机。更重要的是,ATL缺乏图形用户界面的支持,而像Band对象这样的外壳扩展需要GUI支持。话又说回来,ATL的简单易用,多继承模型及智能指针的诱惑让人无法拒绝,就象前面文章中曾用到的ATL的注册器,它让我爱不释手。
MFC又怎么样呢?它的GUI的诱惑力是不可抗拒的。即便你不需要文档/视图结构,它还有命令处理例程和ON_COMMAND_UPDATE_UI处理器——此乃任何UI对象之根本。但只要涉及到COM,MFC便逊色多多。大多数人都是担心它那大象般体积的DLL,但依我之见,那没什么了不起,不错,MFC42.DLL的确肥大,但是没有了它,Windows能转起来吗?它是OS的一部分,Windows中到处到有它的影子。(事实上,看一下Windows 98 的CAB文件就知道,这个肥大的东西就藏在在win98_62.cab文件中,所以说它是Windows的一部分。)自从有了COM以后,MFC越来越成问题:其嵌套类的使用完全失去了个性。稍后还要详细描述这方面的问题。
那么到底使用什么呢?最终,我采各家所长建立了自己的系统,同时使用MFC和ATL。这样就成全了我的工作,并充分利用了MFC和ATL各自的优点,MFC和ATL之间达到了取长补短的效果。COMToys利用MFC的类工厂和IUnknown,但绕开了嵌套类。同时还利用了ATL的智能指针,注册器和多继承,而过滤出了厚重的模板及易造成混淆的对象模型。这种混合方法不仅实用,而且还很发烧(加上是自己写代码,从而总是能更好地理解代码)。
我并不是说COMToys怎么怎么好,只是提供一种思路和想法,希望能抛砖引玉。主要目的是展示完全不必专门使用MFC和ATL,或其它系统就可以编写自己的COM对象创建平台。告诉你如何通过建立一个基本结构框架,然后用C++轻松进行COM编程——甚至比Visual Basic还容易!
粗糙的嵌套类
这是专家的评价,让我们看看代码。为了解释COMToys的工作原理,我要做的第一件事情是说明COMToys被设计用来解决的问题之一:避开MFC使用的嵌套类。为此让我们先简单回顾一下MFC/COM编程的基本概念。
为了用MFC写一个COM,要从CCmdTarget派生自己的类并使用宏实现自己的接口。例如,如果你的类实现IPersistFile,那下面是必须要写的代码:
// 在头文件中
class MyComClass : public CCmdTarget {
BEGIN_INTERFACE_PART(PersistFile, IPersistFile)
STDMETHODIMP GetClassID(LPCLSID pClsID);
STDMETHODIMP IsDirty(void);
……
END_INTERFACE_PART(PersistFile)
};
这些宏在主类中声明一个嵌套类:MyComClass::XPersistFile,并声明一个实例:m_xPersistFile。MyComClass::XPersistFile实现IPersistFile的方法,包括从IUnknown继承的方法。为了让MFC知道这个接口,必须创建一个接口映射:
// 在 .cpp 文件中
BEGIN_INTERFACE_MAP(CMyComClass, CCmdTarget)
INTERFACE_PART(CMyComClass, IID_IPersistFile, PersistFile)
……
END_INTERFACE_MAP()
这个宏为你的类中每个COM接口产生一个细目表。每一个细目表的表项存储接口的IID以及在实现它的嵌套类主类中的偏移量
{ &IID_IPersistFile, offsetof(CMyComClass, m_xPersistFile) }
一旦你声明并实现了这个接口映射,就必须实现它们的方法,包括AddRef,Release,和QueryInterface。因为这个类是嵌套的,必须为COM对象支持的每一个接口编写IUnknown,即使实现都相同。例如:
// AdRef 和Release的代码相同
STDMETHODIMP
CMyComClass::XPersistFile::QueryInterface(...)
{
METHOD_PROLOGUE(CMyComClass, PersistFile)
return pThis->ExternalQueryInterface(...);
}
METHOD_PROLOGUE在MFC中是必不可少的,用以获得它们之间的关系(在任何DLL的入口点,AFX_MANAGE_STATE是不可缺少的)并设置pThis,它指向父类CMyComClass("this"指针地址减去嵌套的偏移量)。CCmdTarget::ExternalQueryInterface搜索你的接口映射查找与请求的接口匹配的IID条目。如果找到,则将偏移量添加到这个指针并返回结果——如果一切正常,它指向实现接口的嵌套对象。这种方法能行得通,但是极其麻烦。 首先,在实现每一个接口的IUnknown时就非常的别扭,它开启了出错的大门,而且因为复制不必要的代码而变得臃肿不堪。 第二,你不能从接口方法中直接访问你的类成员;而是必须通过pThis来存取它们——这简直就是故弄玄虚;从概念上讲,接口方法属于外部类,所以为什么不能像访问其它成员那样访问它的成员呢? 最后,也是最让人讨厌的一点,嵌套类方式无法让你在派生类中重载接口方法。假设你派生一个新类,CMyComClass2,并且你只想重载IPersistFile::SaveCompleted以便设置一个标志,ON_UPDATE_COMMAND_UI处理器将检查这个标志并显示"正在存储….",直到完成存储。你不用做什么。CMyComClass没有可重载的SaveCompleted函数。实现SaveCompleted的类被嵌套在其中,CMyComClass::XPersistFile,没有办法重载它的方法。除了要重载SaveCompleted以外,你还必须在派生类中重新实现整个IPersistFile接口,创建另一个嵌套类,其中包含什么也不做的方法。只是调用一下基类方法,CMyComClass::m_xPersistFile。 我在前面文章的代码中碰到了这个问题。当容器设置现场时,为了让CBandObj派生类做些事情,我不得不提供一个新的虚函数,CBandObj::OnSetSite,并从CBandObj::XDeskBand::SetSite中调用它。其它接口方法如何呢?我是不是要为CBandObj实现的每一个接口方法引入类似OnXxx的东西?我不想这样! 出于这些原因,许多C++程序员——包括ATL代码编写者——都采用多继承进行COM编程。从每个实现的接口派生多个自己的COM类。
class CMyComClass :
public IPersistFile,
public IContextMenu, ...
{
// IUnknown
STDMETHOD_(ULONG, AddRef)();
……
// IPersistFile
STDMETHODIMP GetClassID(LPCLSID pClsID);
……
// IContextMenu
STDMETHOD (QueryContextMenu)(...);
……
};
所有方法属于主类,因此你可以用通常的方式重载它们,而且所有的方法都能直接存取类成员;不需要使用pThis。然后利用C++的魔力,只要实现一次AddRef,Release和 QueryInterface即可,并且同样的实现适用于所有的IUnknown实例。这恰恰是因为C++的规则使然,"纯粹的虚函数总是通过实现它的任何子类进行重定义的。"所有虚表(vtbls)都将有IUnknown的位置,在这个位置指向相同的物理函数。这对于其它可能被多继承的接口也一样;例如,IPersist从IPersistFile 和IPersistStream继承而来。如图十七所示,我准备称它为魔力MI法则。
图十七 多继承
既然多继承如此之好,为什么MFC不用它呢?因为一碰到具体的类——那些有真实函数和数据的类时——多继承便引起混淆。如果你写x = m_foo,这里的 m_foo是个从A继承还是从B继承的呢?而且MFC从CObject派生了它的所有类,用多继承会导致可怕的菱形层次。虽然可以用虚基类来克服这种不足,但事情会更糟。所以创建MFC的那位哥们儿聪明地决定避开多继承。(待续)