從 Dll 中導出類 ,想必大家首先想到的是使用 bpl 包。這種方式有一個不好 ,那就是使用者必須清楚這個包中含有那些類 ,也就是說必須知道類的名字 -- 這在一定意義上是個限制 ,試想一種情況 ,使用者定義了一個底層的抽象類 (abstract class),然後在此基礎上定義了許多應用類 (concrete class),那麼 ,對於使用者來說 ,他希望在不知道具體有哪些類的情況下能使用這些類 -- 這麼說似乎有些玄 ,但實際情況確實如此 ,因為定義抽象類的時候並不能預料到以後會有多少個具體類 -- 那麼這樣的需求 ,要靠什麼樣的技術來實現呢?
其實實現的技術的難度並不大 -- 作者在此將自己實踐的經驗獻給大家 ,算作拋磚引玉 ,希望能看到其他更好的方法!
以下先介紹該方法涉及的一些基礎知識 ,然後用一個例子來說明具體的實現。
一、基本概念
元類 (meta class),也叫類引用類型 (class-reference type),可以看成是一種類的類型 ,以該類型聲明的變量的值代表一個類。比如 : type
TClass = Class of TObject;
這樣就聲明了一個元類的類型。然後可以有這樣的變量聲明 :
Var
AClass: TClass;
那麼 ,就可以有這樣的用法 :
AClass := TObject;
或者 :
AClass := TButton;
或者 :
AClass := TForm;
等等。
因為 TClass 是一個 TObject 類型的元類 ,而 TButton,TForm 等都是自 TObject 派生而來 ,因而 TButton 和 TForm 這樣的值對於 AClass 都是可接受的。
然後 ,我們就可以運用多態的思想 ,靈活運用 AClass 這個類變量了。而這一點也正是下文具體實現的基礎知識。
二、具體實現
第一步 ,建立一個抽象類 :
我們使用這樣一個簡單的聲明 ,該抽象類只提供了一種抽象方法 ,但並不影響我們描述問題 :
TMyBaseForm = Class(TForm)
protected
function GetTitle: pchar; virtual; abstract;
end;
MyBaseFormClass = Class of TMyBaseForm;
暫不探討這麼一個抽象類提供了多少可供實用的方法和接口 ,因為我們要討論的是一種技術上的可行性。假設作者定義此接口的初衷只是希望獲得任意多變化的 Title,而具體 GetTitle 的返回值是什麼需要靠子類來實現。並且 ,作者還希望子類的代碼放在 Dll 中實現 ,與主程序分離 -- 這樣的方式很有些插件的味道 ,或許還能實現 Plug&Play 的某些特性 -- 是不是挺吸引人啊?那麼 ,下一不應該怎麼做呢?
首先主程序和 Dll 程序應當將上述聲明的單元包含進來 ,然後 ,主程序負責實現一個驅動 -- 動態加載 Dll,動態加載類 ; 而 Dll 負責實現子類。
先說 Dll 吧 ,Dll 應當做什麼工作?
第二步 ,Dll 中導出子類 :
我們設計了以下兩個導出函數 :
1 . function GetClassCount: integer; stdcall;
告訴調用者 ,本 Dll 中共有幾個子類 ;
2 . function GetClassTypeByIndex(const iIndex: integer;
var ClassType: MyBaseFormClass): WordBool; stdcall;
以索引方式獲得具體的子類。注意 ,此處的 ClassType 的類型是 MyBaseFormClass,這表明 ,它的值將是一個確定的自 TMyBaseForm 繼承而來的類。
以下是它們可能的一種實現 :
function GetClassCount: integer;
begin
result := 3; // 表明本 Dll 中導出了 3 個類
end;
function GetClassTypeByIndex(const iIndex: integer;
var ClassType: MyBaseFormClass): WordBool;
begin
result := True;
case iIndex of
0: ClassType := TFrmTest1;
1: ClassType := TFrmTest2;
2: ClassType := TFrmTest3;
else
result := False;
end;
end;
當然 ,在該單元的 Use 列表中應當將 TFrmTest1 、 TFrmTest2 以及 TFrmTest3 所在的單元包含進來。而 TFrmTest1 的實現可以象這樣 :
TFrmTest1 = Class(TMyBaseForm)
protected
function GetTitle: PChar; override;
end;
function TFrmTest1.GetTitle: Pchar;
begin
result := 'Hello from TFrmTest1';
end;
末了 ,別忘了將 GetClassCount 和 GetClassByIndex 加到 Exports 列表中。然後 ,Build 該 Dll 工程的時候 ,請將 Project option-package 中的 " 使用運行包 use runtime package" 打勾。至於具體的原因後面講。
至此 ,Dll 方面的工作告一段落。
第三步 ,主程序驅動引擎的實現 :
這一步相對來說容易些 -- 無非是動態加載 Dll,然後調用 GetClassCount 函數 ,接著調用 GetClassByIndex 。關鍵的代碼 :
Var AClass: TMyBaseClass;
AForm: TMyBaseForm;
I, iCount: integer;
blResult: Boolean;
begin
// 略去加載動態庫的部分 ,假定 FPGetClassProc 指向 GetClassCount 函數 ,FPGetClassByIndexProc 指向 GetClassByIndex,則 :
iCount := FPGetClassProc;
for I := 0 to iCount - 1 do
begin
AClass := FPGetClassByIndex(I, blResult);
if blResult then
begin
AForm := AClass.Create(Application);
AForm.Caption := AForm.GetTitle;
AForm.Show;
end;
end;
// …
end;
注意一點 ,和 Dll 相似 ,創建輸出文件的時候 ,也需要選擇使用運行時間包。這是因為 ,如果不使用運行時間包 ,將導致相同的類在內存中有多個副本 ,因而對它們使用 Is 操作符的將返回 False 的結果。