gin 中加上 GUID 以及讓 TPlugin 繼承自 TInterfacedObject 的目的,是為了讓物件擁有 Interfaced RTTI 以及自動參考計數的能力,這樣我們的 TPlugin 物件就會在沒有任何人使用它時自動釋放掉。私有成員 FForm 記錄了此 plugin 物件所建立的視窗的參考,以便控制其壽命,其型態也可以視需要改成 TBaseForm,那麼你的 TBaseForm 的設計得盡量不要經常修改,或者說設計得抽象一些,讓這些核心的類別在比較抽象的層次上面運作。
各個方法的名稱皆可望文生義,程式碼也很簡單,相信你可以猜個八九不離十,這裡就不一一列出,比較值得一提的是 CreateForm 函式與解構元 Destroy,分述如下:
TPlugin.CreateForm - 使用類別參考來建立物件
在 CreateForm 函式裡面,建立 Form 物件的那行程式是這麼寫的: FForm := g_ConcreteClass.Create(Application);
其中 g_ConcreteClass 是一個全域變數,其定義為: var
g_ConcreteClass: TBaseFormClass := nil;
而 TBaseFormClass 是一個類別參考型態(class-reference type),它跟 TBaseForm 定義在同一個單元裡面: type
TBaseFormClass = class of TBaseForm;
TBaseForm = class(TForm)
.....
end;
也就是說我們用一個類別參考型態的變數 g_ConcreteClass 來記錄欲實體化的類別型態,因此在建立 Form 物件之前還必須先設定 g_ConcreteClass 才行,如此 TPlugin 才能以正確的 Form 類別來進行實體化的動作。
您或許會想為什麼要這麼麻煩,直接寫成像 TCustomerForm.Create 這樣不就好了嗎?
簡單地說,是基於維護的考量。由於在整個 TPlugin 的實作裡面,日後唯一可以能會經常變動的就是要被實體化的 Form 類別,使用類別參考使我們免於在 TPlugin 的實作程式碼裡面把類別型態寫死,以後如果要實體化其他的 Form 類別,只要修改 g_ConcreteClass 這個變數就行了,不用再費一番搜尋及替換文字的功夫,還得擔心有沒有哪裡沒有改到;換句話說,我們等於使用類別參考來讓編譯器幫我們完成這個替換文字的動作,而且保證不會遺漏任何地方。
我交替使用了「建立物件」與「實體化」兩種詞彙,其實它們指的是同一件事情:建立某個類別的實體(instance)。
此技巧對於團隊開發也有好處,你只要公佈 TPlugin 和 TBaseForm 兩個單元,然後告訴組員照下面兩個步驟做就行了:
- 從 TBaseForm 衍生一個新類別(可以利用 Delphi 的物件寶庫來簡化這項工作)。
- 在這個新類別的單元的 Uses 子句裡加入 TPlugin 類別所屬的單元,並且在初始化階段把類別名稱指定給 g_ConcreteClass 變數。
在這個範例裡面,我們只有一個 TBaseForm 的後代,叫做 TForm1,因此在 TForm1 的單元裡面會有這一段: uses
DllExport; // TPlugin 類別實作放在這個單元裡面
.....
initialization
g_ConcreteClass := TForm1;
TPlugin.Destroy
解構函式會呼叫 DestroyForm 使 Form 物件一併釋放掉,並且還原 DLL 的 application handle: destructor TPlugin.Destroy;
begin
DestroyForm;
Application.Handle := g_DllAppHandle;
inherited Destroy;
end;
其中 g_DllAppHandle 是一個全域變數,其宣告如下: var
g_DllAppHandle: THandle;
而我們必須在 DLL 初始化的時候將 DLL 本身的 application handle 保存起來: initialization
g_DllAppHandle := Application.Handle;
其實如果 DLL 專案有用 "Build with runtime package" 選項的話,這個保存及還原 application handle 的動作就可以免了。相反地,若不加上保存及還原的動作,而且 DLL 專案不使用 "Build with runtime package" 選項的話,當 DLL 被釋放時就會發生主視窗也被一併關閉的怪異情形。
擅用原始的力量
到此重要的部分應該都已經提到了,您可能會發現我並沒有對 TBaseForm 多做說明,原因是在這個範例程式中 TBaseForm 並沒有什麼特別之處,只是為日後擴充時預留的一個基礎類別,你也許會想要將各個模組共用的功能和視覺化介面集中在此類別以簡化各模組的撰寫工作,以及讓應用程式有一致的操作方式和行為,這部分每個人的需求不同,就請您自行發揮了。
如果你覺得以上的程式碼過於片段零散,無法獲得整體的概念,建議您直接看範例的原始碼,把範例程式執行一遍以觀察程式運作的過程,不了解的地方再回來文件裡尋找解釋,這樣也許會比較容易些。為了方便閱讀,我也把範例程式中比較重要的兩個單元分別列在表一和表二裡面了。
列表一. DllUtils.pas
unit DllUtils;
interface
uses
Windows, Messages, SysUtils, Classes, Forms, Controls;
type
IPlugin = interface
[''''{D3F4445A-C704-42BC-8283-822541668919}'''']
function CreateForm(hMainForm: THandle): THandle;
procedure DestroyForm;
function ShowModalForm: Integer;
end;
TCreatePluginFunc = function (hApp: THandle): IPlugin; stdcall;
function DllCreatePlugin(hLib, hApp: THandle): IPlugin;
implementation
resourcestring
sErrorLoadingDLL = ''''無法載入模組!'''';
sErrorDllProc = ''''無法呼叫 DLL 函式: %s'''';
const
SDllCreatePluginFuncName = ''''CreatePlugin'''';
function DllCreatePlugin(hLib, hApp: THandle): IPlugin;
var
pProc: TFarProc;
CreatePluginFunc: TCreatePluginFunc;
begin
Result := nil;
if hLib = 0 then
Exit;
pProc := GetProcAddress(hLib, PChar(SDllCreatePluginFuncName));
if pProc = nil then
raise Exception.CreateFmt(sErrorDllProc, [SDllCreatePluginFuncName]);
CreatePluginFunc := TCreatePluginFunc(pProc);
Result := CreatePluginFunc(hApp);
end;
end.
列表二. DllExport.pas
unit DllExport;
interface
uses Windows, Classes, Forms, DllUtils, BaseFrm;
type
// Inherited from TInterfacedObject to be reference-counted.
TPlugin = class(TInterfacedObject, IPlugin)
private
FForm: TBaseForm;
public
destructor Destroy; override;
function CreateForm(hMainForm: THandle): THandle;
procedure DestroyForm;
function ShowModalForm: Integer;
end;
function CreatePlugin(hApp: THandle): IPlugin; export; stdcall;
exports
CreatePlugin;
var
g_ConcreteClass: TBaseFormClass := nil;
g_PluginIntf: IPlugin = nil;
g_DllAppHandle: THandle;
implementation
uses Dialogs, SysUtils;
function CreatePlugin(hApp: THandle): IPlugin;
begin
if hApp <> 0 then
Application.Handle := hApp; // Sync Application handle.
if g_PluginIntf = nil then
g_PluginIntf := TPlugin.Create;
Result := g_PluginIntf;
end;
{ TPlugin }
destructor TPlugin.Destroy;
begin
DestroyForm;
Application.Handle := g_DllAppHandle;
inherited Destroy;
end;
function TPlugin.CreateForm(hMainForm: THandle): THandle;
begin
if FForm = nil then
begin
Assert(g_ConcreteClass <> nil, ''''未設定欲實體化的 Form 類別名稱!'''');
FForm := g_ConcreteClass.Create(Application);
FForm.MainFormHandle := hMainForm;
end;
Result := FForm.Handle;
end;
procedure TPlugin.DestroyForm;
begin
if FForm <> nil then
begin
FForm.Release;
FForm := nil;
end;
Application.ProcessMessages;
end;
function TPlugin.ShowModalForm: Integer;
begin
if FForm = nil then
raise Exception.Create(''''DllExoprt: 視窗尚未建立!'''');
Result := FForm.ShowModal;
end;
initialization
g_DllAppHandle := Application.Handle;
end.
範例程式
範例程式可以按此處下載:PluginDLL.zip
下載壓縮檔並解開後,請先閱讀其中的 readme.txt。
可改進之處
你可以試著修改範例程式並強化它,使它可以當作實際開發專案的基礎框架,以下列出幾項可能的改進之處:
- 賦予 TBaseForm 基本的資料處理能力,像是新增、修改、刪除...等。
- 修改使之適用於 modeless form 及 MDI 應用程式。這意味著釋放 DLL 的時機也會改變,你可能會需要一個串列結構將載入的 DLL 記錄起來,通常一個 TStringList 就可以做到。
- 讓一個 plugin 物件可以建立並維護多個不同類型的 Form 物件。
你可能會希望一個 DLL 裡面可以提供多種 form 物件供主程式使用,這些 form 物件之間可能有某種程度的相似或相依關係。根據此需求我們可以整理出 plugin 物件具備以下兩個特性:
- plugin 物件可以建立多種不同類型的 form 物件,而它們都是繼承自基礎的表單類別 TBaseForm。
- 一個 DLL 裡面只需要一個 plugin 物件。
根據 [GHJV95] 書中的定義,Abstract Factory 的用意是:
「提供一個介面來建立同一族系或相依的物件,而毋須指明它們的具象類別(concrete class)」
而 Factory 通常也被實作成 Singleton,這些特性清楚地告訴我們 plugin 物件非常適合實作成一個 Factory。你可能需要在 TPlugin 類別裡面提供一個 RegisterClass 方法,這個方法取代了原先的類別參考型態,原本在 TBaseForm 子類別的單元裡設定 g_ConcreteClass 的敘述將會改成: PluginFactory.RegisterClass(TForm1);
註冊過的類別資訊將會被記錄在一個串列裡面。主程式則可以在建立 form 物件時透過字串來指定要建立的 form 類別名稱,像這樣: APlugin.CreateForm(''''TCustomerForm'''');
plugin 物件的 CreateForm 方法就會到串列中搜尋註冊過的類別,取得對應的類別參考並建立其實體(是不是有點像 COM 所做的事情?)。
嗯,我想這樣的提示應該夠了,最重要的還是要自己實際去撰寫及除錯程式碼以獲得更深刻的體會,真能如此,這個 Design Pattern 就會完全融入你的知識體系裡面,以後不加思索便可以運用自如了。
結語
在這份文件裡面主要是介紹以 Delphi 來設計 plugin 模組的實作過程,其中運用了介面程式設計的技巧(包括介面的參考計數以及物件生命週期的控制)以及 Design Patterns 來解決設計時遭遇的問題,這也是學習的重點之一。
在一個多人開發的專案裡,如果您的責任是設計主程式框架,當您要以 DLL 來切割應用程式時會怎麼做呢?這篇文章裡面展示了一種可能的設計方式,如果您有不同的想法或者對本文有任何建議,都很歡迎您來信指教。
Delphi 的 DLL 記憶體漏洞
最後,雖然不是本文的主題,但也頗值得注意的,就是動態載入的 DLL 在釋放時會有 4K 的記憶體漏洞,而且 Delphi 5 和 6 都有這個問題,你可以閱讀下面兩份文件,其中有詳細的說明並提供解決之道:
- Memory Lost And Found...And Release by Roy Nelson.
http://www.thedelphimagazine.com/samples/1328/1328.htm
- VCL leak fix for dynamic DLLs by Dejan Maksimovic.
http://codecentral.borland.com/codecentral/ccweb.exe/listing?id=16380
註1.
由於 DLL 版本的更新可能使得原本叫用它的程式無法正常運作,因此以不同的檔名區分版本(例如:MFCxx.DLL),使得硬碟裡面必須保存同一種 DLL 的多個版本,即使使用者將應用程式移除了,卻不敢放心的移除相關的 DLL 檔案,以免其他應用程式因為缺少了這個檔案而無法運作,這種情況所形成的問題稱為 DLL hell。COM 的出現有解決此問題的企圖(透過執行時期詢問元件支援的介面),但似乎並不理想,直到 .NET 的問世而終於有了比較好的解決方案。
註2.
可以到 http://www.geocities.com/huanlin_tsai/ 的〔心得分享〕區找到相關文章。不可諱言,以上所說的難免摻雜了個人的因素,也許其他人在使用 package 時並未發生上述問題,而且使用 package 的方式也有許多優點,在此僅將個人實際應用時的狀況與感覺描述出來,若有謬誤之處尚請各方不吝指正。
註3.
Singleton 樣式:提供單一窗口來建立類別的實體,以確保只有一個類別的實體存在。參考 [GHJV95] 的書。
參考資料
- Delphi 學習筆記。作者:錢達智。碁峰資訊,1998。
- [Cantu2001] Marco Cantu. Mastering Delphi 6. SYBEX, 2001.
- [Harmon2000] Eric Harmon. Delphi COM Programming. MTP, 2000.
- [GHJV95] E. Gamma, R. Helm, R. Johnson, J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995. 中文版:物件導向設計模式,葉秉哲。培生,2001。
上一页 [1] [2] [系统软件]用RegDllView揪出所有已注册的dll/ocx [系统软件]Windows系统的活动大陆基石 细看DLL文件 [系统软件]Windows的活动大陆:细看DLL文件 [常用软件]利用msdvm.dll实现微软虚拟桌面 [VB.NET程序]使用VB6.0设计ActiveX DLL事件 [VB.NET程序]Crystal Report(水晶报表)的报表封装成VB的DLL [VB.NET程序]用diskid.dll和disk32.dll获得硬盘序列号 [VB.NET程序]几个 WMI 的例子(初级) - 1 [VB.NET程序]几个 WMI 的例子(初级) - 2 [VB.NET程序]几个 WMI 的例子(初级) - 3
|