Delphi插件(Plug-ins)创建、调试与使用应用程序扩展(一)
有没有使用过Adobe Photoshop?如果用过,你就会对插件的概念比较熟悉。
对外行人来说,插件仅仅是从外部提供给应用程序的代码块而已(举个例子来说,在
一个DLL中)。一个插件和一个普通DLL之间的差异在于插件具有扩展父应用程序功能
的能力。例如,Photoshop本身并不具备进行大量的图像处理功能。插件的加入使其
获得了产生诸如模糊、斑点,以及其他所有风格的奇怪效果,而其中任何一项功能都
不是父应用程序自身所具有的。
对于图像处理程序来说这很不错,可是为什么要花偌大的力气去完成支持插件的商业
应用程序呢?假设,我们举个例子,你的应用程序要产生一些报表。你的客户肯定会
一直要求更新或者增加新的报表。你可以使用一个诸如Report Smith的外部报表生成
器,这是个不怎么样的解决方案,需要发布附加的文件,要对用户进行额外的培训,
等等。你也可以使用QuickReport,不过这会使你身处版本控制的噩梦之中--如果每
改变一次字体你就要Rebuild你的应用程序的话。
然而,只要你把报表做到插件中,你就可以使用它。需要一个新的报表吗?
没问题,只要安装一个DLL,下次应用程序启动时就会看见它了。另外一个例子是处理
来自外部设备(比如条形码扫描器)的数据的应用程序,为了给用户更多的选择,你
不得不支持半打的各种设备。通过将每种设备接口处理例程写成插件,不用对父应用
程序作任何变动就可以获得最大程度的可伸缩性。
入门
在开始写代码之前最重要的事情就是搞清楚你的应用程序到底需要扩展哪
些功能。这是因为插件是通过一个特定的接口与父应用程序交互的,而这个接口将
根据你的需要来定义。在本文中,我们将建立3个插件,以便展示插件与父应用程
序相交互的几种方式。
我们将把插件制作成DLL。不过,在做这项工作之前,我们得先制作一个外
壳程序来载入和测试它们。图1显示的是加载了第一个插件以后的测试程序。第一个
插件没有完成什么大不了的功能,实际上,它所做的只是返回一个描述自己的字符
串。不过,它证明了很重要的一点--不管有没有插件应用程序都可以正常运行。
如果没有插件,它就不会出现在已安装的插件列表中,但是应用程序仍然可以正常
的行使功能。
图1:插件测试外壳程序
我们的插件外壳程序与普通应用程序之间的唯一不同就在于工程源文件
中出现在uses子句中的Sharemem单元和加载插件文件的代码。任何在自身与子DLL
之间传递字符串参数的应用程序都需要Sharemem单元,它是DelphiMM.dll(Delphi
提供该文件)的接口。要测试这个外壳,需要将DelphiMM.dll文件从DelphiBin
目录复制到path环境变量所包含的路径或者应用程序所在目录中。发布最终版本
时也需要同时分发该文件。
插件通过LoadPlugins过程载入到这个测试外壳中,这个过程在主窗口
的FormCreate事件中调用,见图2。该过程使用FindFirst和FindNext函数在应用
程序所在目录中查找插件文件。找到一个文件以后,就使用图3所示的LoadPlugins过
程将其载入。
{ 在应用程序目录下查找插件文件 }
procedure TfrmMain.LoadPlugins;
var
sr: TSearchRec;
path: string;
Found: Integer;
begin
path := ExtractFilePath(Application.Exename);
try
Found := FindFirst(path + cPLUGIN_MASK, 0, sr);
while Found = 0 do begin
LoadPlugin(sr);
Found := FindNext(sr);
end;
finally
FindClose(sr);
end;
end;
图 2: 寻找插件
{ 加载指定的插件 DLL. }
procedure TfrmMain.LoadPlugin(sr: TSearchRec);
var
Description: string;
LibHandle: Integer;
DescribeProc: TPluginDescribe;
begin
LibHandle := LoadLibrary(Pchar(sr.Name));
if LibHandle <> 0 then
begin
DescribeProc := GetProcAddress(LibHandle,
cPLUGIN_DESCRIBE);
if Assigned(DescribeProc) then
begin
DescribeProc(Description);
memPlugins.Lines.Add(Description);
end
else
begin
MessageDlg(File " + sr.Name +
" is not a valid plug-in.,
mtInformation, [mbOK], 0);
end;
end
else
MessageDlg(An error occurred loading the plug-in " +
sr.Name + "., mtError, [mbOK], 0);
end;
图 3: 载入插件
LoadPlugin方法展示了插件机制的核心。首先,插件被写成DLL。其次,
通过LoadLibrary API它被动态的加载。一旦DLL被加载,我们就需要一个访问它
所包含的过程和函数的途径。API调用GetProcAddress提供这种机制,它返回一个
指向所需例程的指针。在我们这个简单的演示中,插件仅仅包含一个名为
DescribePlugin的过程,由常数cPLUGIN_DESCRIBE指定(过程名的大小写非常重
要,传递到GetProcAddress的名称必须与包含在DLL中的例程名称完全一致)。如
果在DLL中没有找到请求的例程,GetProcAddree将返回nil,这样就允许使
用Assigned函数测定返回值。
为了以一种易用的方式存储指向一个函数的指针,有必要为用到的变量
创建一个特定的类型。注意,GetProcAddress的返回值被存储在一个变量中,
DescribeProc,属于TpluginDescribe类型。下面是它的声明:
type
TPluginDescribe = procedure(var Desc: string); stdcall;
由于过程存在于DLL内部,它通过标准调用转换编译所有导出例程,
因此需要使用stdcall指示字。这个过程使用一个var参数,当过程返回的时候它包
含插件的描述。
要调用刚刚获得的过程,只需要使用保存地址的变量作为过程名,后面跟
上任何参数。就我们的例子而言,声明:
DescribeProc(Description)
将会调用在插件中获得的描述过程,并且用描述插件功能的字符串填充Description
变量。
构造插件
我们已经创建好了父应用程序,现在该轮到创建我们希望加载的插件了。
插件文件是一个标准的Delphi DLL,所以我们从Delphi IDE中创建一个新DLL工程,
保存它。由于导出的插件函数将用到字符串参数,所以要在工程的uses子句中把
Sharemen单元放在最前面。图4列出的就是我们这个简单插件的工程源文件。
uses
Sharemem, SysUtils, Classes,
main in main.pas;
{$E plg.}
exports
DescribePlugin;
begin
end.
图 4: 简单插件的工程源文件
虽然插件是一个DLL文件,但是没有必要一定要给它一个.DLL的扩展名。
实际上,一个原因就足以让我们有理由改变扩展名:当父应用程序寻找要加载的文
件时,新的扩展名可以作为特定的文件掩模。通过使用别的扩展名(我们的例子使
用了*.plg),你可以在一定程度上确信应用程序只会载入相应的文件。编译指示
字$X可以实现这个改变,也可以通过Project Options对话框的Application页来设
置扩展名。
第一个例子插件的代码是很简单的。图5显示了包含在一个新单元中的代
码。注意,DescribePlugin原型与外壳应用程序中的TpluginDescribe类型相一致,
使用附加的export保留字指定该过程将被导出。被导出的过程名称也将会出现在主
工程源代码的exports段中(在图4中列出)。
unit main;
inte易做图ce
procedure DescribePlugin(var Desc: string);
export; stdcall;
implementation
procedure DescribePlugin(var Desc: string);
begin
Desc := Test plugin v1.00;
end;
end.
图 5: 例子插件的主程序
在测试这
补充:软件开发 , Delphi ,