下載原始碼
一、前言
上回書介紹了回調介面,在此基礎上,我們理解連接點就容易多了。
二、原理
圖一、連接點組件原理圖。左側為用戶端,右側為服務端(組件對象)
看著好複雜呀......呵呵,其實簡單的緊:(注1)
1、一個 COM 組件,允許有多個連接點對象(IConnectionPoint)。
也就是說可以有多個發生“事件”的源頭。就有3個連接點;
2、管理這些連接點的介面叫“連接點容器”(IConnectionPointContainer)。
連接點容器介面特別簡單,因為只有2個函數,一個是 FindConnectionPoint(),表示尋找你想要的連接點;另一個是
EnumConnectionPoints(),表示列出所有的連接點,然後你去選擇使用哪個。在實際的應用中,尋找法使用最多,佔90%,而枚舉法使用
只佔 10%,一般在支援第三方的外掛程式(Plug in)時才使用。(你想寫個 IE 的外掛程式嗎?我們後面就要講到啦)
3、每一個連接點,可以被多個用戶端的接收器(Sink)串連;
這個我們已經熟悉啦,還記得我們在上回書中為了管理多個回調介面,使用了 cookie 的方式進行區別嗎?!
三、實現組件(一)
1、建立一個空白解決方案。
2、在解決方案中,新增 ATL 項目。樣本程式中項目名稱叫 Simple16, 注意不要選擇“屬性化編程”方式。
3、添加 ATL 類。選擇 “ATL 的簡單對象”。
4、名稱卡片中,輸入組件名稱。樣本程式中是 DispConnect。
5、選項卡片中,介面類型選雙介面。注意一定要選擇“連接點”。
6、增加介面函數。和上回書的程式一樣,增加一個方法計算整數加法, 而通過連接點返回計算結果。
7、下面該增加“事件”函數了。選擇事件介面(_IDispConnectEvents),添加函數。
8、該函數用來返回 Add() 函數的計算結果。
9、建置事件代理類程式碼。選擇組件類對象(CDispConnect),執行滑鼠右鍵菜單“添加連接點”
10、選擇你要讓 IDE 幫你產生哪個連接點的代理程式碼。我們這個組件只有一個連接點,那隻好選擇它了。 (在樣本二的程式中,我們實現了兩個連接點,那麼你就要選擇兩個介面啦)
11、到此,VC 的 IDE 終於幫咱們完成了所有的架構,下面該咱們自己寫真正的任務代碼啦。
STDMETHODIMP CDispConnect::Add(long n1, long n2)
{
long nVal = n1 + n2;
Fire_Result( nVal );// 調用IDE幫我們產生的代理函數代碼,發出事件
return S_OK;
}
四、實現調用者(一)
1、建立一個 MFC 項目。樣本程式中的名稱叫 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 IID &,LPOLESTR *,UINT,LCID,DISPID *)
{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("stdole2.tlb");
...... // 第一個連接點。是 ATL 幫我們產生的
[ // 第2個連接點,需要我們手工添加
uuid(E3330AE1-2B1D-42E6-A8E0-A9CB0D1AC74C), // CLSID 可以用 GUIDGEN.EXE 產生
helpstring("_IDispConnect事件介面")
]
dispinterface _IDispConnectEvents2
{
properties:
methods:
};
[
uuid(4B0FDB44-BAF2-4F25-A2B0-B5ECD5CD440E), // 這是樣本程式的類型庫ID,肯定和你產生是不同的
helpstring("DispConnect Class")
]
coclass DispConnect
{
[default] interface IDispConnect;
[default, source] dispinterface _IDispConnectEvents;
[source] dispinterface _IDispConnectEvents2; // 別忘了,這還有一行
};
};
好了,和前面的方式一樣,增加介面函數、讓IDE幫我們實現代理類代碼、輸入程式碼、修改架構代碼中的BUG。在樣本中,我們的事件函數叫
HRESULT Timer([in] VARIANT varData),varData
中傳遞一個時間類型(VT_DATA)的資訊(注3)。下面我們來看一下代理類代碼中的錯誤:
HRESULT Fire_Timer( VARIANT varDate)
{
HRESULT hr = S_OK;
T * pThis = static_cast(this);
int cConnections = m_vec.GetSize();
for (int iConnection = 0; iConnection < cConnections; iConnection++)
{
pThis->Lock();
CComPtr punkConnection = m_vec.GetAt(iConnection);
pThis->Unlock();
IDispatch * pConnection = static_cast(punkConnection.p);
if (pConnection)
{
CComVariant avarParams[1];
// 原始為:avarParams[0] = varDate;avarParams[0].vt = VT_VARIANT;
// 但可惜這是錯誤的,因為 avarParams[0] = varDate; 就已經正確地完成了賦值
// 再對 avarParams[0].vt 賦值,是引用方式才能這麼操作的。
avarParams[0] = varDate; // 這才是正確的操作
CComVariant varResult;
DISPPARAMS params = { avarParams, NULL, 1, 0 };
hr = pConnection->Invoke(1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms, &varResult, NULL, NULL);
}
}
return hr;
}
在編寫調用者用戶端代碼方面,如果你需要接收時鐘事件,那麼可以仿照樣本一再從 IDispatch 派生一個時鐘接收器。大家下載案例程式碼,裡面有豐富的注釋資訊。
六、小結
連接點,尤其是雙介面的連接點,在遠程(DCOM)環境上運行效率是比較低的。如果你只想完成簡單的“通知”功能,那麼前一回中的“回調介面”是一個明
智的方案,並且可以運行在DCOM環境上。連接點方案當然也很重要,因為微軟的許多應用程式(IE、Office......)都支援連接點,並且
ActiveX 只能通過連接點介面提供“事件”功能。所以,咱們還是都掌握為善吧。善哉 、善哉......
注1:金庸老先生的武俠小說裡,總是用“XX 緊”來表示“很 XX”。我也學一學,嘿嘿。
注2:如果看了好幾遍,您老人家還不會的話,那隻好......先別學了。5555
注
3:DATA 類型就是是8位元組的double,它的整數部分表示從
1899年12月30日開始的總天數,小數部分表示當天的時間已經渡過了一天的多少分之一。這個時間類型,用VARIANT表示,就是VT_DATE類
型,MFC 中用 COleDateTime 表示。樣本程式中有對該類型的操作示範。