一、前言
書接上回,
話說在 doc(Word) 複合檔案中,已經解決了儲存 xls(Excel) 資料的問題了。那麼,接下來又要解決另一個問題:當 WORD
程式讀取複合檔案,遇到了 xls 資料的時候,它該如何啟動 Excel 呢?啟動後,又如何讓 Excel 自己去讀入、解析、顯示 xls
資料呢?
二、CLSID 概念
有一個非常簡單的解決方案,那就是在對象資料的前面,儲存有處理這個資料的程式名。(見左上)
圖一、CLSID 的概念
這的確是一個簡單的方法,但同時問題也很嚴重。在“張三”的電腦上,Excel 的路徑是:"c:/office/Excel.exe",如果把這個 doc 檔案複製到“李四”的電腦上使用,而“李四”的 Excel 的路徑是:
"d:/Program files/Microsoft Office/Office/Excel.exe",完蛋了:-(
於是,微軟想出了一個解決方案,那就是不使用直接的路徑表示方法,而使用一個叫
CLSID(注1)的方式間接描述這些對象資料的處理常式路徑。CLSID
其實就是一個號碼,或者說是一個16位元組的數。觀察註冊表(),在HKCR/CLSID/{......}主鍵
下,LocalServer32(DLL組件使用InprocServer32) 中儲存著程式路徑名稱。CLSID 的結構定義如下:
typedef struct _GUID {
DWORD Data1;// 隨機數
WORD Data2;// 和時間相關
WORD Data3;// 和時間相關
BYTE Data4[8];// 和網卡MAC相關
} GUID;
typedef GUID CLSID; // 組件ID
typedef GUID IID; // 介面ID
#define REFCLSID const CLSID &
// 常見的聲明和賦值方法
CLSID CLSID_Excel = {0x00024500,0x0000,0x0000,{0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};
struct __declspec(uuid("00024500-0000-0000-C000-000000000046")) CLSID_Excel;
class DECLSPEC_UUID("00024500-0000-0000-C000-000000000046") CLSID_Excel;
// 註冊表中的表示方法
{00024500-0000-0000-C000-000000000046}
用一個號碼間接表示程式名,的確是個 Good idea,實現了組件位置的透明性,並方便地擴充出
DCOM(遠程組件)。但,但,但,但.....CLSID
有16個位元組共128位位元,幹嗎用這麼長的數字呀?遙想當年......我還在上幼兒園的時候,人們設計了 socket,用 TCP/IP
協議進行網路通訊。每個參與通訊的電腦都有一個4位元組的 IP 表示編號地址,範圍是 0,0,0,0 ~ 255,255,255,255
共42億個地址。可是沒想到啊,沒想到,自從 Internet 選擇了TCP/IP
協議後,42億個地址就不夠全世界的勞動人民分配啦。除了勞動人民,還有冰箱、彩電、電飯鍋、手機、手提電腦......這些都需要連網呀。在辦公室通過
網路開啟電飯鍋給我燜飯,下班回家後就能吃現成的啦,多幸福呀?!(註:在我們家老婆是領導,所以是我做飯。咳......)
由於前車之鑒,微軟這次設計 CLSID/IID 就使用了GUID概念的16個位元組,這下好啦,全世界60億人口,每個人每秒鐘分配10億個號碼,那麼需要分配1800億年。反正等到地球沒有了都不會使用完的:-)
三、產生 CLSID
- 如果使用開發環境編寫組件程式,則IDE會自動幫你產生 CLSID;
- 你可以手工寫 CLSID,但千萬不要和人家已經產生的 CLSID 重複呀,所以嚴重地不推薦;(可是微軟的CLSID都是手工寫的,這叫“只許州官放火,不許百姓點燈”) ;
- 程式中,可以用函數 CoCreateGuid() 產生 CLSID;
- 使用工具產生 GUID(注2);
vc6.0版本運行:"vc目錄/Common/Tools/GuidGen.exe"程式(你可以參照上迴文章中介紹的方法,把這個工具程式加到開發環境中,方便調用)。vc.net版本,在菜單“工具/建立GUID”中,就可以執行了。
四、ProgID 概念
每一個COM組件都需要指定一個
CLSID,並且不能重名。它之所以使用16個位元組,就是要從機率上保證重複是“不可能”的。但是,(世界上就怕“但是”二字)微軟為了使用方便,也支援
另一個字串名稱方式,叫 ProgID(注3)。見註冊表的ProgID 子鍵內容(注4)。由於 CLSID 和 ProgID
其實是一個概念的兩個不同的表示形式,所以我們在程式中可以隨便使用任何一種。(有些人就是討厭,說話不算數。明明 GUID
的目的就是禁止重複,但居然又允許使用 ProgID?!ProgID
是一個字串的名字,重複的可能性就太大了呀。趕明兒我也寫個程式,我打算這個程式的 ProgID
叫“Excel.Application”,嘿嘿)下面介紹一下 CLSID 和 ProgID 之間的轉換方法和相關的函數:
| 函數 |
功能說明 |
| CLSIDFromProgID()、CLSIDFromProgIDEx() |
由 ProgID 得到 CLSID。沒什麼好說的,你自己都可以寫,查註冊表貝 |
| ProgIDFromCLSID() |
由 CLSID 得到 ProgID,調用者使用完成後要釋放 ProgID 的記憶體(注5) |
| CoCreateGuid() |
隨機產生一個 GUID |
| IsEqualGUID()、IsEqualCLSID()、IsEqualIID() |
比較2個ID是否相等 |
| StringFromCLSID()、StringFromGUID2()、StringFromIID() |
由 CLSID,IID 得到註冊表中CLSID樣式的字串,注意釋放記憶體 |
五、介面(Interface)的來曆
到此,我們已經知道了 CLSID 或 ProgID 唯一地表示一個元件服務程式,那麼根據這些ID,就可以載入運行組件,並為用戶端程式提供服務了。(啟動組件程式的方法,會陸續介紹)。接下來先討論如何調用組件提供的函數?-----介面。
作為用戶端程式員,它希望或者說他要求:我的程式唯寫一次,然後不做任何修改就可以調用任意一個組件。舉例來說:
你可以在 Word 中嵌入 Excel,也可以嵌入 Picture,也可以嵌入任何第三方發表的 ActiveX 文檔......也就是說,連 Word 自己都不知道使用它的人將會在 doc 裡面插入什麼東東;
你可以在 HTML 檔案中插入一個 ActiveX,也可以插入一個程式指令碼Script,......你自己寫的外掛程式也可以插入到 IE 環境中。為了完成你的功能, 你絕對也不會去讓微軟修改IE吧?!
這個要求實在有點難度,Office 開發停滯了。說來話巧,一天老O(Office 項目的總工程師)和小B(VB 項目的總工程師)一起喝酒,老O向小B傾訴了他的煩惱:
老O:怎麼能讓我寫的程式C,可以調用其它人寫的程式S中的函數?(C表示客戶程式,S表示提供服務的程式)
小B:你是不是喝糊塗了?讓S作成 DLL,你去 LoadLibrary()、GetProcAddress()、...FreeLibrary()?!
老O:廢話!要是這麼簡單就好了。問題是,連我都不知道這個S程式是幹什麼的?能幹什嗎?我怎麼調用呀?
小B:哦......這個比較進階,但我現在不能告訴你,因為我怕你印象不深。
老O:~!·#¥%……—*......
小B:是這樣的,在VB中,我們制定了一個標準,這個標準允許任何一個VB開發人員,把他自己寫的某個功能的小程式放在VB的工具列上,這樣就好象他擴充了 VB 的功能一樣。
老O:哦?就是那個叫什麼 VBX 的濫玩意兒?
小B:我呸......別看 VBX 這個東西不起眼兒,的確我也沒看上它。但你猜怎麼著?現在有成千上萬的 VB 程式愛好者把他們寫的各式各樣功能的 VBX 小程式,放到網上,讓大家共用那。
老O:哦~~~,那你們的這個 VBX 標準是什嗎?
小B:嘿嘿......其實特簡單,就是在 VBX 中必須實現7個函數,這7個函數名稱和功能必須是:初始化、釋放、顯示、訊息處理......,而至於它內部想幹什麼,我也管不著。我只是在需要的時候調用我需要的這7個函數。
老O:哦~~~,這樣呀......對了,我現有個急事,我先走了。88,你付帳吧......
小B:喂!喂喂...... 走這麼急幹什麼,錢包都掉了:-)
老O雖然丟了錢包,仍然興奮地沖回辦公室,他開始了思考......
1、我的程式C,要能調用任何人寫的程式B。那麼B必須要按照我事先的要求,提供我需要的函數F1(),F2(),F3(),K1(),K2()。
2、BASIC 是解釋執行,因此它的函數不用考慮書寫順序,只要給出函數名,解譯器就能找到。但我使用的是 C++呀......
3、C++編譯後的代碼中沒有函數名,只有函數地址,因此我必須改進為用VTAB(虛函數表)表示函數入口:
圖二、VTAB 的結構
4、還不夠好,需要改進一下,因為所有的函數地址都放在一個表中會不靈活、不好修改、不易擴充。恩,有了!按照函數功能的類型進行分類:
圖三、多個 VTAB 的結構
5、
問題又來了,現在有2個 VTAB
虛函數表,那麼怎麼能夠從一個表找到另一個表那?恩又有辦法了,我要求你必須要實現一個函數,並且這個函數地址必須放在所有表的開頭(表中的第一個函數指
針),這個函數就叫
QueryInterface()吧,完成從一個表尋找到另一個表的功能:(除了QueryInterface()函數,順便也完成另外兩個函數,叫
AddRef() 和 Release()。這兩個函數的功能以後再說)
圖四、COM 介面結構
6、為了以後描述方便,不再使用(圖四)的方法了,而使用圖五這樣簡潔的樣式:
圖五、COM 介面結構的簡潔圖示
六、介面(Interface)概念
1、函數是通過 VTAB 虛函數表提供其地址, 從另一個角度來看,不管用什麼語言開發,編譯器產生的代碼都能產生這個表。這樣就實現了組件的“二進位特性”輕鬆實現了組件的跨語言要求。
2、假設有一個指標型變數儲存著 VTAB 的首地址,則這個變數就叫“介面指標”(注6), 變數命名的時候,習慣上加上"I"開頭。另外為了區分不同的介面,每個介面 也都要有一個名字,該名字就和 CLSID 一樣,使用 GUID 方式,叫 IID。
3、介面一經發表,就不能再修改了。不然就會出現向前相容的問題。這個性質叫“介面不變性”。
4、組件中必須有3個函數,QueryInterface、AddRef、Release,它們3個函數也組成一個介面,叫"IUnknown"。(注7)
5、任何介面,其實都包含了 IUnknown 介面。隨著你接觸到更多的介面就會了更體會解到介面的另一個性質“繼承性”。
6、在任何介面上,調用表中的第一個函數,其實就是調用 QueryInterface()函數,就得到你想要的另外一個介面指標。這個性質叫“介面的傳遞性”
7、
C/C++語言中需要事先對函式宣告,那麼就
會要求組件也必須提供C語言的標頭檔。不行!為了能使COM具有跨語言的能力,決定不再為任何語言提供對應的函數介面聲明,而是獨立地提供一個叫類型庫
(TLB)的聲明。每個語言的IDE環境自己去根據TLB產生自己語言需要的封裝。這個性質叫“介面聲明的獨立性”(注8)
七、客戶程式與組件之間的協商調用
回到我們的上一個話題,Word中嵌入一個組件,那麼Word是如何協商使用這個組件的那?下面是容器和組件之間的一個類比對話過程:
| |
容器 協商部分 |
組件 應答部分 |
| 1 |
根據CLSID啟動組件 。 CoCreateInstance() |
產生對象,執行建構函式,執行初始化動作。 |
| 2 |
你有IUnknown介面嗎? |
有,給你! |
| 3 |
恩,太好了,那麼你有IPersistStorage介面嗎?(注9) IUnknown::QueryInterface(IID_IPersistStorage...) |
沒有! |
| 4 |
真差勁,連這個都沒有。那你有IPersistStreamInit介面嗎?(注10) IUnknown::QueryInterface(IID_IPersistStreamInit...) |
哈,這個有,給! |
| 5 |
好,好,這還差不多。你現在給我初始化吧。 IPersistStreamInit::InitNew() |
OK,初始化完成了。 |
| 6 |
完成了?好!現在你讀資料去吧。 IPersistStreamInit::Load() |
讀完啦。我根據資料,已經在視窗中顯示出來了。 |
| 7 |
好,現在咱們各自處理使用者的滑鼠、鍵盤訊息吧...... |
...... |
| 8 |
哎呀!使用者要儲存退出程式了。你的資料被使用者修改了嗎? IPersistStreamInit::IsDirty() |
改了,使用者已經修改啦。 |
| 9 |
那好,那麼使用者修改後,你的資料需要多大的儲存空間呀? IPersistStreamInit::GetSizeMax() |
恩,我算算呀......好了,總共需要500KB。 |
| 10 |
暈,你這麼個小玩意居然佔用這麼大空間?!......好了,你可以存了。 IPersistStreamInit::Save() |
謝謝,我已經存好了。 |
| 11 |
恩。拜拜了您那。(注11) IPersistStreamInit::Release();IUnknown::Release() |
執行解構函式,刪除對象。 |
| 12 |
我自己也該退出了...... PostQuitMessage() |
|
容器(或者說用戶端)就是這樣和組件進行對話,協商調用的。如果組件甲實現了 IA 介面,那麼容器就會使用它,如果組件乙沒有提供
IA 介面,但是它提供了 IB 介面,那麼容器就會調用 IB
介面的函數......如此,容器程式根本就不需要知道組件到底是幹什麼的,組件到底是用什麼語言開發的,組件的磁碟位置到底在哪裡,它都可以正常運行。
太奇妙了!太精彩了!怎一個“爽”字了得!
八、小結
第二回中,介紹了兩個非常重要的概念:CLSID
和
Interface。由於全篇都是概念描述而沒有樣本程式相配合,可能讀者的理解還不太深入、不徹底。別著急,我們馬上就要進入到組件程式設計階段了,到
那個時候,你根據具體的程式碼,再回過頭來再次閱讀本迴文章,沒讀懂?哦......再讀!慢慢地您老人家就懂了:-)
留作業啦......
1、IDispatch 介面的 IID 是多少?(哎~~~ 笨笨,在來源程式中,用滑鼠右鍵執行Go to definition 呀)
2、IPicture 介面有幾個函數?功能是什嗎?(別玩了!你多大了?想不想在程式中顯示 JPG 映像呀,看 MSDN 去)
想知道為什麼COM函數總是返回 HRESULT 嗎?想知道如何使用 BSTR、VARIANT 嗎?想知道 COM
中應該如何使用記憶體嗎?想知道如何使用 UNICODE
嗎?......恩~~~,我現在不能告訴你,我現在告訴你,怕你印象不深!且聽下回分解......
注1:CLSID = Class ID 上回書已經介紹了把CLSID寫入複合檔案的函數:WriteClassStg()、IStorage::SetClass()。
注2:GUID 全域唯一標示符,CLSID/IID 其實是借用了GUID的概念。
注3:ProgID = Program ID,等價於 CLSID, 是用字串表示的。
注
4:註冊表子鍵 ProgID 和 VersionIndependentProgID 分別表示真正的 ProgID 和版本無關的
ProgID。比如在我電腦上安裝的 Excel,它的 ProgID = "Excel.Application.9",而
VersionIndependentProgID = "Excel.Application"。
注5:COM 組件的記憶體管理,見後續的文章。
注6:Interface = 介面,以前微軟不叫它介面,而叫協議Protocol。其實我 到認為這個詞更貼切一些。
注7:IUnknown 這個名字起的好,居然叫“我不知道”:-),它的 IID 叫 IID_IUnknown,如果用註冊表樣式表示,那麼它的值是{00000000-0000-0000-C000-000000000046}。
注8:TLB是由一個描述介面的檔案 IDL 經過編譯產生的。IDL 的說明,見後續的文章吧。
注9:IPersistStorage 是用複合檔案的儲存(Storage)功能來儲存/讀取資料用的一個介面。
注10:IPersistStreamInit 是用複合檔案的流(Stream)功能來儲存/讀取資料用的一個介面。
注11:拜拜了您那 = 英語北京話,再見。