在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
对外行人来说,插件仅仅是从外部提供给应用程序的代码块而已(举个例子来说,在 一个DLL中)。一个插件和一个普通DLL之间的差异在于插件具有扩展父应用程序功能 的能力。例如,Photoshop本身并不具备进行大量的图像处理功能。插件的加入使其 获得了产生诸如模糊、斑点,以及其他所有风格的奇怪效果,而其中任何一项功能都 不是父应用程序自身所具有的。 对于图像处理程序来说这很不错,可是为什么要花偌大的力气去完成支持插件的商业 应用程序呢?假设,我们举个例子,你的应用程序要产生一些报表。你的客户肯定会 一直要求更新或者增加新的报表。你可以使用一个诸如Report Smith的外部报表生成 器,这是个不怎么样的解决方案,需要发布附加的文件,要对用户进行额外的培训, 等等。你也可以使用QuickReport,不过这会使你身处版本控制的噩梦之中--如果每 改变一次字体你就要Rebuild你的应用程序的话。 然而,只要你把报表做到插件中,你就可以使用它。需要一个新的报表吗? 没问题,只要安装一个DLL,下次应用程序启动时就会看见它了。另外一个例子是处理 来自外部设备(比如条形码扫描器)的数据的应用程序,为了给用户更多的选择,你 不得不支持半打的各种设备。通过将每种设备接口处理例程写成插件,不用对父应用 程序作任何变动就可以获得最大程度的可伸缩性。 入门 在开始写代码之前最重要的事情就是搞清楚你的应用程序到底需要扩展哪 图1:插件测试外壳程序 我们的插件外壳程序与普通应用程序之间的唯一不同就在于工程源文件 图 2: 寻找插件 图 3: 载入插件 构造插件 我们已经创建好了父应用程序,现在该轮到创建我们希望加载的插件了。 图 4: 简单插件的工程源文件 图 5: 例子插件的主程序 调试 现在介绍一下Delphi 3中一个较好的功能:从IDE中调试DLL的能力。在DLL工 延伸父应用 这个简单的插件不错,不过它不能做什么有用的事情。第二个例子就是纠正这个问题。 这个插件的目标就是在父应用程序的主菜单中加入一个项目。这个菜单项目,当被单 击时,就会执行插件内的一些代码。图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; http://www.cnblogs.com/carcode/admin/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); http://www.cnblogs.com/carcode/admin/file://查/找InitPlugin. InitProc := GetProcAddress(LibHandle, cPLUGIN_INIT); if Assigned(InitProc) then begin http://www.cnblogs.com/carcode/admin/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)的开发变得更加容易了。 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论