有没有使用过Adobe Photoshop如果用过你就会对插件的概念比较熟悉对外行人来说插件仅仅是从外部提供给应用程序的代码块而已(举个例子来说在一个DLL中)一个插件和一个普通DLL之间的差异在于插件具有扩展父应用程序功能的能力例如Photoshop本身并不具备进行大量的图像处理功能插件的加入使其获得了产生诸如模糊斑点以及其他所有风格的奇怪效果而其中任何一项功能都不是父应用程序自身所具有的
对于图像处理程序来说这很不错可是为什么要花偌大的力气去完成支持插件的商业应用程序呢?假设我们举个例子你的应用程序要产生一些报表你的客户肯定会一直要求更新或者增加新的报表你可以使用一个诸如Report Smith的外部报表生成器这是个不怎么样的解决方案需要发布附加的文件要对用户进行额外的培训等等你也可以使用QuickReport不过这会使你身处版本控制的噩梦之中——如果每改变一次字体你就要Rebuild你的应用程序的话
然而只要你把报表做到插件中你就可以使用它需要一个新的报表吗?没问题只要安装一个DLL下次应用程序启动时就会看见它了另外一个例子是处理来自外部设备(比如条形码扫描器)的数据的应用程序为了给用户更多的选择你不得不支持半打的各种设备通过将每种设备接口处理例程写成插件不用对父应用程序作任何变动就可以获得最大程度的可伸缩性
入门
在开始写代码之前最重要的事情就是搞清楚你的应用程序到底需要扩展哪些功能这是因为插件是通过一个特定的接口与父应用程序交互的而这个接口将根据你的需要来定义在本文中我们将建立个插件以便展示插件与父应用程序相交互的几种方式
我们将把插件制作成DLL不过在做这项工作之前我们得先制作一个外壳程序来载入和测试它们第一个插件没有完成什么大不了的功能实际上它所做的只是返回一个描述自己的字符串不过它证明了很重要的一点——不管有没有插件应用程序都可以正常运行如果没有插件它就不会出现在已安装的插件列表中但是应用程序仍然可以正常的行使功能
我们的插件外壳程序与普通应用程序之间的唯一不同就在于工程源文件中出现在uses子句中的Sharemem单元和加载插件文件的代码任何在自身与子DLL之间传递字符串参数的应用? 都需要Sharemem单元它是DelphiMMdll(Delphi提供该文件)的接口要测试这个外壳需要将DelphiMMdll文件从Delphi\Bin目录复制到path环境变量所包含的路径或者应用程序所在目录中发布最终版本时也需要同时分发梦募插件通过LoadPlugins过程载入到这个测试外壳中这个过程在主窗口的FormCreate事件中调用该过程使用FindFirst和FindNext函数在应用程序所在目录中查找插件文件找到一个文件以后就使用LoadPlugins过程将其载入
{ 在应用程序目录下查找插件文件 }
procedure TfrmMainLoadPlugins;
var
sr: TSearchRec;
path: string;
Found: Integer;
begin
path := ExtractFilePath(ApplicationExename);
try
Found := FindFirst(path + cPLUGIN_MASK sr);
while Found = do begin
LoadPlugin(sr);
Found := FindNext(sr);
end;
finally
FindClose(sr);
end;
end;
{ 加载指定的插件 DLL }
procedure TfrmMainLoadPlugin(sr: TSearchRec);
var
Description: string;
LibHandle: Integer;
DescribeProc: TPluginDescribe;
begin
LibHandle := LoadLibrary(Pchar(srName));
if LibHandle $#@;$#@; then
begin
DescribeProc := GetProcAddress(LibHandle cPLUGIN_DESCRIBE);
if Assigned(DescribeProc) then
begin
DescribeProc(Description);
memPluginsLinesAdd(Description);
end
else
begin
MessageDlg(File + srName + is not a valid plugin
mtInformation [mbOK] );
end;
end
else
MessageDlg(An error occurred loading the plugin +
srName + mtError [mbOK] );
end;
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单元放在最前面
uses
Sharemem SysUtils Classes
main in mainpas;
{$E plg}
exports
DescribePlugin;
begin
end
虽然插件是一个DLL文件但是没有必要一定要给它一个DLL的扩展名实际上一个原因就足以让我们有理由改变扩展名当父应用程序寻找要加载的文件时新的扩展名可以作为特定的文件掩模通过使用别的扩展名(我们的例子使用了*plg)你可以在一定程度上确信应用程序只会载入相应的文件编译指示字$X可以实现这个改变也可以通过Project Options对话框的Application页来设置扩展名
第一个例子插件的代码是很简单的注意DescribePlugin原型与外壳应用程序中的TpluginDescribe类型相一致使用附加的export保留字指定该过程将被导出被导出的过程名称也将会出现在主工程源代码的exports段中
unit main;
interface
procedure DescribePlugin(var Desc: string);
export; stdcall;
implementation
procedure DescribePlugin(var Desc: string);
begin
Desc := Test plugin v;
end;
end
在测试这个插件之前要先把它复制到主应用程序的路径下最简单的办法就是在主目录的子目录下创建插件然后把输出路径设置为主路径(Project Options对话框的Directories/Conditionals也可以作这个设置)
调试
现在介绍一下Delphi 中一个较好的功能从IDE中调试DLL的能力在DLL工程中可以通过Run paramaters对话框指定某程序为宿主应用程序这就是指向将调用DLL的应用程序的路径(在我们这个例子中就是刚刚创建的测试外壳程序)然后你就可以在DLL代码中设置断点并且按F运行它就像在一个普通应用程序中做的那样Delphi会运行指定的宿主程序并且通过编译带有调试信息的DLL把你指引到DLL代码内的断点处