編譯/趙湘寧
原著: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樹形物件模型而帶來的負擔,並不意味COMToys也必須有這個負擔。COMToys使用多繼承進行COM編程。為了清楚的說明它的工作原理,讓我們看看建立在COMToys中CBandObj的新版本。對於應用程式層的程式像MyBands來說——CBandObj與其前身沒什麼兩樣。不同的只是現在CBandObj派生於多個父類CWnd 和IDeskBand。
// 在BandObj.h檔案中
class CBandObj : public CWnd,public IDeskBand
{
DECLARE_IUnknown();
// IDeskBand
HRESULT GetBandInfo(...);
……
};
因為Band對象是個視窗,所以CBandObj要從CWnd派生。同時要實現IDeskBand,所以還要從IDeskBand派生。DECLARE_IUnknown是一個COMToys宏,在其代碼塊中聲明了AddRef,Release和QueryInterface。這個宏降低了錯誤輸入的可能性並且形式可讀性更強。如果聲明了介面,當然就得實現它,為此,COMToys還提供了另一個宏。
// 在BandObj.cpp檔案中
IMPLEMENT_IUnknownCT(CBandObj);
就這麼簡單,一行代碼搞掂。CT的意思是CCmdTarget,而不是COMToys;應該將這一行看成是:"為CCmdTarget 的IUnknown實現"(記住,CBandObj從CWnd繼承了CCmdTarget),IMPLEMENT_IUnknownCT為調用CCmdTarget的AddRef,,Release和QueryInterface產生庫存實現。
ULONG CBandObj::AddRef()
{
CMDTARGENTRYTR("CBandObj(%p)::AddRef,
count=%d/n", this, m_dwRef+1);
return ExternalAddRef();
}
ExternalAddRef,ExternalRelease和ExternalQueryInterface是MFC用於實現IUnknown的標準。它們再調用組建——除非使用彙總,此時它們調用外部IUnknown。不要糾纏在內部/外部這些東西上;它只是MFC實現的細節。重點是COMToys使用CCmdTarget實現IUnknown。想一想——如果這是一條能行得通的路,為什麼不用它呢?CMDTARGENTRYTR是個COMToys宏,它為CCmdTarget的衍生類別產生標準的入口或條目。它等價於METHOD_PROLOGUE。它初始化MFC的狀態併產生TRACE診斷。
AFX_MANAGE_STATE(m_pModuleState);
CTTRACEFN(...);
還有一個宏是CMDTARGENTRY,它是一個沒有TRACE診斷的版本,用於非CCmdTarget入口(象外來的DllGetClassObject),COMToys有MFCENTRY 和 MFCENTRYTR,它們用AfxGetStaticModuleState初始化狀態,而不是用m_pModuleState,下面的代碼列出了用於IMPLEMENT_IUnknownCT的所有宏擴充:
// 用於 IMPLEMENT_IUnknownCT(CMyComClass)的宏擴充
STDMETHODIMP_(ULONG) CMyComClass::AddRef()
{
CMDTARGENTRYTR(_T("CMyComClass(%p)::AddRef "),this);
DWORD dwRef = ExternalAddRef();
CTTRACE(_T("> returns count=%d/n"),dwRef);
return dwRef;
}
STDMETHODIMP_(ULONG) CMyComClass::Release()
{
CMDTARGENTRYTR(_T("CMyComClass(%p)::Release "),this);
DWORD dwRef = ExternalRelease();
CTTRACE(_T("> returns count=%d/n"),dwRef);
return dwRef;
}
STDMETHODIMP CMyComClass::QueryInterface(REFIID iid, LPVOID* ppvRet)
{
CTCHECKARG(ppvRet);
CMDTARGENTRYTR(_T("CMyComClass(%p)::QueryInterface(%s) "), this, _TR(iid));
HRESULT hr = ExternalQueryInterface(&iid, ppvRet);
CTTRACE(_T("> returns %s, *ppv=%p, count=%d/n"),
_TR(hr), *ppvRet, CCmdTarget::m_dwRef);
return hr;
}
// 擴充 CMDTARGENTRYTR.
// 注: 當建立 CCmdTarget 對象時, MFC 初始化 m_pModuleState = AfxGetModuleState();
//
AFX_MANAGE_STATE(m_pModuleState);
TRACE(...);
目前還要對CBandObj的IUnknown實現進行編碼,但這裡存在一個問題,容器什麼時候調用QueryInterface來請求IID_IDeskBand,CBandObj將請求傳遞給CCmdTarget——但MFC如何知道返回什麼介面指標(vtbl)?我在前面討論過,MFC尋找包含在嵌套類位移量中的介面映射。CBandObj不具備介面映射——COMToys也不進行介面映射。那麼當有人用指定的IID進行調用時,CCmdTarget如何找到這個介面呢? 解開這個謎團的是CCmdTarget的虛函數:GetInterfaceHook。由於MFC設計者的先見之明,這個問題得到瞭解決。在進行介面映射之前,CCmdTarget 會調用 GetInterfaceHook。
// 偽碼
HRESULT
CCmdTarget::InternalQueryInterface
(REFIID iid, void** ppv)
{
*ppv = GetInterfaceHook(iid);
if (*ppv)
return S_OK;
// 搜尋介面映射
…
}
另外,為了旁路掉整個介面映射,只要在CBandObj中實現GetInterfaceHook就可以了。
LPUNKNOWN CBandObj::GetInterfaceHook(void* piid)
{
REFIID iid = *((IID*)piid);
if (iid==IID_IUnknown)
return (IDeskBand*)this;
if (iid==IID_IDeskBand)
return (IDeskBand*)this;
return NULL;
}
無論請求什麼介面 ,CBandObj以標準的多繼承方式返回相應的強制轉換類型。但對於IUnknown不能進行IUnknown*強制轉換,原因是在類型還不明確的情況下是不能轉換的;這時不知道所指的是哪一個IUnknown,IDeskBand中的那一個,仰或是其它介面中的某一個?還沒有明確告知呢。所以只能強制轉換某些繼承了IUnknown的單繼承類。其實哪一個並不重要,因為它們全都指向相同的函數(參見圖十七)。 我原本想在多繼承的基礎上添加自己的介面映射版本來代替舊式風格——但我看不到那樣做有什麼好處。在每個介面上多加兩行代碼有多難呢?有時候舊式風格既簡單又不易產生混亂。 綜上所述,可以總結出用COMToys實現一個基本的COM對象的方法:從CCmdTarget直接派生,或從CCmdTarget的衍生類別——如CWnd——間接派生,用DECLARE_IUnknown宏聲明IUnknown,並用IMPLEMENT_IUnknownCT宏實現它,並編寫GetInterfaceHook返回正確的介面指標。當然還得實現方法,對於Band對象,必須實現IDeskBand::GetBandInfo。
HRESULT CBandObj::GetBandInfo(...)
{
// do it
}
......
(待續)