下載原始碼
一、前言
我的 COM 組件運行時產生一個視窗,當使用者雙擊該視窗的時候,我需要通知調用者;
我的 COM 組件用線程方式下載網路上的一個檔案,當我完成任務後,需要通知調用者;
我的 COM 組件完成一個鐘錶的功能,當預定時間到達的時候,我需要通知調用者;
... ... ... ...
本回書開始話說 COM 的事件、通知、連接點......這些內容比較多,我分兩次(共四回)來介紹。
二、通知的方法
當程式甲方內部發生了某個事件的時候,需要通知乙方,無非使用幾個方法:
| 通知方式 |
簡單說明 |
評論 |
| 直接訊息 |
PostMessage() PostThreadMessage() |
向視窗或線程發個訊息 |
你什麼時候執行我就不管啦 |
| SendMessage() |
馬上執行訊息響應函數 |
不執行完訊息處理函數不會返回 |
| SendMessage(WM_COPYDATA...) |
發訊息的同時,還可以帶過去一些自訂的資料 |
比較常用,所以單獨列了出來 |
| 間接訊息 |
InvalidateRect() SetTimer() ...... |
被調用的函數會發送相關的一些訊息 |
這樣的函數太多了 |
| 回呼函數 |
GetOpenFileName()...... |
當使用者改變檔案選擇的時候,執行回呼函數 |
嗨!哥們,這是我的電話,有事就言語一聲。 |
在 COM 的時代,以上這些方法就基本上不能玩轉了,因為...您想呀 COM 組件是運行在分布式環境中的,地球另一邊電腦上啟動並執行組件,怎麼可能給你的視窗發訊息那?當然不能!(但話又說回來,對於 ActiveX 這樣只能在本地啟動並執行組件,當然也可以發送視窗訊息的啦。)
回呼函數的方式,是設計 COM
通知方法的基礎。回呼函數,本質上是預先把某一函數的指標告訴我,當我有必要的時候,就直接呼叫該函數了,而這個回呼函數做了什麼,怎麼做的,我是根本不
關心的。好了,問你個問題:啥是 COM
的介面?介面其實就是一組相關函數的集合(這個定義不嚴謹,但你可以這麼理解哈)。因此,在COM中不使用“回呼函數”而是使用“回調介面”(說的再清楚
一些,就是使用一大堆封裝好的“回呼函數”集) ,回調介面,我們也叫“接收器介面”。
圖一、用戶端傳遞接收器介面指標給COM。當發生事件時,COM調用接收器介面函數完成通知
本回樣本程式完成的功能是:
用戶端啟動組件(Simple11.IEvent1.1)並得到介面指標 IEvent1 *;
調用介面方法 IEvent1::Advise() 把用戶端內部的一個接收器(sink)介面指標(ICallBack *)傳遞到元件伺服器中;
調用 IEvent1::Add() 去計算兩個整數的和;
但是計算結果並不通過該函數返回,而是通過 ICallBack::Fire_Result() 返回給用戶端;
當用戶端不再需要接受事件的時候,調用 IEvent1::Unadvise() 斷開和組件的聯絡。
三、組件實現步驟
1、建立一個解決方案
2、在解決方案中,建立一個 ATL 項目。樣本程式中項目名稱叫 Simple12,取消“屬性化”,其它接受預設選項。
3、選擇項目,執行滑鼠右鍵功能表命令“添加/添加類”。
3-1、左側分類選擇 ATL,右側模板選擇 Atl 簡單對象
3-2、名稱卡片中,輸入組件名稱。樣本程式中是 Event1(注1)
3-3、選項卡片中,修改介面類型“自訂”(注2)
4、選擇 IEnvent1 介面,滑鼠右鍵菜單“添加/添加方法”
圖二、增加介面函數 Add([in] long n1,[in] long n2)
圖三、增加介面函數 Advise([in] ICallBack *pCallBack,[out] long *pdwCookie)
圖四、增加介面函數 Unadvise([in] long dwCookie)
你應該注意到了,在Add()函數中,並沒有[out]、[retval] 這樣的 IDL 屬性,嘿嘿,因為我們本來就不打算通過 Add()
函數直接得到計算結果。不然怎麼示範回調介面呀:-) 另外,在函數 Advise()中,需要返回一個整數
dwCookie,這是幹什嗎?道理很簡單,因為我們的組件想同時支援多個對象的回調串連。因此當用戶端傳遞一個介面給我們組件的時候,我返回給它唯一的
一個 cookie 號碼來表示身份,將來中斷連線的時候 Unadvise(),它需要把這個 cookie
身份號再給我,這樣我就知道是誰想斷開了。
5、增加回調介面 ICallBack 的 IDL 定義。開啟 IDL 檔案並手工輸入(黑體字部分為手工輸入的) ,然後儲存:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(DB72DF86-70E9-4ABC-B2F8-5E04062D3B2E),// 這個 IID 可以用 GUDIGEN.EXE 產生
helpstring("ICallBack 介面"),
pointer_default(unique)
]
interface ICallBack : IUnknown
{
};
[
object,// 以下內容同樣本程式,當然如果是你自己產生的程式就肯定有差別的啦
uuid(DB72DF85-70E9-4ABC-B2F8-5E04062D3B2E),
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(FBA1E0F0-49CD-4B77-B9B1-4DC066AF8A8E),
version(1.0),
helpstring("Simple12 1.0 類型庫")
]
library SIMPLE11Lib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(53E00126-B1A0-4510-B9BC-75ED87CE2DB7),
helpstring("Event1 Class")
]
coclass Event1
{
[default] interface IEvent1;
// 需要手工輸入,據說 VB 使用的話,不能有 [source,default] 屬性
[source, default] interface ICallBack;
};
};
6、增加回調介面函數
圖五、增加回調介面函數
其實和以前的方法一樣,只要注意別選錯了介面就好。
圖六、增加介面函數 Fire_Result([in] long nResult)
我們計算整數和,得到結果後,就是要靠這個回調介面函數去反饋給用戶端呀。
7、添加組件內部儲存回調介面指標的數組
剛才已經說過,我們這個組件打算支援多個對象的回調串連,因此我們要使用一個數組來儲存。由於 vc.net 無法用嚮導來添加數組形式的成員變數,我們還是開啟 CEvent1 類的標頭檔,手工輸入吧:
......
private:
ICallBack * m_pCallBack[10];
......
儲存一個數組可以有多種方式。樣本程式比較簡單,定義了一個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 )// 居然給我一個null 指標?!
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 是 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
對它進行了封裝,所以實現起來反而比較簡單。不介紹又不行,因為微軟絕大數支援事件的組件都是用連接點實現的,咳......討厭的微軟(注4)。
注1:本來設想多舉幾個例子,因此第一個叫 Event1,可寫完後,感覺程式已經比較複雜了,就沒繼續再做了。
注2:當然,你選擇使用雙介面 Dual 也沒有問題。但要注意到在下面的步驟,增加回調介面修改 IDL 檔案的時候,我們是要使用 Custom(從IUnknown派生,而不是從IDispatch派生)的。
注3:一坨一坨經常用來形容一堆一堆的狗屎。
注4:微軟的同志們,玩笑話不要當真呀!我還靠著你來吃飯那。