COM 組件設計與應用(十一)IDispatch 及雙接口的調用

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

一、前言     前段時間,由於工作比較忙,沒有能及時地寫作。其間收到了很多網友的來信詢問和鼓勵,在此一併表示感謝。咳......我也需要工作來養家餬口呀......      上回書介紹了兩種方法來寫自動化(IDispatch)接口的組件程序,一是用 MFC 方式編寫“純粹”的 IDispatch 接口;二是用 ATL 方式編寫“雙接口”的組件。

二、IDispatch 接口和雙接口     使用者要想調用普通的 COM 組件功能,必須要加載這個組件的類型庫(Type library)文件 tlb(比如在 VC 中使用 #import)。然而,在腳本程序中,由於腳本是被解釋執行的,所以無法使用加載類型庫的方式進行預編譯。那麼腳本解釋器如何使用 COM 組件那?這就是自動化(IDispatch)組件大顯身手的地方了。IDispatch 接口需要實現4個函數,調用者只通過這4個函數,就能實現調用自動化組件中所有的函數。這4個函數功能如下:  

HRESULT GetTypeInfoCount(     [out] UINT * pctinfo) 組件中提供幾個類型庫?當然一般都是一個啦。 但如果你在一個組件中實現了多個 IDispatch 接口,那就不一定啦(注1)
HRESULT GetTypeInfo(     [in] UINT iTInfo,     [in] LCID lcid,     [out] ITypeInfo ** ppTInfo) 調用者通過該函數取得他想要的類型庫。 幸好,在 99% 的情況下,我們都不用關心這兩個函數的實現,因爲 MFC/ATL 都幫我們完成了默認的一個實現,如果是自己完成函數代碼,甚至可以直接返回 E_NOTIMPL 表示沒有實現。(注2)
HRESULT GetIDsOfNames(     [in] REFIID riid,     [in,size_is(cNames)] LPOLESTR * rgszNames,      [in] UINT cNames,     [in] LCID lcid,     [out,size_is(cNames)] DISPID * rgDispId) 根據函數名稱取得函數序號,爲調用 Invoke() 做準備。 所謂函數序號,大家去觀察雙接口 IDL 文件和 MFC 的 ODL 文件,每一個函數和屬性都會有 [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 寫的組件程序,我們也不必關心這個函數的實現。如果是自己寫代碼,則該函數類似如下實現: switch(dispIdMember) {      case 1: .....; break;      case 2: .....; break;      .... } 其實,就是根據序號進行分支調用啦。(注3)  

     從 Invoke() 函數的實現就可以看出,使用 IDispatch 接口的程序,其執行效率是比較低的。ATL 從效率出發,實現了一種叫“雙接口(dual)”的接口模式。下面我們來看看,到底什麼是雙接口: 圖一、雙接口(dual) 結構示意圖      從上圖中可以看出,所謂雙接口,其實是在一個 VTAB 的虛函數表中容納了三個接口(因爲任何接口都是從 IUnknown 派生的,所以就不強調 IUnknown 了,叫做雙接口)。我們如果從任意一個接口中調用 QueryInterface()得到另外的接口指針的話,其實,得到的指針地址都是同一個。雙接口有什麼好處那?答:好呀,多好呀,特別好呀......  

使用方式 因爲 所以
腳本語言使用組件 解釋器只認識 IDispatch 接口 可以調用,但執行效率最低
編譯型語言使用組件 它認識 IDispatch 接口 可以調用,執行效率比較低
編譯型語言使用組件 它裝載類型庫後,就認識了 Ixxx 接口 可以直接調用 Ixxx 函數,效率最高啦

結論

雙接口,既滿足腳本語言的使用方便性,又滿足編譯型語言的使用高效性。 於是,我們寫的所有的 COM 組件接口,都用雙接口實現嗎? 錯!否!NO! 如果不是明確非要支持腳本的調用,則最好不要使用雙接口,因爲:

如果所有函數都放在一個雙接口中,那麼層次、結構、分類不清
如果使用多個雙接口,則會產生其它問題(注4)
雙接口、IDispatch接口只支持自動化的參數類型,使用受到限制,某些情況下很不方便嘍
還有很多弊病呦,不過現在我想不起來嘍......

三、使用方法     如果你的開發環境是 vc6.0,那麼我們使用第九回中的Simple6組件爲例,快去下載呀......     如果你的開發環境是 vc.net 2003,那麼用第十回中的Simple8組件爲例,快去下載呀......      嘿嘿,其實不下載也沒有關係,因爲你只要下載本回的示例程序,裏面已經包含了所需的組件。但使用前不要忘了去註冊呀:regsvr32.exe simple6.dll regsvr32.exe simple8.dll (注意別忘了輸入組件的安裝目錄)。註冊成功後,就可以使用了,使用方法有:  

示例程序 自動化組件的使用方式 簡要說明
示例0 在腳本中調用 第九回/第十回中,已經做了介紹
示例1 使用 API 方式調用 揭示 IDispatch 的調用原理,但傻子纔去這麼使用那,會累死了
示例2 使用 CComDispatchDriver 的智能指針包裝類 比直接使用 API 方式要簡單多啦,這個不錯!
示例3 使用 MFC 裝載類型庫的包裝方式 簡單!好用!常用!但它本質上是使用 IDispatch 接口,所以執行效率稍差
示例4 使用 #import 方式加載類型庫方式 #import 方式使用組件,咱們在第七回中講過啦。常用!對雙接口組件,直接調用自定義接口函數,不再經過 IDispatch,因此執行效率最高啦
示例x vbjavac#bcbdelphi....... 反正我不會,自己去請教高人去吧 :-(

示例一、IDispatch 調用原理篇

void demo()
{
 ::CoInitialize( NULL );  // COM 初始化

 CLSID clsid;    // 通過 ProgID 得到 CLSID
 HRESULT hr = ::CLSIDFromProgID( L"Simple8.DispSimple.1", &clsid );
 ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說明沒有註冊組件

 IDispatch * pDisp = NULL; // 由 CLSID 啓動組件,並得到 IDispatch 指針
 hr = ::CoCreateInstance( clsid, NULL, CLSCTX_ALL, IID_IDispatch, (LPVOID *)&pDisp );
 ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說明沒有初始化 COM

 LPOLESTR pwFunName = L"Add"; // 準備取得 Add 函數的序號 DispID
 DISPID dispID;     // 取得的序號,準備保存到這裏
 hr = pDisp->GetIDsOfNames(  // 根據函數名,取得序號的函數
  IID_NULL,
  &pwFunName,     // 函數名稱的數組
  1,       // 函數名稱數組中的元素個數
  LOCALE_SYSTEM_DEFAULT,  // 使用系統默認的語言環境
  &dispID );     // 返回值
 ASSERT( SUCCEEDED( hr ) );  // 如果失敗,說明組件根本就沒有 ADD 函數

 VARIANTARG v[2];     // 調用 Add(1,2) 函數所需要的參數
 v[0].vt = VT_I4; v[0].lVal = 2; // 第二個參數,整數2
 v[1].vt = VT_I4; v[1].lVal = 1; // 第一個參數,整數1

 DISPPARAMS dispParams = { v, NULL, 2, 0 }; // 把參數包裝在這個結構中
 VARIANT vResult;   // 函數返回的計算結果

 hr = pDisp->Invoke(   // 調用函數
  dispID,     // 函數由 dispID 指定
  IID_NULL,
  LOCALE_SYSTEM_DEFAULT, // 使用系統默認的語言環境
  DISPATCH_METHOD,  // 調用的是方法,不是屬性
  &dispParams,   // 參數
  &vResult,    // 返回值
  NULL,     // 不考慮異常處理
  NULL);     // 不考慮錯誤處理
 ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說明參數傳遞錯誤

 CString str;   // 顯示一下結果
 str.Format("1 + 2 = %d", vResult.lVal );
 AfxMessageBox( str );

 pDisp->Release();  // 釋放接口指針
 ::CoUninitialize();  // 釋放 COM
}

示例二、CComDispatchDriver 智能指針包裝類的使用方法

void demo()
{
 // 已經進行過了 COM 初始化

 CLSID clsid;    // 通過 ProgID 取得組件的 CLSID
 HRESULT hr = ::CLSIDFromProgID( L"Simple8.DispSimple.1", &clsid );
 ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說明沒有註冊組件

 CComPtr < IUnknown > spUnk; // 由 CLSID 啓動組件,並取得 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個參數的函數
  L"Add",    // 函數名是 Add
  &v1,    // 第一個參數,值爲整數1
  &v2,    // 第二個參數,值爲整數2
  &vResult);   // 返回值
 ASSERT( SUCCEEDED( hr ) ); // 如果失敗,說明或者沒有 ADD 函數,或者參數錯誤

 CString str;   // 顯示一下結果
 str.Format("1 + 2 = %d", vResult.lVal );
 AfxMessageBox( str );
}

示例程序中使用了 Invoke2()函數,其實你根據不同的函數,還可以使用 Invoke0()Invoke1()InvokeN()PutProperty()GetProperty()......等等等,的確很方便。 示例三、加載類型庫,產生包裝類來使用      這個方法使用更簡單一些,如果你觀察 MFC 幫你產生的包裝類的實現,你就會發現,其實它調用的是 IDispatch 接口函數。使用 vc6.0 的朋友,步驟如下: 1、建立一個 MFC 的應用程序 2、開啓 ClassWizard,執行 Add Class,選擇 From a type library 圖二、加載類型庫 3、然後找到你要使用的組件文件 simple6.dll(tlb 文件也可以),選擇接口後確認 圖三、選擇類型庫中需要包裝的接口 4、在適當的地方輸入調用代碼

#include "simple6.h" // 包裝類的頭文件

void demo() 
{
 // 已經進行過了 COM 初始化

 IDispSimple spDisp;  // 包裝類的對象

 spDisp.CreateDispatch( _T("Simple6.DispSimple.1") ) //啓動組件
 spDisp.xxx(...); // 調用函數

 spDisp.ReleaseDispatch(); // 釋放接口
}

     使用 vc.net 的朋友,步驟如下: 1、建立一個 MFC 的應用程序 2、執行菜單“添加/添加類”,選擇 MFC 分類中的“類型庫中的MFC類” 圖四、添加類型庫中的MFC類 3、選擇組件文件 simple8.dll(tlb 文件),並選擇需要包裝的接口 圖五、選擇文件和接口 4、在適當的位置輸入調用代碼

#include "CDispSimple.h" // 包裝類的頭文件

void demo()
{
 // 已經進行過了 COM 初始化

 CDispSimple spDisp; // 包裝類的對象
 spDisp.CreateDispatch( _T("Simple8.DispSimple.1") ) // 啓動組件
 spDisp.xxx(...); // 調用函數

 spDisp.ReleaseDispatch(); // 釋放接口
}

示例四、使用 #import 方式調用組件      #import 方式在第七回中已經作過介紹,這裏就不多羅嗦了。大家下載本回的示例程序後,自己去看吧。並且一定要掌握這個方法,因爲它的運行效率是最快的呀。 四、小結      留作業啦。在我們以前所實現的所有組件程序中,只添加了接口方法(函數),而沒有添加接口屬性(變量),你自己練習一下吧,很簡單的,然後寫個程序調用看看。其實對於 VC 來說,調用屬性和調用方法沒有太大的區別(vc 把屬性包裝爲 GetXXX()/PutXXX()getXXX()/putXXX()的函數方式),但在另外一些語言中(比如腳本語言)則更方便,設置屬性值是:對象.屬性 = 變量或常量,獲取屬性值是:變量 = 對象.屬性。      本回書至此做一了斷,更多組件設計和使用的知識,且聽下回分解...... () 的時候,大家再回味 IDispatch::GetTypeInfo()吧。 注3:在後面介紹“事件”的時候,我們會自己真正去實現一個 IDispatch::Invoke() 函數。 注4:介紹多個雙接口實現的時候,會談到這個問題。


注1:多個自動化接口的實現方法,我們以後再說。 注2:將來介紹 ITypeLib::GetTypeInfo

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