COM組件設計與應用(十五)連接點(vc6.0)

http://www.vckbase.com/code/downcode.asp?id=2775 下載源碼

一、前言   上回書介紹了回調接口,在此基礎上,我們理解連接點就容易多了。 二、原理 圖一、連接點組件原理圖。左側爲客戶端,右側爲服務端(組件對象)   看着好複雜呀......呵呵,其實簡單的緊:(注1) 1、一個 COM 組件,允許有多個連接點對象(IConnectionPoint)。     也就是說可以有多個發生“事件”的源頭。上圖就有3個連接點; 2、管理這些連接點的接口叫“連接點容器”(IConnectionPointContainer)。     連接點容器接口特別簡單,因爲只有2個函數,一個是 FindConnectionPoint(),表示查找你想要的連接點;另一個是 EnumConnectionPoints(),表示列出所有的連接點,然後你去選擇使用哪個。在實際的應用中,查找法使用最多,佔90%,而枚舉法使用只佔 10%,一般在支持第三方的插件(Plug in)時才使用。(你想寫個 IE 的插件嗎?我們後面就要講到啦) 3、每一個連接點,可以被多個客戶端的接收器(Sink)連接;     這個我們已經熟悉啦,還記得我們在上回書中爲了管理多個回調接口,使用了 cookie 的方式進行區別嗎?! 三、實現組件(一) 1、建立一個工作區(WorkSpace) 2、在工作區中,建立一個 ATL 工程(Project)。示例程序中工程名稱叫 Simple15,接受全部默認選項。 3、ClassView 中,執行鼠標右鍵菜單命令 New Atl Object...,添加 ALT 類。 4、左側分類 Category 選擇 Objects,右側 Objects 選擇 SimpleObject(其實就是默認項目)。 5、名稱 Name 卡片中,輸入組件名稱。示例程序中是 DispConnect。 6、屬性 Attributes 卡片中,接口類型選 Dual 雙接口。注意一定要選擇 Support Connection Points 來支持連接點。 7、ClassView 中,選擇接口(IDispConnect),鼠標右鍵菜單添加函數 Add Method... 8、增加函數。和上回書的程序一樣,增加一個接口函數計算加法,但通過連接點接口返回計算結果。 9、下面該增加“事件”函數了。選擇事件接口(_IDispConnectEvents),添加函數。 10、該函數用來返回 Add() 函數的計算結果。 11、切換到 FileView 卡片,編譯IDL文件。當然你也可以直接編譯全部工程。其實編譯的目的是爲了從IDL文件產生TLB文件,因爲 VC 的 IDE 環境只有知道了 TLB 後,才能生成下面的“事件代理類的程序代碼”。 12、生成事件代理類程序代碼。選擇組件類對象(CDispConnect),執行鼠標右鍵菜單“實現連接點” 13、選擇你要讓 IDE 幫你生成哪個連接點的代理程序代碼。我們這個組件只有一個連接點,那隻好選擇它了。 (在示例二中,我們需要實現兩個連接點,那個時候,你就要選擇兩個了) 14、到此,VC 的 IDE 終於幫咱們完成了所有的框架,下面該咱們自己寫真正的任務代碼啦。

STDMETHODIMP CDispConnect::Add(long n1, long n2)
{
 long nVal = n1 + n2;
 Fire_Result( nVal ); // 調用IDE幫我們生成的代理函數代碼,發出事件

 return S_OK;
}

15、修正 IDE 產生的代碼中的錯誤。你不用死記硬背錯誤點,只要編譯一下就會報出錯誤了。一般 VC6 幫我們生成的代碼中,有2個地方可能會有BUG。一是打開頭文件,找到連接點影射宏,修改如下:

BEGIN_CONNECTION_POINT_MAP(CDispConnect)
 CONNECTION_POINT_ENTRY(DIID__IDispConnectEvents) // 修改 IID_XXXX 爲 DIID_XXXX
END_CONNECTION_POINT_MAP()

  這個錯誤簡直可恨,既然我們使用的是雙接口連接點,它生成的代碼居然不會判斷嗎?另一個可能的錯誤可能發生在代理類中的 Fire_xxxx() 函數中。在示例程序中的 Fire_Result() 函數代碼,大家自己去閱讀,簡單說就是循環地取得每個和自己連接對象(每個cookie表示的對象)的接口指針,(如果是自動化接口,則再取得 IDispatch 接口指針),然後調用事件函數。你不理解它現在沒有太大的關係,不過在後面的示例二中,它給我們產生的代碼是有錯誤的,我們需要進行修改。這是後話,待會兒再說。 四、實現調用者(一) 1、建立一個 MFC 工程(Project)。示例程序中的工程名稱叫 Use。 2、按照咱們以前所學的知識,添加 #import、AfxOleInit()、......不多浪費口條了。如果你還不會,那麼請重新從“第四回”再次閱讀。 (注2) 3、這裏只介紹一下重點部分。我們需要在調用者工程中,增加“接收器”對象。還記得上回書中的增加“回調接收器”對象的方法嗎?上回中,我們的回調接口是從 IUnknown 繼承下來的。本回中,由於我們的組件是雙接口(Dual)的,連接點也是雙接口的,因此這次我們的接收器要從 IDispatch 派生啦。 4、完成 CSink 類的接口函數(虛函數)

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::GetTypeInfoCount(unsigned int *)
{ return E_NOTIMPL; } // 不用實現,反正也不用

STDMETHODIMP CSink::GetTypeInfo(unsigned int,unsigned long,struct ITypeInfo ** )
{ return E_NOTIMPL; } // 不用實現,反正也不用

STDMETHODIMP CSink::GetIDsOfNames(const struct _GUID &,unsigned short ** ,unsigned int,unsigned long,long *)
{ return E_NOTIMPL; } // 不用實現,反正也不用

STDMETHODIMP CSink::Invoke(
 long dispID,
 const struct _GUID &,
 unsigned long,
 unsigned short,
 struct tagDISPPARAMS * pParams,
 struct tagVARIANT *,
 struct tagEXCEPINFO *,
 unsigned int *)
{  // 只需要實現這個就足夠啦
 switch(dispID) // 根據不同的dispID,完成不同的回調函數
 {
 case 1:
  ...... // 這裏就能接收到 COM 發出的事件啦
  break;
 case 2:
  ...... // 事件的代號 dispID 其實就是 IDL 文件中的連接點函數的id(n)的號碼
  break;
 default: break;
 }
 return S_OK;
}

、示例(二)   示例程序中的第2個組件(MultConnect),我們再增加一個連接點( _IDispConnectEvents2 )。這個接口對象負責完成一個時鐘,每間隔一定的毫秒就向調用者發出“時鐘事件”。增加第二個連接點的方法是要手工修改 IDL 文件

......
library MULTCONNECTLib
{
 importlib("stdole32.tlb");
 importlib("stdole2.tlb");

 ...... // 第一個,ATL 框架默認給我們生成的連接點接口描述
 [ // 需要手工增加第二個或更多個連接點
  uuid(F81DB93F-4F63-4A55-8114-A32BC78466D3), // CLSID 可以用 GUIDGEN.EXE 來產生
  helpstring("_IDispConnectEvents2 Interface")
 ]
 dispinterface _IDispConnectEvents2
 {
  properties:
  methods:
 };
 [
  uuid(9461BE82-0D64-4E3B-B0DB-2306D1BFE3F0), // 這是示例程序的類型庫ID,肯定和你生成的不一樣的啦
  helpstring("DispConnect Class")
 ]
 coclass DispConnect
 {
  [default] interface IDispConnect;
  [default, source] dispinterface _IDispConnectEvents;
  [source] dispinterface _IDispConnectEvents2; // 別忘了,這裏還有一行呢
 };
};

  好了,和前面的方式一樣,增加接口函數、編譯IDL文件、讓IDE幫我們實現代理類代碼、輸入程序代碼、修改框架代碼中的BUG。在示例中,我們的事件函數叫 HRESULT Timer([in] VARIANT varData),varData 中傳遞一個時間類型(VT_DATA)的信息(注3)。下面我們來看一下代理類代碼中的錯誤:

HRESULT Fire_Timer(VARIANT varDate)
{
    CComVariant varResult;
    T* pT = static_cast(this);
    int nConnectionIndex;
    CComVariant* pvars = new CComVariant[1];
    int nConnections = m_vec.GetSize();
  
    for (nConnectionIndex = 0; nConnectionIndex < nConnections; nConnectionIndex++)
    {
     pT->Lock();
     CComPtr sp = m_vec.GetAt(nConnectionIndex);
     pT->Unlock();
     IDispatch* pDispatch = reinterpret_cast(sp.p);
     if (pDispatch != NULL)
     {
     VariantClear(&varResult);
     // 原始代碼,這裏居然是 pvars[0]=&varData?愚蠢之極!只好你自己修改啦
    pvars[0] = varDate;
     DISPPARAMS disp = { pvars, NULL, 1, 0 };
     pDispatch->Invoke(0x1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp, &varResult, NULL, NULL);
     }
    }
    delete[] pvars;
    return varResult.scode;
}

  在編寫調用者客戶端代碼方面,如果你需要接收時鐘事件,那麼可以仿照示例一再從 IDispatch 派生一個時鐘接收器。大家下載事例程序代碼,裏面有豐富的註釋信息。 六、小結   連接點,尤其是雙接口的連接點,在遠程(DCOM)環境上運行效率是比較低的。如果你只想完成簡單的“通知”功能,那麼前一回中的“回調接口”是一個明智的方案,並且可以運行在DCOM環境上。連接點方案當然也很重要,因爲微軟的許多應用程序(IE、Office......)都支持連接點,並且 ActiveX 只能通過連接點接口提供“事件”功能。所以,咱們還是都掌握爲善吧。善哉 、善哉......


注1:金庸老先生的武俠小說裏,總是用“XX 緊”來表示“很 XX”。我也學一學,嘿嘿。 注2:如果看了好幾遍,您老人家還不會的話,那隻好......先別學了。5555 注3:DATA 類型就是是8字節的double,它的整數部分表示從 1899年12月30日開始的總天數,小數部分表示當天的時間已經渡過了一天的多少分之一。這個時間類型,用VARIANT表示,就是VT_DATE類型,MFC 中用 COleDateTime 表示。示例程序中有對該類型的操作示範。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章