COM 組件設計與應用(十一)
IDispatch 及雙接口的調用
作者:楊老師
下載源代碼
一、前言
前段時(shí)間,由于工作比較忙,沒(méi)有能及時(shí)地寫(xiě)作。其間收到了很多網(wǎng)友的來(lái)信詢(xún)問(wèn)和鼓勵,在此一并表示感謝???/span>......我也需要工作來(lái)養家糊口呀......
上回書(shū)介紹了兩種方法來(lái)寫(xiě)自動(dòng)化(IDispatch)接口的組件程序,一是用 MFC 方式編寫(xiě)“純粹”的 IDispatch 接口;二是用 ATL 方式編寫(xiě)“雙接口”的組件。
二、IDispatch 接口和雙接口
使用者要想調用普通的 COM 組件功能,必須要加載這個(gè)組件的類(lèi)型庫(Type library)文件 tlb(比如在 VC 中使用 #import)。然而,在腳本程序中,由于腳本是被解釋執行的,所以無(wú)法使用加載類(lèi)型庫的方式進(jìn)行預編譯。那么腳本解釋器如何使用 COM 組件那?這就是自動(dòng)化(IDispatch)組件大顯身手的地方了。IDispatch 接口需要實(shí)現4個(gè)函數,調用者只通過(guò)這4個(gè)函數,就能實(shí)現調用自動(dòng)化組件中所有的函數。這4個(gè)函數功能如下:
| HRESULT GetTypeInfoCount( [out] UINT * pctinfo) | 組件中提供幾個(gè)類(lèi)型庫?當然一般都是一個(gè)啦。 但如果你在一個(gè)組件中實(shí)現了多個(gè) IDispatch 接口,那就不一定啦(注1) |
| HRESULT GetTypeInfo( [in] UINT iTInfo, [in] LCID lcid, [out] ITypeInfo ** ppTInfo) | 調用者通過(guò)該函數取得他想要的類(lèi)型庫。 幸好,在 99% 的情況下,我們都不用關(guān)心這兩個(gè)函數的實(shí)現,因為 MFC/ATL 都幫我們完成了默認的一個(gè)實(shí)現,如果是自己完成函數代碼,甚至可以直接返回 E_NOTIMPL 表示沒(méi)有實(shí)現。(注2) |
| HRESULT GetIDsOfNames( [in] REFIID riid, [in,size_is(cNames)] LPOLESTR * rgszNames, [in] UINT cNames, [in] LCID lcid, [out,size_is(cNames)] DISPID * rgDispId) | 根據函數名稱(chēng)取得函數序號,為調用 Invoke() 做準備。 所謂函數序號,大家去觀(guān)察雙接口 IDL 文件和 MFC 的 ODL 文件,每一個(gè)函數和屬性都會(huì )有 [id(序號)....] 這樣的描述。 |
| HRESULT Invoke( [in] DISPID dispIdMember, [in] REFIID riid, [in] LCID lcid, [in] WORD wFlags, [in,out] DISPPARAMS * pDispParams, [out] VARIANT * pVarResult, [out] EXCEPINFO * pExcepInfo, [out] UINT * puArgErr) | 根據序號,執行函數。 使用 MFC/ATL 寫(xiě)的組件程序,我們也不必關(guān)心這個(gè)函數的實(shí)現。如果是自己寫(xiě)代碼,則該函數類(lèi)似如下實(shí)現: switch(dispIdMember) { case 1: .....; break; case 2: .....; break; .... } 其實(shí),就是根據序號進(jìn)行分支調用啦。(注3) |
從 Invoke() 函數的實(shí)現就可以看出,使用 IDispatch 接口的程序,其執行效率是比較低的。ATL 從效率出發(fā),實(shí)現了一種叫“雙接口(dual)”的接口模式。下面我們來(lái)看看,到底什么是雙接口:
圖一、雙接口(dual) 結構示意圖
從上圖中可以看出,所謂雙接口,其實(shí)是在一個(gè) VTAB 的虛函數表中容納了三個(gè)接口(因為任何接口都是從 IUnknown 派生的,所以就不強調 IUnknown 了,叫做雙接口)。我們如果從任意一個(gè)接口中調用 QueryInterface()得到另外的接口指針的話(huà),其實(shí),得到的指針地址都是同一個(gè)。雙接口有什么好處那?答:好呀,多好呀,特別好呀......
| 使用方式 | 因為 | 所以 |
| 腳本語(yǔ)言使用組件 | 解釋器只認識 IDispatch 接口 | 可以調用,但執行效率最低 |
| 編譯型語(yǔ)言使用組件 | 它認識 IDispatch 接口 | 可以調用,執行效率比較低 |
| 編譯型語(yǔ)言使用組件 | 它裝載類(lèi)型庫后,就認識了 Ixxx 接口 | 可以直接調用 Ixxx 函數,效率最高啦 |
| 結論 | 雙接口,既滿(mǎn)足腳本語(yǔ)言的使用方便性,又滿(mǎn)足編譯型語(yǔ)言的使用高效性。 | |
| 如果所有函數都放在一個(gè)雙接口中,那么層次、結構、分類(lèi)不清 | ||
| 如果使用多個(gè)雙接口,則會(huì )產(chǎn)生其它問(wèn)題(注4) | ||
| 雙接口、IDispatch接口只支持自動(dòng)化的參數類(lèi)型,使用受到限制,某些情況下很不方便嘍 | ||
| 還有很多弊病呦,不過(guò)現在我想不起來(lái)嘍...... | ||
三、使用方法
如果你的開(kāi)發(fā)環(huán)境是 vc6.0,那么我們使用第九回中的Simple6組件為例,快去下載呀......
如果你的開(kāi)發(fā)環(huán)境是 vc.net 2003,那么用第十回中的Simple8組件為例,快去下載呀......
嘿嘿,其實(shí)不下載也沒(méi)有關(guān)系,因為你只要下載本回的示例程序,里面已經(jīng)包含了所需的組件。但使用前不要忘了去注冊呀:regsvr32.exe simple6.dll 或 regsvr32.exe simple8.dll (注意別忘了輸入組件的安裝目錄)。注冊成功后,就可以使用了,使用方法有:
| 示例程序 | 自動(dòng)化組件的使用方式 | 簡(jiǎn)要說(shuō)明 |
| 示例0 | 在腳本中調用 | 在第九回/第十回中,已經(jīng)做了介紹 |
| 示例1 | 使用 API 方式調用 | 揭示 IDispatch 的調用原理,但傻子才去這么使用那,會(huì )累死了 |
| 示例2 | 使用 CComDispatchDriver 的智能指針包裝類(lèi) | 比直接使用 API 方式要簡(jiǎn)單多啦,這個(gè)不錯! |
| 示例3 | 使用 MFC 裝載類(lèi)型庫的包裝方式 | 簡(jiǎn)單!好用!常用!但它本質(zhì)上是使用 IDispatch 接口,所以執行效率稍差 |
| 示例4 | 使用 #import 方式加載類(lèi)型庫方式 | #import 方式使用組件,咱們在第七回中講過(guò)啦。常用!對雙接口組件,直接調用自定義接口函數,不再經(jīng)過(guò) IDispatch,因此執行效率最高啦 |
| 示例x | vb、java、c#、bcb、delphi....... | 反正我不會(huì ),自己去請教高人去吧 :-( |
示例一、IDispatch 調用原理篇
void demo(){::CoInitialize( NULL ); // COM 初始化CLSID clsid; // 通過(guò) ProgID 得到 CLSIDHRESULT hr = ::CLSIDFromProgID( L"Simple8.DispSimple.1", &clsid );ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說(shuō)明沒(méi)有注冊組件IDispatch * pDisp = NULL; // 由 CLSID 啟動(dòng)組件,并得到 IDispatch 指針hr = ::CoCreateInstance( clsid, NULL, CLSCTX_ALL, IID_IDispatch, (LPVOID *)&pDisp );ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說(shuō)明沒(méi)有初始化 COMLPOLESTR pwFunName = L"Add"; // 準備取得 Add 函數的序號 DispIDDISPID dispID; // 取得的序號,準備保存到這里hr = pDisp->GetIDsOfNames( // 根據函數名,取得序號的函數IID_NULL,&pwFunName, // 函數名稱(chēng)的數組1, // 函數名稱(chēng)數組中的元素個(gè)數LOCALE_SYSTEM_DEFAULT, // 使用系統默認的語(yǔ)言環(huán)境&dispID ); // 返回值ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說(shuō)明組件根本就沒(méi)有 ADD 函數VARIANTARG v[2]; // 調用 Add(1,2) 函數所需要的參數v[0].vt = VT_I4; v[0].lVal = 2; // 第二個(gè)參數,整數2v[1].vt = VT_I4; v[1].lVal = 1; // 第一個(gè)參數,整數1DISPPARAMS dispParams = { v, NULL, 2, 0 }; // 把參數包裝在這個(gè)結構中VARIANT vResult; // 函數返回的計算結果hr = pDisp->Invoke( // 調用函數dispID, // 函數由 dispID 指定IID_NULL,LOCALE_SYSTEM_DEFAULT, // 使用系統默認的語(yǔ)言環(huán)境DISPATCH_METHOD, // 調用的是方法,不是屬性&dispParams, // 參數&vResult, // 返回值NULL, // 不考慮異常處理NULL); // 不考慮錯誤處理ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說(shuō)明參數傳遞錯誤CString str; // 顯示一下結果str.Format("1 + 2 = %d", vResult.lVal );AfxMessageBox( str );pDisp->Release(); // 釋放接口指針::CoUninitialize(); // 釋放 COM}示例二、CComDispatchDriver 智能指針包裝類(lèi)的使用方法void demo(){// 已經(jīng)進(jìn)行過(guò)了 COM 初始化CLSID clsid; // 通過(guò) ProgID 取得組件的 CLSIDHRESULT hr = ::CLSIDFromProgID( L"Simple8.DispSimple.1", &clsid );ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說(shuō)明沒(méi)有注冊組件CComPtr < IUnknown > spUnk; // 由 CLSID 啟動(dòng)組件,并取得 IUnknown 指針hr = ::CoCreateInstance( clsid, NULL, CLSCTX_ALL, IID_IUnknown, (LPVOID *)&spUnk );ASSERT( SUCCEEDED( hr ) );CComDispatchDriver spDisp( spUnk ); // 構造只能指針CComVariant v1(1), v2(2), vResult; // 參數hr = spDisp.Invoke2( // 調用2個(gè)參數的函數L"Add", // 函數名是 Add&v1, // 第一個(gè)參數,值為整數1&v2, // 第二個(gè)參數,值為整數2&vResult); // 返回值ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說(shuō)明或者沒(méi)有 ADD 函數,或者參數錯誤CString str; // 顯示一下結果str.Format("1 + 2 = %d", vResult.lVal );AfxMessageBox( str );}示例程序中使用了 Invoke2()函數,其實(shí)你根據不同的函數,還可以使用 Invoke0()、Invoke1()、InvokeN()、PutProperty()、GetProperty()......等等等,的確很方便。

#include "simple6.h" // 包裝類(lèi)的頭文件void demo(){// 已經(jīng)進(jìn)行過(guò)了 COM 初始化IDispSimple spDisp; // 包裝類(lèi)的對象spDisp.CreateDispatch( _T("Simple6.DispSimple.1") ) //啟動(dòng)組件spDisp.xxx(...); // 調用函數spDisp.ReleaseDispatch(); // 釋放接口} 使用 vc.net 的朋友,步驟如下:

#include "CDispSimple.h" // 包裝類(lèi)的頭文件void demo(){// 已經(jīng)進(jìn)行過(guò)了 COM 初始化CDispSimple spDisp; // 包裝類(lèi)的對象spDisp.CreateDispatch( _T("Simple8.DispSimple.1") ) // 啟動(dòng)組件spDisp.xxx(...); // 調用函數spDisp.ReleaseDispatch(); // 釋放接口}示例四、使用 #import 方式調用組件聯(lián)系客服