組件對象模型(Component Object Model,以下簡稱COM)是組件對象之間相互接口的規范,凡是遵循COM接口規范的對象彼此之間能相互通信和交互,即使這些對象是由不同的廠商、用不同的語言、在不同的Windows版本甚至不同的機器上編寫和建立的。Delphi支持COM接口規范,Object Pascal語言增加了對象接口的方法。用Delphi創建的COM對象還可以工作在MTS(Microsoft Transaction Server)環境中。
軟件重用是業界追求的目標,人們一直希望能夠像搭積木一樣隨意“裝配”應用程序,組件對象就充當了積木的角色。所謂組件對象,實際上就是預定義好的、能完成一定功能的服務或接口。問題是,這些組件對象如何與應用程序、如何與其他組件對象共存並相互通信和交互?這就需要制定?個規范,讓這些組件對象按統一的標准方式工作。
COM是個二進制規范,它與源代碼無關。這樣,即使COM對象由不同的編程語言創建,運行在不同的進程空間和不同的操作系統平台,這些對象也能相互通信。COM既是規范,也是實現,它以COM庫(OLE32.dll和貼OLEAut32.dll)的形式提供了訪問COM對象核心功能的標准接口以及一組API函數,這些API函數用於創建和管理COM對象。COM本質上仍然是客戶服務器模式。客戶(通常是應用程序)請求創建COM對象並通過COM對象的接口操縱COM對象。服務器根據客戶的請求創建並管理COM對象。客戶和服務器這兩種角色並不是絕對的。
組件對象與一般意義上的對象既相似也有區別。一般意義上的對象是一種把數據和操縱數據的方法封裝在一起的數據類型的實例,而組件對象則使用接口(Interface)而不是方法來描述自己並提供服務。所謂接口,其精確定義是“基於對象的一組語義上相關的功能”,實際上是一個純虛類,真正實現接口的是接口對象)(Interface Object)。一個COM對象可以只有一個接口,例如Wndows 95/98外殼擴展;也可以有許多接口,例如Ac咖ex控件一般就有多個接口,客戶可以從很多方面來操縱ActiveX控件。接口是客戶與服務器通信的唯一途徑。如果一個組件對象有多個接口,則通過一個接口不能直接訪問其他接口。但是,COM允許客戶調用COM庫中的QueryInterface()去查詢組件對象所支持的其他接口。從這個意義上講,組件對象有點像接口對象的經紀人。
在調用QueryInterface()後,如果組件對象正好支持要查詢的接口,則QueryInterface()將返回該接口的指針。如果組件對象不支持該接口,則QueryInterface()將返回一個出錯信息。
所以,QueryInterface()是很有用的,它可以動態了解組件對象所支持的接口。接口是團向對象編程思想的一種體現,它隱藏了COM對象實現服務的細節。COM對象可以完全獨立於訪問它的客戶,只要接口本身保持不變即可。如果需要更新接口,則可以重新定義一個新的接口,對於使用老接口的客戶來說,代碼得到了最大程度的保護。
認識GUID、CLSID、IID
在一個復雜的系統中,可能充斥著大量的組件對象.每個組件對象可能又有大量的樓cJ為了保證這些接口彼此不會沖突,Microsoft規定用GUID來標識組件對象和接口。GUID是Globally Unique Identifier的縮寫.意為全局唯一標舊符.GUID可以標識組件對象的類,這時候GUID也稱為CLSID(Class Identifier的縮寫)。GUID也可以標識組件對象的接口,這時候GUID也稱為IID(Interface Identifier的縮寫)。
引用計數
引用計數是一種機制,使組件對象具有?定的“智能性”。它的工作原理是這樣的:當接口對象第一次創建時,引用計數的初始值為1。當有?-個客戶請求獲得接口對象的指針時,就調用AddRef()使該計數加1.當一個客戶不再需要組件對象的服務時.它應當調用Release()。注意,Release()並不真正釋放接口對象,因為可能還有其他客戶正在使用接口;Release()只是使引用計數減1。只有當引用汁數正好減為零時.接口對象才被刪除。下面舉例說明引用計數的作用。假設客戶A向服務器請求IMalloc接口,服務器收到請求後.首先看該接口對象是否存在。如果沒有.就創建?個接口對象,並凋用AddRef()使引用計數變為1,同時把該接口對象的指針傳遞給客戶A。假設這時候客戶B也加入進來,並且也是請求IMalloc接口。由於此時IMalloc接口對象己存在,所以服務器只是簡單地返回一個指針,並且調用AddRef()使引用計數變為2,當客戶A不再需要IMalloc接口時,它就調用Release()試圖釋放這個接口。顯然,這時候不能刪除Imalloc接口對象,因為客屍B還正用著呢。可見,引用計數這種機制使服務區知道如何管理自己的接口。
引用汁數這種機制也帶來?個問題,就是調用AddRef()和Release()不能出現混亂。一旦出現混亂,可能導致接口對象水遠不被刪除或者過早地被刪除。
虛擬方法表
COM是個二進制規范,任何開發環境只要遵守這個規范都可以生產出COM對象。COM采用一種稱為虛擬方法表的文法來解決方法調用。不過,COM接口與Objetc Pascal的類還是行-?些區別的:COM接口中凡是要表露給客戶的方法必須聲明為純虛的,客戶得到的只是指向虛擬方法表的指針,具體實現接口的是接口對象。
如果建立了同一個COM對象的多個實例,則虛擬方法表是共享的.但每個實例的數據是私有的。在DELPHI種,用abstract指示字來聲明純虛方法。例如:
TMyPureVirtualClass=class
public
procedure MyMethod;virtual;abstract;
…
end;
IUnknOwn接口
正如TObjetc是所有類的祖先一樣,IUnknown是所有接口的祖先。這樣,凡是取得了接口對象指針的客戶總是能訪問COM對象的核心服務,諸如AddRef(),Release()和QueryInterface(),這三個核心服務管理著接口對象的生存期。AddRef()和Release()比較簡單.都沒有參數。而QueryInterface()則比較復雜,它有兩個參數:一個是IID參數,用於指定要查詢的接口;另一個是Obj參數,用於返回找到的接口對象的指釘;如果COM對象不支持所查詢的接口,則Obj參數將返回nil。
AddRef()和Release()前均加了下劃線前綴,這是為了更加醒目。過去,COM對象必須自己維護引用計數,也就是說,必須調用AddRef()和Release()來把引用計數加1或減1。COM的另一個核心服務QueryInterface()也是不可缺少的,客戶只有調用QueryInterface()才能申請到另一個接口指針。由於采用了ActiveX框架,所以引用計數是有TComObject對象自動維護的,應用程序不再需要直接與IUnknown接口打交道。
這裡順便介紹一下COM模型中稱為Interface Aggregation的概念。面向對象的編程思想允許通過繼承(Inheritance)來實現軟件重用。在COM模型中沒有繼承的概念,而是通過Interface Aggregation技術把多個接口聚合起來,共同完成某一復雜的功能。
In-Process COM服務器的形式是DLL,它可以輸出COM對象,並映射到客戶的進程地址空間中運行。In-Process服務器的優勢在於,客戶可以直接調用COM對象的接口。
要創建一個In-Process COM服務器,先要建立一個ActiveX庫作為COM對象的容器。
為此,可以使用“File”菜單上的“New”命令,翻到“ActiveX”頁。 雙擊“Activex NbrW”圖標,就會自動創建一個Ac6vex庫。
一個In-Precess類型的COM服務器必須引出下面4個例程:
function DllRegisterServer:HResult;stdcall;
function DllUnRegisterServer:HResult;stdcall;
function DllGetClaasObject(const CLSID,II:TGUID;var obj):HResult;stdcall
function DllCanUnloadNow:HResult;stdcall;
ComServ單元已經實現了這幾個例程。因此,只要在項目文件中引出它們即可。DllRegisterServer()用於注冊COM服務器以及服務器中的所有COM對象。每個COM對象在注冊表的HKEY_CLASSES_ROOT\CLSID\{xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx}下都有各自的鍵。其中,(x……)代表COM對象的CLSID。對於In-Process類型的COM服務器來說,還有一個鍵叫InProcServer32,這個鍵的默認值是服務器文件在磁盤上的路徑。 DllUnregisterServer()用於撤消DllRegisterServer()所做的工作,即從注冊表中取消COM服務器以及COM對象的注冊。
DllGetClassObjetc()用於獲取一個COM對象的類工廠。CLSID參數用於指定COM對象的CLSID,HD參數用丁指定要獲取的類工廠的接口IID(通常設為IClassFactory的IID)。如果這個函數調用成功,obj參數將返回一個指向類工廠的指針。
DllCanUnloadNow()用於判斷COM服務器是否應當從內存中卸載。只要服務器中有一個COM對象被引用,這個函數就應當返回S_PALSE,表明DLL不應當卸載。如果服務器中沒有一個COM對象被引用,這個函數應當返回S_TRUE。
要在服務器中加入COM對象,可以使用“File”菜單上的“New”命令,翻到“ActiveX"頁,然後雙擊“COM Object”圖標,Delphi 5將啟動COM對象向導.這裡說的COM對象是非常簡單的。如果要創建特定形式的COM對象,諸如OLEAutomation對象或者ActiveX件.則必須使用Delphi提供的專門向導。具體方法如下:
1、在“Class Name”框內輸入C0M對象的類名,不必以T打頭。
2、在“Instancing”框內指定COM對象的實例模式。對於In?Process類型的服務器來說不必指定實例模式。
3、在“Threading Model",櫃內選擇一種線程模式,可以設為以下值:
Single:整個COM服務器都是單線程的
Apartment:每個COM對象的實例有單獨的線程。這樣,凡是需要共享的數據(諸如全局變量)必須用線程同步對象保護;
Free:一個COM對象的多個實例可以同時運行,這意味著COM對象必須保護自己的實例數據,以避免多個實例相沖突:
Both:同時支持Aartment和Free兩種線程模式。
在“Implementd Interfaces”框內輸入讓COM對象實現的接口名稱(可選)。默認情況下向導所創建的C0M對象只實現IUnknown接口。如果選中“Include Type Library”復選櫃,向導將生成一個類型庫。
如果選中“Mark interface OleAutormation”復選框,將使接口支持Ole Autormation。不過,類型庫中的數據類型必須是與Ole Autormation兼容的類型。單擊擊“OK”按鈕,向導將創建一個COM對象。如果選中丁“Include Type Library”復選櫃,向導將創建?個類型庫。同時,向導將生成COM對象的單元文件。
一個COM對象的單元:
Unit Unit2;
Interface
uses
windows,ActiveX,Classes,Comobj,Project2_TLB,StdVcl;
type
TXXH=class(TTypedComObjetc,IXXH)
Protectd
{Declare IXXH methods here)
end;
implementation
uses ComServ;
initialization
TTypedComObjetcFactory.Create(ComServer,TXXH,Class_XXH,ciMultiInstance,tmApartment);可以看出,用Delphi 5創建的COM對象,代碼非常簡潔,這主要是因為Object Pascal語言引入了對象接口的語法以及采用了ActiveX框架。接口對象是一個類,但保留字class後列山了兩個祖先:第一個祖先必須是TObject的派生類,這裡是TTypedComObjetc;第二個祖先是要實現的接口,這裡是IXXH。第一個祖先可以是其他已聲明過的接口對象,表示正在聲明的接口對象同時支持多個接口。接口的第一個成貝必須是CLSID。在某些需要傳遞CLSID常量的場合.可以直接用接口名稱來代替CLSID常量。當然,目前IXXH接口中還沒有其他成員。
COM對象的實例是通過類工廠來建立的。每個COM對象都有一個類工廠。類工廠本身的實例是在單元的initialization部分建立的。這樣,一旦COM服務器調入內存運行,就會創建類工廠的實例,也就隨時可以府客戶的請求創建COM對象的實例。
要讓Windows能找到COM服務器,COM服務器必須在Windows的注冊表中登記注冊。這需要借助於一個叫服REGSVR32.EXE的命令行程序。
如果沒有REGSVR32.EX,則可以用一個文本編輯器建立一個“注冊表項目”文件,其擴展名是.REG。“注冊表項目”文件應當遵循一定的格式。請參考下面的例子:
REGEDIT4
[HKEY_CLASSES_ROOT\CLSID\{0AA1740-310E-11D0-A45E-444553540000}]
@="MyCOMServer"
[HKEY_CLASSES_ROOT\CLSID\{0AA1740-310E-11D0-A45E-444553540000}\InProcServer32]
@="C:\\DELPHI\\COMServer\\MyComServer.DLL"
建立了注冊表項目文件後,只要在資源管理器中雙擊這個文件,Windows就會把“注冊表項目”文件中的信息加到注冊表中。注冊了COM服務器後,就可以打開Windows的注冊表,查看COM服務器的注冊情況。