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下程序组织的一种重要方式,使用动态链接库可以极大地保护用户在不同开发工具、不同时期所做的工作。利用动态链接库,用户可以逐步去构筑自己的程序模块库,为今后的工作积累素材。