编写可复用性更好的C++代码——Band对象和COMToys(二)

来源:互联网 发布:e。target js 编辑:程序博客网 时间:2024/05/17 03:35

编译/赵湘宁

原著: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服务程序的注册


BandObj的类层次结构

    阅读了第一部分内容以后,现在你应该有点明白如何用BandObj来编写band对象了(定义一个GUID,然后调用AddBandClass),下面我们进一步深入BandObj,揭示其工作原理。从BandObj中我们可以看到它涉及三个类:CBandObjDll, CBandObjFactory, 和 CBandObj,这三个类与MFC之间的关系如图五所示:

图五

    CBandObjFactory是创建CBandObj对象的类工厂。通常不必直接使用这个类。当你从InitInstance函数中调用AddBandClass时,CBandObjDll将创建一个新的类工厂,并将这个类工厂添加到一个列表中:

BOOL CBandObjDll::AddBandClass(...){   CBandObjFactory* pFact =      OnCreateFactory(...);   pFact->m_pNextBandFact =      m_pBandFactories;   m_pBandFactories = pFact;   return TRUE;} OnCreateFactory 是一个虚函数,仅仅返回一个新的类工厂。CBandObjFactory*  CBandObjDll::OnCreateFactory(...){   return new CBandObjFactory(...);}
    我在这里提供OnCreateFactory的目的是一旦你想要派生自己专用的工厂类的话,就可以从CBandObjFactory派生并重载OnCreateFactory,让BandObj使用它。为清晰起见,我省略了参数,这里的参数与传递到AddBandClass的参数一样:类ID,MFC运行时类,种类ID和资源ID。CBandObjFactory传递头两个参数到MFC,后两个参数是自己用的。
CBandObjFactory::CBandObjFactory(REFCLSID clsid,    CRuntimeClass* pClass,   const CATID& catid, UINT nIDRes)   :  COleObjectFactory(clsid, pClass, FALSE, NULL){   m_catid = catid;   m_nIDRes = nIDRes;}      
    BandObj并不像通常的方式处理类工厂,一般在MFC中编写COM对象时,都使用DECLARE_OLECREATE 和IMPLEMENT_OLECREATE,它把COleObjectFactory创建成一个静态对象。使用这些宏的问题之一是它们将类COleObjectFactory写死在代码中了,这样你就无法使用其它的类,问题之二是它们将类工厂创建成静态数据,再一次将CYourClass::命名的类工厂代码写死了。所以说,何必非得用这些宏呢?之所以提供它们是出于方便。如果想在堆中而不是在栈中创建自己的类工厂并使用某些其它类的话,这样做是有好处的。只要我从COleObjectFactory派生,当COM调用DLL创建对象时,每一个对象工厂类COleObjectFactory会将自己添加到某个MFC搜索的主列表中,这样MFC也就会在创建对象时找到这个类。通过在堆中动态创建类工厂,从而BandObj能在程序员视野中隐藏起来。

MyBands服务程序的注册

    从技术上讲,现在还是没有完全弄清楚band对象的全貌,但是不要着急,下面我们来研究一下注册问题。注册就像呼吸之于鼻子一样,在COM中不可或缺。下面是Web搜索框在我的机器上的注册表中的注册入口:
HKEY_CLASSES_ROOT   CLSID     {4647E383-520B-11d2-A0D0-004033D0645D} = "&Web 搜索框"       InprocServer32 = MyBands.dll         ThreadingModel=Apartment       Implemented Categories         {00021492-0000-0000-C000-000000000046}      
前面的三个注册内容是所有进程内COM服务器都有的:
CLSID--band对象类的ID,
InprocServer32--指进程内服务器,必须是DLL
ThreadingModel--线程模型,
最后一个注册条目是我们在前面曾经提过的种类ID:Categories。一般来说,某个COM对象声明其种类时都是将其列在HKCR/CLSID/guid/Implemented Categories入口。
COM对象的注册和注销要通过regsvr32.exe程序实现。当你使用下面的命令时:
regsvr32.exe MyBands.dll      //注册regsvr32.exe  /u MyBands.dll      //注销    
regsvr32调用专门的入口DllRegisterServer来注册MyBands。如果使用 /u 参数,它就调用DllUnregisterServer来注销MyBands。BandObj.cpp文件中提供了它们的缺省标准实现,同时实现的还有DllGetClassObject 和 DllCanUnloadNow。这些缺省的实现调用专门的MFC函数完成相应的工作,如:
STDAPI DllRegisterServer(){   AFX_MANAGE_STATE(AfxGetStaticModuleState());   return COleObjectFactory::UpdateRegistryAll(TRUE) ? S_OK : SELFREG_E_CLASS;}
    COleObjectFactory::UpdateRegistryAll遍历所有类工厂并对每一个类工厂调用UpdateRegistry(TRUE)。
DllUnregisterServer也一样,只是它用FALSE来调用MFC函数。COleObjectFactory::UpdateRegistry函数很好地实现了注册控制,但它不识别band的种类,所以还必须一些编写代码来做这件事情。

简化繁琐的注册工作

    如果你曾经用Windows的API来注册组件对象的话,那你肯定知道那有多痛苦。RegCreateKey, RegSetValue, RegClose. . .我在刚开始接触这些东西的时候完全被搞蒙了。所幸的是还有一个更好的方法实现这册,那就是用ATL的注册器(Registrar)。它是我最喜欢的一个ATL特性之一。有了它,COM对象的注册易如反掌。它的功能有点像REGEDIT,允许加载专门的.RGS脚本(类似于.REG文件),利用脚本不仅能添加,还能删除注册入口。实际上只要给定某个脚本,注册器就(或多或少)知道如何注销(unregister)。也就是说注册和注销可以使用相同的脚本,这真是太帅了,因为注销组件的工作往往被大多数程序员忽略(太懒的缘故),能自动注销就不至于污染注册表。
使用ATL的注册器很容易。例如:
CComPtr<IRegistrar>ireg; ireg.CoCreateInstance(CLSID_Registrar,                        NULL, CLSCTX_INPROC); ireg->FileRegister("foo.rgs");      
    这段代码第一行中的尖括弧很有意思,它是一个ATL的智能指针(下文将要讨论)。CoCreateInstance创建一个注册器对象,然后直接使用它即可。但是我对尖括弧有点神经质,讨厌在代码中看到这种尖括弧,除非必须用它来做比较和转换。所以我在COMToys中写了一个小类 CIRegistrar 来进一步简化它,并且隐藏了不顺眼的尖括弧,这个类实际上是封装了ATL智能指针:
CTRegistrar r; r->FileRegister("foo.rgs");      
   这样一来,你只要实例化CTRegistrar,由构造函数调用CoCreateInstance,FileRegister("foo.rgs")方法的调用不变。IRegistrar具备从脚本文件--甚至是资源--进行注册和注销的能力,IRegistrar的所有方法都在atliface.h文件中。CBandObjFactory::UpdateRegistry中有一个通用实现负责查找与类工厂有相同ID的注册资源并调用注册器加载它。
BOOL CBandObjFactory::UpdateRegistry(BOOL bRegister){   static const LPOLESTR RT_REGISTRY =      OLESTR("REGISTRY");   UINT nID = GetResourceID();   if (!::FindResource(AfxGetResourceHandle(),     MAKEINTRESOURCE(nID), CString(RT_REGISTRY)))     return FALSE;   CTRegistrar iReg;   OnInitRegistryVariables(iReg); // see below   LPOLESTR lposModuleName = /* get module pathname */   HRESULT hr = bRegister ?     iReg->ResourceRegister(lposModuleName, nID,        RT_REGISTRY) :     iReg->ResourceUnregister(lposModuleName, nID,        RT_REGISTRY);   return SUCCEEDED(hr);}      
    这里使用了典型的MFC处理资源的方法,BandObj很好地利用了资源IDs。对注册和注销自己的band对象要做的全部工作就是写一个注册脚本--而不必写任何代码!并且在代码中只涉及一个函数。实际上,你甚至都不用写注册脚本,因为BandObj例子程序已经写好了一个脚本文件,它适用于任何band对象。直接使用它即可。
// 例子中把脚本都放在了应用程序的资源中MyBands.rc IDR_INFOBAND REGISTRY DISCARDABLE "BandObj.rgs" IDR_COMMBAND REGISTRY DISCARDABLE "BandObj.rgs" IDR_DESKBAND REGISTRY DISCARDABLE "BandObj.rgs"      
   不过,三个不同的COM对象怎么可能使用相同的注册脚本呢?它们不是有不同的名字和类IDs吗? 这就是IRegistrar的好处之所在。看一下注册脚本BandObj.rgs。到处是%CLSID%, %ClassName%, 和 %MODULE% ,这些标志是什么意思呢? 这些都是变量。在处理脚本之前,注册器会用实际的值(类ID,类名和模块名)替代这些变量。那它怎么知道使用什么值呢?因为你告诉它了--或者说是BandObj告诉它了。你可能注意到了在UpdateRegistry中有一个对OnInitRegistryVariables的调用。就是在这个地方,BandObj定义了它的变量。
BOOL CBandObjFactory::OnInitRegistryVariables(IRegistrar* pReg){     USES_CONVERSION;     pReg->AddReplacement(OLESTR("CLSID"),  StringFromCLSID(m_clsid));     pReg->AddReplacement(OLESTR("MODULE"), T2OLE(GetModuleName()));     pReg->AddReplacement(OLESTR("ClassName"), T2OLE(GetClassName()));     return TRUE;}      
下面是我在CBandObjFactory中建立的全部变量列表,它们都自动由BandObj定义。
%CLSID%     = class ID (GUID)(COleObjectFactory::m_clsid)%MODULE%    = DLL的全路径名%Title%     = 标题(资源子串 0)%ClassName% = 人可读的COM类名 (资源子串 1)%ProgID%    = ProgID (资源子串 2)
    要想添加自己的变量,如%TimeStamp% 或者 %MyReleaseVersion%,,只要派生一个新类厂并重载OnInitRegistryVariables就可以了。不要忘了调用基类,因为MFC为每一个类厂调用UpdateRegistry,而对每个类而言,变量都被重新初始化。所以在MyBands中,第一个类厂的%CLSID% 是 CLSID_MYINFOBAND,第二个类厂的%CLSID% 是 CLSID_ MYCOMMBAND,而第三个类厂的%CLSID% 是 CLSID_MYDESKBAND。同一个脚本处理三种对象的注册,酷毙了!。
    IRegistrar这么酷,所以我写了自己的命令行实用程序 RGSRUN来加载RGS文件,它对于测试和调试脚本或者从注册表种删除多余的垃圾都非常有用,有些功能是REGEDIT所没有的。这个实用程序与本文的例子一起提供。我在autoexec.bat文件中使用RGSRUN加载一个文件:autoexec.rgs,其中设置了不同的Explorer选项,Windows每次启动都会自动完成加载。

种类注册

    如果你仔细阅读本文,就会注意到前面提到的脚本文件中没有关于band对象种类注册的内容。为什么呢?BandObj在哪里注册它的种类信息呢?我用了另外一个变量来做这件事情,它就是%catid%,而COM库中也总是有接口应用于此,种类也不例外。正式的种类注册方法是通过ICatRegister实现的:
BOOL CBandObjFactory::UpdateRegistry(BOOL bReg){   ......   // 使用ICatRegister 注册/注销种类   CTCatRegister iCat;   REFIID clsid = m_clsid;   hr = bRegister ?     iCat->RegisterClassImplCategories(clsid, 1,       &m_catid) :     iCat->UnRegisterClassImplCategories(clsid, 1,       &m_catid);     // 返回,旁路掉MFC     return hr==S_OK; }     
由此可见,使用ATL智能指针,COMToys类以及CTCatRegister使编程更轻松。你只要声明实例就行了。(待续)
原创粉丝点击