編譯/趙湘寧
原著: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
第七部分 類的實現
類的混合實現
到目前為止,我介紹了一種用多繼承代替嵌套類在MFC中實現COM對象的方法。基本思路是忽略MFC宏和介面映射,並調用GetInterfaceHook來返回介面指標。很明顯,這已經使編程容易了許多,但可複用性體現在哪呢?開發COMToys的主要目的是讓它具備可複用性特點來實現普通的COM介面。為此,COMToys使用了混合類。
混合類被設計成利用多繼承特性與其它類混合。通常混合類與主類的關係是直交的,也就是說它提供與類層次的無關性來避免菱形派生樹(請原諒我的矛盾修飾法),不用懷疑,CBandObj比我在前面四個部分中討論的要複雜一些,其新版本的代碼如下:
//更新的 CBandObj ///////////////////////////////////////// // CBandObj —— 典型的實現多介面的 COM 類別 // 標頭檔.h : class CBandObj : public CWnd, // interfaces // public IOleWindow, // 從IDeskBand繼承 // public IDockingWindow, // 從IDeskBand繼承 public IDeskBand, public IObjectWithSite, public IInputObject, // public IPersist, // 從IPersistStream繼承 public IPersistStream, public IContextMenu, // 使用ComToys實現 public CTOleWindow, public CTDockingWindow, public CTPersist, public CTPersistStream, public CTInputObject, public CTInputObjectSite, public CTContextMenu { public: CBandObj(REFCLSID clsid); virtual ~CBandObj(); // 改寫以便實現 QueryInterface virtual LPUNKNOWN GetInterfaceHook(const void* iid); // 用ComToys實現的介面 DECLARE_IUnknown(); DECLARE_IOleWindow(); DECLARE_IDockingWindow(); DECLARE_IInputObject(); DECLARE_IPersist(); DECLARE_IPersistStream(); DECLARE_IContextMenu(); DECLARE_IObjectWithSite(); // IDeskBand STDMETHOD (GetBandInfo) (DWORD, DWORD, DESKBANDINFO*); }; // 實現檔案 .cpp : CBandObj::CBandObj(REFCLSID clsid) : // 初始化所有實作類別 CTMfcComObj(this), CTOleWindow(this), CTDockingWindow(this), CTPersist(clsid), CTPersistStream(), CTInputObject(this), CTContextMenu(this, CTMfcComObjFactory::GetFactory(clsid)->GetResourceID()) {...... } ////////////////////////////////// // 這些宏用ComToys實現了所有介面 // IMPLEMENT_IUnknown (CBandObj, CTMfcComObj); IMPLEMENT_IOleWindow (CBandObj, CTOleWindow); IMPLEMENT_IDockingWindow (CBandObj, CTDockingWindow); IMPLEMENT_IPersist (CBandObj, CTPersist); IMPLEMENT_IContextMenu (CBandObj, CTContextMenu); IMPLEMENT_IPersistStream (CBandObj, CTPersistStream); IMPLEMENT_IInputObject (CBandObj, CTInputObject); ///////////////////////////////////////////////////////////////////// // 改寫後旁路掉MFC在CCmdTarget::InternalQueryInterface中的介面映射 // LPUNKNOWN CBandObj::GetInterfaceHook(const void* piid) { REFIID iid = *((IID*)piid); if (iid==IID_IUnknown) return (IDeskBand*)this; #define IF_INTERFACE(iid, iface) / if (iid==IID_##iface) / return (iface*)this; / IF_INTERFACE(iid, IOleWindow); IF_INTERFACE(iid, IDockingWindow); IF_INTERFACE(iid, IObjectWithSite); IF_INTERFACE(iid, IInputObject); IF_INTERFACE(iid, IPersist); IF_INTERFACE(iid, IPersistStream); IF_INTERFACE(iid, IContextMenu); IF_INTERFACE(iid, IDeskBand); return NULL; }
乍一看這些類與ATL如此類似!且看下文的分析。
CBandObj從IDeskBand繼承IOleWindow 和 IDockingWindow,所以必須實現它們。對於IUnknown來說,COMToys的宏聲明方法,DECLARE_IOleWindow 和 DECLARE_IDockingWindow僅聲明大多數派生的方法,而不是繼承的方法(馬上就會明白為什麼),這兩個宏都是不可少的。DECLARE_IDeskBand宏是不存在的,因為COMToys不具備實現它的類;IDeskBand由CBandObj實現。對於IUnknown,COMToys有更多的宏實現此介面。
新的宏用CTOleWindow實現了IOleWindow,並用CTDockingWindow 實現了IDockingWindow。IMPLEMENT_IWhatsIt宏通過產生調用類實現的方法將某個介面定義(抽象類別)與介面實現(具體的類)進行連結。
// 由IMPLEMENT_IOleWindow產生 HRESULT CBandObj::GetWindow(HWND* pHwnd) { CMDTARGENTRYTR( _T("CBandObj(%p)::IOleWindow::GetWindow/n"), this); return CTOleWindow::GetWindow(pHwnd); }
一旦你聲明並實現了這些介面,剩下的事情是通過在GetInterfaceHook中編寫代碼,讓MFC知道它們:
// 在CBandObj::GetInterfaceHook中 if (iid==IID_IOleWindow) return (IOleWindow*)this; if (iid==IID_IDockingWindow) return (IDockingWindow*)this;
很簡單,如果理解了它們在IOleWindow中的工作原理,便會明白CBandObj的代碼大多是相同東西的重複。對於新的COM類實現的每個IWhatsIt介面,要從兩個類派生:COM介面本身(IWhatsIt)和實現CTWhatsIt的CT類。執行類可以是COMToys所帶的一個類或是自己的類。兩種情況的介面和實現都是分開的。這樣感覺比較專業,因為它也是COM的基本原則。 現在假設已經有了實現某些介面的CTWhatsIt,為了在自己的COM類中使用它,必須做四件事情:
- 派生IWhatsIt 和 CTWhatsIt;
- 用DECLARE_IWhatsIt聲明方法;
- 用IMPLEMENT_IWhatsIt(CMyComClass, CTWhatsIt)來實現這些方法;
- 並在CMyComClass::GetInterfaceHook中添加幾行相應的代碼,看上面就知道只有總共五行代碼,就這麼簡單。
實現執行類
前面的討論都是假設已經具備了實現介面的CTWhatsIt!至少是已知如何將介面實現局部化在一個C++類中——換句話說,就是如何重用介面實現。為了提供更加方便的應用,COMToys內建了許多現成的介面實現。CTPersist, CTPersistStream, CTOleWindow, CTDockingWindow, CTInputObject等等。它們是如何工作的呢? 有關執行類的第一個讓人奇怪的地方是它們不是從其對應的介面派生而來的!而只是帶有相同名字和簽名方法的類。
// 不從IOleWindow派生! class CTOleWindow { public: // 與主類相同的宏 DECLARE_IOleWindow(); };
介面和實現之間沒有真正的聯絡;所謂的聯絡完全是詞彙上的。這就是另外一個使用DECLARE_IFoo的理由:它保證獲得正確的名字和簽名。在CTOleWindow中的編排也不會產生錯誤,簡單地定義一個新函數即可。 關於混合類的另外一個要注意的事情是方法實現只針對派生介面,而不是繼承的介面。IDockingWindow 實現 ShowDW,CloseDW 和ResizeBorderDW——而不是GetWindow 或ContextSensitiveHelp,它們從IOleWindow繼承。所以要想實現IDockingWindow,就必須混合CTDockingWindow 和CTOleWindow。同樣,CTPersistStream也不實現GetClassID,它是從IPersist繼承的。 為什麼我要這麼做呢?第一,它混合并匹配不同的實作類別。第二,可選擇性考慮。例如,假設我從CTPersist派生CTPersistFile 和CTPersistStream,它們都有GetClassID函數(我最初就是這麼做的)。現在使用CTPersistFile 和CTPersistStream的任何類都有GetClassID的兩個拷貝,這樣造成了不明確的多繼承。圖十七 中的多繼承規則(Magic MI)只用於純粹的虛函數,而非具體函數,由此所有多繼承介面獲得相同的具體函數。想象得到,儘管希望IPersistFile 和 IPersistStream有不同的GetClassID實現,但百分之九十九的時間做不到。通過在CTPersistFile 和 CTPersistStream,中只實現大多數派生方法,COMToys可以進行一次而且是僅有的一次混合來實現想要的CTPersist。當編寫下面的代碼後:
IMPLEMENT_IPersist(CMyComClass, CTPersist);
產生的GetClassID方法便替代IPersistStream 和 IPersistFile中的GetClassID。 最後正是在這些方法中我不得不針對語言規範編寫一些代碼。所幸的是它不算太麻煩。只是將前面用嵌套類實現的BandObj轉換成本文中對應的CTXxx類,將必須的變數轉換成資料成員並在建構函式中進行初始化。例如,CTPersist的代碼如下:
class CTPersist { public: const CLSID& m_clsid; CTPersist(const CLSID& clsid) : m_clsid(clsid) { } STDMETHODIMP GetClassID(LPCLSID pClassID) { return pClassID ? (*pClassID=m_clsid, S_OK) : E_UNEXPECTED; } };
CTPersist儲存著類ID的引用,並不是類ID本身。此乃COMToys的一般原則:執行類不儲存實際資料,只有外部對象的指標或引用。對於CTPersist,它無關緊要,因為一旦運行中的對象類ID改變,它便無所適從。但一般來說,資料是可以改變的,所以讓父類擁有資料是明智的,這樣它就可以自由的處理這些資料。CTPersistFile 和 CTPersistStream都有一個m_bModified修改標誌,但它是對BOOL類型的一個引用,而不是BOOL類型。如果主類改變了實際的標誌,執行類自動改變,不用自己去調用諸如SetModified之類的函數。作為一個一般的編程規則,對狀態的操作最好是用按需方式進行(demand-pull),而不要用先入方式(supply-push),並且整個系統的每個狀態變數自始至終只應該有一個拷貝。 CTMfcContextMenu的情況類似,它儲存著對僅僅一個CMenu的引用,主類必須在構造時提供。
// 在COMToys.h檔案中 class CTMfcContextMenu { public: CCmdTarget* m_pCmdTarget; CMenu& m_ctxMenu; CTMfcContextMenu(CCmdTarget* pTarg, CMenu& menu) : m_pCmdTarget(pTarg), m_ctxMenu(menu) { }…… }; // 在BandObj.cpp檔案中CBandObj::CBandObj(REFCLSID clsid) : CTMfcContextMenu(this, m_menu), ... {…… }
因為CTMfcContextMenu::m_ctxMenu是個引用,CBandObj可以任何方式改變菜單,不必顯式通知CTMfcContextMenu。CBandObj依賴這個特性,因為當使用者按右鍵Band時,它產生其浮動菜單。十二)。CTMfcContextMenu的實現基本上照搬前面代碼中相應的部分。
圖十二 CBandObj 操作功能表
當容器調用IContextMenu::QueryContextMenu獲得功能表項目時,CTMfcContextMenu用希望的CMenu項填寫菜單,但不是在建立CcmdUI對象之前,並且通過CCmdTarget發送,從而ON_UPDATE_COMMAND_UI處理器都能掛上菜單。同樣,當容器調用InvokeCommand,CTMfcContextMenu發送這個命令到ON_COMMAND處理器。CTMfcContextMenu甚至能通過尋找串資源來處理提示串。簡言之,CTMfcContextMenu將COM語言轉換成MFC語言。剩下的事情只是給它一個菜單和命令目標(通常是COM類本身)。所有的命令處理與在MFC應用中處理一樣,不用再次實現IContextMenu——用COMToys就能搞掂。(待續)