學(xué)習(xí)筆記
起源及復(fù)合文件
復(fù)合文件概念:
文件的 COM 結(jié)構(gòu)化存儲的實(shí)現(xiàn)。復(fù)合文件將單獨(dú)對象存儲在單一的、結(jié)構(gòu)化文件中,此文件由兩個主要元素組成:存儲對象和流對象。二者結(jié)合使用,可象文件內(nèi)的文件系統(tǒng)一樣起作用。
特點(diǎn):
(1)復(fù)合文件的內(nèi)部是使用指針構(gòu)造的一棵樹進(jìn)行管理的。使用的是單向指針,因此當(dāng)做定位操作的時候,向后定位比向前定位要快
(2)復(fù)合文件中的“流對象”,是真正保存數(shù)據(jù)的空間。它的存儲單位為512字節(jié)。
(3)不同的進(jìn)程,或同一個進(jìn)程的不同線程可以同時訪問一個復(fù)合文件的不同部分而互不干擾
(4)復(fù)合文件則提供了非常方便的“增量訪問”能力
(5)當(dāng)頻繁地刪除文件,復(fù)制文件后,磁盤空間會變的很零碎,需要使用磁盤整理工具進(jìn)行重新整合。和磁盤管理非常相似,復(fù)合文件也會產(chǎn)生這個問題,在適當(dāng)?shù)臅r候也需要整理,但比較簡單,只要調(diào)用一個函數(shù)就可以完成了
復(fù)合文件函數(shù)
復(fù)合文件的函數(shù)和磁盤目錄文件的操作非常類似。所有這些函數(shù),被分為3種類型:WIN API 全局函數(shù),存儲 IStorage 接口函數(shù),流 IStream 接口函數(shù)(“接口”看成是完成一組相關(guān)操作功能的函數(shù)集合)
小結(jié):
復(fù)合文件,結(jié)構(gòu)化存儲,是微軟組件思想的起源,在此基礎(chǔ)上繼續(xù)發(fā)展出了持續(xù)性、命名、ActiveX、對象嵌入、現(xiàn)場激活......一系列的新技術(shù)、新概念。因此理解和掌握復(fù)合文件是非常重要的,即使在你的程序中并沒有全面使用組件技術(shù),復(fù)合文件技術(shù)也是可以單獨(dú)被應(yīng)用的。
GUID 和接口
CLSID(注1)的方式間接描述這些對象數(shù)據(jù)的處理程序路徑。CLSID 其實(shí)就是一個號碼,或者說是一個16字節(jié)的數(shù)。CLSID 的結(jié)構(gòu)定義如下:
typedef struct _GUID {
DWORD Data1; // 隨機(jī)數(shù)
WORD Data2; // 和時間相關(guān)
WORD Data3; // 和時間相關(guān)
BYTE Data4[8]; // 和網(wǎng)卡MAC相關(guān)
} GUID;
typedef GUID CLSID; // 組件ID
typedef GUID IID; // 接口ID
產(chǎn)生 CLSID
1. 如果使用開發(fā)環(huán)境編寫組件程序,則IDE會自動幫你產(chǎn)生 CLSID;
2. 你可以手工寫 CLSID,但千萬不要和人家已經(jīng)生成的 CLSID 重復(fù)呀,所以嚴(yán)重地不推薦
3. 程序中,可以用函數(shù) CoCreateGuid() 產(chǎn)生 CLSID;
4. 使用工具產(chǎn)生 GUID(注2);
微軟為了使用方便,也支持另一個字符串名稱方式,叫 ProgID。由于 CLSID 和 ProgID 其實(shí)是一個概念的兩個不同的表示形式,所以在程序中可以隨便使用任何一種。疑問?!字符串名稱容易重復(fù)啊!!
介紹一下 CLSID 和 ProgID 之間的轉(zhuǎn)換方法和相關(guān)的函數(shù):
函數(shù) |
功能說明 |
CLSIDFromProgID() CLSIDFromProgIDEx() |
由 ProgID得到CLSID。沒什么好說的,你自己都可以寫,查注冊表貝 |
ProgIDFromCLSID() |
由 CLSID 得到 ProgID,調(diào)用者使用完成后要釋放 ProgID 的內(nèi)存(注5) |
CoCreateGuid() |
隨機(jī)生成一個 GUID |
IsEqualGUID()、IsEqualCLSID()、IsEqualIID() |
比較2個ID是否相等 |
StringFromCLSID()、StringFromGUID2()、StringFromIID() |
由 CLSID,IID 得到注冊表中CLSID樣式的字符串,注意釋放內(nèi)存 |
關(guān)于接口
COM是接口(組件)的集合,接口是方法和屬性的集合。 要了解COM,就得先了解IUnknown接口,IUnknown接口的C++形式的定義如下:
interface IUnknown
{
virtual HRESULT _stdcall QueryInterface([in]REFIID iid,[out]void * * ppv)=0;
virtual ULONG _stdcall AddRef(void)=0;
virtual ULONG _stdcall Release(void)=0;
}
她實(shí)現(xiàn)了“接口查詢”和“引用計(jì)數(shù)”,她是一個純抽象基類。 所有COM 定義的接口都必須從她繼承。
IUnknown是所有接口的基礎(chǔ),他負(fù)責(zé)兩項(xiàng)工作:
IUnknown::QueryInterface負(fù)責(zé)得到該組件的其他接口的指針
IUnknown::AddRef/Release負(fù)責(zé)管理該組件的生存期,但有人使用該組件時,保證該組件不會被意外刪除;再沒人使用該組件時,保證該組件被自動刪除
下面是容器和組件之間的一個模擬對話過程:
|
容器 協(xié)商部分 |
組件 應(yīng)答部分 |
1 |
根據(jù)CLSID啟動組件 。 |
生成對象,執(zhí)行構(gòu)造函數(shù),執(zhí)行初始化動作。 |
2 |
你有IUnknown接口嗎? |
有,給你! |
3 |
恩,太好了,那么你有IPersistStorage接口嗎?(注9) |
沒有! |
4 |
真差勁,連這個都沒有。那你有IPersistStreamInit接口嗎?(注10) |
哈,這個有,給! |
5 |
好,好,這還差不多。你現(xiàn)在給我初始化吧。 |
OK,初始化完成了。 |
6 |
完成了?好!現(xiàn)在你讀數(shù)據(jù)去吧。 |
讀完啦。我根據(jù)數(shù)據(jù),已經(jīng)在窗口中顯示出來了。 |
7 |
好,現(xiàn)在咱們各自處理用戶的鼠標(biāo)、鍵盤消息吧...... |
...... |
8 |
哎呀!用戶要保存退出程序了。你的數(shù)據(jù)被用戶修改了嗎? |
改了,用戶已經(jīng)修改啦。 |
9 |
那好,那么用戶修改后,你的數(shù)據(jù)需要多大的存儲空間呀? |
恩,我算算呀......好了,總共需要500KB。 |
10 |
暈,你這么個小玩意居然占用這么大空間?!......好了,你可以存了。 |
謝謝,我已經(jīng)存好了。 |
11 |
恩。拜拜了您那。(注11) |
執(zhí)行析構(gòu)函數(shù),刪除對象。 |
12 |
我自己也該退出了...... |
|
數(shù)據(jù)類型
簡單調(diào)用組件
1、組件的啟動和釋放
圖一 組件調(diào)用機(jī)制
由上圖可以看出,當(dāng)調(diào)用組件的時候,其實(shí)是依靠代理(運(yùn)行在本地)和存根(運(yùn)行在遠(yuǎn)端)之間的通訊完成的。具體來說,當(dāng)客戶程序通過 CoCreateInstance() 函數(shù)啟動組件,則代理接管該調(diào)用,它和存根通訊,存根則它所在的本地(相對于客戶程序來說就是遠(yuǎn)程了)執(zhí)行 new 操作加載對象。遵守幾個原則:
(1)啟動組件得到一個接口指針(Interface)后,不要調(diào)用AddRef()。因?yàn)橄到y(tǒng)知道你得到了一個指針,所以它已經(jīng)幫你調(diào)用了AddRef()函數(shù);
(2)通過QueryInterface()得到另一個接口指針后,不要調(diào)用AddRef()。因?yàn)?/span>......和上面的道理一樣;
(3)當(dāng)你把接口指針賦值給(保存到)另一個變量中的時候,請調(diào)用AddRef();
(4)當(dāng)不需要再使用接口指針的時候,務(wù)必執(zhí)行Release()釋放;
(5)當(dāng)使用智能指針的時候,可以省略指針的維護(hù)工作;
2、內(nèi)存分配和釋放
函數(shù)內(nèi)部根據(jù)實(shí)際需要動態(tài)申請內(nèi)存,而調(diào)用者負(fù)責(zé)釋放。這雖然違背了上述原則,但 COM 從方便性和效率出發(fā),確實(shí)是這么設(shè)計(jì)的。
|
C語言 |
C++語言 |
Windows 平臺 |
COM |
IMalloc 接口 |
BSTR |
申請 |
malloc() |
new |
GlobalAlloc() |
CoTaskMemAlloc() |
Alloc() |
SysAllocString() |
重新申請 |
realloc() |
|
GlobalReAlloc() |
CoTaskRealloc() |
Realloc() |
SysReAllocString() |
釋放 |
free() |
delete |
GlobalFree() |
CoTaskMemFree() |
Free() |
SysFreeString() |
以上這些函數(shù)必須要按類型配合使用(比如:new 申請的內(nèi)存,則必須用 delete 釋放)。在 COM 內(nèi)部,當(dāng)然你可以隨便使用任何類型的內(nèi)存分配釋放函數(shù),但組件如果需要與客戶進(jìn)行內(nèi)存的交互,則必須使用上表中的后三類函數(shù)族。
(1)BSTR 內(nèi)存在上回書中,已經(jīng)有比較豐富的介紹了,不再重復(fù);
(2)CoTaskXXX()函數(shù)族,其本質(zhì)上就是調(diào)用C語言的函數(shù)(malloc...);
(3)IMalloc 接口又是對 CoTaskXXX() 函數(shù)族的一個包裝。包裝后,同時增強(qiáng)了一些功能,比如:IMalloc::GetSize()可以取得尺寸,使用 IMallocSpy 可以監(jiān)視內(nèi)存的使用;
3、參數(shù)傳遞方向
參數(shù)是動態(tài)分配的內(nèi)存指針,那么遵守如下的規(guī)定:
方向 |
申請人 |
釋放人 |
提示 |
[in] |
調(diào)用者 |
調(diào)用者 |
組件接收指針后,不能重新分配內(nèi)存 |
[out] |
組件 |
調(diào)用者 |
組件返回指針后,調(diào)用者“愛咋咋地”(注3) |
[in,out] |
調(diào)用者 |
調(diào)用者 |
組件可以重新分配內(nèi)存 |
用 ATL 寫第一個組件(關(guān)于流程多寫代碼就熟悉了,這里不重復(fù)!)
1、建立 ATL 工程方法
圖一、建立 ATL DLL 工程
Dynamic Link Library(DLL) 表示建立一個 DLL 的組件程序。
Executable(EXE) 表示建立一個 EXE 的組件程序。
Service(EXE) 表示建立一個服務(wù)程序,系統(tǒng)啟動后就會加載并執(zhí)行的程序。
Allow merging of proxy/stub code 選擇該項(xiàng)表示把“代理/存根”代碼合并到組件程序中,否則需要單獨(dú)編譯,單獨(dú)注冊代理存根程序。代理/存根,這個是什么概念?還記得我們在上回書中介紹的嗎?當(dāng)調(diào)用者調(diào)用進(jìn)程外或遠(yuǎn)程組件功能的時候,其實(shí)是代理/存根負(fù)責(zé)數(shù)據(jù)交換的。關(guān)于代理/存根的具體變成和操作,以后再說啦......
Support MFC 除非有特殊的原因,我們寫 ATL 程序,最好不要選擇該項(xiàng)。
2、增加 ATL 對象類方法
圖二、選擇建立簡單COM對象
Category Object 普通組件。其中可以選擇的組件對象類型很多,但本質(zhì)上,就是讓向?qū)臀覀兡J(rèn)加上一些接口。比如我們選 "Simple Object",則向?qū)Ыo我們的組件加上 IUnknown 接口;我們選 "Internet Explorer Object",則向?qū)С思由?/span> IUnknown 接口外,再增加一個給 IE 所使用的 IObjectWithSite 接口。當(dāng)然了,我們完全可以手工增加任何接口。
Category Controls ActiveX 控件。其中可以選擇的 ActiveX 類型也很多。我們在后續(xù)的專門介紹 ActiveX 編程中再討論。
Category Miscellaneous 輔助雜類組件。
Categroy Data Access 數(shù)據(jù)庫類組件(我最討厭數(shù)據(jù)庫編程了,所以我也不會)。
圖四、接口屬性
Threading Model 選擇組件支持的線程模型。默認(rèn)Apartment,它代表什么那?簡單地說:當(dāng)在線程中調(diào)用組件函數(shù)的時候,這些調(diào)用會排隊(duì)進(jìn)行。因此,這種模式下,我們可以暫時不用考慮同步的問題。
Interface 接口基本類型。Dual 表示支持雙接口(注2),這個非常 非常重要,非常非常常用,但我們今天不講。Custom 表示自定義借口。切記!切記!我們的這第一個 COM 程序中,一定要選擇它!!!!(如果你選錯了,請刪除全部內(nèi)容,重新來過。)
Aggregation 我們寫的組件,將來是否允許被別人聚合(注3)使用。Only 表示必須被聚合才能使用,有點(diǎn)類似 C++ 中的純虛類,你要是總工程師,只負(fù)責(zé)設(shè)計(jì)但不親自寫代碼的話,才選擇它。
Support ISupportErrorInfo 是否支持豐富信息的錯誤處理接口。以后就講。
Support Connection Points 是否支持連接點(diǎn)接口(事件、回調(diào))。以后就講。
Free Threaded Marshaler 暫時我也不知道
3、添加接口函數(shù)方法
編譯、注冊、調(diào)用
關(guān)于編譯
2-1 最小依賴
“最小依賴”,表示編譯器會把 ATL 中必須使用的一些函數(shù)靜態(tài)連接到目標(biāo)程序中。這樣目標(biāo)文件尺寸會稍大,但獨(dú)立性更強(qiáng),安裝方便;反之系統(tǒng)執(zhí)行的時候需要有 ATL.DLL 文件的支持。如何選擇設(shè)置為“最小依賴”呢?答案是:刪除預(yù)定義宏“_ATL_DLL”,操作方法見圖一、圖二。
圖一、在vc6.0中,設(shè)置方法
圖二、在 vc.net 2003中,設(shè)置方法
2-2 CRT庫
如果在 ATL 組件程序中調(diào)用了 CRT 的運(yùn)行時刻庫函數(shù),比如開平方 sqrt() ,那么編譯的時候可能會報錯“error LNK2001: unresolved external symbol _main”。怎么辦?刪除預(yù)定義宏“_ATL_MIN_CRT”!操作方法也見圖一、圖二。(vc.net 2003 中的這個項(xiàng)目屬性叫“在 ATL 中最小使用 CRT”)
2-3 MBCS/UNICODE
這個不多說了,在預(yù)定義宏中,分別使用 _MBCS 或 _UNICODE。
2-4 IDL 的編譯
COM 在設(shè)計(jì)初期,就定了一個目標(biāo):要能實(shí)現(xiàn)跨語言的調(diào)用。既然是跨語言的,那么組件的接口描述就必須在任何語言環(huán)境中都要能夠認(rèn)識。怎么辦?用 .h 文件描述?------ C語言程序員笑了,真方便!BASIC 程序員哭了:-( 因此,微軟使用了一個新的文件格式---IDL文件(接口定義描述語言)。IDL 是一個文本文件,它的語言語法比較簡單,很象C。具體 IDL 文件的講解,見下一回《COM 組件設(shè)計(jì)與應(yīng)用(八)之添加新接口》。IDL 經(jīng)過編譯,生成二進(jìn)制的等價類型庫文件 TLB 提供給其它語言來使用。圖三示意了 ATL COM 程序編譯的過程:
圖三、ATL 組件程序編譯過程
說明1:編譯后,類型庫以 TLB 文件形式單獨(dú)存在,同時也保存在目標(biāo)文件的資源中。因此,我們將來在 #import 引入類型庫的時候,既可以指定 TLB 文件,也可以指定目標(biāo)文件;
說明2:我們作為 C/C++ 的程序員,還算是比較幸福的。因?yàn)?/span> IDL 編譯后,特意為我們提供了 C 語言形式的接口文件。
說明3:IDL 編譯后生成代理/存根源程序,有:dlldata.c、xxx_p.c、xxxps.def、xxxps.mak,我們可以用 NMAKE.EXE 再次編譯來產(chǎn)生真正的代理/存根DLL目標(biāo)文件(注1)。
關(guān)于注冊
情況1:當(dāng)我們使用 ATL 編寫組件程序,注冊不用我們來負(fù)責(zé)。編譯成功后,IDE 會幫我們自動注冊;
情況2:當(dāng)我們使用 MFC 編寫組件程序,由于編譯器不知道你寫的是否是 COM 組件,所以它不會幫我們自動注冊。這個時候,我們可以執(zhí)行菜單“Tools\Register Control”來注冊。
情況3:當(dāng)我們寫一個具有 COM 功能的 EXE 程序時,注冊的方法就是運(yùn)行一次這個程序;
情況4:當(dāng)我們需要使用第三方提供的組件程序時,可以命令行運(yùn)行“regsvr32.exe 文件名”來注冊。順便說一句,反注冊的方法是“regsvr32.exe /u 文件名”;
情況5:當(dāng)我們需要在程序中(比如安裝程序)需要執(zhí)行注冊,那么:
typedef HRESULT (WINAPI * FREG)();
TCHAR szWorkPath[ MAX_PATH ];
::GetCurrentDirectory( sizeof(szWorkPath), szWorkPath ); // 保存當(dāng)前進(jìn)程的工作目錄
::SetCurrentDirectory( 組件目錄 ); // 切換到組件的目錄
HMODULE hDLL = ::LoadLibrary( 組件文件名 ); // 動態(tài)裝載組件
if(hDLL)
{
FREG lpfunc = (FREG)::GetProcAddress( hDLL, _T("DllRegisterServer") ); // 取得注冊函數(shù)指針
// 如果是反注冊,可以取得"DllUnregisterServer"函數(shù)指針
if ( lpfunc ) lpfunc(); // 執(zhí)行注冊。這里為了簡單,沒有判斷返回值
::FreeLibrary(hDLL);
}
::SetCurrentDirectory(szWorkPath); // 切換回原先的進(jìn)程工作目錄
上面的示例,在多數(shù)情況下可以簡化掉切換工作目錄的代碼部分。但是,如果這個組件在裝載的時候,它需要同時加載一些必須依賴的DLL時,有可能由于它自身程序的 BUG 導(dǎo)致無法正確定位。咳......還是讓我們自己寫的程序,來彌補(bǔ)它的錯誤吧......誰讓咱們是好人呢 ,誰讓咱們的水平比他高呢,誰讓咱們在 vckbase 上是個“榜眼”呢......
關(guān)于組件調(diào)用
總的來說,調(diào)用組件程序大概有如下方法:
#include 方法 |
IDL編譯后,為方便C/C++程序員的使用,會產(chǎn)生xxx.h和xxx_i.c文件。我們真幸福,直接#include后就可以使用了 |
#import 方法 |
比較通用的方法,vc 會幫我們產(chǎn)生包裝類,讓我們的調(diào)用更方便 |
加載類型庫包裝類 方法 |
如果組件提供了 IDispatch 接口,用這個方法調(diào)用組件是最簡單的啦。不過還沒講IDispatch,只能看以后的文章啦 |
加載ActiveX包裝類 方法 |
ActiveX 還沒介紹呢,以后再說啦 |
實(shí)現(xiàn)多接口
按照函數(shù)的功能進(jìn)行分類,把不同功能分類的函數(shù)用多個接口表現(xiàn)出來。這樣可以有如下的一些好處:
1、一個接口中的函數(shù)個數(shù)有限、功能集中,使用者容易學(xué)習(xí)、記憶和調(diào)用。一個接口到底提供多少個函數(shù)合適那?答案是:如果你是黑猩猩,那么一個接口最多3個函數(shù),如果你是人,那么一個接口最好不要超過7個函數(shù)。(注1)
2、容易維護(hù)。至少你肉眼搜索的時候也方便一些呀。
3、容易升級。當(dāng)我們給組件增加函數(shù)的時候,不要修改已經(jīng)發(fā)表的接口,而是提供一個新的接口來完成功能擴(kuò)展
實(shí)現(xiàn)
1 import "oaidl.idl";
2 import "ocidl.idl";
3 [
4 object,
5 uuid(072EA6CA-7D08-4E7E-B2B7-B2FB0B875595),
6 helpstring("IMathe Interface"),
7 pointer_default(unique)
8 ]
9 interface IMathe : IUnknown
10 {
11 [helpstring("method Add")] HRESULT Add([in] long n1, [in] long n2, [out,retval] long *pnVal);
12 };
13 [
14 uuid(CD
15 version(1.0),
16 helpstring("Simple3 1.0 Type Library")
17 ]
18 library SIMPLE3Lib
19 {
20 importlib("stdole32.tlb");
21 importlib("stdole2.tlb");
22 [
23 uuid(C
24 helpstring("Mathe Class")
25 ]
26 coclass Mathe
27 {
28 [default] interface IMathe;
29 };
30 };
1-2 |
引入 IUnknown 和ATL已經(jīng)定義的其它接口描述文件。import 類似與 C 語言中的 #include |
3-12 |
一個接口的完整描述 |
4 |
object 表示本塊描述的是一個接口。IDL文件是借用了PRC遠(yuǎn)程數(shù)據(jù)交換格式的說明方法 |
5 |
uuid(......) 接口的 IID,這個值是 ATL 自動生成的,可以手工修改或用 guidgen.exe 產(chǎn)生(注3) |
6 |
在某些軟件或工具中,能看到這個提示 |
7 |
定義接口函數(shù)中參數(shù)所使用指針的默認(rèn)屬性(注4) |
9 |
接口叫 IMathe 派生自 IUnknown,于是 IMathe 接口的頭三個函數(shù)一定就是QueryInterface,AddRef和Release |
10-12 |
接口函數(shù)列表 |
13-30 |
類型庫的完整描述(類型庫的概念以后再說),下面所說明的行,是需要先了解的 |
18 |
#import 時候的默認(rèn)命名空間 |
23 |
組件的 CLSID,CoCreateInstance()的第一個參數(shù)就是它 |
27-29 |
接口列表 |
28 |
[default]表示誰提供了IUnknown接口 |
手工修改IDL文件,黑體字部分是手工輸入的。完成后保存。
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(072EA6CA-7D08-4E7E-B2B7-B2FB0B875595),
helpstring("IMathe Interface"),
pointer_default(unique)
]
interface IMathe : IUnknown
{
[helpstring("method Add")] HRESULT Add([in] long n1, [in] long n2, [out,retval] long *pnVal);
};
[ // 所謂手工輸入,其實(shí)也是有技巧的:把上面的接口描述(IMathe)復(fù)制、粘貼下來,然后再改更方便哈
object,
uuid(072EA6CB-7D08-4E7E-B2B7-B2FB0B875595), // 手工或用工具產(chǎn)生的 IID
helpstring("IStr Interface"),
pointer_default(unique)
]
interface IStr : IUnknown
{
// 目前還沒有任何接口函數(shù)
};
[
uuid(CD
version(1.0),
helpstring("Simple3 1.0 Type Library")
]
library SIMPLE3Lib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(C
helpstring("Mathe Class")
]
coclass Mathe
{
[default] interface IMathe;
interface IStr; // 別忘了呦,這里還有一個那
};
};
3-4、打開頭文件(Mathe.h),手工增加類的派生關(guān)系和接口入口表 ,然后保存。
class ATL_NO_VTABLE CMathe :
public CComObjectRootEx <CComSingleThreadModel>,
public CComCoClass <CMathe, &CLSID_Mathe>,
public IMathe, // 別忘了,這里加一個逗號
public IStr // 增加一個基類
{
public:
CMathe()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_MATHE)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CMathe) // 接口入口表。這里填寫的接口,才能被QueryInterface()找到
COM_INTERFACE_ENTRY(IMathe)
COM_INTERFACE_ENTRY(IStr)
END_COM_MAP()
3-5、好了,一切就緒。接下來,就可以在 IStr 接口中增加函數(shù)了。
IDispatch 接口
自動化組件,其實(shí)就是實(shí)現(xiàn)了 IDispatch 接口的組件。IDispatch 接口有4個函數(shù),解釋語言的執(zhí)行器就通過這僅有的4個函數(shù)來執(zhí)行組件所提供的功能。IDispatch 接口用 IDL 形式說明如下:(注1)
[
object,
uuid(00020400-0000-0000-C000-000000000046), // IDispatch 接口的 IID = IID_IDispatch
pointer_default(unique)
]
interface IDispatch : IUnknown
{
typedef [unique] IDispatch * LPDISPATCH; // 轉(zhuǎn)定義 IDispatch * 為 LPDISPATCH
HRESULT GetTypeInfoCount([out] UINT * pctinfo); // 有關(guān)類型庫的這兩個函數(shù),咱們以后再說
HRESULT GetTypeInfo([in] UINT iTInfo,[in] LCID lcid,[out] ITypeInfo ** ppTInfo);
HRESULT GetIDsOfNames( // 根據(jù)函數(shù)名字,取得函數(shù)序號(DISPID)
[in] REFIID riid,
[in, size_is(cNames)] LPOLESTR * rgszNames,
[in] UINT cNames,
[in] LCID lcid,
[out, size_is(cNames)] DISPID * rgDispId
);
[local] // 本地版函數(shù)
HRESULT Invoke( // 根據(jù)函數(shù)序號,解釋執(zhí)行函數(shù)功能
[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
);
[call_as(Invoke)] // 遠(yuǎn)程版函數(shù)
HRESULT RemoteInvoke(
[in] DISPID dispIdMember,
[in] REFIID riid,
[in] LCID lcid,
[in] DWORD dwFlags,
[in] DISPPARAMS * pDispParams,
[out] VARIANT * pVarResult,
[out] EXCEPINFO * pExcepInfo,
[out] UINT * pArgErr,
[in] UINT cVarRef,
[in, size_is(cVarRef)] UINT * rgVarRefIdx,
[in, out, size_is(cVarRef)] VARIANTARG * rgVarRef
);
}
用 MFC 實(shí)現(xiàn)自動化組件
3-1:建立一個工作區(qū)(Workspace)
3-2:建立一個 MFC DLL 工程(Project),工程名稱為“Simple
3-3:一定要選擇 automation,切記!切記!
3-4:建立新類
3-5:在新建類中支持automation
Class information - Name 你隨便寫個類名子啦
Class information - Base class 一定要從 CComTarget 派生呀,只有它才提供了 IDispatch 的支持
Automation - None 表示不支持自動化,你要選擇了它,那就白干啦
Automation - Automation 支持自動化,但不能被直接實(shí)例化。后面在講解多個 IDispatch 的時候就用到它了,現(xiàn)在先不要著急。
Automation - Createable by type ID 一定要選擇這個項(xiàng)目,這樣我們在后面的調(diào)用中,VB就能夠CreateObject(),VC就能夠CreateDispatch()對組件對象實(shí)例化了。注意一點(diǎn),這個 ID 其實(shí)就是組件的 ProgID 啦。
3-6:啟動 ClassWizard,選擇 Automation 卡片,準(zhǔn)備建立函數(shù)
3-7:添加函數(shù)。我們要寫一個整數(shù)加法函數(shù)Add()。
IDispatch 及雙接口的調(diào)用 雙接口表示在一個接口中,同時支持自定義接口和 IDispatch
、IDispatch 接口和雙接口
使用者要想調(diào)用普通的 COM 組件功能,必須要加載這個組件的類型庫(Type library)文件 tlb(比如在 VC 中使用 #import)。然而,在腳本程序中,由于腳本是被解釋執(zhí)行的,所以無法使用加載類型庫的方式進(jìn)行預(yù)編譯。那么腳本解釋器如何使用 COM 組件那?這就是自動化(IDispatch)組件大顯身手的地方了。IDispatch 接口需要實(shí)現(xiàn)4個函數(shù),調(diào)用者只通過這4個函數(shù),就能實(shí)現(xiàn)調(diào)用自動化組件中所有的函數(shù)。這4個函數(shù)功能如下:
HRESULT GetTypeInfoCount( |
組件中提供幾個類型庫?當(dāng)然一般都是一個啦。 |
HRESULT GetTypeInfo( |
調(diào)用者通過該函數(shù)取得他想要的類型庫。 |
HRESULT GetIDsOfNames( |
根據(jù)函數(shù)名稱取得函數(shù)序號,為調(diào)用 Invoke() 做準(zhǔn)備。 |
HRESULT Invoke( |
根據(jù)序號,執(zhí)行函數(shù)。 |
從 Invoke() 函數(shù)的實(shí)現(xiàn)就可以看出,使用 IDispatch 接口的程序,其執(zhí)行效率是比較低的。ATL 從效率出發(fā),實(shí)現(xiàn)了一種叫“雙接口(dual)”的接口模式。下面我們來看看,到底什么是雙接口:
圖一、雙接口(dual) 結(jié)構(gòu)示意圖
從上圖中可以看出,所謂雙接口,其實(shí)是在一個 VTAB 的虛函數(shù)表中容納了三個接口(因?yàn)槿魏谓涌诙际菑?/span> IUnknown 派生的,所以就不強(qiáng)調(diào) IUnknown 了,叫做雙接口)。我們?nèi)绻麖娜我庖粋€接口中調(diào)用 QueryInterface()得到另外的接口指針的話,其實(shí),得到的指針地址都是同一個。雙接口有什么好處那?答:好呀,多好呀,特別好呀......
使用方式 |
因?yàn)?/span> |
所以 |
腳本語言使用組件 |
解釋器只認(rèn)識 IDispatch 接口 |
可以調(diào)用,但執(zhí)行效率最低 |
編譯型語言使用組件 |
它認(rèn)識 IDispatch 接口 |
可以調(diào)用,執(zhí)行效率比較低 |
編譯型語言使用組件 |
它裝載類型庫后,就認(rèn)識了 Ixxx 接口 |
可以直接調(diào)用 Ixxx 函數(shù),效率最高啦 |
結(jié)論 |
雙接口,既滿足腳本語言的使用方便性,又滿足編譯型語言的使用高效性。 |
|
如果所有函數(shù)都放在一個雙接口中,那么層次、結(jié)構(gòu)、分類不清 |
||
如果使用多個雙接口,則會產(chǎn)生其它問題(注4) |
||
雙接口、IDispatch接口只支持自動化的參數(shù)類型,使用受到限制,某些情況下很不方便嘍 |
||
還有很多弊病呦,不過現(xiàn)在我想不起來嘍...... |
使用方法
示例程序 |
自動化組件的使用方式 |
簡要說明 |
示例0 |
在腳本中調(diào)用 |
|
示例1 |
使用 API 方式調(diào)用 |
揭示 IDispatch 的調(diào)用原理,但傻子才去這么使用那,會累死了 |
示例2 |
使用 CComDispatchDriver 的智能指針包裝類 |
比直接使用 API 方式要簡單多啦,這個不錯! |
示例3 |
使用 MFC 裝載類型庫的包裝方式 |
簡單!好用!常用!但它本質(zhì)上是使用 IDispatch 接口,所以執(zhí)行效率稍差 |
示例4 |
使用 #import 方式加載類型庫方式 |
#import 方式使用組件,咱們在第七回中講過啦。常用!對雙接口組件,直接調(diào)用自定義接口函數(shù),不再經(jīng)過 IDispatch,因此執(zhí)行效率最高啦 |
錯誤與異常處理
事件和通知
流程:
客戶端啟動組件(Simple11.IEvent1.1)并得到接口指針 IEvent1 *;
調(diào)用接口方法 IEvent1::Advise() 把客戶端內(nèi)部的一個接收器(sink)接口指針(ICallBack *)傳遞到組件服務(wù)器中;
調(diào)用 IEvent1::Add() 去計(jì)算兩個整數(shù)的和;
但是計(jì)算結(jié)果并不通過該函數(shù)返回,而是通過 ICallBack::Fire_Result() 返回給客戶端;
當(dāng)客戶端不再需要接受事件的時候,調(diào)用 IEvent1::Unadvise() 斷開和組件的聯(lián)系。
連接點(diǎn)
持續(xù)性
屬性包