初探Delphi中的插件编程

插件结构的编程需要一个插件容器来控制各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为后缀。

=550) window.open('/upload/20080315181507424.jpg');" src="http://zsrimg.ikafan.com/upload/20080315181507424.jpg" onload="if(this.width>'550')this.width='550';if(this.height>'1000')this.height='1000';" border=0>

  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会将登陆的用户名用参数传递过来,得到用户名后就可以进行一些权限控制,表现在界面初始化上。

  其代码如下

unit UnitOfficeEntrance;

interface
uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms;
 function ShowDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;stdcall;
 function FreeDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;stdcall;
implementation
 uses UnitOfficialMainForm; // 改成MAINFORM的unit
var
 DLL_Form:TFormOfficialMain; //改成MAINFORM的NAME

 //-----------------------------------------
 //Name: ShowDLLForm
 //Func: DLL插件调用入口函数
 //Para: AHandle 挂靠程序句柄; ACaption 本窗体标题
 //Rtrn: N/A
 //Auth: CST
 //Date: 2005-6-3
 //-----------------------------------------

 function ShowDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;
 begin
  result:=true;
 try
  Application.Handle:=AHandle; //挂靠到主程序容器
  DLL_Form:=TFormOfficialMain.Create(Application); //改成MAINFORM的NAME
 try
  with DLL_Form do
  begin
   Caption := ACaption;
   StatusBar.Panels.Items[0].Text := AUserID;
   //Configure UI
   Show ;
  end;
 except
  on e:exception do
  begin
   dll_form.Free;
  end;
 end;
 except
  result:=false;
  end;
 end;

//-----------------------------------------
//Name: FreeDLLForm
//Func: DLL插件调用出口函数
//Para: AHandle 挂靠程序句柄
//Rtrn: true/false
//Auth: CST
//Date: 2005-6-11
//-----------------------------------------

function FreeDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;
begin
 Application.Handle:=AHandle; //挂靠到主程序容器
 if DLL_Form.Showing then DLL_Form.Close; //如果窗口打开先关闭,触发FORM.CLOSEQUERY可取消关闭过程
 if not DLL_Form.Showing then
 begin
  DLL_Form.Free;
  result:=true;
 end //仍然打开状态,说明CLOSEQUERY.CANCLOSE=FALSE
 else
 begin
  result:=false;
 end;
end;
end.

  DLL工程文件代码如下:

library Official;

{ Important note about DLL memory management: ShareMem must be the

first unit in your library's USES clause AND your project's (select

Project-View Source) USES clause if your DLL exports any procedures or

functions that pass strings as parameters or function results. This

applies to all strings passed to and from your DLL--even those that

are nested in records and classes. ShareMem is the interface unit to

the BORLNDMM.DLL shared memory manager, which must be deployed along

with your DLL. To avoid using BORLNDMM.DLL, pass string information

using PChar or ShortString parameters. }

uses

SysUtils,

Classes,

UnitOfficialDetailForm in 'UnitOfficialDetailForm.pas' {FormOfficialDetail},

UnitOfficialMainForm in 'UnitOfficialMainForm.pas' {FormOfficialMain},

UnitOfficeEntrance in 'UnitOfficeEntrance.pas',

UnitOfficialClass in '..\..\Public\Library\UnitOfficialClass.pas',

UnitMyDataAdatper in '..\..\Public\Library\UnitMyDataAdatper.pas',

UnitMyHeaders in '..\..\Public\Library\UnitMyHeaders.pas';

{$R *.res}

exports ShowDLLForm,FreeDLLForm; //接口函数

begin

end.

  插件程序一旦调用了DLL窗口,窗口实例将会保持在HALL窗口的上层,因此不用担心遮挡的问题。

 容器程序的实现

  1、接口函数的引入

  调用DLL库中的函数有显式和隐式两种方式,显式调用更灵活,因此我们使用显示调用。在Delphi中需要为接口函数申明函数类型,然后实例化函数类型的实例,该实例实际是一个指向函数的指针,通过指针我们可以访问到函数并传递参数、获取返回值。在单元文件的Interface部分加入函数类的申明:

type

//定义接口函数类型,接口函数来自DLL接口

TShowDLLForm = Function(AHandle:THandle; ACaption: String; AUserID:string):Boolean;stdcall;

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。
使用上文定义的函数类型定义函数指针变量,然后使用@操作符获得函数地址,这样就可以使用指针变量访问函数。主要代码如下:

……
var
 ShowDLLForm: TShowDLLForm; //DLL接口函数实例
 FreeDLLForm: TFreeDLLForm;
begin
 try
 begin
  APlugin.ProcAddr := LoadLibrary(PChar(sPath));
  APlugin.FuncFreeAddr := GetProcAddress(APlugin.ProcAddr,'FreeDLLForm');
  APlugin.FuncAddr := GetProcAddress(APlugin.ProcAddr ,'ShowDLLForm');

  @ShowDLLForm:=APlugin.FuncAddr ;
  @FreeDLLForm:=APlugin.FuncFreeAddr;
  if ShowDllForm(Self.Handle, APlugin.Caption , APlugin.UserID) then
   Result:=True
   ……

  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部分加入如下代码:

type

 //定义插件信息类

 TMyPlugins = class
 Caption:String; //DLL窗体标题
 DllFileName:String; //DLL文件路径
 WndClass:String; //窗体标识
 UserID:string; //用户名
 ProcAddr:THandle; //LOADLIBRARY载入的库句柄
 FuncAddr:Pointer; //SHOWDLLFORM函数指针
 FuncFreeAddr:Pointer; //FREEDLLFORM函数指针
end;

……

  为每个插件创建一个TMyPlugins的实例,下文会讨论对这些实例的初始化方法。

  3) 插件载入函数

  在本示例中DLL窗口是在HALL中触发打开子窗口的事件中载入并显示的。按钮事件触发后,先根据插件结构体实例判断DLL是否已经加载,如果已经加载,则控制窗口的显示或关闭;如果没有加载则访问数据表将字段赋值到插件结构体中,然后执行载入、获得指针的工作。

  局部代码如下

……
//-----------------------------------------

//Name: OpenPlugin

//Func: 插件信息类控制过程: 初始化==》设置权限==》载入DLL窗口

//Para: APlugin-TMyPlugins; sAlias别名; iFuncValue权限值

//Rtrn: N/A

//Auth: CST

//Date: 2005-6-2

//-----------------------------------------

procedure TFormHall.OpenPlugin(AFromActn: TAction ;APlugin:TMyPlugins; sAlias:string; sUserID:string);
 var hWndPlugin:HWnd;
begin
 
 //判断插件窗口是否已经载入 hWndPlugin:=FindWindow(PChar(APlugin.WndClass),nil);
 if hWndPlugin <> 0 then //插件窗口已经载入
 begin
  if not IsWindowVisible(hWndPlugin) then
  begin
   AFromActn.Checked := True;
   ShowWindow(hWndPlugin,SW_SHOWDEFAULT); //显示
  end
  else
  begin
   AFromActn.checked := False;
   ShowWindow(hWndPlugin,SW_HIDE) ;
  end;
  Exit; //离开创建插件过程
 end;

//初始化插件类实例

if not InitializeMyPlugins(APlugin,sAlias) then
begin
 showmessage('初始化插件类错误。');
 exit;
end;

//获得当前权限值

APlugin.UserID := sUserID;
//载入DLL窗口

if not LoadShowPluginForm(APlugin) then
begin
 showmessage('载入中心插件出错。');
 exit;
 end;
end;

//-----------------------------------------
//Name: InitializeMyPlugins
//Func: 初始化MYPLUGIN实例 (Caption | DllFileName | IsLoaded)
//Para: APlugin-TMyPlugins
//Rtrn: N/A
//Auth: CST
//Date: 2005-6-2
//-----------------------------------------

function TFormHall.InitializeMyPlugins(APlugin:TMyPlugins; sAlias:String):Boolean;
var
 strSQL:string;
 myDA:TMyDataAdapter;
begin
 Result:=False;
 myDA:=TMyDataAdapter.Create;
 strSQL:='SELECT * FROM SystemModuleList WHERE modAlias='+QuotedStr(sAlias);
 try
  myDA.RetrieveData(strSQL);
 except
  on E:Exception do
  begin
   result:=false;
   myDA.Free ;
   exit;
  end;
 end;
try
 begin
  with myDA.MyDataSet do
 begin
  if Not IsEmpty then
 begin
  APlugin.Caption:= FieldByName('modName').Value;
  APlugin.DllFileName := FieldByName('modFile').Value;
  APlugin.WndClass := FieldByName('modWndClass').Value ;
  result:=True;
 end;
Close;
 end; //end of with...do...
 end; //end of try
 except
  on E:Exception do
begin
 Result:=False;
 myDA.Free ;
 Exit;
 end; //end of exception
end; //end of try...except

 myDA.Free ;
end;

//-----------------------------------------

//Name: LoadShowPluginForm

//Func: 载入DLL插件并显示窗口

//Para: APlugin-TMyPlugins

//Rtrn: true-创建成功

//Auth: CST

//Date: 2005-6-2

//-----------------------------------------

function TFormHall.LoadShowPluginForm (const APlugin:TMyPlugins):boolean;

var
 ShowDLLForm: TShowDLLForm; //DLL接口函数实例
 FreeDLLForm: TFreeDLLForm;
 sPath:string; //DLL文件的完整路径
begin
 try
 begin
  sPath:=ExtractFilepath(Application.ExeName)+ 'plugins\' + APlugin.DllFileName ;
  APlugin.ProcAddr := LoadLibrary(PChar(sPath));
  APlugin.FuncFreeAddr := GetProcAddress(APlugin.ProcAddr,'FreeDLLForm');
  APlugin.FuncAddr := GetProcAddress(APlugin.ProcAddr ,'ShowDLLForm');
  @ShowDLLForm:=APlugin.FuncAddr ;
  @FreeDLLForm:=APlugin.FuncFreeAddr;
  if ShowDllForm(Self.Handle, APlugin.Caption , APlugin.UserID) then
   Result:=True
  else
   Result:=False;
  end;
  except
   on E:Exception do
  begin
   Result:=False;
   ShowMessage('载入插件模块错误,请检查PLUGINS目录里的文件是否完整。');
  end;
 end;
end;

……

  4) DLL窗口控制

  正如3)中的代码说明的那样,DLL窗口的打开和关闭只是在表象层,关闭窗口并没有真正释放DLL窗口,只是调用API函数FindWindow根据窗口标识(就是Form.name)获得窗体句柄,用SHOWWINDOW函数的nCmdShow参数控制窗口显示/隐藏。

  其实这是我这个程序实现的不好的一个地方,如果在DLL窗口中使用Self.close方法会引起内存错误,实在能力有限没有办法解决,因此出此下策。所以每个DLL程序主窗口的关闭按钮都必须隐藏掉。 :-P

  5) DLL库的释放

  在程序退出时,必须根据插件信息实例逐一释放DLL库。释放DLL库的函数如下:

procedure TFormHall.ClosePlugin(aPLG:TMyPlugins);
var
 FreeDLLForm:TFreeDLLForm;
begin
 if aPLG.ProcAddr = 0 then exit;
 if aPLG.FuncFreeAddr = nil then exit;
 @FreeDLLForm:=aPLG.FuncFreeAddr;
 if not FreeDLLForm(Application.Handle,'','') then
  showMessage('err');
end;

  小结

  本实例程序运行效果如下:


=550) window.open('/upload/20080315181507979.jpg');" src="http://zsrimg.ikafan.com/upload/20080315181507979.jpg" onload="if(this.width>'550')this.width='550';if(this.height>'1000')this.height='1000';" border=0>

  我以上的方法中,因为有不少能力有限没有解决的问题,所以采用了一些看起来不太合理的掩饰方法,希望大家能在做了一点尝试后设计出更好的解决方法,我也希望能学到更多的好方法。

(0)

相关推荐

  • 初探Delphi中的插件编程

    插件结构的编程需要一个插件容器来控制各DLL的运行情况,将划分好的每个子系统安排到一个DLL库文件中.对每个DLL程序需要为容器预留接口函数,一般接口函数包括:启动调用DLL库的函数.关闭DLL库的函数.通过接口函数,插件容器可以向DLL模块传递参数实现动态控制.具体实现细节我将在下文说明并给出响应代码. 您可能需要先了解一下DELPHI中UNIT的结构,工程的结构.本文没有深入讨论DLL编程的理论细节,只是演示了一些实用的代码,我当时学习的是刘艺老师的<DELPHI深入编程>一书. 我也处于

  • Delphi中对时间操作方法汇总

    一般来说在delphi中用于描述时间的有几种数据结构,而对时间的操作,实质上就是对这些结构的操作. TDateTime类型: Delphi中最常用的表示日期时间的数据类型TDateTime类型,和普通的整形数一样,你可以给日期定义一个日期型变量以便在程序中进行操作.TdateTime类型实质上是一个Double型的数,在Delphi中是这样定义TdateTime类型: type TDateTime = type Double ,具体的算法是用Double数的整数部分表示日期,以1989年12月3

  • Delphi中判断文件是否为文本文件的函数

    在自己编写文本文件读取函数的时候,你首先会遇到的第一个问题就是:对于给定的一个文件名,怎么知道它所代表磁盘文件的确是文本文件?这里有一个很简单的方法:把给定的那个文件看作是无类型的二进制文件,然后顺序地读出这个文件的每一个字节,如果文件里有一个字节的值等于0,那么这个文件就不是文本文件:反之,如果这个文件中没有一个字节的值是0的话,就可以判定这个文件是文本文件了.这是原理,下面看看在Delphi 中怎样编程来实现它-- 复制代码 代码如下: function IsTextFile(FileNam

  • 浅析Python中的元编程

    目录 什么是元编程 元编程应用场景 综合实战 什么是元编程 Python元编程是指在运行时对Python代码进行操作的技术,它可以动态地生成.修改和执行代码,从而实现一些高级的编程技巧.Python的元编程包括元类.装饰器.动态属性和动态导入等技术,这些技术都可以帮助我们更好地理解和掌握Python语言的特性和机制.元编程在一些场景下非常有用,比如实现ORM框架.实现特定领域的DSL.动态修改类的行为等.掌握好Python元编程技术可以提高我们的编程能力和代码质量. 想要搞定元编程,必须要理解和

  • Delphi中使用ISuperObject解析Json数据的实现代码

    Java.Php等语言中都有成熟的框架来解析Json数据,可以让我们使用很少的代码就把格式化好的json数据转换成程序可识别的对象或者属性,同时delphi中也有这样的组件来实现此功能,即IsuperObject.如果还没有这个组件的请在网上搜索下载或者在下面留言处留下你的邮箱向本人索取. 下面先说一下ISuperObject中几个常用的函数 function SO(const s: SOString = '{}'): ISuperObject; overload; 此函数传入json数据字符串

  • Delphi 中内存映射对于大文件的使用

    Delphi 中内存映射对于大文件的使用 平时很少使用大文件的内存映射,碰巧遇到了这样的要求,所以把过程记录下来,当给各位一个引子吧,因为应用不算复杂,可能有考虑不到的地方,欢迎交流. 对于一些小文件,用普通的文件流就可以很好的解决,可是对于超大文件,比如2G或者更多,文件流就不行了,所以要使用API的内存映射的相关方法,即使是内存映射,也不能一次映射全部文件的大小,所以必须采取分块映射,每次处理一小部分. 先来看几个函数 CreateFile :打开文件 GetFileSize : 获取文件尺

  • delphi中exit,abort,break,continue的区别介绍

    delphi中表示跳出的有break,continue, exit,abort, halt, runerror. 1.break 强制退出循环(只能放在循环中),用于从For语句,while语句或repeat语句中强制退出. 2.continue 用于从For语句,while语句或repeat语句强行结束本次称称循环,并开始下一次循环. 3.exit 用于从当前代码块中退出.若该代码为主程序,则终止该程序,若是函数或过程,则立即该过程或函数. 4.abort 中止程序的运行,产生不报错的异常信息

  • Yii2框架引用bootstrap中日期插件yii2-date-picker的方法

    本文实例讲述了Yii2框架引用bootstrap中日期插件yii2-date-picker的方法.分享给大家供大家参考,具体如下: 最近在学习yii2 框架,发现框架很强大,而且结合了时下许多新的技术在里面. 简单记录一下yii2-date-picker插件的使用方法: 首先先了解一下github中的相关资源:https://github.com/2amigos/yii2-date-picker-widget 其次:在自己的项目中利用上面链接中的相关方法下载此资源,命令为: php compos

  • Python中的并发编程实例

    一.简介 我们将一个正在运行的程序称为进程.每个进程都有它自己的系统状态,包含内存状态.打开文件列表.追踪指令执行情况的程序指针以及一个保存局部变量的调用栈.通常情况下,一个进程依照一个单序列控制流顺序执行,这个控制流被称为该进程的主线程.在任何给定的时刻,一个程序只做一件事情. 一个程序可以通过Python库函数中的os或subprocess模块创建新进程(例如os.fork()或是subprocess.Popen()).然而,这些被称为子进程的进程却是独立运行的,它们有各自独立的系统状态以及

  • 利用Fn.py库在Python中进行函数式编程

    尽管Python事实上并不是一门纯函数式编程语言,但它本身是一门多范型语言,并给了你足够的自由利用函数式编程的便利.函数式风格有着各种理论与实际上的好处(你可以在Python的文档中找到这个列表): 形式上可证 模块性 组合性 易于调试及测试 虽然这份列表已经描述得够清楚了,但我还是很喜欢Michael O.Church在他的文章"函数式程序极少腐坏(Functional programs rarely rot)"中对函数式编程的优点所作的描述.我在PyCon UA 2012期间的讲座

随机推荐