一、前言
我的 COM 組件運行時產生一個窗口,當用戶雙擊該窗口的時候,我需要通知調用者; 我的 COM 組件用線程方式下載網絡上的一個文件,當我完成任務後,需要通知調用者; 我的 COM 組件完成一個鐘錶的功能,當預定時間到達的時候,我需要通知調用者; ... ... ... ... 本回書開始話說 COM 的事件、通知、連接點......這些內容比較多,我分兩次(共四回)來介紹。 二、通知的方法 當程序甲方內部發生了某個事件的時候,需要通知乙方,無非使用幾個方法:
在 COM 的時代,以上這些方法就基本上不能玩轉了,因爲...您想呀 COM 組件是運行在分佈式環境中的,地球另一邊計算機上運行的組件,怎麼可能給你的窗口發消息那?當然不能!(但話又說回來,對於 ActiveX 這樣只能在本地運行的組件,當然也可以發送窗口消息的啦。) import "oaidl.idl"; import "ocidl.idl"; [ object, uuid(7E659BB1-FB79-4188-9661-65CA22B6A3E6), // 這個 IID 可以用 GUDIGEN.EXE 產生 helpstring("ICallBack Interface"), pointer_default(unique) ] interface ICallBack : IUnknown { }; [ object, // 以下內容同示例程序,當然如果是你自己生成的程序就肯定有差別的啦 uuid(7E659BB0-FB79-4188-9661-65CA22B6A3E6), helpstring("IEvent1 Interface"), pointer_default(unique) ] interface IEvent1 : IUnknown { [helpstring("method Add")] HRESULT Add([in] long n1, [in] long n2); [helpstring("method Advise")] HRESULT Advise([in] ICallBack * pCallBack, [out] long * pdwCookie); [helpstring("method Unadvise")] HRESULT Unadvise([in] long dwCookie); }; [ uuid(695C9BB2-2AE9-4232-8225-17AB8BD3BABC), version(1.0), helpstring("Simple11 1.0 Type Library") ] library SIMPLE11Lib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); [ uuid(6FCF997C-C811-49DB-9D16-46FAF8D24822), helpstring("Event1 Class") ] coclass Event1 { [default] interface IEvent1; // 需要手工輸入,據說 VB 使用的話,不能有 [source,default] 屬性 [source, default] interface ICallBack; }; };6、增加回調接口函數 圖五、增加回調接口函數 其實和以前的方法一樣,只要注意別選錯了接口就好。 圖六、增加接口函數 Fire_Result([in] long nResult) 我們計算整數和,得到結果後,就是要靠這個回調接口函數去反饋給客戶端呀。 7、添加組件內部保存回調接口指針的數組 剛纔已經說過,我們這個組件打算支持多個對象的回調連接,因此我們要使用一個數組來保存。在 ClassView 中,選擇 CEvent1 類,增加成員變量 Add Member Variable... 圖七、增加保存 ICallBack * 的數組 當然,保存一個數組可以有多種方式。示例程序比較簡單,定義了一個10個元素空間的成員數組變量。如果你已經學會了使用 STL,那麼你也可以用 vector 等容器來實現。注意!注意!注意!在構造函數中別忘了初始化數組元素爲 NULL。 8、好了,下面開始完成所有代碼 STDMETHODIMP CEvent1::Add(long n1, long n2) { long nResult = n1 + n2; for( int i=0; i<10; i++) { if( m_pCallBack[i] ) // 如果回調接口有效 m_pCallBack[i]->Fire_Result( nResult ); // 則發出事件/通知 } return S_OK; } STDMETHODIMP CEvent1::Advise(ICallBack *pCallBack, long *pdwCookie) { if( NULL == pCallBack ) // 居然給我一個空指針?! return E_INVALIDARG; for( int i=0; i<10; i++) // 尋找一個保存該接口指針的位置 { if( NULL == m_pCallBack[i] ) // 找到了 { m_pCallBack[i] = pCallBack; // 保存到數組中 m_pCallBack[i]->AddRef(); // 指針計數器 +1 *pdwCookie = i + 1; // cookie 就是數組下標 // +1 的目的是避免使用0,因爲0表示無效 return S_OK; } } return E_OUTOFMEMORY; // 超過10個連接,內存不夠用啦 } STDMETHODIMP CEvent1::Unadvise(long dwCookie) { if( dwCookie<1 || dwCookie>10 ) // 這是誰幹的呀?亂給參數 return E_INVALIDARG; if( NULL == m_pCallBack[ dwCookie - 1 ] ) // 參數錯誤,或該接口指針已經無效了 return E_INVALIDARG; m_pCallBack[ dwCookie -1 ]->Release(); // 指針計數器 -1 m_pCallBack[ dwCookie -1 ] = NULL; // 空出該下標的數組元素 return S_OK; }四、客戶端實現步驟 大家下載示例程序後,去瀏覽客戶端的實現程序吧。這裏我只說明一下關於接收器是如何構造的: 圖八、從 ICallBack 派生接收器類 CSink 從 ICallBack 派生一個類 CSink。確認後 IDE 會有一個警告,說它找不到 ICallBack 的頭文件,不用理它,因爲只有當編譯的時候,#import 纔會爲我們生成 xxxx.tlh、xxxx.tli 文件,這些文件就有 ICallBack 的聲明啦。 這裏 ICallBack 是 COM 接口,因此 CSink 是不能事例化的,如果你去編譯,會得到一坨一坨(注3)的錯誤,報告說你沒有實現 virtual 函數。然後,我們可以按照錯誤報告,去實現所有的虛函數: // STDMETHODIMP 是宏,等價於 long __stdcall STDMETHODIMP CSink::QueryInterface(const struct _GUID &iid,void ** ppv) { *ppv=this; // 不管想得到什麼接口,其實都是對象本身 return S_OK; } ULONG __stdcall CSink::AddRef(void) { return 1; }// 做個假的就可以,因爲反正這個對象在程序結束前是不會退出的 ULONG __stdcall CSink::Release(void) { return 0; }// 做個假的就可以,因爲反正這個對象在程序結束前是不會退出的 STDMETHODIMP CSink::raw_Fire_Result(long nResult) { ... ... // 把計算結果顯示在窗口中 return S_OK; }五、小結 COM 組件實現事件、通知這樣的功能有兩個基本方法。今天介紹的回調接口方式非常好,速度快、結構清晰、實現也不復雜;下回書介紹連接點方式(Support Connection Points),連接點方法其實並不太好,速度慢(如果是遠程DCOM方式,要謹慎選擇它)、結構複雜、唯一的好處就是 ATL 對它進行了包裝,所以實現起來反而比較簡單。不介紹又不行,因爲微軟絕大數支持事件的組件都是用連接點實現的,咳......討厭的微軟 |
OLE技術專題——COM的連接點事件(上)(轉)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.