OLE技術專題——COM的連接點事件(上)(轉)

一、前言
  我的 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、建立一個工作區(WorkSpace)
2、在工作區中,建立一個 ATL 工程(Project)。示例程序中工程名稱叫 Simple11,接受全部默認選項。
3、ClassView 中,執行鼠標右鍵菜單命令 New Atl Object...,添加 ALT 類。
   3-1、左側分類 Category 選擇 Objects,右側 Objects 選擇 SimpleObject(其實就是默認項目)
   3-2、名稱 Name 卡片中,輸入組件名稱。示例程序中是 Event1(注1)
   3-3、屬性 Attributes 卡片中,修改接口類型 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)

  你應該注意到了,在Add()函數中,並沒有[out]、[retval] 這樣的 IDL 屬性,嘿嘿,因爲我們本來就不打算通過 Add() 函數直接得到計算結果。不然怎麼演示回調接口呀:-) 另外,在函數 Advise()中,需要返回一個整數 dwCookie,這是幹什麼?道理很簡單,因爲我們的組件想同時支持多個對象的回調連接。因此當客戶端傳遞一個接口給我們組件的時候,我返回給它唯一的一個 cookie 號碼來表示身份,將來斷開連接的時候 Unadvise(),它需要把這個 cookie 身份號再給我,這樣我就知道是誰想斷開了。
5、增加回調接口 ICallBack 的 IDL 定義。打開 IDL 文件並手工輸入(黑體字部分爲手工輸入的) ,然後保存:

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 對它進行了包裝,所以實現起來反而比較簡單。不介紹又不行,因爲微軟絕大數支持事件的組件都是用連接點實現的,咳......討厭的微軟
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章