3 利用DLLs實現數據傳輸
3.1 DLLs中的全局內存
Windows規定:DLLs並不擁有它打開的任何文件或它分配的任何全局內存塊。這些對象由直接或間接調用DLLs的應用程序擁有。這樣,當應用程序中止時,它擁有的打開的文件自動關閉,它擁有的全局內存塊自動釋放。這就意味著保存在DLLs全局變量中的文件和全局內存塊變量在DLLs
沒有被通知的情況下就變為非法。這將給其它使用該DLLs的應用程序造成困難。
為了避免出現這種情況,文件和全局內存塊句柄不應作為DLLs的全局變量,而是作為DLLs中過程或函數的參數傳遞給DLLs使用。調用DLLs的應用程序應該負責對它們的維護。
但在特定情況下,DLLs也可以擁有自己的全局內存塊。這些內存塊必須用gmem_DDEShare屬性進行分配。這樣的內存塊直到被DLLs顯示釋放或DLLs退出時都保持有效。
由DLLs管理的全局內存塊是應用程序間進行數據傳輸的又一途徑,下面我們將專門討論這一問題。
3.2 利用DLLs實現應用程序間的數據傳輸
利用DLLs實現應用程序間的數據傳輸的步驟為:
1. 編寫一個DLLs程序,其中擁有一個用gmem_DDEShare屬性分配的全局內存塊;
2. 服務器程序調用DLLs,向全局內存塊寫入數據;
3. 客戶程序調用DLLs,從全局內存塊讀取數據。
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用於從全局內存堆中分配一塊內存,並返回該內存塊的句柄。該函數包括兩個參數,第一個參數用於設置內存塊的分配標志。可以使用的分配標志如下表所示。
表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顯式釋放內存。
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;
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;
4 利用DLLs實現窗體重用
實現窗體重用是Delphi DLLs功能中一個引人注目的特色。當你創建了一個令自己滿意的通用窗體並希望能在不同應用程序中使用,特別是希望能在非Delphi 應用程序中使用時,把窗體做進一個動態鏈接庫中是最適當的。這樣即使用其它工具開發的應用程序,如C++、Visual Basic等,也都可以去調用它。
包含窗體的DLLs有100K左右的部件庫(Component Library)開銷。可以通過把幾個窗體編譯成一個DLLs來最小化這筆開銷。DLl中的不同窗體可以共享部件庫。
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函數或過程的調用完全一致,不再贅述。讀者可參看下面的例子。
4.2 窗體重用實例
下面我們通過一個具體的實例來說明窗體重用的設計過程。我們在一個名為passform.dll 的文件中儲存了一個口令設置窗口和一個口令檢查窗口。而後在一個Delphi 編寫的程序和一個VB編寫的程序中進行調用。事實證明這種方法是完全可行的。
4.2.1 窗體重用DLLs的設計
窗體重用DLLs的設計依照(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.
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;
根據口令檢查的結果,在標簽框中顯示相應的信息。
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參考書。
4.3 小結
本章我們討論的是動態鏈接庫編程。許多可視化開發工具(如Visual Basic)不支持 DLLs的創建,而Delphi在這裡又有上乘的表現。特別是窗體重用機制是Delphi對Windows下DLLs編程的一個重大改進。在一般的DLLs編程中也體現了Delphi快捷、方便的特點。動態鏈接庫是
Windows下程序組織的一種重要方式,使用動態鏈接庫可以極大地保護用戶在不同開發工具、不同時期所做的工作。利用動態鏈接庫,用戶可以逐步去構築自己的程序模塊庫,為今後的工作積累素材。