編譯/趙湘寧
原著: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使編程更輕鬆。你只要聲明執行個體就行了。(待續)