編寫可複用性更好的C++代碼——Band對象和COMToys(九)

來源:互聯網
上載者:User

編譯/趙湘寧

原著: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
第六部分:設計和構造COMToys
第七部分:類的實現
第八部分:類廠和註冊以及美中不足

第九部分 且看看COMToys的更多特性以及與MFC的比較

COMToys的更多特性

除了主要的功能以外,COMToys還有一些其它特性。

智能指標
    ATL的智能指標是我不得不讚揚的一個東西。如果你到目前為止還沒用使用過智能指標的話,那麼就趕快終結這個時代吧!我的意思是,終止對AddRef 和 Release的一切操心。不管你的函數有多少退出路徑,智能指標都能保證所有的引用計數正確無誤。免得你殫精竭慮,費盡心思和功夫來尋找引用計數的bugs。有過這種經曆的兄弟都知道,那是一個可怕的噩夢:


圖十九 Aaaaaaaah

對於那些一見到模板就往後退的傢伙來說,COMToys有一個宏隱藏了那些尖括弧:

#define DECLARE_SMARTPTR(ifacename) /
typedef CComQIPtr SP##ifacename;

現在你可以寫:

DECLARE_SMARTPTR(IPersist);      

    為了定義新類型SPIPersist。以便在任何出現IPersist的地方都能使用這個新類型。SPIUnknown是個專門派生的,因為它需要CComPtr,而不是CComQIPtr。(沒有必要為IUnknown調用QueryInterface——每個介面都具備)。下面的程式碼片段是一些典型的使用和沒有使用智能指標的COM代碼,通過比較,相信不再需要更多的解釋: 

 ///////////////////////////////////////////////////////////////////////
// 簡化後的智能指標代碼:
// 典型的沒有用智能指標實現的 COM 函數,當有多個退出路徑時,這種做法很糟糕。
//
//
STDMETHODIMP CExplorerBar::SetSite(IUnknown* punkSite)
{
// If a site is being held, release it.
if(m_pSite) {
m_pSite->Release();
m_pSite = NULL;
}
// If punkSite is not NULL, a new site is being set.
if (punkSite) {

......

// Get and keep the IInputObjectSite pointer.
if (SUCCEEDED(punkSite->QueryInterface(IID_IInputObjectSite,
(LPVOID*)&m_pSite))) {
return S_OK;
}
return E_FAIL;
}
return S_OK;
}

/////////////////////////////////////////////////////////////////////////////////////
// 用智能指標實現的相同的 COM 函數,從 12 行代碼減少到 2 行。
// 因為 m_pSite 被聲明為 CComQIPtr,沒有必要調用 QueryInterface(IID_IInputObjectSite),
// 也沒有任何必要調用 AddRef 累加新的指標或調用Release釋放舊的指標。智能指標全都搞掂
//
STDMETHODIMP CBandObj::SetSite(IUnknown* punkSite)
{
if (m_pSite = punkSite) {
......
}
return punkSite && !m_pSite ? E_FAIL : S_OK;
}

    有關ATL智能指標更深層次的探討請參考另外一篇專題文章:“用ATL建立輕量級的COM對象”。這篇文章還描述了智能指標可能導致麻煩的罕見情形。 多線程 COMToys並不能實現所有的COM對象,它的應用僅限於外殼擴充。其執行緒模式是單線程公寓(STA),這種情況下,類成員的安全執行緒是自動的,並且只有全域量需要保護。有靜態成員(全域量)的COMToys類同時也有臨界部分,g_mydata,在存取它之前必須鎖定。COMToys的一個小類CTLockData可以很輕鬆完成這種鎖定,由建構函式鎖定臨界部分,而由解構函式進行解鎖,所以你要做的只是用下面的代碼:

 { // protected block of code
CTLockData lock(g_mydata);
// do your worst
……
}

    由於有了智能指標,CTLockData的好處是不必記住解鎖,即便函數或代碼塊有多個出口。C++保證解構函式會被調用並且在控制離開時解鎖。CTFactory在任何需要存取其全域類工廠列表時使用CTLockData。 

調試診斷
    理解任何COM對象運作行為的最佳方法是只要介面方法被調用,就輸出其診斷資訊。沒有這樣的診斷,便不可能弄清楚Band對象是如何工作的。COMToys有一個內建的跟蹤機制。IMPLEMENT_ IWhatsIt宏在每一個函數的開始處(頂部)產生TRACE語句,所以只要開啟跟蹤開關(COMToys::bTRACE = TRUE),就可以看到對象正在做什麼。COMToys使用CTraceFn類。
    COMToys能產生人們可讀的介面名。這個診斷系統使用一個叫DbgName的重載函數來獲得各種不同對象的"調試名"。DbgName(WM_DESTROY)返回WM_DESTROY;DbgName(SCODE)返回S_OK 或 E_OUTOFMEMORY之類的值;而DbgName(CWnd*)返回視窗名字。 
    正如所期望的那樣,DbgName(REFIID)返回人可讀的GUID,如:{EB0FE172-1A3A-11D0-89B3-00A0C90A90AC},但如何知道這一長串東西代表的就是IDeskBand呢?COMToys有一個辦法:

 DEBUG_BEGIN_INTERFACE_NAMES()
DEBUG_INTERFACE_NAME(IDeskBand)
DEBUG_END_INTERFACE_NAMES()

    現在DbgName(IID_IDeskBand)返回的是IDeskBand,而不是那讓人費解的十六進位。這個宏產生一個局部表,它被連結到一個DbgName可以搜尋的全域表中。詳細內容請參見原始碼中的debug.h 和 debug.cpp檔案。 

COMToys 動態連結程式庫DLL 
    下載COMToys的原始碼以後可以用六種方式編譯:與MFC靜態連結的靜態庫(static library),與MFC動態連結的靜態庫(static library),或者與MFC動態連結(擴充DLL)的DLL,用Debug和Release模式編譯它們從而形成六個版本。 

存在的問題
    哪個系統會沒有問題呢?包括COMToys在內還有很多地方需要完善。我唯寫了Band對象所需要的那一部分。因此,像CTPersistStream 和 CTPersistFile並未做任何實質性的事情;它們只有一個修改標誌。像微軟在bug文檔中慣用的伎倆一樣:當你不能解決某個bug的時候,就把它當做一個特性。把它說成:“這是設計行為云云......”
    COMToys的另一個令人討厭的是宏調試問題——有時你無法在調試器中跟蹤進去。幸好我到哪都不喜歡使用調試器,而是用自己的跟蹤方法。 

COMToys和MFC 
    在進行總結之前,關於COMToys和MFC之間的關係,我想再多羅嗦幾句。COMToys似乎很依賴MFC。我對此不以為然,因為我喜歡MFC並總是樂於使用它。但有些人覺得MFC是負擔和累贅。我覺得不要陷入MFC和ATL之間的爭論,但我必須指出,從本質上講,COMToys將介面融於實體類的方法是獨立於MFC的,也就是說在這一點上它不依賴MFC。 
    當我編寫COMToys的時候,我仔細地注意了在哪裡需要MFC,哪裡不需要它。因此,CTOleWindow,CTDockingWindow和CTInputObject使用了控制代碼(HWND,HACCEL)來代替CWnd*之類的MFC對象,因為它們不需要MFC來打包任何東西。另一方面,CTMfcContextMenu需要CMenu和CCmdTarget,因為它完成所有的命令常式。同樣,CTMfcModule調用MFC來實現標準的DLL入口。通常在設計與MFC有關的類時,我在名字中都包含有“Mfc”。 
    除了實現像CTMfcContextMenu之類的類以外,COMToys只有三個地方確實需要MFC支援。IMPLEMENT_IUnknownCT假設某個CCmdTarget派生主類實現IUnknown。CTFactory沒有像它應該做的那樣實現IClassFactory,所以你需要一個COleObjectFactory衍生類別工廠來建立對象。CTModule不實現模組鎖定或者DllGetClassObject,所以你需要CTMfcModule來獲得這些重要的細節。為了完全將COMToys完全和MFC隔離,你只要填補這些縫隙就可以了。 
    至於IUnknown,必須由一個基本的CTUnknown來實現AddRef,Release,和 QueryInterface。AddRef/Release可以很簡單地對資料成員m_dwRef進行++/--操作,而QueryInterface調用理論上等價的GetInterfaceHook,GetInterfaceHook必須由外部類提供。你可以派生另一個實現(把它叫做CTUnknownMTA),這個實現用InterlockedIncrement 和 InterlockedDecrement代替++/--。有了這些實現,你的COM類就可以用IMPLEMENT_ IUnknown(CMyComClass, CTUnknown)來代替當前設計用於CCmdTarget 的IMPLEMENT_IUnknownCT宏。這樣做就有點像ATL了——但不涉及模板。 
    至於類工廠,必須為CTFactory實現IClassFactory。這個實現可以調用某個名字類似OnCreateObject的虛函數,每一個COM類都不必須通過返回“new CMyComClass”來提供這個虛函數。(COleObjectFactory用MFC運行時系統建立對象。) 
    為了鉤掛類工廠,必須實現CTModule::OnDllGetClassObject。它將搜尋類工廠列表來尋找誰的CLSID與請求的匹配,然後調用它的CreateInstance方法。 
    最後還有兩件事情要做:一是要在CTModule中編寫一些代碼來進行模組鎖定(CTModule::OnDllCanUnloadNow);二是當獲得DLL_PROCESS_ATTACH/DETACH.時,在DllMain中初始化或終止這個模組。 
    做完以上的事情,你就可以使用COMToys來編寫COM對象了,用它編寫傾向於ATL的COM對象,或者比ATL還要ATL。COMToys模型有兩層:基礎層獨立於MFC,較高層的類使用MFC。我進行這種分層不是沒有道理,它確實必要。如果有時間我會為此再寫一篇專題文章。 

來自黑洞恐怖的奇聞軼事 
    沒有哪一個項目是順順利利完成而不經曆波折與磨難,COMToys也一樣。它發生在我測試註冊代碼的時候,我注意到在調用ResourceUnregister時,IRegistrar沒有完全刪除該刪除的東西。我猜測它是出於安全起見才這麼做的。於是我決定加幾行代碼在登出類的同時刪除HKCR/CLSID/clsid。我查到MFC中有一個現成的函數來做這件事情:AfxOleUnregisterClass。這個函數有兩個參數,一個是某個類的ID,另一個是ProgID。於是我加上了這一行代碼:

AfxOleUnregisterClass(m_clsid, GetProgID());      

    為了測試My Code,我運行regsvr32.exe /u,然後運行REGEDIT去看註冊表,按F5重新整理,顯示結果二十。看到沒有!HKEY_CLASSES_ROOT已經全部展開。我的整個HKEY_CLASSES_ROOT不見了,只剩下空空如也的CLSID鍵。


圖二十 啊!我的註冊表索引值到哪去了?

    怎麼會這樣啊;一定是REGEDIT搞錯了。我退出REGEDIT並從"開始"菜單中重新運行REGEDIT,結果給我顯示出一個小對話方塊。"這個程式引用了一個不存在的lnk檔案……",這些資訊看似無所謂,但實際上我面臨的形勢嚴峻。我案頭上的東西不翼而飛。資源管理員失蹤了?IE也沒了?每個表徵圖被替換成莫名其妙的表徵圖。我甚至不能啟動MS-DOS視窗。我氣得惱羞成怒,差點把顯示器砸了。只好重啟機器。但很不幸,我被欺騙了。MFC把整個註冊表刪掉了。我沮喪地望著螢幕無可奈何,想著最後一次備份是在什麼時候...... 
    幸運的是我用一個買來的程式每天都自動備份註冊表。但lnk檔案都沒了怎麼運行呢?終於我還能從"開始"菜單中到"運行"視窗,然後敲入command.com/cmd.exe開啟外殼MS-DOS視窗。進到ConfigSafe目錄並運行它將註冊表恢複到前一天的配置。到這我才鬆了口氣。 
    這件事情導致系統臨近崩潰的邊緣。後來Band對象不能插入,它們不需要ProgID。MyBands的註冊表串沒有ProgID,所以CTFactory::GetProgID自然返回空CString。注意是空empty,不是NULL。我將這個空串傳到AfxOleUnregisterClass函數,它包含如下的程式碼:

if (pszProgID != NULL)
_AfxRecursiveRegDeleteKey(HKEY_CLASSES_ROOT,
(LPTSTR)pszProgID);

    我想不需要再做進一步解釋了,函數名中的單詞“recursive”說明了一切。MFC象傻子一樣欣然地刪除了HKEY_CLASSES_ROOT下的所有東西。微軟的人會修複這個問題嗎! 這次的經驗證明,在操作註冊表的時候,以下幾點是一定要注意的:

1、必須要小心謹慎,這一點怎麼強調都不過分。
2、如果程式中要處理註冊表鍵,要針對NULL值和空串增加一些附加的安全檢查代碼。
3、如果沒有用某個程式每天備份註冊表,那麼現在就趕快備份吧,亡羊補牢,為時不晚。
4、不要相信MFC。不要相信任何人。並且如果你有心臟病,那麼就不要做程式員(開玩笑)。

總結 
    無論你是否決定在自己的程式中使用COMToys,我都希望你至少認識到使用ATL、MFC或任何其它從ATL/MFC繼承的系統並不是絕對的。重要的是不要陷入任何一個系統的怪圈中,而要按照自己方式來使用它們。我就樂意將COMToys看成是帶多繼承的MFC,或者無模板的ATL。 
    我還希望你能明白,花點時間建立一點兒自己的編程平台或者說基礎結構(COMToys包括執行類只有2400行左右的代碼,)這樣你可以極大地方便自己的C++ COM編程。宏,智能指標,跟蹤——這些簡單的工具對編程也大有協助,使編程更加容易。最終的COMToys庫原始碼不可能在此全部列出,需要的話可以全部下載。總之,用COMToys來編寫BandObj對象很簡單,只需將一些預先做好的東西粘合在一起並加入你要的新特性。它的宗旨就是可重用性。不僅僅是在BandObj中使用,它也可用於其它的應用。今後還可以進一步實現IPersistStream介面以擴充更大的功能。 COMToys並不是十全十美,也沒有哪個系統能做到。但COMToys在實踐中運行良好,而且我用它建立了Band對象和一個快速的瀏覽檔案的瀏覽器。今後我會不斷完善COMToys,所以請關注最新開發動向和原始碼。

最後祝大家編程愉快!

(全文完)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.