前言
我写Delphi程序是从MIS系统入门的,开始尝试子系统划分的时候采用的是MDI窗体的结构。随着系统功能的扩充,不断有新的子系统加入系统中,单个工程会变得非常大,每次做一点修改都要重新编译,单个工程的形式也不利于团队协作。为了提高工作效率,我希望利用DLL动态链接库的形式实现插件结构的编程。
插件结构的编程需要一个插件容器来控制各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为后缀。
![image001.jpg]()
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会将登陆的用户名用参数传递过来,得到用户名后就可以进行一些权限控制,表现在界面初始化上。
其代码如下
1
unit UnitOfficeEntrance;
2
![]()
3
interface
4
uses
5
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms;
6
function ShowDLLForm(AHandle: THandle; ACaption: string; AUserID: string):
boolean
;stdcall;
7
function FreeDLLForm(AHandle: THandle; ACaption: string; AUserID: string):
boolean
;stdcall;
8
implementation
9
uses UnitOfficialMainForm;
//
改成MAINFORM的unit
10
var
11
DLL_Form:TFormOfficialMain;
//
改成MAINFORM的NAME
12
![]()
13
//
-----------------------------------------
14
//
Name: ShowDLLForm
15
//
Func: DLL插件调用入口函数
16
//
Para: AHandle 挂靠程序句柄; ACaption 本窗体标题
17
//
Rtrn: N/A
18
//
Auth: CST
19
//
Date: 2005-6-3
20
//
-----------------------------------------
21
22
function ShowDLLForm(AHandle: THandle; ACaption: string; AUserID: string):
boolean
;
23
begin
24
result:
=
true
;
25
try
26
Application.Handle:
=
AHandle;
//
挂靠到主程序容器
27
DLL_Form:
=
TFormOfficialMain.Create(Application);
//
改成MAINFORM的NAME
28
try
29
with DLL_Form
do
30
begin
31
Caption :
=
ACaption;
32
StatusBar.Panels.Items[
0
].Text :
=
AUserID;
33
//
Configure UI
34
Show ;
35
end;
36
except
37
on e:exception
do
38
begin
39
dll_form.Free;
40
end;
41
end;
42
except
43
result:
=
false
;
44
end;
45
end;
46
![]()
47
//
-----------------------------------------
48
//
Name: FreeDLLForm
49
//
Func: DLL插件调用出口函数
50
//
Para: AHandle 挂靠程序句柄
51
//
Rtrn: true/false
52
//
Auth: CST
53
//
Date: 2005-6-11
54
//
-----------------------------------------
55
56
function FreeDLLForm(AHandle: THandle; ACaption: string; AUserID: string):
boolean
;
57
begin
58
Application.Handle:
=
AHandle;
//
挂靠到主程序容器
59
if
DLL_Form.Showing then DLL_Form.Close;
//
如果窗口打开先关闭,触发FORM.CLOSEQUERY可取消关闭过程
60
if
not DLL_Form.Showing then
61
begin
62
DLL_Form.Free;
63
result:
=
true
;
64
end
//
仍然打开状态,说明CLOSEQUERY.CANCLOSE=FALSE
65
else
66
begin
67
result:
=
false
;
68
end;
69
end;
70
end.
DLL工程文件代码如下:
1
library Official;
2
![]()
3
![]()
4
![]()
{ Important note about DLL memory management: ShareMem must be the
5![]()
6
first unit in your library’s USES clause AND your project’s (select
7![]()
8
Project-View Source) USES clause if your DLL exports any procedures or
9![]()
10
functions that pass strings as parameters or function results. This
11![]()
12
applies to all strings passed to and from your DLL--even those that
13![]()
14
are nested in records and classes. ShareMem is the interface unit to
15![]()
16
the BORLNDMM.DLL shared memory manager, which must be deployed along
17![]()
18
with your DLL. To avoid using BORLNDMM.DLL, pass string information
19![]()
20
using PChar or ShortString parameters. }
21
![]()
22
![]()
23
uses
24
![]()
25
SysUtils,
26
![]()
27
Classes,
28
![]()
29
![]()
UnitOfficialDetailForm in ’UnitOfficialDetailForm.pas’
{FormOfficialDetail}
,
30
![]()
31
![]()
UnitOfficialMainForm in ’UnitOfficialMainForm.pas’
{FormOfficialMain}
,
32
![]()
33
UnitOfficeEntrance in ’UnitOfficeEntrance.pas’,
34
![]()
35
UnitOfficialClass in ’..\..\Public\Library\UnitOfficialClass.pas’,
36
![]()
37
UnitMyDataAdatper in ’..\..\Public\Library\UnitMyDataAdatper.pas’,
38
![]()
39
UnitMyHeaders in ’..\..\Public\Library\UnitMyHeaders.pas’;
40
![]()
41
![]()
42
![]()
{$R *.res}
43
![]()
44
exports ShowDLLForm,FreeDLLForm;
//
接口函数
45
46
begin
47
![]()
48
end.
容器程序的实现
1、接口函数的引入
调用DLL库中的函数有显式和隐式两种方式,显式调用更灵活,因此我们使用显示调用。在Delphi中需要为接口函数申明函数类型,然后实例化函数类型的实例,该实例实际是一个指向函数的指针,通过指针我们可以访问到函数并传递参数、获取返回值。在单元文件的Interface部分加入函数类的申明:
1
type
2
![]()
3
//
定义接口函数类型,接口函数来自DLL接口
4
5
TShowDLLForm
=
Function(AHandle:THandle; ACaption: String; AUserID:string):Boolean;stdcall;
6
![]()
7
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。
使用上文定义的函数类型定义函数指针变量,然后使用@操作符获得函数地址,这样就可以使用指针变量访问函数。主要代码如下:
1
……
2
var
3
ShowDLLForm: TShowDLLForm;
//
DLL接口函数实例
4
FreeDLLForm: TFreeDLLForm;
5
begin
6
try
7
begin
8
APlugin.ProcAddr :
=
LoadLibrary(PChar(sPath));
9
APlugin.FuncFreeAddr :
=
GetProcAddress(APlugin.ProcAddr,’FreeDLLForm’);
10
APlugin.FuncAddr :
=
GetProcAddress(APlugin.ProcAddr ,’ShowDLLForm’);
11
![]()
12
@ShowDLLForm:
=
APlugin.FuncAddr ;
13
@FreeDLLForm:
=
APlugin.FuncFreeAddr;
14
if
ShowDllForm(Self.Handle, APlugin.Caption , APlugin.UserID) then
15
Result:
=
True
16
……
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部分加入如下代码:
1
type
2
![]()
3
//
定义插件信息类
4
5
TMyPlugins
=
class
6
Caption:String;
//
DLL窗体标题
7
DllFileName:String;
//
DLL文件路径
8
WndClass:String;
//
窗体标识
9
UserID:string;
//
用户名
10
ProcAddr:THandle;
//
LOADLIBRARY载入的库句柄
11
FuncAddr:Pointer;
//
SHOWDLLFORM函数指针
12
FuncFreeAddr:Pointer;
//
FREEDLLFORM函数指针
13
end;
14
![]()
15
……
为每个插件创建一个TMyPlugins的实例,下文会讨论对这些实例的初始化方法。
3) 插件载入函数
在本示例中DLL窗口是在HALL中触发打开子窗口的事件中载入并显示的。按钮事件触发后,先根据插件结构体实例判断DLL是否已经加载,如果已经加载,则控制窗口的显示或关闭;如果没有加载则访问数据表将字段赋值到插件结构体中,然后执行载入、获得指针的工作。
局部代码如下
1
……
2
//
-----------------------------------------
3
![]()
4
//
Name: OpenPlugin
5
![]()
6
//
Func: 插件信息类控制过程: 初始化==》设置权限==》载入DLL窗口
7
![]()
8
//
Para: APlugin-TMyPlugins; sAlias别名; iFuncValue权限值
9
![]()
10
//
Rtrn: N/A
11
![]()
12
//
Auth: CST
13
![]()
14
//
Date: 2005-6-2
15
![]()
16
//
-----------------------------------------
17
18
procedure TFormHall.OpenPlugin(AFromActn: TAction ;APlugin:TMyPlugins; sAlias:string; sUserID:string);
19
var hWndPlugin:HWnd;
20
begin
21
22
//
判断插件窗口是否已经载入 hWndPlugin:=FindWindow(PChar(APlugin.WndClass),nil);
23
if
hWndPlugin
<>
0
then
//
插件窗口已经载入
24
begin
25
if
not IsWindowVisible(hWndPlugin) then
26
begin
27
AFromActn.Checked :
=
True;
28
ShowWindow(hWndPlugin,SW_SHOWDEFAULT);
//
显示
29
end
30
else
31
begin
32
AFromActn.checked :
=
False;
33
ShowWindow(hWndPlugin,SW_HIDE) ;
34
end;
35
Exit;
//
离开创建插件过程
36
end;
37
![]()
38
//
初始化插件类实例
39
40
if
not InitializeMyPlugins(APlugin,sAlias) then
41
begin
42
showmessage(’初始化插件类错误。’);
43
exit;
44
end;
45
![]()
46
//
获得当前权限值
47
48
APlugin.UserID :
=
sUserID;
49
//
载入DLL窗口
50
51
if
not LoadShowPluginForm(APlugin) then
52
begin
53
showmessage(’载入中心插件出错。’);
54
exit;
55
end;
56
end;
57
![]()
58
//
-----------------------------------------
59
//
Name: InitializeMyPlugins
60
//
Func: 初始化MYPLUGIN实例 (Caption | DllFileName | IsLoaded)
61
//
Para: APlugin-TMyPlugins
62
//
Rtrn: N/A
63
//
Auth: CST
64
//
Date: 2005-6-2
65
//
-----------------------------------------
66
67
function TFormHall.InitializeMyPlugins(APlugin:TMyPlugins; sAlias:String):Boolean;
68
var
69
strSQL:string;
70
myDA:TMyDataAdapter;
71
begin
72
Result:
=
False;
73
myDA:
=
TMyDataAdapter.Create;
74
strSQL:
=
’SELECT
*
FROM SystemModuleList WHERE modAlias
=
’
+
QuotedStr(sAlias);
75
try
76
myDA.RetrieveData(strSQL);
77
except
78
on E:Exception
do
79
begin
80
result:
=
false
;
81
myDA.Free ;
82
exit;
83
end;
84
end;
85
try
86
begin
87
with myDA.MyDataSet
do
88
begin
89
if
Not IsEmpty then
90
begin
91
APlugin.Caption:
=
FieldByName(’modName’).Value;
92
APlugin.DllFileName :
=
FieldByName(’modFile’).Value;
93
APlugin.WndClass :
=
FieldByName(’modWndClass’).Value ;
94
result:
=
True;
95
end;
96
Close;
97
end;
//
end of with
do
98
end;
//
end of try
99
except
100
on E:Exception
do
101
begin
102
Result:
=
False;
103
myDA.Free ;
104
Exit;
105
end;
//
end of exception
106
end;
//
end of try
except
107
108
myDA.Free ;
109
end;
110
![]()
111
![]()
112
![]()
113
//
-----------------------------------------
114
![]()
115
//
Name: LoadShowPluginForm
116
![]()
117
//
Func: 载入DLL插件并显示窗口
118
![]()
119
//
Para: APlugin-TMyPlugins
120
![]()
121
//
Rtrn: true-创建成功
122
![]()
123
//
Auth: CST
124
![]()
125
//
Date: 2005-6-2
126
![]()
127
//
-----------------------------------------
128
129
function TFormHall.LoadShowPluginForm (
const
APlugin:TMyPlugins):
boolean
;
130
![]()
131
var
132
ShowDLLForm: TShowDLLForm;
//
DLL接口函数实例
133
FreeDLLForm: TFreeDLLForm;
134
sPath:string;
//
DLL文件的完整路径
135
begin
136
try
137
begin
138
sPath:
=
ExtractFilepath(Application.ExeName)
+
’plugins\’
+
APlugin.DllFileName ;
139
APlugin.ProcAddr :
=
LoadLibrary(PChar(sPath));
140
APlugin.FuncFreeAddr :
=
GetProcAddress(APlugin.ProcAddr,’FreeDLLForm’);
141
APlugin.FuncAddr :
=
GetProcAddress(APlugin.ProcAddr ,’ShowDLLForm’);
142
@ShowDLLForm:
=
APlugin.FuncAddr ;
143
@FreeDLLForm:
=
APlugin.FuncFreeAddr;
144
if
ShowDllForm(Self.Handle, APlugin.Caption , APlugin.UserID) then
145
Result:
=
True
146
else
147
Result:
=
False;
148
end;
149
except
150
on E:Exception
do
151
begin
152
Result:
=
False;
153
ShowMessage(’载入插件模块错误,请检查PLUGINS目录里的文件是否完整。’);
154
end;
155
end;
156
end;
157
![]()
158
……
4) DLL窗口控制
正如3)中的代码说明的那样,DLL窗口的打开和关闭只是在表象层,关闭窗口并没有真正释放DLL窗口,只是调用API函数FindWindow根据窗口标识(就是Form.name)获得窗体句柄,用SHOWWINDOW函数的nCmdShow参数控制窗口显示/隐藏。
其实这是我这个程序实现的不好的一个地方,如果在DLL窗口中使用Self.close方法会引起内存错误,实在能力有限没有办法解决,因此出此下策。所以每个DLL程序主窗口的关闭按钮都必须隐藏掉。 :-P
5) DLL库的释放
在程序退出时,必须根据插件信息实例逐一释放DLL库。释放DLL库的函数如下:
1
procedure TFormHall.ClosePlugin(aPLG:TMyPlugins);
2
var
3
FreeDLLForm:TFreeDLLForm;
4
begin
5
if
aPLG.ProcAddr
=
0
then exit;
6
if
aPLG.FuncFreeAddr
=
nil then exit;
7
@FreeDLLForm:
=
aPLG.FuncFreeAddr;
8
if
not FreeDLLForm(Application.Handle,’’,’’) then
9
showMessage(’err’);
10
end;
小结
本实例程序运行效果如下:
![image008.jpg]()
我以上的方法中,因为有不少能力有限没有解决的问题,所以采用了一些看起来不太合理的掩饰方法,希望大家能在做了一点尝试后设计出更好的解决方法,我也希望能学到更多的好方法。