初探Delphi 7 中的插件编程
初探Delphi 7 中的插件编程 
1 前言
1.1 写作目的
我写Delphi程序是从MIS系统入门的,开始尝试子系统划分的时候采用的是MDI窗体结构。随着系统功能的扩充,不断有新的子系统加入系统中,单个工程会变得非常大,每次做一点修改都要重新编译,单个工程的形式也不利于团队协作。为了提高工作效率,我希望利用DLL动态链接库的形式实现插件结构的编程。
插件结构的编程需要一个插件容器来控制各DLL运行情况,将划分好的每个子系统安排到一个DLL文件中。对每个DLL程序需要为容器预留接口函数,一般接口函数包括:启动调用DLL库的函数、关闭DLL库的函数。通过接口函数,插件容器可以向DLL模块传递参数实现动态控制。具体实现细节我将在下文说明并给出响应代码
1.2 阅读对象
您可能需要先了解一下DELPHIUNIT的结构,工程的结构。本文没有深入讨论DLL编程的理论细节,只是演示了一些实用的代码,我当时学习的是刘艺老师的《DELPHI深入编程》一书。
2 示例程序简介
为了便于阅读我将使用一个MIS系统的部分程序代码演示插件编程的一些方法。示例程序是典型的C/S结构DBMS应用程序,我们关注的部分将是框架程序(下文简称Hall)的控制语句和dll插件程序的响应控制。
2.1 程序结构
插件容器Hall使用一个独立的工程创建,Hall的主窗口的作用相当于MDI程序中的MDI容器窗体,Hall中将显式调用Dll中的接口函数。
每个插件程序独立使用各自的工程,与普通工程不同的是,DLL工程创建的是Dll Wizard,相应编译生成的文件是以DLL为后缀。
2.2 接口设计
实例程序Narcissus中我们预留两个接口函数:
ShowDLLForm
该函数将应用程序的句柄传递给DLL子窗口,DLL程序将动态创建DLL窗体的实例。还可以将一些业务逻辑用参数的形式传递给DLL子窗口,比如窗体名称、当前登陆的用户名等。初次调用一个DLL窗体实例时使用此函数创建。
FreeDLLForm
该函数将显示释放DLL窗口实例,在退出应用程序时调用每个DLL窗体的FreeDLLForm方法来释放创建的实例,不然会引起内存只读错误。同样,也可以将一些在释放窗体时需要做的业务逻辑用参数的形式传递给DLL窗体。
2.3 调试方式
DLL窗体程序无法直接执行,需要有一个插件容器来调用。应此我们需要先实现一个基本的Hall程序,然后将Hall.exe保存在一个固定的目录中。对每个DLL工程做如下设置
1.        打开DLL工程
2.        选择菜单 Run – Parameters
3.        在弹出的窗口中浏览到我们的容器这样在调试DLL程序时将会自动调用Hall程序,利用Hall中预留的调用接口调试DLL程序。
3 插件程序的基本实现
DLL程序的设计方式和普通WINAPP没有很大的区别,只是所有的窗口都是作为一种特殊的资源保存在DLL库中,需要手动调用,而不像WINAPP中会有工程自动创建。声明接口函数的方法很简单
1.      UnitImplementation部分中声明函数
2.      在函数声明语句的尾部加上stdcall标记
3.      在工程代码(Project – View Source)的begin语句之前,用exports语句声明函数接口
为了使代码简洁,我个人喜欢在工程中独立添加一个Unit单元(File – New -- Unit),然后将所有要输出的函数体定义在此单元中,不要忘记将引用到的窗体的Unituses进来。我命名这个单元为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;                // 改成MAINFORMunit
var
    DLL_Form:TFormOfficialMain;      //改成MAINFORMNAME
//-----------------------------------------
//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);  //改成MAINFORMNAME
        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
//-----------------------------------------
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;

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窗口的上层,因此不用担心遮挡的问题。
4 容器程序的实现。
4.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
接下来我们将详细讨论这几个步骤。
4.2    载入DLL库文件
通过调用API函数LoadLibrary可以将DLL库载入到内存中,在此我们不讨论DLL对内存管理的影响。LoadLibrary的参数是DLL文件的地址路径,如果载入成功会返回一个CARDINAL类型的变量作为DLL库的句柄;如果目标文件不存在或其他原因导致载入DLL文件失败会返回一个0
4.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.4    一个具体的实现方法
为了结构化管理插件,方便今后的系统扩充,我们可以结合数据库记录可用的DLL信息,然后通过查询数据库记录动态访问DLL程序。
4.4.1        系统模块表设计
对于MIS系统,可以利用已有的DBS条件建立一个系统模块表,记录DLL文件及映射到系统模块中的相关信息
字段
作用
类型
AutoID
索引
INT
modAlias
模块别称
VARCHAR
modName
模块名称
VARCHAR
modWndClass
窗体唯一标识
VARCHAR
modFile
DLL路径
VARCHAR
modMemo
备注
TEXT
 模块别称是用来在编程设计阶段统一命名的规则,特别是团队开发时可以供队员参考。
 模块名称将作为ACAPTION参数传递给SHOWDLLFORM函数作为DLL窗口的标题。
 窗体唯一标识是DLL子模块中主窗口的CLASSNAME,用来在运行时确定要控制的窗口。
 DLL路径保存DLL文件名称,程序中将转换为绝对路径。
4.4.2        插件信息数据结构
定义一个记录插件相关信息的数据接口可以集中控制DLL插件。在Interface部分加入如下代码:
type
    //定义插件信息类
    TMyPlugins = class
        Caption:String;            //DLL窗体标题
        DllFileName:String;        //DLL文件路径
        WndClass:String;           //窗体标识
        UserID:string;            //用户名
        ProcAddr:THandle;          //LOADLIBRARY载入的库句柄
        FuncAddrointer;          //SHOWDLLFORM函数指针
        FuncFreeAddrointer;      //FREEDLLFORM函数指针
    end;
……
为每个插件创建一个TMyPlugins的实例,下文会讨论对这些实例的初始化方法。
4.4.3        插件载入函数
在本示例中DLL窗口是在HALL中触发打开子窗口的事件中载入并显示的。按钮事件触发后,先根据插件结构体实例判断DLL是否已经加载,如果已经加载,则控制窗口的显示或关闭;如果没有加载则访问数据表将字段赋值到插件结构体中,然后执行载入、获得指针的工作。
局部代码如下
……
//-----------------------------------------
//Name: OpenPlugin
//Func: 插件信息类控制过程: 初始化==》设置权限==》载入DLL窗口
//Para: APlugin-TMyPlugins; sAlias别名; iFuncValue权限值
//Rtrn: N/A
//-----------------------------------------
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
//-----------------------------------------
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 do...
        end;    //end of try
    except
        on E:Exception do
        begin
            Result:=False;
            myDA.Free ;
            Exit;
        end;    //end of exception
    end;    //end pt
    myDA.Free ;   
end;
//-----------------------------------------
//Name: LoadShowPluginForm
//Func: 载入DLL插件并显示窗口
//Para: APlugin-TMyPlugins
//Rtrn: true-创建成功
//-----------------------------------------
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.4.4        DLL窗口控制
正如4.4.3中的代码说明的那样,DLL窗口的打开和关闭只是在表象层,关闭窗口并没有真正释放DLL窗口,只是调用API函数FindWindow根据窗口标识(就是Form.name)获得窗体句柄,用SHOWWINDOW函数的nCmdShow参数控制窗口显示/隐藏。
其实这是我这个程序实现的不好的一个地方,如果在DLL窗口中使用Self.close方法会引起内存错误,实在能力有限没有办法解决,因此出此下策。所以每个DLL程序主窗口的关闭按钮
都必须隐藏掉。 :-P
4.4.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;
5 小结
本实例程序运行效果如下:我以上的方法中,因为有不少能力有限没有解决的问题,所以采
用了一些看起来不太合理的掩饰方法,希望大家能在做了一点尝试后设计出更好的解决方法,我也希望能学到更多的好方法。

本文只给出了部分代码,如果您需要完整的代码可以写邮件给我。我也很欢迎各位能对我的方法提出建议和意见,这是我第一次尝试DLL编程的一点感受,希望能给同样也有兴趣的朋友一点线索。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。