初探Delphi中的插件编程
前言
我写Delphi程序是从MIS系统入门的,开始尝试子系统划分的时候采用的是MDI窗体的结构。随着系统功能的扩充,不断有新的子系统加入系统中,单个工程会变得非常大,每次做一点修改都要重新编译,单个工程的形式也不利于团队协作。为了提高工作效率,我希望利用DLL动态链接库的形式实现插件结构的编程。
插件结构的编程需要一个插件容器来控制各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为后缀。
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: --
- // -----------------------------------------
- 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[ ].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: --
- // -----------------------------------------
- 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 ’,
- UnitOfficialMainForm in ’ UnitOfficialMainForm.pas ’,
- UnitOfficeEntrance in ’ UnitOfficeEntrance.pas ’,
- UnitOfficialClass in ’ .. .. PublicLibraryUnitOfficialClass.pas ’,
- UnitMyDataAdatper in ’ .. .. PublicLibraryUnitMyDataAdatper.pas ’,
- UnitMyHeaders in ’ .. .. PublicLibraryUnitMyHeaders.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: --
- //-----------------------------------------
- procedure TFormHall.OpenPlugin(AFromActn: TAction ;APlugin:TMyPlugins; sAlias:string; sUserID:string);
- var hWndPlugin:HWnd;
- begin
- //判断插件窗口是否已经载入 hWndPlugin:=FindWindow(PChar(APlugin.WndClass),nil);
- if hWndPlugin <> 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: --
- //-----------------------------------------
- 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: --
- //-----------------------------------------
- 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 = then exit;
- if aPLG.FuncFreeAddr = nil then exit;
- @FreeDLLForm:=aPLG.FuncFreeAddr;
- if not FreeDLLForm(Application.Handle,’’,’’) then
- showMessage(’err’);
- end;
小结
我以上的方法中,因为有不少能力有限没有解决的问题,所以采用了一些看起来不太合理的掩饰方法,希望大家能在做了一点尝试后设计出更好的解决方法,我也希望能学到更多的好方法。
初探Delphi中的插件编程的更多相关文章
- 转发 Delphi中线程类TThread 实现多线程编程
Delphi中有一个线程类TThread是用来实现多线程编程的,这个绝大多数Delphi书藉都有说到,但基本上都是对TThread类的几个成员作一简单介绍,再说明一下Execute的实现和Synchr ...
- Delphi中任务栏状态区的编程
在Windows桌面的任务栏上有一个凹陷的区域,其中显示着系统时钟以及一些图标,这个长方形的区域便是Windows的任务栏状态区(taskbar status area).本文将介绍使用Borland ...
- Delphi中线程类TThread实现多线程编程2---事件、临界区、Synchronize、WaitFor……
接着上文介绍TThread. 现在开始说明 Synchronize和WaitFor 但是在介绍这两个函数之前,需要先介绍另外两个线程同步技术:事件和临界区 事件(Event) 事件(Event)与De ...
- delphi中formatFloat代码初探(在qt下实现floatformat的函数)
由于项目需要,需要在qt下实现floatformat的函数.之前写过一个,但是写得不好.决定重新写一个,参考delphi xe2下的实现.把xe2下的相关代码都看了一遍,xe2的代码思路在这里贴出来. ...
- Delphi中正常窗口的实现
摘要: 在Delphi的VCL库中,为了使用以及实现的方便,应用对象Application创建了一个用来处理消息响应的隐藏窗口.而正是这个窗口,使得用VCL开发出来的程序存在着与其他窗口不能正常排列平 ...
- 分享在winform下实现模块化插件编程-优化版
上一篇<分享在winform下实现模块化插件编程>已经实现了模块化编程,但我认为不够完美,存在以下几个问题: 1.IAppContext中的CreatePlugInForm方法只能依据完整 ...
- 分享在winform下实现模块化插件编程
其实很早之前我就已经了解了在winform下实现插件编程,原理很简单,主要实现思路就是:先定一个插件接口作为插件样式及功能的约定,然后具体的插件就去实现这个插件接口,最后宿主(应用程序本身)就利用反射 ...
- Delphi中使用比较少的一些语法
本文是为了加强记忆而写,这里写的大多数内容都是在编程的日常工作中使用频率不高的东西,但是又十分重要. ---Murphy 1,构造和析构函数: a,构造函数: 一般基于TComponent组件的派生类 ...
- jQuery 插件编程精讲与技巧
适应的读者: 1.有一定的jquery编程基础但是想在技能上有所提升的人 2.前端开发的程序员 3.对编程感兴趣的学生 为什么要学习jquery插件的编写? 为什么要学习jquery插件的编写?相信这 ...
随机推荐
- HDU 6205 2017沈阳网络赛 思维题
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6205 题意:给你n堆牌,原本每一堆的所有牌(a[i]张)默认向下,每次从第一堆开始,将固定个数的牌(b ...
- Linux软件安装install命令
install 1.作用 install命令的作用是安装或升级软件或备份数据,它的使用权限是所有用户. 2.格式 (1)install [选项]... 来源 目的地 (2)install [选项]. ...
- python_线程、进程和协程
线程 Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元. #!/usr/bin/env python #coding=utf-8 __author__ = 'yinjia' i ...
- CEPH 使用SSD日志盘+SATA数据盘, 随OSD数目递增对性能影响的递增测试
最近建设新机房,趁项目时间空余较多,正好系统的测试一下CEPH集群性能随OSD数目的变化情况, 新ceph集群测试结果如下: 1)4k随机读在3/6/9osd host下的性能差不多,吞吐量约50~6 ...
- android4.0 锁屏实现(转)
转载请表明出处:http://blog.csdn.net/wdaming1986/article/details/8837023 好了,言归正传,说说锁屏了,其实把锁屏做成apk的形式,会引起很多问题 ...
- 用strtok函数分割字符串
用strtok函数分割字符串 需要在loadrunner里面获得“15”(下面红色高亮的部分),并做成关联参数. //Body response 内容: <BODY><; PRE&g ...
- lr_start_transaction/lr_end_transaction事物组合
lr_start_transaction/lr_end_transaction事物组合 总结一下: lr_start_transaction与lr_end_transaction 为使用最多的事物创造 ...
- volatile 的一个经典例子
volatile 关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程 ...
- PHP包管理
前言 在nodejs中,存在npm,python中也存在pip,而php之前不存在类似的东西,导致想要安装一个包,只能去复制代码,但是现在,使用composer可以简单的安装一个包(但是compose ...
- Mac 命令行美化
在 mac 中使用原生的命令行工具,竟然没有 git 命令的自动补全,在 git 仓库下也看不到当前的分支名,不能忍.于是,开始一波改造. 目标:命名 Tab 自动补全:可以显示分支名: 一番 Goo ...