插件結構的程式設計需要一個插件容器來控制各DLL的運作情況,將分割好的每個子系統安排到一個DLL庫檔案中。對每個DLL程式需要為容器預留介面函數,一般介面函數包含:啟動呼叫DLL函式庫的函數、關閉DLL函式庫的函數。透過介面函數,插件容器可以向DLL模組傳遞參數實現動態控制。具體實作細節我將在下文說明並給出回應代碼。
您可能需要先了解DELPHI中UNIT的結構,工程的結構。本文沒有深入討論DLL程式設計的理論細節,只是示範了一些實用的程式碼,我當時學習的是劉藝老師的《DELPHI深入程式設計》一書。
我也處於DELPHI的入門階段,只是覺得這次的DLL開發有一些值得討論的地方,所以寫這篇文章,希望各位能對我做的不好的地方慷慨建議。
範例程式簡介
為了方便閱讀我將使用一個MIS系統的部分程式碼來示範插件程式設計的一些方法。範例程式是典型的C/S結構DBMS應用程序,我們關注的部分將是框架程式(下文簡稱Hall)的控制語句和dll插件程式的回應控制。
1、程序結構
插件容器Hall使用一個獨立的工程創建,Hall的主視窗的作用相當於MDI程式中的MDI容器窗體,Hall中將明確呼叫Dll中的介面函數。
每個插件程式獨立使用各自的工程,與普通工程不同的是,DLL工程創建的是Dll Wizard,對應編譯產生的檔案是以DLL為後綴。
=550) window.open('/upload/20080315181507424.jpg');" src="http://files.VeVB.COm/upload/20080315181507424.jpg" onload="if(this.width>'550')this.width='550';if(this.height>'1000')this.height='1000';" border=0>
2、介面設計
實例程式Narcissus中我們預留兩個介面函數:
ShowDLLForm
此函數將應用程式的句柄傳遞給DLL子窗口,DLL程式將動態建立DLL窗體的實例。也可以將一些業務邏輯用參數的形式傳遞給DLL子窗口,例如窗體名稱、目前登陸的使用者名稱等。初次呼叫一個DLL窗體實例時使用此函數建立。
FreeDLLForm
函數會顯示釋放DLL視窗實例,在退出應用程式時呼叫每個DLL窗體的FreeDLLForm方法來釋放已建立的實例,不然會造成記憶體唯讀錯誤。同樣,也可以將一些在釋放窗體時需要做的業務邏輯用參數的形式傳遞給DLL窗體。
3.調試方式
DLL窗體程式無法直接執行,需要有一個插件容器來呼叫。應此我們需要先實作一個基本的Hall程序,然後將Hall.exe保存在固定的目錄中。對每個DLL工程做以下設定:
1) 開啟DLL工程
2) 選擇選單Run Parameters
3) 在彈出的視窗中瀏覽到我們的容器Hall.exe
這樣在調試DLL程序時將會自動呼叫Hall程序,利用Hall中預留的呼叫接口調試DLL程序。
插件程式的基本實現
DLL程式的設計方式和普通WINAPP沒有很大的區別,只是所有的視窗都是作為一種特殊的「資源」保存在DLL庫中,需要手動調用,而不像WINAPP中會有工程自動創建。宣告介面函數的方法很簡單
1) 在Unit的Implementation部分中聲明函數
2) 在函數宣告語句的尾部加上stdcall標記
3) 在工程代碼(Project View Source)的begin語句之前,用exports語句聲明函數接口
為了讓程式碼簡潔,我個人喜歡在工程中獨立添加一個Unit單元(File New -- Unit),然後將所有要輸出的函數體定義在此單元中,不要忘記將引用到的窗體的Unit也uses進來。我命名這個單元為UnitEntrance,在ShowDLLForm函數中初始化了要顯示的視窗並呼叫Show方法顯示,HALL會將登陸的用戶名用參數傳遞過來,得到用戶名後就可以進行一些權限控制,表現在介面初始化上。
其程式碼如下
unit UnitOfficeEntrance;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms;
function ShowDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;stdcall;
function FreeDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;stdcall;
implementation
uses UnitOfficialMainForm; // 改成MAINFORM的unit
var
DLL_Form:TFormOfficialMain; //改成MAINFORM的NAME
//-----------------------------------------
//Name: ShowDLLForm
//Func: DLL插件呼叫入口函數
//Para: AHandle 掛靠程式句柄; ACaption 本窗體標題
//Rtrn: N/A
//Auth: CST
//Date: 2005-6-3
//-----------------------------------------
function ShowDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;
begin
result:=true;
try
Application.Handle:=AHandle; //掛靠到主程式容器
DLL_Form:=TFormOfficialMain.Create(Application); //改成MAINFORM的NAME
try
with DLL_Form do
begin
Caption := ACaption;
StatusBar.Panels.Items[0].Text := AUserID;
//Configure UI
Show ;
end;
except
on e:exception do
begin
dll_form.Free;
end;
end;
except
result:=false;
end;
end;
//-----------------------------------------
//Name: FreeDLLForm
//Func: DLL外掛呼叫出口函數
//Para: AHandle 掛靠程式句柄
//Rtrn: true/false
//Auth: CST
//Date: 2005-6-11
//-----------------------------------------
function FreeDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;
begin
Application.Handle:=AHandle; //掛靠到主程式容器
if DLL_Form.Showing then DLL_Form.Close; //如果視窗開啟先關閉,觸發FORM.CLOSEQUERY可取消關閉過程
if not DLL_Form.Showing then
begin
DLL_Form.Free;
result:=true;
end //仍然開啟狀態,說明CLOSEQUERY.CANCLOSE=FALSE
else
begin
result:=false;
end;
end;
end.
DLL工程文件代碼如下:
library Official;
{ Important note about DLL memory management: ShareMem must be the
first unit in your library's USES clause AND your project's (select
Project-View Source) USES clause if your DLL exports any procedures or
functions that pass strings as parameters or function results. This
applies to all strings passed to and from your DLL--even those that
are nested in records and classes. ShareMem is the interface unit to
the BORLNDMM.DLL shared memory manager, which must be deployed along
with your DLL. To avoid using BORLNDMM.DLL, pass string information
using PChar or ShortString parameters. }
uses
SysUtils,
Classes,
UnitOfficialDetailForm in 'UnitOfficialDetailForm.pas' {FormOfficialDetail},
UnitOfficialMainForm in 'UnitOfficialMainForm.pas' {FormOfficialMain},
UnitOfficeEntrance in 'UnitOfficeEntrance.pas',
UnitOfficialClass in '../../Public/Library/UnitOfficialClass.pas',
UnitMyDataAdatper in '../../Public/Library/UnitMyDataAdatper.pas',
UnitMyHeaders in '../../Public/Library/UnitMyHeaders.pas';
{$R *.res}
exports ShowDLLForm,FreeDLLForm; //介面函數
begin
end.
一旦插件程式呼叫了DLL窗口,窗口實例將會保持在HALL窗口的上層,因此不用擔心遮擋的問題。
容器程式的實現
1、介面函數的引入
呼叫DLL函式庫中的函數有顯式和隱式兩種方式,顯式呼叫更靈活,因此我們使用顯示呼叫。在Delphi中需要為介面函數申明函數類型,然後實例化函數類型的實例,該實例實際上是一個指向函數的指針,透過指針我們可以存取函數並傳遞參數、取得返回值。在單元檔的Interface部分加入函數類別的申明:
type
//定義介面函數類型,介面函數來自DLL介面
TShowDLLForm = Function(AHandle:THandle; ACaption: String; AUserID:string):Boolean;stdcall;
TFreeDLLForm = Function(AHandle:THandle; ACaption: String; AUserID:string):boolean;stdcall;
顯示呼叫庫函數需要以下幾個步驟:
1) 載入DLL庫文件
2) 取得函數位址
3) 執行函數
4) 釋放DLL函式庫
接下來我們將詳細討論這幾個步驟。
2、載入DLL庫文件
透過呼叫API函數LoadLibrary可以將DLL函式庫載入到記憶體中,在此我們不討論DLL對記憶體管理的影響。 LoadLibrary的參數是DLL檔案的位址路徑,如果載入成功會回傳一個CARDINAL類型的變數作為DLL函式庫的句柄;如果目標檔案不存在或其他原因導致載入DLL檔案失敗會回傳一個0。
3.實例化介面函數
取得介面函數指標的API函數為GetProcAddress(庫檔句柄,函數名稱),若找到函數則會傳回函數的指針,若失敗則傳回NIL。
使用上文定義的函數類型定義函數指標變量,然後使用@運算元獲得函數位址,這樣就可以使用指標變數存取函數。主要程式碼如下:
……
var
ShowDLLForm: TShowDLLForm; //DLL介面函數實例
FreeDLLForm: TFreeDLLForm;
begin
try
begin
APlugin.ProcAddr := LoadLibrary(PChar(sPath));
APlugin.FuncFreeAddr := GetProcAddress(APlugin.ProcAddr,'FreeDLLForm');
APlugin.FuncAddr := GetProcAddress(APlugin.ProcAddr ,'ShowDLLForm');
@ShowDLLForm:=APlugin.FuncAddr ;
@FreeDLLForm:=APlugin.FuncFreeAddr;
if ShowDllForm(Self.Handle, APlugin.Caption , APlugin.UserID) then
Result:=True
……
4、一個具體的實作方法
為了結構化管理插件,方便今後的系統擴充,我們可以結合資料庫記錄可用的DLL信息,然後透過查詢資料庫記錄動態存取DLL程式。
1) 系統模組表設計
對於MIS系統,可以利用現有的DBS條件建立一個系統模組表,記錄DLL檔案及映射到系統模組中的相關信息
字段名作用類型
AutoID 索引INT
modAlias 模組別稱VARCHAR
modName 模組名稱VARCHAR
modWndClass 窗體唯一標識VARCHAR
modFile DLL路徑VARCHAR
modMemo 備註TEXT
・模組別稱是用來在程式設計階段統一命名的規則,特別是團隊開發時可以供隊員參考。
・模組名稱將作為ACAPTION參數傳遞給SHOWDLLFORM函數作為DLL視窗的標題。
・窗體唯一識別是DLL子模組中主視窗的CLASSNAME,用來在運作時決定要控制的視窗。
・DLL路徑儲存DLL檔案名稱,程式中將轉換為絕對路徑。
2) 插件資訊資料結構
定義一個記錄插件相關資訊的資料介面可以集中控制DLL插件。在Interface部分加入以下程式碼:
type
//定義插件資訊類
TMyPlugins = class
圖說:String; //DLL窗體標題
DllFileName:String; //DLL檔案路徑
WndClass:String; //窗體標識
UserID:string; //使用者名稱
ProcAddr:THandle; //LOADLIBRARY載入的函式庫句柄
FuncAddr:Pointer; //SHOWDLLFORM函數指針
FuncFreeAddr:Pointer; //FREEDLLFORM函數指標
end;
……
為每個外掛程式建立一個TMyPlugins的實例,下文會討論對這些實例的初始化方法。
3) 插件載入函數
在本範例中DLL視窗是在HALL中觸發開啟子視窗的事件中載入並顯示的。按鈕事件觸發後,先根據插件結構體實例判斷DLL是否已加載,如果已經加載,則控制視窗的顯示或關閉;如果沒有加載則訪問資料表將字段賦值到插件結構體中,然後執行載入、獲得指針的工作。
局部程式碼如下
……
//-----------------------------------------
//Name: OpenPlugin
//Func: 外掛程式資訊類別控制過程:初始化==設定權限 ==》載入DLL窗口
//Para: APlugin-TMyPlugins; sAlias別名; iFuncValue權限值
//Rtrn: N/A
//Auth: CST
//Date: 2005-6-2
//-----------------------------------------
procedure TFormHall.OpenPlugin(AFromActn: TAction ;APlugin:TMyPlugins; sAlias:string; sUserID:string);
var hWndPlugin:HWnd;
begin
//判斷插件視窗是否已經載入hWndPlugin:=FindWindow(PChar(APlugin.WndClass),nil);
if hWndPlugin <> 0 then //插件視窗已經載入
begin
if not IsWindowVisible(hWndPlugin) then
begin
AFromActn.Checked := True;
ShowWindow(hWndPlugin,SW_SHOWDEFAULT); //顯示
end
else
begin
AFromActn.checked := False;
ShowWindow(hWndPlugin,SW_HIDE) ;
end;
Exit; //離開創建插件過程
end;
//初始化插件類別實例
if not InitializeMyPlugins(APlugin,sAlias) then
begin
showmessage('初始化插件類別錯誤。');
exit;
end;
//取得目前權限值
APlugin.UserID := sUserID;
//載入DLL視窗
if not LoadShowPluginForm(APlugin) then
begin
showmessage('載入中心插件出錯。');
exit;
end;
end;
//-----------------------------------------
//Name: InitializeMyPlugins
//Func: 初始化MYPLUGIN實例(Caption | DllFileName | IsLoaded)
//Para: APlugin-TMyPlugins
//Rtrn: N/A
//Auth: CST
//Date: 2005-6-2
//-----------------------------------------
function TFormHall.InitializeMyPlugins(APlugin:TMyPlugins; sAlias:String):Boolean;
var
strSQL:string;
myDA:TMyDataAdapter;
begin
Result:=False;
myDA:=TMyDataAdapter.Create;
strSQL:='SELECT * FROM SystemModuleList WHERE modAlias='+QuotedStr(sAlias);
try
myDA.RetrieveData(strSQL);
except
on E:Exception do
begin
result:=false;
myDA.Free ;
exit;
end;
end;
try
begin
with myDA.MyDataSet do
begin
if Not IsEmpty then
begin
APlugin.Caption:= FieldByName('modName').Value;
APlugin.DllFileName := FieldByName('modFile').Value;
APlugin.WndClass := FieldByName('modWndClass').Value ;
result:=True;
end;
Close;
end; //end of with...do...
end; //end of try
except
on E:Exception do
begin
Result:=False;
myDA.Free ;
Exit;
end; //end of exception
end; //end of try...except
myDA.Free ;
end;
//-----------------------------------------
//Name: LoadShowPluginForm
//Func: 載入DLL外掛程式並顯示視窗
//Para: APlugin-TMyPlugins
//Rtrn: true-建立成功
//Auth: CST
//Date: 2005-6-2
//-----------------------------------------
function TFormHall.LoadShowPluginForm (const APlugin:TMyPlugins):boolean;
var
ShowDLLForm: TShowDLLForm; //DLL介面函數實例
FreeDLLForm: TFreeDLLForm;
sPath:string; //DLL檔的完整路徑
begin
try
begin
sPath:=ExtractFilepath(Application.ExeName)+ 'plugins/' + APlugin.DllFileName ;
APlugin.ProcAddr := LoadLibrary(PChar(sPath));
APlugin.FuncFreeAddr := GetProcAddress(APlugin.ProcAddr,'FreeDLLForm');
APlugin.FuncAddr := GetProcAddress(APlugin.ProcAddr ,'ShowDLLForm');
@ShowDLLForm:=APlugin.FuncAddr ;
@FreeDLLForm:=APlugin.FuncFreeAddr;
if ShowDllForm(Self.Handle, APlugin.Caption , APlugin.UserID) then
Result:=True
else
Result:=False;
end;
except
on E:Exception do
begin
Result:=False;
ShowMessage('載入插件模組錯誤,請檢查PLUGINS目錄裡的檔案是否完整。');
end;
end;
end;
……
4) DLL視窗控制
如同3)中的程式碼所說明的那樣,DLL視窗的開啟和關閉只是在表象層,關閉視窗並沒有真正釋放DLL窗口,只是呼叫API函數FindWindow根據視窗識別(就是Form.name)取得窗體句柄,用SHOWWINDOW函數的nCmdShow參數控制視窗顯示/隱藏。
其實這是我這個程式實現的不好的地方,如果在DLL視窗中使用Self.close方法會造成記憶體錯誤,實在能力有限沒有辦法解決,因此出此下策。所以每個DLL程式主視窗的關閉按鈕都必須隱藏掉。 :-P
5) DLL函式庫的釋放
在程式退出時,必須根據插件資訊實例逐一釋放DLL庫。釋放DLL函式庫的函數如下:
procedure TFormHall.ClosePlugin(aPLG:TMyPlugins);
var
FreeDLLForm:TFreeDLLForm;
begin
if aPLG.ProcAddr = 0 then exit;
if aPLG.FuncFreeAddr = nil then exit;
@FreeDLLForm:=aPLG.FuncFreeAddr;
if not FreeDLLForm(Application.Handle,'','') then
showMessage('err');
end;
小結
本實例程式運作效果如下:
=550) window.open('/upload/20080315181507979.jpg');" src="http://files.VeVB.COm/upload/20080315181507979.jpg" onload="if(this.width>'550')this.width='550';if(this.height>'1000')this.height='1000';" border=0>
在我以上的方法中,因為有不少能力有限沒有解決的問題,所以採用了一些看起來不太合理的掩飾方法,希望大家能在做了一點嘗試後設計出更好的解決方法,我也希望能學到更多的好方法。