COM組件設計與應用(十三)
事件和通知(VC6.0)
作者:楊老師
下載源代碼
一、前言
我的 COM 組件運行時(shí)產(chǎn)生一個(gè)窗口,當用戶(hù)雙擊該窗口的時(shí)候,我需要通知調用者;
我的 COM 組件用線(xiàn)程方式下載網(wǎng)絡(luò )上的一個(gè)文件,當我完成任務(wù)后,需要通知調用者;
我的 COM 組件完成一個(gè)鐘表的功能,當預定時(shí)間到達的時(shí)候,我需要通知調用者;
... ... ... ...
本回書(shū)開(kāi)始話(huà)說(shuō) COM 的事件、通知、連接點(diǎn)......這些內容比較多,我分兩次(共四回)來(lái)介紹。
二、通知的方法
當程序甲方內部發(fā)生了某個(gè)事件的時(shí)候,需要通知乙方,無(wú)非使用幾個(gè)方法:
| 通知方式 | 簡(jiǎn)單說(shuō)明 | 評論 | |
| 直接消息 | PostMessage() PostThreadMessage() | 向窗口或線(xiàn)程發(fā)個(gè)消息 | 你什么時(shí)候執行我就不管啦 |
| SendMessage() | 馬上執行消息響應函數 | 不執行完消息處理函數不會(huì )返回 | |
| SendMessage(WM_COPYDATA...) | 發(fā)消息的同時(shí),還可以帶過(guò)去一些自定義的數據 | 比較常用,所以單獨列了出來(lái) | |
| 間接消息 | InvalidateRect() SetTimer() ...... | 被調用的函數會(huì )發(fā)送相關(guān)的一些消息 | 這樣的函數太多了 |
| 回調函數 | GetOpenFileName()...... | 當用戶(hù)改變文件選擇的時(shí)候,執行回調函數 | 嗨!哥們,這是我的電話(huà),有事就言語(yǔ)一聲。 |
在 COM 的時(shí)代,以上這些方法就基本上不能玩轉了,因為...您想呀 COM 組件是運行在分布式環(huán)境中的,地球另一邊計算機上運行的組件,怎么可能給你的窗口發(fā)消息那?當然不能?。ǖ?huà)又說(shuō)回來(lái),對于 ActiveX 這樣只能在本地運行的組件,當然也可以發(fā)送窗口消息的啦。)
回調函數的方式,是設計 COM 通知方法的基礎?;卣{函數,本質(zhì)上是預先把某一函數的指針告訴我,當我有必要的時(shí)候,就直接呼叫該函數了,而這個(gè)回調函數做了什么,怎么做的,我是根本不關(guān)心的。好了,問(wèn)你個(gè)問(wèn)題:啥是 COM 的接口?接口其實(shí)就是一組相關(guān)函數的集合(這個(gè)定義不嚴謹,但你可以這么理解哈)。因此,在COM中不使用“回調函數”而是使用“回調接口”(說(shuō)的再清楚一些,就是使用一大堆包裝好的“回調函數”集) ,回調接口,我們也叫“接收器接口”。
圖一、客戶(hù)端傳遞接收器接口指針給COM。當發(fā)生事件時(shí),COM調用接收器接口函數完成通知
本回示例程序完成的功能是:
客戶(hù)端啟動(dòng)組件(Simple11.IEvent1.1)并得到接口指針 IEvent1 *;
調用接口方法 IEvent1::Advise() 把客戶(hù)端內部的一個(gè)接收器(sink)接口指針(ICallBack *)傳遞到組件服務(wù)器中;
調用 IEvent1::Add() 去計算兩個(gè)整數的和;
但是計算結果并不通過(guò)該函數返回,而是通過(guò) ICallBack::Fire_Result() 返回給客戶(hù)端;
當客戶(hù)端不再需要接受事件的時(shí)候,調用 IEvent1::Unadvise() 斷開(kāi)和組件的聯(lián)系。
三、組件實(shí)現步驟
1、建立一個(gè)工作區(WorkSpace)
2、在工作區中,建立一個(gè) ATL 工程(Project)。示例程序中工程名稱(chēng)叫 Simple11,接受全部默認選項。
3、ClassView 中,執行鼠標右鍵菜單命令 New Atl Object...,添加 ALT 類(lèi)。
3-1、左側分類(lèi) Category 選擇 Objects,右側 Objects 選擇 SimpleObject(其實(shí)就是默認項目)
3-2、名稱(chēng) Name 卡片中,輸入組件名稱(chēng)。示例程序中是 Event1(注1)
3-3、屬性 Attributes 卡片中,修改接口類(lèi)型 Interface 為定制的 Custom(注2)
4、ClassView 中,選擇接口(IEvent1),鼠標右鍵菜單添加函數 Add Method...
圖二、增加接口函數 Add([in] long n1,[in] long n2)
圖三、增加接口函數 Advise([in] ICallBack *pCallBack,[out] long *pdwCookie)
圖四、增加接口函數 Unadvise([in] long dwCookie)
你應該注意到了,在A(yíng)dd()函數中,并沒(méi)有[out]、[retval] 這樣的 IDL 屬性,嘿嘿,因為我們本來(lái)就不打算通過(guò) Add() 函數直接得到計算結果。不然怎么演示回調接口呀:-) 另外,在函數 Advise()中,需要返回一個(gè)整數 dwCookie,這是干什么?道理很簡(jiǎn)單,因為我們的組件想同時(shí)支持多個(gè)對象的回調連接。因此當客戶(hù)端傳遞一個(gè)接口給我們組件的時(shí)候,我返回給它唯一的一個(gè) cookie 號碼來(lái)表示身份,將來(lái)斷開(kāi)連接的時(shí)候 Unadvise(),它需要把這個(gè) cookie 身份號再給我,這樣我就知道是誰(shuí)想斷開(kāi)了。
5、增加回調接口 ICallBack 的 IDL 定義。打開(kāi) IDL 文件并手工輸入(黑體字部分為手工輸入的) ,然后保存:
import "oaidl.idl";import "ocidl.idl";[object,uuid(7E659BB1-FB79-4188-9661-65CA22B6A3E6), // 這個(gè) IID 可以用 GUDIGEN.EXE 產(chǎn)生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;// 需要手工輸入,據說(shuō) VB 使用的話(huà),不能有 [source,default] 屬性[source, default] interface ICallBack;};};6、增加回調接口函數


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 ); // 則發(fā)出事件/通知}return S_OK;}STDMETHODIMP CEvent1::Advise(ICallBack *pCallBack, long *pdwCookie){if( NULL == pCallBack ) // 居然給我一個(gè)空指針?!return E_INVALIDARG;for( int i=0; i<10; i++) // 尋找一個(gè)保存該接口指針的位置{if( NULL == m_pCallBack[i] ) // 找到了{m_pCallBack[i] = pCallBack; // 保存到數組中m_pCallBack[i]->AddRef(); // 指針計數器 +1*pdwCookie = i + 1; // cookie 就是數組下標 // +1 的目的是避免使用0,因為0表示無(wú)效return S_OK;}}return E_OUTOFMEMORY; // 超過(guò)10個(gè)連接,內存不夠用啦}STDMETHODIMP CEvent1::Unadvise(long dwCookie){if( dwCookie<1 || dwCookie>10 ) // 這是誰(shuí)干的呀?亂給參數return E_INVALIDARG;if( NULL == m_pCallBack[ dwCookie - 1 ] ) // 參數錯誤,或該接口指針已經(jīng)無(wú)效了return E_INVALIDARG;m_pCallBack[ dwCookie -1 ]->Release(); // 指針計數器 -1m_pCallBack[ dwCookie -1 ] = NULL; // 空出該下標的數組元素return S_OK;}四、客戶(hù)端實(shí)現步驟
// STDMETHODIMP 是宏,等價(jià)于 long __stdcallSTDMETHODIMP CSink::QueryInterface(const struct _GUID &iid,void ** ppv){*ppv=this; // 不管想得到什么接口,其實(shí)都是對象本身return S_OK;}ULONG __stdcall CSink::AddRef(void){ return 1; }// 做個(gè)假的就可以,因為反正這個(gè)對象在程序結束前是不會(huì )退出的ULONG __stdcall CSink::Release(void){ return 0; }// 做個(gè)假的就可以,因為反正這個(gè)對象在程序結束前是不會(huì )退出的STDMETHODIMP CSink::raw_Fire_Result(long nResult){... ... // 把計算結果顯示在窗口中return S_OK;}五、小結聯(lián)系客服