10.1 Windows的動態鏈接庫原理
動態鏈接庫(DLLs)是從C語言函數庫和Pascal庫單元的概念發展而來的。所有的C語言標准庫函數都存放在某一函數庫中,同時用戶也可以用LIB程序創建自己的函數庫。在鏈接應用程序的過程中,鏈接器從庫文件中拷貝程序調用的函數代碼,並把這些函數代碼添加到可執行文件中。這種方法同只把函數儲存在已編譯的.OBJ文件中相比更有利於代碼的重用。
但隨著Windows這樣的多任務環境的出現,函數庫的方法顯得過於累贅。如果為了完成屏幕輸出、消息處理、內存管理、對話框等操作,每個程序都不得不擁有自己的函數,那麼Windows程序將變得非常龐大。Windows的發展要求允許同時運行的幾個程序共享一組函數的單一拷貝。動態鏈接庫就是在這種情況下出現的。動態鏈接庫不用重復編譯或鏈接,一旦裝入內存,Dlls函數可以被系統中的任何正在運行的應用程序軟件所使用,而不必再將DLLs函數的另一拷貝裝入內存。
10.1.1 動態鏈接庫的工作原理
“動態鏈接”這幾字指明了DLLs是如何工作的。對於常規的函數庫,鏈接器從中拷貝它需要的所有庫函數,並把確切的函數地址傳送給調用這些函數的程序。而對於DLLs,函數儲存在一個獨立的動態鏈接庫文件中。在創建Windows程序時,鏈接過程並不把DLLs文件鏈接到程序上。直到程序運行並調用一個DLLs中的函數時,該程序才要求這個函數的地址。此時Windows才在DLLs中尋找被調用函數,並把它的地址傳送給調用程序。采用這種方法,DLLs達到了復用代碼的極限。
動態鏈接庫的另一個方便之處是對動態鏈接庫中函數的修改可以自動傳播到所有調用它的程序中,而不必對程序作任何改動或處理。
DLLs不僅提供了函數重用的機制,而且提供了數據共享的機制。任何應用程序都可以共享由裝入內存的DLLs管理的內存資源塊。只包含共享數據的DLLs稱為資源文件。如Windows的字體文件等。
10.1.2 Windows系統的動態鏈接庫
Windows本身就是由大量的動態鏈接庫支持的。這包括Windows API函數 ( KRNLx86.EXE,USER.EXE,GDI.EXE,…),各種驅動程序文件,各種帶有.Fon和.Fot 擴展名的字體資源文件等。Windows還提供了針對某一功能的專用DLLs,如進行DDE編程的ddeml.dll,進行程序安裝的ver.dll等。
雖然在編寫Windows程序時必然要涉及到DLLs,但利用Delphi ,用戶在大部分時候並不會注意到這一點。這一方面是因為Delphi提供了豐富的函數使用戶不必直接去使用Windows API;另一方面即使使用Windows API,由於Delphi把API函數和其它Windows DLLs函數重新組織到了幾個庫單元中,因而也不必使用特殊的調用格式。所以本章的重點放在編寫和調用用戶自定義的DLLs上。
使用傳統的Windows編程方法來創建和使用一個DLLs是一件很令人頭痛的事,正如傳統的Windows編程方法本身就令人生畏一樣。用戶需要對定義文件、工程文件進行一系列的修改以適應創建和使用DLLs的需要。Delphi的出現,在這一方面,正如在其它許多方面所做的那樣,減輕了開發者的負擔。更令人興奮的是Delphi利用DLLs 實現了窗體的重用機制。用戶可以將自己設計好的窗體儲存在一個DLLs中,在需要的時候可隨時調用它。
10.2 DLLs的編寫和調用
10.2.1 DLLs的編寫
在Delphi環境中,編寫一個DLLs同編寫一個一般的應用程序並沒有太大的區別。事實上作為DLLs 主體的DLL函數的編寫,除了在內存、資源的管理上有所不同外,並不需要其它特別的手段。真正的區別在工程文件上。
在絕大多數情況下,用戶幾乎意識不到工程文件的存在,因為它一般不顯示在屏幕上。如果想查看工程文件,則可以打開VIEw菜單選擇Project Source項,此時工程文件的代碼就會出現在屏幕的Code Editor(代碼編輯器)中。
一般工程文件的格式為:
program 工程標題;
uses 子句;
程序體
而DLLs工程文件的格式為:
library 工程標題;
uses 子句;
exprots 子句;
程序體
它們主要的區別有兩點:
1.一般工程文件的頭標用program關鍵字,而DLLs工程文件頭標用library 關鍵字。不同的關鍵字通知編譯器生成不同的可執行文件。用program關鍵字生成的是.exe文件,而用library關鍵字生成的是.dll文件;
2.假如DLLs要輸出供其它應用程序使用的函數或過程,則必須將這些函數或過程列在exports子句中。而這些函數或過程本身必須用export編譯指令進行編譯。
根據DLLs完成的功能,我們把DLLs分為如下的三類:
1.完成一般功能的DLLs;
2.用於數據交換的DLLs;
3.用於窗體重用的DLLs。
這一節我們只討論完成一般功能的DLLs,其它內容將在後邊的兩節中討論。
10.2.1.1 編寫一般DLLs的步驟
編寫一般DLLs的步驟如下:
1.利用Delphi的應用程序模板,建立一個DLLs程序框架。
對於Delphi 1.0的用戶,由於沒有DLLs模板,因此:
(1).建立一個一般的應用程序,並打開工程文件;
(2).移去窗體和相應的代碼單元;
(3).在工程文件中,把program改成library,移去Uses子句中的Forms,並添加適當的庫單元(一般SysUtils、Classes是需要的),刪去begin...end之間的所有代碼。
2.以適當的文件名保持文件,此時library後跟的庫名自動修改;
3.輸入過程、函數代碼。如果過程、函數准備供其它應用程序調用,則在過程、函數頭後加上export 編譯指示;
4.建立exports子句,包含供其它應用程序調用的函數和過程名。可以利用標准指示 name 、Index、resident以方便和加速過程/函數的調用;
5.輸入庫初始化代碼。這一步是可選的;
6.編譯程序,生成動態鏈接庫文件。
10.2.1.2 動態鏈接庫中的標准指示
在動態鏈接庫的輸出部分,用到了三個標准指示:name、Index、resident。
1.name
name後面接一個字符串常量,作為該過程或函數的輸出名。如:
exports
InStr name MyInstr;
其它應用程序將用新名字(MyInstr)調用該過程或函數。如果仍利用原來的名字(InStr),則在程序執行到引用點時會引發一個系統錯誤。
2.Index
Index指示為過程或函數分配一個順序號。如果不使用Index指示,則由編譯器按順序進行分配。
Index後所接數字的范圍為1...32767。使用Index可以加速調用過程。
3.resident
使用resident,則當DLLs裝入時特定的輸出信息始終保持在內存中。這樣當其它應用程序調用該過程時,可以比利用名字掃描DLL入口降低時間開銷。
對於那些其它應用程序常常要調用的過程或函數,使用resident指示是合適的。例如:
exports
InStr name MyInStr resident;
10.2.1.3 DLLs中的變量和段
一個DLLs擁有自己的數據段(DS),因而它聲明的任何變量都為自己所私有。調用它的模塊不能直接使用它定義的變量。要使用必須通過過程或函數界面才能完成。而對DLLs來說,它永遠都沒有機會使用調用它的模塊中聲明的變量。
一個DLLs沒有自己的堆棧段(SS),它使用調用它的應用程序的堆棧。因此在DLL中的過程、函數絕對不要假定DS = SS。一些語言在小模式編譯下有這種假設,但使用Delphi可以避免這種情況。Delphi絕不會產生假定DS = SS的代碼,Delphi的任何運行時間庫過程/函數也都不作這種假定。需注意的是如果讀者想嵌入匯編語言代碼,絕不要使SS和DS登錄同一個值。
10.2.1.4 DLLs中的運行時間錯和處理
由於DLLs無法控制應用程序的運行,導致很難進行異常處理,因此編寫DLLs時要十分小心,以確保被調用時能正常執行 。當DLLs中發生一個運行時間錯時,相應DLLs並不一定從內存中移去(因為此時其它應用程序可能正在用它),而調用DLLs的程序異常中止。這樣造成的問題是當DLLs已被修改,重新進行調用時,內存中保留的仍然可能是以前的版本,修改後的程序並沒有得到驗證。對於這個問題,有以下兩種解決方法:
1.在程序的異常處理部分顯式將DLL卸出內存;
2.完全退出Windows,而後重新啟動,運行相應的程序。
同一般的應用程序相比,DLL中運行時間錯的處理是很困難的,而造成的後果也更為嚴重。因此要求程序設計者在編寫代碼時要有充分、周到的考慮。
10.2.1.5 庫初始化代碼的編寫
傳統Windows中動態鏈接庫的編寫,需要兩個標准函數:LibMain和WEP,用於啟動和關閉DLL。在LibMain中,可以執行開鎖DLL數據段、分配內存、初始化變量等初始化工作;而WEP在從內存中移去DLLs前被調用,一般用於進行必要的清理工作,如釋放內存等。Delphi用自己特有的方式實現了這兩個標准函數的功能。這就是在工程文件中的begin...end部分添加初始化代碼。和傳統Windows編程方法相比,它的主要特色是:
1.初始化代碼是可選的。一些必要的工作(如開鎖數據段)可以由系統自動完成。所以大部分情況下用戶不會涉及到;
2.可以設置多個退出過程,退出時按順序依次被調用;
3.LibMain和WEP對用戶透明,由系統自動調用。
初始化代碼完成的主要工作是:
1.初始化變量、分配全局內存塊、登錄窗口對象等初始化工作。在(10.3.2)節“利用DLLs實現應用程序間的數據傳輸”中,用於數據共享的全局內存塊就是在初始化代碼中分配的。
2.設置DLLs退出時的執行過程。Delphi有一個預定義變量ExitProc用於指向退出過程的地址。用戶可以把自己的過程名賦給ExitProc。系統自動調用WEP函數,把ExitProc指向的地址依次賦給WEP執行,直到ExitProc為nil。
下邊的一段程序包含一個退出過程和一段初始化代碼,用來說明如何正確設置退出過程。
library Test;
{$S-}
uses WinTypes, WinProcs;
var
SaveExit: Pointer;
procedure LibExit; far;
begin
if ExitCode = wep_System_Exit then
begin
{ 系統關閉時的相應處理 }
end
else
begin
{ DLL卸出時的相應處理 }
end;
ExitProc := SaveExit; { 恢復原來的退出過程指針 }
end;
begin
{DLL的初始化工作 }
SaveExit := ExitProc; { 保存原來的退出過程指針 }
ExitProc := @LibExit; { 安裝新的退出過程 }
end.
在初始化代碼中,首先把原來的退出過程指針保存到一個變量中,而後再把新的退出過程地址賦給ExitProc。而在自定義退出過程LibExit結束時再把ExitProc的值恢復。由於ExitProc是一個系統全局變量,所以在結束時恢復原來的退出過程是必要的。
退出過程LibExit中使用了一個系統定義變量ExitCode,用於標志退出時的狀態。 ExitCode的取值與意義如下:
表10.1 ExitCode的取值與意義
━━━━━━━━━━━━━━━━━━━━━
取 值 意 義
—————————————————————
WEP_System_Exit Windows關閉
WEP_Free_DLLx DLLs被卸出
━━━━━━━━━━━━━━━━━━━━━
退出過程編譯時必須關閉stack_checking,因而需設置編譯指示 {$S-} 。
10.2.1.6 編寫一般DLLs的應用舉例
在下面的程序中我們把一個字符串操作的函數儲存到一個DLLs中,以便需要的時候調用它。應該注意的一點是:為了保證這個函數可以被其它語言編寫的程序所調用,作為參數傳遞的字符串應該是無結束符的字符數組類型(即PChar類型),而不是Object Pascal的帶結束符的Srting類型。程序清單如下:
library Example;
uses
SysUtils,
Classes;
{返回字符在字符串中的位置}
function InStr(SourceStr: PChar;Ch: Char): Integer; export;
var
Len,i: Integer;
begin
Len := strlen(SourceStr);
for i := 0 to Len-1 do
if SourceStr[i] = ch then
begin
Result := i;
Exit;
end;
Result := -1;
end;
exports
Instr Index 1 name 'MyInStr' resident;
begin
end.
10.2.2 調用DLLs
有兩種方法可用於調用一個儲存在DLLs中的過程。
1.靜態調用或顯示裝載
使用一個外部聲明子句,使DLLs在應用程序開始執行前即被裝入。例如:
function Instr(SourceStr : PChar;Check : Char); Integer; far; external 'UseStr';
使用這種方法,程序無法在運行時間裡決定DLLs的調用。假如一個特定的DLLs在運行時無法使用,則應用程序將無法執行。
2.動態調用或隱式裝載
使用Windows API函數LoadLibray和GetProcAddress可以實現在運行時間裡動態裝載DLLs並調用其中的過程。
若程序只在其中的一部分調用DLLs的過程,或者程序使用哪個DLLs, 調用其中的哪個過程需要根據程序運行的實際狀態來判斷,那麼使用動態調用就是一個很好的選擇。
使用動態調用,即使裝載一個DLLs失敗了,程序仍能繼續運行。
10.2.3 靜態調用
在靜態調用一個DLLs中的過程或函數時,external指示增加到過程或函數的聲明語句中。被調用的過程或函數必須采用遠調用模式。這可以使用far過程指示或一個{$F +}編譯指示。
Delphi全部支持傳統Windows動態鏈接庫編程中的三種調用方式,它們是:
● 通過過程/函數名
● 通過過程/函數的別名
● 通過過程/函數的順序號
通過過程或函數的別名調用,給用戶編程提供了靈活性,而通過順序號(Index)調用可以提高相應DLL的裝載速度。
10.2.4 動態調用
10.2.4.1 動態調用中的API函數
動態調用中使用的Windows API函數主要有三個,即:Loadlibrary,GetProcAddress和Freelibrary。
1.Loadlibrary: 把指定庫模塊裝入內存
語法為:
function Loadlibrary(LibFileName: PChar): THandle;
LibFileName指定了要裝載DLLs的文件名,如果LibFileName沒有包含一個路徑,則Windows按下述順序進行查找:
(1)當前目錄;
(2)Windows目錄(包含win.com的目錄)。函數GetWindowDirectory返回這一目錄的路徑;
(3)Windows系統目錄(包含系統文件如gdi.exe的目錄)。函數GetSystemDirectory返回這一目錄的路徑;
(4)包含當前任務可執行文件的目錄。利用函數GetModuleFileName可以返回這一目錄的路徑;
(5)列在PATH環境變量中的目錄;
(6)網絡的映象目錄列表。
如果函數執行成功,則返回裝載庫模塊的實例句柄。否則,返回一個小於HINSTANCE_ERROR的錯誤代碼。錯誤代碼的意義如下表:
表10.2 Loadlibrary返回錯誤代碼的意義
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
錯誤代碼 意 義
——————————————————————————————————————
0 系統內存不夠,可執行文件被破壞或調用非法
2 文件沒有被發現
3 路徑沒有被發現
5 企圖動態鏈接一個任務或者有一個共享或網絡保護錯
6 庫需要為每個任務建立分離的數據段
8 沒有足夠的內存啟動應用程序
10 Windows版本不正確
11 可執行文件非法。或者不是Windows應用程序,或者在.EXE映
像中有錯誤
12 應用程序為一個不同的操作系統設計(如OS/2程序)
13 應用程序為MS DOS4.0設計
14 可執行文件的類型不知道
15 試圖裝載一個實模式應用程序(為早期Windows版本設計)
16 試圖裝載包含可寫的多個數據段的可執行文件的第二個實例
19 試圖裝載一個壓縮的可執行文件。文件必須被解壓後才能被裝裁
20 動態鏈接庫文件非法
21 應用程序需要32位擴展
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
假如在應用程序用Loadlibrary調用某一模塊前,其它應用程序已把該模塊裝入內存,則Loadlibrary並不會裝載該模塊的另一實例,而是使該模塊的“引用計數”加1。
2.GetProcAddress:撿取給定模塊中函數的地址
語法為:
function GetProcAddress(Module: THandle; ProcName: PChar): TFarProc;
Module包含被調用的函數庫模塊的句柄,這個值由Loadlibrary返回。如果把Module設置為nil,則表示要引用當前模塊。
ProcName是指向含有函數名的以nil結尾的字符串的指針,或者也可以是函數的次序值。如果ProcName參數是次序值,則如果該次序值的函數在模塊中並不存在時,GetProcAddress仍返回一個非nil的值。這將引起混亂。因此大部分情況下用函數名是一種更好的選擇。如果用函數名,則函數名的拼寫必須與動態鏈接庫文件EXPORTS節中的對應拼寫相一致。
如果GetProcAddress執行成功,則返回模塊中函數入口處的地址,否則返回nil。
3.Freelibrary:從內存中移出庫模塊
語法為:
procedure Freelibrary(Module : THandle);
Module為庫模塊的句柄。這個值由Loadlibrary返回。
由於庫模塊在內存中只裝載一次,因而調用Freelibrary首先使庫模塊的引用計數減一。如果引用計數減為0,則卸出該模塊。
每調用一次Loadlibrary就應調用一次FreeLibray,以保證不會有多余的庫模塊在應用程序結束後仍留在內存中。
10.2.4.2 動態調用舉例
對於動態調用,我們舉了如下的一個簡單例子。系統一共包含兩個編輯框。在第一個編輯框中輸入一個字符串,而後在第二個編輯框中輸入字符。如果該字符包含在第一個編輯框的字符串中,則標簽框顯示信息:“位於第n位。”,否則顯示信息:“不包含這個字符。”。如圖是程序的運行界面。
輸入檢查功能的實現在Edit2的OnKeyPress事件處理過程中,程序清單如下。
procedure TForm1.Edit2KeyPress(Sender: TObject; var Key: Char);
var
order: Integer;
txt: PChar;
PFunc: TFarProc;
Moudle: THandle;
begin
Moudle := Loadlibrary('c:\dlls\example.dll');
if Moudle > 32 then
begin
Edit2.text := '';
Pfunc := GetProcAddress(Moudle,'Instr');
txt := StrAlloc(80);
txt := StrPCopy(txt,Edit1.text);
Order := TInstr(PFunc)(txt,Key);
if Order = -1 then
Label1.Caption := '不包含這個字符 '
else
Label1.Caption := '位於第'+IntToStr(Order+1)+'位';
end;
Freelibrary(Moudle);
end;
在利用GetProcAddess返回的函數指針時,必須進行強制類型轉換:
Order := TInstr(PFunc)(text,Key);
TInStr是一個定義好了的函數類型:
type
TInStr = function(Source: PChar;Check: Char): Integer;
10.3 利用DLLs實現數據傳輸
10.3.1 DLLs中的全局內存
Windows規定:DLLs並不擁有它打開的任何文件或它分配的任何全局內存塊。這些對象由直接或間接調用DLLs的應用程序擁有。這樣,當應用程序中止時,它擁有的打開的文件自動關閉,它擁有的全局內存塊自動釋放。這就意味著保存在DLLs全局變量中的文件和全局內存塊變量在DLLs沒有被通知的情況下就變為非法。這將給其它使用該DLLs的應用程序造成困難。
為了避免出現這種情況,文件和全局內存塊句柄不應作為DLLs的全局變量,而是作為DLLs中過程或函數的參數傳遞給DLLs使用。調用DLLs的應用程序應該負責對它們的維護。
但在特定情況下,DLLs也可以擁有自己的全局內存塊。這些內存塊必須用gmem_DDEShare屬性進行分配。這樣的內存塊直到被DLLs顯示釋放或DLLs退出時都保持有效。
由DLLs管理的全局內存塊是應用程序間進行數據傳輸的又一途徑,下面我們將專門討論這一問題。
10.3.2 利用DLLs實現應用程序間的數據傳輸
利用DLLs實現應用程序間的數據傳輸的步驟為:
1. 編寫一個DLLs程序,其中擁有一個用gmem_DDEShare屬性分配的全局內存塊;
2. 服務器程序調用DLLs,向全局內存塊寫入數據;
3. 客戶程序調用DLLs,從全局內存塊讀取數據。
10.3.2.1 用於實現數據傳輸的DLLs的編寫
用於實現數據傳輸的DLLs與一般DLLs的編寫基本相同,其中特別的地方是:
1. 定義一個全局變量句柄:
var
hMem: THandle;
2. 定義一個過程,返回該全局變量的句柄。該過程要包含在exports子句中。如:
function GetGlobalMem: THandle; export;
begin
Result := hMem;
end;
3. 在初始化代碼中分配全局內存塊:
程序清單如下:
begin
hMem := GlobalAlloc(gmem_MOVEABLE and gmem_DDEShare,num);
if hMem = 0 then
MessageDlg('Could not allocate memory',mtWarning,[mbOK],0);
end.
num是一個預定義的常數。
Windows API函數GlobalAlloc用於從全局內存堆中分配一塊內存,並返回該內存塊的句柄。該函數包括兩個參數,第一個參數用於設置內存塊的分配標志。可以使用的分配標志如下表所示。
表10.3 全局內存塊的分配標志
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
標 志 意 義
—————————————————————————————————
gmem_DDEShare 分配可由應用程序共享的內存
gmem_Discardable 分配可拋棄的內存(只與gmem_Moveable連用)
gmem_Fixed 分配固定內存
gmem_Moveable 分配可移動的內存
gmem_Nocompact 該全局堆中的內存不能被壓縮或拋棄
gmem_Nodiscard 該全局堆中的內存不能被拋棄
gmem_NOT_Banked 分配不能被分段的內存
gmem_Notify 通知功能。當該內存被拋棄時調用GlobalNotify函數
gmem_Zeroinit 將所分配內存塊的內容初始化為零
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
有兩個預定義的常用組合是:
GHND = gmem_Moveable and gmem_Zeroinit
GPTK = gmem_Fixed and gmem_Zeroinit
第二個參數用於設置欲分配的字節數。分配的字節數必須是32的倍數,因而實際分配的字節數可能比所設置的要大。
由於用gmem_DDEShare分配的內存在分配內存的模塊終止時自動拋棄,因而不必調用GlobalFree顯式釋放內存。
10.3.2.2 服務器程序的編寫
服務器程序必須包含對DLL的調用代碼,如:
function GetGlobalMem: THandle; far; external 'c:\dlls\glbmem';
通過調用該函數,服務器可以獲得全局內存塊的句柄。
在寫入數據前,服務器必須鎖定全局內存,以避免在寫入過程中Windows移動該內存塊的位置。
函數GlobalLock鎖定全局內存並返回指向該內存塊的指針:
pMem := GlobalLock(hMem);
對pMem的任何修改都會反映到全局內存塊中。
對內存塊進行操作後,調用GlobalUnLock進行解鎖。內存塊操作之後盡早解鎖,有利於Windows充分利用內存資源。
服務器寫入數據的實現代碼如下。
var
hMem: THandle;
pMem: PChar;
begin
hMem := GetGlobalMem; {獲得全局內存塊的句柄}
if hMem <> 0 then
begin
pMem := GlobalLock(hMem); {加鎖全局內存塊}
if pMem <> nil then
begin
StrPCopy(pMem,Memo1.text); {向全局內存塊寫入數據}
GlobalUnlock(hMem); {解鎖全局內存塊}
end
else
MessageDlg('Couldnot Lock memory block',mtWarning,[mbOK],0);
end;
10.3.2.3 客戶程序的編寫
客戶程序幾乎是服務器程序的翻版。唯一的區別在於一個是寫入數據,一個是下載數據。
下面是客戶從全局內存塊下載數據的程序清單。
var
hMem: THandle;
pMem: PChar;
begin
hMem := GetGlobalMem; {獲得全局內存塊的句柄}
if hMem <> 0 then
begin
pMem := GlobalLock(hMem); {加鎖全局內存塊}
if pMem <> nil then
begin
Memo1.text := StrPas(pMem); {從全局內存塊讀取數據}
GlobalUnlock(hMem); {解鎖全局內存塊}
end
else
MessageDlg('Couldnot Lock memory block',mtWarning,[mbOK],0);
end;
10.4 利用DLLs實現窗體重用
實現窗體重用是Delphi DLLs功能中一個引人注目的特色。當你創建了一個令自己滿意的通用窗體並希望能在不同應用程序中使用,特別是希望能在非Delphi 應用程序中使用時,把窗體做進一個動態鏈接庫中是最適當的。這樣即使用其它工具開發的應用程序,如C++、Visual Basic等,也都可以去調用它。
包含窗體的DLLs有100K左右的部件庫(Component Library)開銷。可以通過把幾個窗體編譯成一個DLLs來最小化這筆開銷。DLl中的不同窗體可以共享部件庫。
10.4.1 利用DLLs實現窗體重用的一般步驟
利用DLLs實現窗體重用的步驟是:
1.在集成開發環境(IDE)中,按自己的需要設計一個窗體;
2.編寫一個用於輸出的函數或過程。在該函數或過程中,設計的窗體被實例化;
3.重復步驟1、2,直到完成所有重用窗體的設計;
4.打開工程文件,進行修改,以適應生成 .dll文件的需要:
(1).把保留字program設為library;
(2).從uses子句中去掉Forms單元;
(3).移去begin,end之間的所有代碼;
(4).在uses子句下,begin…end塊之前,添加保留字exprots。exports 後是輸出函數名或過程名。
5.編譯生成DLLs文件;
6.在其它應用程序中調用重用窗體。
重用窗體的調用同一般DLLs函數或過程的調用完全一致,不再贅述。讀者可參看下面的例子。
10.4.2 窗體重用實例
下面我們通過一個具體的實例來說明窗體重用的設計過程。我們在一個名為passform.dll 的文件中儲存了一個口令設置窗口和一個口令檢查窗口。而後在一個Delphi 編寫的程序和一個VB編寫的程序中進行調用。事實證明這種方法是完全可行的。
10.4.2.1 窗體重用DLLs的設計
窗體重用DLLs的設計依照(10.4.1)中介紹的步驟進行。DLLs中的兩個窗體 SetPassWordForm和GetPassWordForm分別用於設置和檢查口令。它們的設計界面如圖所示。
窗體類TSetPassWordForm定義了兩個數據成員VerifIEd和PassWord,用於記錄口令確認狀態和設置的口令。TSetPassWordForm的定義如下:
type
TSetPassWordForm = class(TForm)
Label1: TLabel;
Edit1: TEdit;
OKBtn: TBitBtn;
CancelBtn: TBitBtn;
procedure FormCreate(Sender: TObject);
procedure Edit1KeyPress(Sender: TObject; var Key: Char);
private
{ Private declarations }
VerifIEd: Boolean;
public
{ Public declarations }
PassWord: PChar;
end;
窗口生成時,對數據成員和部件狀態進行初始化:
procedure TSetPassWordForm.FormCreate(Sender: TObject);
begin
VerifIEd := False;
PassWord := StrAlloc(40);
OKBtn.Enabled := False;
Label1.Caption := 'Please Input PassWord:';
end;
按鈕OKBtn在程序啟動時Enabled屬性設置為False,直到口令被正確設置後Enabled屬性才恢復為True。這樣就保證了只有口令被正確設置後,口令設置窗口才能正常關閉。否則只能按Cancel按鈕取消。
在口令設置代碼單元中定義了一個輸出函數SetPassWord,用於生成口令設置窗口並返回設置的口令:
function SetPassWord(PWord: PChar): Boolean;
var
SetPassWordForm: TSetPassWordForm;
begin
Result := False;
SetPassWordForm := TSetPassWordForm.Create(Application);
try
with SetPassWordForm do
if ShowModal = mrOK then
begin
StrCopy(PWord,StrUpper(PassWord));
Result := True;
end;
finally
SetPassWordForm.Free;
end;
end;
口令成功設置,把PassWord的值拷貝給PWord輸出,並返回True。應該注意的是由於 PWord本身就是指針類型,指向一個字符串的地址,因而雖然PWord用於輸出,但在參數表中仍為傳值參數,而不是傳址參數。另外調用函數StrCopy,要求PWord在傳入前已分配內存,否則會導致一個一般保護錯。try...finally用於保護窗口所占用內存資源在任何情況下都能正常釋放,讀者可參看第十二章。
在口令設置窗口中,為了確保用戶記住了設置的口令,在用戶輸入並按回車鍵後,要求用戶再次輸入進行確認。只有用戶重新輸入的字符串與原設置口令相同,口令設置窗口才能正常關閉 。否則將原設置口令清空,要求用戶再次輸入。以上功能的實現在編輯框的OnKeyPress事件處理過程中。
procedure TSetPassWordForm.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if Edit1.text = '' then Exit;
if Key = #13 then
begin
if VerifIEd then
if StrPas(PassWord) = Edit1.text then
begin
OKBtn.Enabled := True;
Edit1.Enabled := False;
OKBtn.SetFocus;
end
else
begin
VerifIEd := False;
MessageDlg('PassWord is InValid.',mtWarning,[mbOK],0);
Edit1.text := '';
PassWord := '';
Label1.Caption := 'Please Input PassWord:';
end
else
begin
VerifIEd := True;
StrPCopy(PassWord,Edit1.text);
Edit1.text := '';
Label1.caption := 'Please Verify PassWord:';
end;
Key := #0;
end;
end;
口令檢查窗口的實現相對簡單,只定義了一個輸出函數GetPassWord,用於生成口令檢查窗口並返回口令檢查的結果。
function GetPassword(PassWord: PChar): Boolean;
var
GetPasswordForm: TGetPassWordForm;
begin
Result := False;
GetPasswordForm := TGetPassWordForm.Create(Application);
try
with GetPassWordForm do
if ShowModal = mrOK then
if UpperCase(Edit1.Text) <> StrPas(StrUpper(PassWord)) then
MessageDlg('Invalid PassWord', mtWarning, [mbOK], 0)
else
Result := True;
finally
PassWordForm.Free;
end;
end;
PassWord為輸入的參數,不能為空,由調用以上函數的程序負責維護。
窗口中用戶輸入口令時回顯在屏幕上的字符由編輯框的PassWordChar屬性確定。
在DLLs的工程文件中,把兩個輸出函數寫到exports子句中。
library PassForm;
uses
GetPass in 'GETPASS.PAS' {PassWordForm},
Setpass in 'SETPASS.PAS' {SetPassWordForm};
exports
GetPassword,SetPassWord;
begin
end.
10.4.2.2 Delphi應用程序調用重用窗體
在Delphi應用程序中調用重用窗體,首先必須包含passform.dll的兩個輸出函數:
function GetPassword(PassWord: PChar): Boolean;
far; external 'c:\dlls\PassForm';
function SetPassword(PassWord: PChar): Boolean;
far; external 'c:\dlls\PassForm';
這位於程序單元的implementation部分。
口令設置部分的實現代碼為:
procedure TForm1.SetButtonClick(Sender: TObject);
begin
PassWord := StrAlloc(40);
if SetPassWord(PassWord) = False then
MessageDlg('PassWord is not set',mtInformation,[mbOK],0);
end;
首先為口令字符串分配內存。當口令設置窗體按Cancel按鈕取消時,顯示相應的信息。
口令檢查部分的實現代碼為:
procedure TForm1.TestButtonClick(Sender: TObject);
begin
if PassWord = nil then
begin
MessageDlg('Set passWord first', mtInformation, [mbOK], 0);
SetButton.SetFocus;
Exit;
end;
if GetPassword(PassWord) then
Label1.Caption := 'You are Wellcome !'
else
Label1.Caption := 'Sorry,You are InValid User.';
end;
根據口令檢查的結果,在標簽框中顯示相應的信息。
10.4.2.3 VB應用程序調用重用窗體
VB是微軟公司極力推薦的一個可視化開發工具。它雖然並不支持動態鏈接庫的創建,但可以調用標准的Windows API動態鏈接庫和用其它語言編寫的動態鏈接庫。為了驗證所生成DLLs的普適性,我們用VB開發了一個簡單的程序來調用passform.dll中儲存的窗體。
下面是VB程序的完整代碼,和Delphi程序的對應部分基本一致。
Option Explicit
Declare Function GetPassWord Lib "c:\dlls\passform.dll" (ByVal PassWord As String) As Integer
Declare Function SetPassWord Lib "c:\dlls\passform.dll" (ByVal PassWord As String) As Integer
Dim PassWord As String * 40
Sub Check_Click ()
If PassWord = "" Then
MsgBox ("Enter sample passWord first")
SetPass.SetFocus
Else
If GetPassWord(PassWord) Then
StatusLbl.Caption = "You are Welcome!"
Else
StatusLbl.Caption = "Sorry,You are Invalid User."
End If
End If
End Sub
Sub SetPass_Click ()
If SetPassWord(PassWord) = 0 Then
MsgBox ("PassWord is not Set.")
End If
End Sub
有關VB編程的一些具體問題,讀者可參看有關的VB參考書。
10.4.3 小結
本章我們討論的是動態鏈接庫編程。許多可視化開發工具(如Visual Basic)不支持 DLLs的創建,而Delphi在這裡又有上乘的表現。特別是窗體重用機制是Delphi對Windows下DLLs編程的一個重大改進。在一般的DLLs編程中也體現了Delphi快捷、方便的特點。動態鏈接庫是 Windows下程序組織的一種重要方式,使用動態鏈接庫可以極大地保護用戶在不同開發工具、不同時期所做的工作。利用動態鏈接庫,用戶可以逐步去構築自己的程序模塊庫,為今後的工作積累素材。