【轉載】COM 組件設計與應用(十一)—— IDispatch 及雙接口的調用

原文:http://vckbase.com/index.php/wv/1236.htmlphp

 

1、前言html

前段時間,因爲工做比較忙,沒有能及時地寫做。其間收到了不少網友的來信詢問和鼓勵,在此一併表示感謝。咳......我也須要工做來養家餬口呀......java

上回書介紹了兩種方法來寫自動化(IDispatch)接口的組件程序,一是用 MFC 方式編寫「純粹」的IDispatch 接口;二是用 ATL 方式編寫「雙接口」的組件。c#

2、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)」的接口模式。下面咱們來看看,到底什麼是雙接口:spa

圖1、雙接口(dual) 結構示意圖.net

從上圖中能夠看出,所謂雙接口,實際上是在一個 VTAB 的虛函數表中容納了三個接口(由於任何接口都是從IUnknown 派生的,因此就不強調 IUnknown 了,叫作雙接口)。咱們若是從任意一個接口中調用QueryInterface()獲得另外的接口指針的話,其實,獲得的指針地址都是同一個。雙接口有什麼好處那?答:好呀,多好呀,特別好呀......設計

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

結論指針

雙接口,既知足腳本語言的使用方便性,又知足編譯型語言的使用高效性。
因而,咱們寫的全部的 COM 組件接口,都用雙接口實現嗎?
錯!否!NO!
若是不是明確非要支持腳本的調用,則最好不要使用雙接口,由於:

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

3、使用方法

若是你的開發環境是 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....... 反正我不會,本身去請教高人去吧 :-(

示例1、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
}

  

示例2、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()......等等等,的確很方便。

示例3、加載類型庫,產生包裝類來使用

這個方法使用更簡單一些,若是你觀察 MFC 幫你產生的包裝類的實現,你就會發現,其實它調用的是IDispatch 接口函數。使用 vc6.0 的朋友,步驟以下:

一、創建一個 MFC 的應用程序

二、開啓 ClassWizard,執行 Add Class,選擇 From a type library

圖2、加載類型庫

三、而後找到你要使用的組件文件 simple6.dll(tlb 文件也能夠),選擇接口後確認

圖3、選擇類型庫中須要包裝的接口

四、在適當的地方輸入調用代碼

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

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

	IDispSimple spDisp;		// 包裝類的對象

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

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

  

使用 vc.net 的朋友,步驟以下:

一、創建一個 MFC 的應用程序

二、執行菜單「添加\添加類」,選擇 MFC 分類中的「類型庫中的MFC類」

圖4、添加類型庫中的MFC類

三、選擇組件文件 simple8.dll(或 tlb 文件),並選擇須要包裝的接口

圖5、選擇文件和接口

四、在適當的位置輸入調用代碼

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

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

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

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

  

示例4、使用 #import 方式調用組件

#import 方式在第七回中已經做過介紹,這裏就很少羅嗦了。你們下載本回的示例程序後,本身去看吧。而且必定要掌握這個方法,由於它的運行效率是最快的呀。

4、小結

留做業啦。在咱們之前所實現的全部組件程序中,只添加了接口方法(函數),而沒有添加接口屬性(變量),你本身練習一下吧,很簡單的,而後寫個程序調用看看。其實對於 VC 來講,調用屬性和調用方法沒有太大的區別(vc 把屬性包裝爲 GetXXX()/PutXXX()或getXXX()/putXXX()的函數方式),但在另一些語言中(好比腳本語言)則更方便,設置屬性值是:對象.屬性 = 變量或常量,獲取屬性值是:變量 = 對象.屬性。

本回書至此作一了斷,更多組件設計和使用的知識,且聽下回分解......

注1:多個自動化接口的實現方法,咱們之後再說。

注2:未來介紹 ITypeLib::GetTypeInfo() 的時候,你們再回味 IDispatch::GetTypeInfo()吧。

注3:在後面介紹「事件」的時候,咱們會本身真正去實現一個 IDispatch::Invoke() 函數。

注4:介紹多個雙接口實現的時候,會談到這個問題。

相關文章
相關標籤/搜索