COM 組件設計與應用(十一)--IDispatch 及雙介面的調用

來源:互聯網
上載者:User
 

下載原始碼

一、前言
    前段時間,由於工作比較忙,沒有能及時地寫作。其間收到了很多網友的來信詢問和鼓勵,在此一併表示感謝。咳......我也需要工作來養家糊口呀......
    上回書介紹了兩種方法來寫自動化(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 vb、java、c#、bcb、delphi....... 反正我不會,自己去請教高人去吧

樣本一、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()的函數方式),但在另外一些語言中(比如指令碼語言)則更方便,設定屬性值是:對象.屬性 = 變數或常量,擷取屬性值是:變數 = 對象.屬性。
    本回書至此做一了斷,更多組件設計和使用的知識,且聽下回分解......

注1:多個自動化介面的實現方法,我們以後再說。
注2:將來介紹 ITypeLib::GetTypeInfo() 的時候,大家再回味 IDispatch::GetTypeInfo()吧。
注3:在後面介紹“事件”的時候,我們會自己真正去實現一個 IDispatch::Invoke() 函數。
注4:介紹多個雙介面實現的時候,會談到這個問題。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.