延伸父应用
这个简单的插件不错,不过它不能做什么有用的事情。第二个例子就是纠正这个问题。
这个插件的目标就是在父应用程序的主菜单中加入一个项目。这个菜单项目,当被单
击时,就会执行插件内的一些代码。图6显示外壳程序的改进版,两个插件都已经加
载。在这个版本的外壳程序中,一个名为Plug-in的新菜单项目,被添加到主菜单中。
插件会在运行时加入一个菜单项。
图6:加载了两个插件的外壳程序的改进版
为了实现这个目的,我们必须在插件DLL中定义第二个接口。现有的DLL只导出了一
个过程,DescribePlugin。第二个插件将声明一个叫做InitPlugin的过程。不过,在
这个过程可以在主应用程序中看到以前,必须修改LoadPlugin来配合它。
图7所示的代码展示了改进的过程。
procedure TfrmMain.LoadPlugin(sr: TSearchRec);
var
Description: string;
LibHandle: Integer;
DescribeProc: TPluginDescribe;
InitProc: TPluginInit;
begin
LibHandle := LoadLibrary(Pchar(sr.Name));
if LibHandle <> 0 then
begin
// 查找 DescribePlugin.
DescribeProc := GetProcAddress(LibHandle,
cPLUGIN_DESCRIBE);
if Assigned(DescribeProc) then
begin
// 调用 DescribePlugin.
DescribeProc(Description);
memPlugins.Lines.Add(Description);
// 查找 InitPlugin.
InitProc := GetProcAddress(LibHandle, cPLUGIN_INIT);
if Assigned(InitProc) then
begin
// 调用 InitPlugin.
InitProc(mnuMain);
end;
end
else
begin
MessageDlg('File "' + sr.Name +
'" is not a valid plugin.',
mtInformation, [mbOK], 0);
end;
end
else
begin
MessageDlg('An error occurred loading the plugin "' +
sr.Name + '".', mtInformation, [mbOK], 0);
end;
end;
图 7: 改进过的LoadPlugin方法
如你所见,当GetProcAddress第一次查找调用描述过程之后,又调用了一次
GetProcAddress。这一次,我们要寻找的是常量cPLUGIN_INIT,定义如下:
const
cPLUGIN_INIT = 'InitPlugin';
返回值存储在TpluginInit类型的变量中,定义如下:
type
TPluginInit = procedure(ParentMenu: TMainMenu); stdcall;
当InitPlugin方法被执行时,父应用程序的主菜单被当作一个参数传递给它。这个
过程可以按照自己的意愿修改菜单。由于所有GetProcAddress的返回值都用assigned
测试,新版本的LoadPlugin过程仍然会加载不包含InitPlugin过程的第一个插件。在
这个过程中第一次调用寻找DescribePlugin方法会通过,第二次寻找InitPlugin会
无响应失败。
现在新的接口已经定义好了,可以为新的InitPlugin方法编写代码了。像原先一样,
新插件的实现代码存在于一个单独的单元中。图8显示了修改过的包含InitPlugin方法
的main.pas。
unit main;
interface
uses Dialogs, Menus;
type
THolder = class
public
procedure ClickHandler(Sender: TObject);
end;
procedure DescribePlugin(var Desc: string);
export; stdcall;
procedure InitPlugin(ParentMenu: TMainMenu);
export; stdcall;
var
Holder: THolder;
implementation
procedure DescribePlugin(var Desc: string);
begin
Desc := 'Test plugin 2 - Menu test';
end;
procedure InitPlugin(ParentMenu: TMainMenu);
var
i: TMenuItem;
begin
// 创建新菜单项.
i := NewItem('Plugin &Test', scNone, False, True,
Holder.ClickHandler, 0, 'mnuTest');
ParentMenu.Items[1].Add(i);
end;
procedure THolder.ClickHandler;
begin
ShowMessage('Clicked!');
end;
initialization
Holder := THolder.Create;
finalization
Holder.Free;
end.
图 8: 第二个插件的代码
很明显,对原始插件的第一个改变就是增加了InitPlugin过程。像原先一样,带有
export关键字的原型被加入到单元顶端的列表中,过程名也被加入到工程源代码的
exports子句列表中。这个过程使用NewItem函数创建一个新的菜单项,返回值是
TmenuItem对象。新菜单项通过下列语句被加入到应用程序主菜单中:
ParentMenu.Items[1].Add(I);
在测试外壳主菜单上的Items[1]是菜单项Plug-in,所以这个语句在Plugin菜单条
上添加一个叫Plug-in Test的菜单项。
为了处理对新菜单项的响应,作为它的第五个参数,NewItem可以接受一个
TNotifyEvent类型的过程,这个过程将在菜单项被点击时调用。不幸的是,按照定
义,这种类型的过程是一个对象方法,然而在我们的插件中并没有对象。如果我们想
用通常的指针来指向函数,那么得到的将只会是Delphi编译器的抱怨。所以,唯一的
解决办法就是创建一个处理菜单点击的对象。这就是Tholder类的用处。它只有一个方
法,是一个叫做ClickHandler的过程。一个叫做Holder的全局变量,在修改过的main.pas
的var段中被声明为Tholder类型,并且在单元的initialization段中被创建。现在我们就
有一个对象了,我们可以拿它的方法(Holder.ClickHandler)当作NewItem函数的参数。
搞了这一通,ClickHandler除了显示一个"Clicked!"消息对话框以外什么以没干。
也许这不怎么有趣,不过它仍然证明了一点:插件DLL成功的修改了父应用的主菜单,
表现了它的新用途。并且如同第一个例子一样,不管这个插件在不在应用程序都能执行。
由于我们创建了一个对象来处理菜单点击,那么在不再需要这个插件时,就要释放这
个对象。修改后的单元中会在finalization段中处理这件事情。Finalization端时与
initialization段相对应的,如果前面有一个initialization段,那么在应用程序终
止时finalization段一定会得到执行。把下面的语句
Holder.Free
加到finalization段中,以确保Holder对象会被正确的释放。
显而易见,虽然这个插件只是修改了外壳应用的主菜单,但是它可以轻易地操纵传
递到InitPlugin过程中的任何其他对象。如果有必要,插件也可以打开自己的对话框,
向列表框(List boxes)和树状视图(tree views)中添加项目,或者在画布(canvas)
中绘画。
事件驱动的插件
到现在为止我们所描述的技术可以产生一种通用的扩展应用程序的方法。通过增加
新菜单、窗体和对话框,就可以实现全新的功能而不必对父应用做任何修改。不过仍
然有一个限制:这只是一种单侧(one-sided)机制。正如所看到的,系统依赖用户的
某些操作才能启动插件代码,比如点击菜单或者类似的动作。代码运行起来以后,又要
依靠另外一个用户动作来停止它,例如,关闭插件可能已经打开的窗体。克服这种缺
陷的一种可行的方法就是使插件可以响应父应用中的动作--模仿在Delphi中工作地
很好的事件驱动编程模型的确有效。
在最后一个例子插件中,我们将创建一种机制,插件可以藉此响应父应用中产生的事
件。通常情况下,可以通过判定需要触发哪些事件、在父应用中为每个事件创建一个
Tlist对象来实现。然后每个Tlist对象都被传递到插件的初始化过程中,如果插件想
在某个事件中执行动作,它就把负责执行的函数地址加入到对应的TList中。父应用在
适当的时刻循环这些函数指针的列表,按次序调用每个函数。通过这种方法,就为多
个插件在同一事件中执行动作提供了可能。
应用程序产生的事件完全依赖于程序已确定的功能。例如,一个TCP/IP网络应用程序
可能希望通过TclientSocket的onRead事件通知插件数据抵达,而一个图形应用程序可
能对调色板的变化更感兴趣。
为了说明事件驱动的插件应答的概念,我们将创建一个用于限制主窗口最小尺寸
的插件。这个例子有点儿造作,因为把这个功能做到应用程序里边会比这简单的多。
不过这个例子的优点在语容易编码而且易于理解,而这正是本文想要做到的。
很明显,我们要做的第一件事情就是决定到底要产生哪些事件。在本例中,答案
很简单:要限制一个应用程序窗口的尺寸,有必要捕获并且修改Windows消息
WM_GETMINMAXSINFO。因此,要创建一个完成这项功能的插件,我们必须捕获这个消
息并且在这个消息处理器中调用插件例程。这就是我们要创建的事件。
接下来我们要创建一个TList来处理这个事件。在主窗体的initialization段中将
会创建lstMinMax对象,然后,创建一个消息处理器来捕获Windows消息
WM_GETMINMAXINFO。图9中的代码显示了这个消息处理器。
{ 捕获 WM_GETMINMAXINFO. 为每个消息调用插件例程. }
procedure TfrmMain.MinMaxInfo(var msg: TMessage);
var
m: PMinMaxInfo; file://在 Windows.pas 中定义.
i: Integer;
begin
m := pointer(msg.Lparam);
for i := 0 to lstMinMax.count -1 do begin
TResizeProc(lstMinMax[i])(m.ptMinTrackSize.x,
m.ptMinTrackSize.y);
end;
end;
图 9: WM_GETMINMAXINFO 的消息处理器
外壳应用的LoadPlugin过程必须再次修改以便调用初始化例程。这个新初始化
函数把我们的TList当作参数接受,在其中加入修改消息参数的函数地址。图10
显示了LoadPlugin过程的最终版本,它可以执行到目前为止所讨论的全部几个插件
的初始化工作。
{ 加载指定的插件DLL. }
procedure TfrmMain.LoadPlugin(sr: TSearchRec);
var
Description: string;
LibHandle: Integer;
DescribeProc: TPluginDescribe;
InitProc: TPluginInit;
InitEvents: TInitPluginEvents;
begin
LibHandle := LoadLibrary(Pchar(sr.Name));
if LibHandle <> 0 then
begin
// 查找 DescribePlugin.
DescribeProc := GetProcAddress(LibHandle,
cPLUGIN_DESCRIBE);
if Assigned(DescribeProc) then
begin
// 调用 DescribePlugin.
DescribeProc(Description);
memPlugins.Lines.Add(Description);
file://查找InitPlugin.
InitProc := GetProcAddress(LibHandle, cPLUGIN_INIT);
if Assigned(InitProc) then
begin
file://调用InitPlugin.
InitProc(mnuMain);
end;
// 为第三方插件查找 InitPluginEvents
InitEvents := GetProcAddress(LibHandle,
cPLUGIN_INITEVENTS);
if Assigned(InitEvents) then
begin
// 调用 InitPlugin.
InitEvents(lstMinMax);
end;
end
else
begin
MessageDlg('File "' + sr.Name +
'" is not a valid plugin.',
mtInformation, [mbOK], 0);
end;
end
else
begin
MessageDlg('An error occurred loading the plugin "' +
sr.Name + '".', mtInformation, [mbOK], 0);
end;
end;
最后一步是创建插件自身。如同前面的几个例子,插件展示一个标志自身的描述
过程。它也带有一个初始化例程,在本例中只是接受一个TList作为参数。最后,它还
包含一个没有引出(Export)的历程,叫做AlterMinTrackSize,它将修改传递给它的
数值。
最终插件的完整代码。
unit main;
interface
uses Dialogs, Menus, classes;
procedure DescribePlugin(var Desc: string);
export; stdcall;
procedure InitPluginEvents(lstResize: TList);
export; stdcall;
procedure AlterMinTrackSize(var x, y: Integer); stdcall;
implementation
procedure DescribePlugin(var Desc: string);
begin
Desc := 'Test plugin 3 - MinMax';
end;
procedure InitPluginEvents(lstResize: TList);
begin
lstResize.Add(@AlterMinTrackSize);
end;
procedure AlterMinTrackSize(var x, y: Integer);
begin
x := 270;
y := 220;
end;
end.
最终插件的代码
InitPluginEvents过程是这个插件的初始化例程。它接受一个TList作为参数。这
个TList就是在父应用程序中创建的保存相应函数地址的列表。下面的语句:
lstResize.Add(@AlterMinTrackSize);
把AlterMinTrackSize函数的地址加入到了这个列表中。它被声明为类型stdcall以
便与其他过程相配,不过用不着export指示字。由于函数被直接通过它的地址调用,
所以也就没有必要按照通常的方式把它从DLL中引出。
所以,事件序列如下所列:
1、 在应用程序初始化时,创建一个TList对象。
2、 在启动时这个列表被传递到插件的初始化过程InitPluginEvents中。
3、 插件过程把一个过程的地址加入到列表中。
4、 每次窗口大小改变时所产生的Windows消息WM_GETMINMAXINFO被我们的应用程
序所捕获。
5、 该消息被我们的消息处理器TfrmMain.MainMaxInfo所处理,见图10。
6、 消息处理器遍历列表并调用它所包含的函数,把当前的X和Y最小窗口尺寸作为
参数传递。要注意,TList类只是存储指针,所以如果想用保存的地址做些什么事情
的话,我们必须把指针转换成所需要的类型--在本例中,要转换成TresizeProc。
TResizeProc = procedure (var x, y: Integer); stdcall;
7、 插件过程AlterMinTrackSize(列表中的指针所指向的),接受X和Y值作为可变
的var参数并且修改它们。
8、 控制权返回到父应用的消息处理器,按照最小窗口尺寸的新值继续运行下去。
9、 应用程序退出时TList会在主代码的finalization段被释放。
结论
使用该体系结构时,可能利用Delphi提供的package功能是个不错的主意。在通
常情况下,我不是一个分割运行时模块的狂热爱好者,但是当你认为任一包含大量代
码的Delphi DLL超过200KB时,它就开始变得有意义了。
这篇文章应该还是有些用处的,至少它可以让你思考一些程序设计方面的问题,比
如如何让它变得更加灵活。我知道如果我在以前的应用程序中使用一些这种技术的
话,我就可以省掉在修改程序方面的好多工作。我并不想把插件作为一种通用的解
决方案。很明显,有些情况下额外的复杂度无法验证其正确性,或者应用程序压根儿
就不打算把自身搞成几块可扩展的单元。还有一些其它的方法也可以达成同样的效果。
Delphi自身提供了一个接口来创作能集成到IDE中的模块,比起我所说明的技术这种方
法更加面向对象(或者说更"干净"),而我也确信你可以在自己的应用中模仿这一技
术。在运行时加载Delphi包也不是做不到的。探索一下这种可能性吧。
本文所介绍的技术在Delphi 4下工作的很好。实际上,Delphi 4增加了工程选项,使
这类应用程序加强DLL(application-plus-DLL)的开发变得更加容易了。