城西人 發表於 2008-3-15 18:15:11

初探Delphi中的插件编程

<DIV></DIV>插件结构的编程需要一个插件容器来控制各DLL的运行情况,将划分好的每个子系统安排到一个DLL库文件中。对每个DLL程序需要为容器预留接口函数,一般接口函数包括:启动调用DLL库的函数、关闭DLL库的函数。通过接口函数,插件容器可以向DLL模块传递参数实现动态控制。具体实现细节我将在下文说明并给出响应代码。<br><br>  您可能需要先了解一下DELPHI中UNIT的结构,工程的结构。本文没有深入讨论DLL编程的理论细节,只是演示了一些实用的代码,我当时学习的是刘艺老师的《DELPHI深入编程》一书。<br><br>  我也处于DELPHI的入门阶段,只是觉得这次的DLL开发有一些值得讨论的地方,所以写这篇文章,希望各位能对我做的不好的地方慷慨建议。<br><br>  示例程序简介<br><br>  为了便于阅读我将使用一个MIS系统的部分程序代码演示插件编程的一些方法。示例程序是典型的C/S结构DBMS应用程序,我们关注的部分将是框架程序(下文简称Hall)的控制语句和dll插件程序的响应控制。<br><br>  1、程序结构<br><br>  插件容器Hall使用一个独立的工程创建,Hall的主窗口的作用相当于MDI程序中的MDI容器窗体,Hall中将显式调用Dll中的接口函数。<BR>每个插件程序独立使用各自的工程,与普通工程不同的是,DLL工程创建的是Dll Wizard,相应编译生成的文件是以DLL为后缀。<BR><B></B><BR><IMG onclick="if(this.width>=550) window.open('/upload/20080315181507424.jpg');" src="/upload/20080315181507424.jpg" onload="if(this.width>'550')this.width='550';if(this.height>'1000')this.height='1000';" border=0> <br><br><br><br><BR>  2、接口设计<br><br>  实例程序Narcissus中我们预留两个接口函数:<br><br>  ShowDLLForm<br><br>  该函数将应用程序的句柄传递给DLL子窗口,DLL程序将动态创建DLL窗体的实例。还可以将一些业务逻辑用参数的形式传递给DLL子窗口,比如窗体名称、当前登陆的用户名等。初次调用一个DLL窗体实例时使用此函数创建。<br><br>  FreeDLLForm<br><br>  该函数将显示释放DLL窗口实例,在退出应用程序时调用每个DLL窗体的FreeDLLForm方法来释放创建的实例,不然会引起内存只读错误。同样,也可以将一些在释放窗体时需要做的业务逻辑用参数的形式传递给DLL窗体。<br><br>  3、调试方式<br><br>  DLL窗体程序无法直接执行,需要有一个插件容器来调用。应此我们需要先实现一个基本的Hall程序,然后将Hall.exe保存在一个固定的目录中。对每个DLL工程做如下设置:<br><br>  1) 打开DLL工程<br><br>  2) 选择菜单 Run – Parameters<br><br>  3) 在弹出的窗口中浏览到我们的容器Hall.exe<br><br>  这样在调试DLL程序时将会自动调用Hall程序,利用Hall中预留的调用接口调试DLL程序。<br><br> 插件程序的基本实现<br><br>  DLL程序的设计方式和普通WINAPP没有很大的区别,只是所有的窗口都是作为一种特殊的“资源”保存在DLL库中,需要手动调用,而不像WINAPP中会有工程自动创建。声明接口函数的方法很简单<br><br>  1) 在Unit的Implementation部分中声明函数<br><br>  2) 在函数声明语句的尾部加上stdcall标记<br><br>  3) 在工程代码(Project – View Source)的begin语句之前,用exports语句声明函数接口<br><br>  为了使代码简洁,我个人喜欢在工程中独立添加一个Unit单元(File – New -- Unit),然后将所有要输出的函数体定义在此单元中,不要忘记将引用到的窗体的Unit也uses进来。我命名这个单元为UnitEntrance,在ShowDLLForm函数中初始化了要显示的窗口并调用Show方法显示,HALL会将登陆的用户名用参数传递过来,得到用户名后就可以进行一些权限控制,表现在界面初始化上。 <br><br>  其代码如下<br><br>unit UnitOfficeEntrance;<br><br>interface<BR>uses<BR> Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms;<BR> function ShowDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;stdcall;<BR> function FreeDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;stdcall;<BR>implementation<BR> uses UnitOfficialMainForm; // 改成MAINFORM的unit<BR>var<BR> DLL_Form:TFormOfficialMain; //改成MAINFORM的NAME<br><br> //-----------------------------------------<BR> //Name: ShowDLLForm<BR> //Func: DLL插件调用入口函数<BR> //Para: AHandle 挂靠程序句柄; ACaption 本窗体标题<BR> //Rtrn: N/A<BR> //Auth: CST<BR> //Date: 2005-6-3<BR> //-----------------------------------------<br><br> function ShowDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;<BR> begin<BR>  result:=true;<BR> try<BR>  Application.Handle:=AHandle; //挂靠到主程序容器<BR>  DLL_Form:=TFormOfficialMain.Create(Application); //改成MAINFORM的NAME<BR> try<BR>  with DLL_Form do<BR>  begin<BR>   Caption := ACaption;<BR>   StatusBar.Panels.Items.Text := AUserID;<BR>   //Configure UI<BR>   Show ;<BR>  end;<BR> except<BR>  on e:exception do<BR>  begin<BR>   dll_form.Free;<BR>  end;<BR> end;<BR> except<BR>  result:=false;<BR>  end;<BR> end;<br><br>//-----------------------------------------<BR>//Name: FreeDLLForm<BR>//Func: DLL插件调用出口函数<BR>//Para: AHandle 挂靠程序句柄<BR>//Rtrn: true/false<BR>//Auth: CST<BR>//Date: 2005-6-11<BR>//-----------------------------------------<br><br>function FreeDLLForm(AHandle: THandle; ACaption: string; AUserID: string):boolean;<BR>begin<BR> Application.Handle:=AHandle; //挂靠到主程序容器<BR> if DLL_Form.Showing then DLL_Form.Close; //如果窗口打开先关闭,触发FORM.CLOSEQUERY可取消关闭过程<BR> if not DLL_Form.Showing then<BR> begin<BR>  DLL_Form.Free;<BR>  result:=true;<BR> end //仍然打开状态,说明CLOSEQUERY.CANCLOSE=FALSE<BR> else<BR> begin<BR>  result:=false;<BR> end;<BR>end;<BR>end. <br><br>  DLL工程文件代码如下:<br><br>library Official;<br><br><BR>{ Important note about DLL memory management: ShareMem must be the<br><br>first unit in your library's USES clause AND your project's (select<br><br>Project-View Source) USES clause if your DLL exports any procedures or<br><br>functions that pass strings as parameters or function results. This<br><br>applies to all strings passed to and from your DLL--even those that<br><br>are nested in records and classes. ShareMem is the interface unit to<br><br>the BORLNDMM.DLL shared memory manager, which must be deployed along<br><br>with your DLL. To avoid using BORLNDMM.DLL, pass string information<br><br>using PChar or ShortString parameters. }<br><br><BR>uses<br><br>SysUtils,<br><br>Classes,<br><br>UnitOfficialDetailForm in 'UnitOfficialDetailForm.pas' {FormOfficialDetail},<br><br>UnitOfficialMainForm in 'UnitOfficialMainForm.pas' {FormOfficialMain},<br><br>UnitOfficeEntrance in 'UnitOfficeEntrance.pas',<br><br>UnitOfficialClass in '..\..\Public\Library\UnitOfficialClass.pas',<br><br>UnitMyDataAdatper in '..\..\Public\Library\UnitMyDataAdatper.pas',<br><br>UnitMyHeaders in '..\..\Public\Library\UnitMyHeaders.pas';<br><br><BR>{$R *.res}<br><br>exports ShowDLLForm,FreeDLLForm; //接口函数<br><br>begin<br><br>end. <br><br>  插件程序一旦调用了DLL窗口,窗口实例将会保持在HALL窗口的上层,因此不用担心遮挡的问题。<br><br> 容器程序的实现<br><br>  1、接口函数的引入<br><br>  调用DLL库中的函数有显式和隐式两种方式,显式调用更灵活,因此我们使用显示调用。在Delphi中需要为接口函数申明函数类型,然后实例化函数类型的实例,该实例实际是一个指向函数的指针,通过指针我们可以访问到函数并传递参数、获取返回值。在单元文件的Interface部分加入函数类的申明:<br><br>type<br><br>//定义接口函数类型,接口函数来自DLL接口<br><br>TShowDLLForm = Function(AHandle:THandle; ACaption: String; AUserID:string):Boolean;stdcall;<br><br>TFreeDLLForm = Function(AHandle:THandle; ACaption: String; AUserID:string):boolean;stdcall; <br><br>  显示调用库函数需要如下几个步骤:<br><br>  1) 载入DLL库文件<br><br>  2) 获得函数地址<br><br>  3) 执行函数<br><br>  4) 释放DLL库<br><br>  接下来我们将详细讨论这几个步骤。<br><br>  2、载入DLL库文件<br><br>  通过调用API函数LoadLibrary可以将DLL库载入到内存中,在此我们不讨论DLL对内存管理的影响。LoadLibrary的参数是DLL文件的地址路径,如果载入成功会返回一个CARDINAL类型的变量作为DLL库的句柄;如果目标文件不存在或其他原因导致载入DLL文件失败会返回一个0。<br><br>  3、实例化接口函数<br><br>  获得接口函数指针的API函数为GetProcAddress(库文件句柄,函数名称),如果找到函数则会返回该函数的指针,如果失败则返回NIL。<BR>使用上文定义的函数类型定义函数指针变量,然后使用@操作符获得函数地址,这样就可以使用指针变量访问函数。主要代码如下: <br><br>……<BR>var<BR> ShowDLLForm: TShowDLLForm; //DLL接口函数实例<BR> FreeDLLForm: TFreeDLLForm;<BR>begin<BR> try<BR> begin<BR>  APlugin.ProcAddr := LoadLibrary(PChar(sPath));<BR>  APlugin.FuncFreeAddr := GetProcAddress(APlugin.ProcAddr,'FreeDLLForm');<BR>  APlugin.FuncAddr := GetProcAddress(APlugin.ProcAddr ,'ShowDLLForm');<br><br>  @ShowDLLForm:=APlugin.FuncAddr ;<BR>  @FreeDLLForm:=APlugin.FuncFreeAddr;<BR>  if ShowDllForm(Self.Handle, APlugin.Caption , APlugin.UserID) then<BR>   Result:=True<BR>   …… <br><br>  4、一个具体的实现方法<br><br>  为了结构化管理插件,方便今后的系统扩充,我们可以结合数据库记录可用的DLL信息,然后通过查询数据库记录动态访问DLL程序。<br><br>  1) 系统模块表设计<br><br>  对于MIS系统,可以利用已有的DBS条件建立一个系统模块表,记录DLL文件及映射到系统模块中的相关信息<br><br>字段名 作用 类型 <BR>AutoID 索引 INT <BR>modAlias 模块别称 VARCHAR <BR>modName 模块名称 VARCHAR <BR>modWndClass 窗体唯一标识 VARCHAR <BR>modFile DLL路径 VARCHAR <BR>modMemo 备注 TEXT <br><br>  ·模块别称是用来在编程设计阶段统一命名的规则,特别是团队开发时可以供队员参考。<br><br>  ·模块名称将作为ACAPTION参数传递给SHOWDLLFORM函数作为DLL窗口的标题。<br><br>  ·窗体唯一标识是DLL子模块中主窗口的CLASSNAME,用来在运行时确定要控制的窗口。<br><br>  ·DLL路径保存DLL文件名称,程序中将转换为绝对路径。<br><br>  2) 插件信息数据结构<br><br>  定义一个记录插件相关信息的数据接口可以集中控制DLL插件。在Interface部分加入如下代码:<br><br>type<br><br> //定义插件信息类<br><br> TMyPlugins = class<BR> Caption:String; //DLL窗体标题<BR> DllFileName:String; //DLL文件路径<BR> WndClass:String; //窗体标识<BR> UserID:string; //用户名<BR> ProcAddr:THandle; //LOADLIBRARY载入的库句柄<BR> FuncAddr:Pointer; //SHOWDLLFORM函数指针<BR> FuncFreeAddr:Pointer; //FREEDLLFORM函数指针<BR>end;<br><br>…… <br><br>  为每个插件创建一个TMyPlugins的实例,下文会讨论对这些实例的初始化方法。<br><br>  3) 插件载入函数<br><br>  在本示例中DLL窗口是在HALL中触发打开子窗口的事件中载入并显示的。按钮事件触发后,先根据插件结构体实例判断DLL是否已经加载,如果已经加载,则控制窗口的显示或关闭;如果没有加载则访问数据表将字段赋值到插件结构体中,然后执行载入、获得指针的工作。 <br><br>  局部代码如下<br><br>……<BR>//-----------------------------------------<br><br>//Name: OpenPlugin<br><br>//Func: 插件信息类控制过程: 初始化==》设置权限==》载入DLL窗口<br><br>//Para: APlugin-TMyPlugins; sAlias别名; iFuncValue权限值<br><br>//Rtrn: N/A<br><br>//Auth: CST<br><br>//Date: 2005-6-2<br><br>//-----------------------------------------<br><br>procedure TFormHall.OpenPlugin(AFromActn: TAction ;APlugin:TMyPlugins; sAlias:string; sUserID:string);<BR> var hWndPlugin:HWnd;<BR>begin<BR>  <BR> //判断插件窗口是否已经载入 hWndPlugin:=FindWindow(PChar(APlugin.WndClass),nil);<BR> if hWndPlugin &lt;&gt; 0 then //插件窗口已经载入<BR> begin<BR>  if not IsWindowVisible(hWndPlugin) then<BR>  begin<BR>   AFromActn.Checked := True;<BR>   ShowWindow(hWndPlugin,SW_SHOWDEFAULT); //显示<BR>  end<BR>  else<BR>  begin<BR>   AFromActn.checked := False;<BR>   ShowWindow(hWndPlugin,SW_HIDE) ;<BR>  end;<BR>  Exit; //离开创建插件过程<BR> end;<br><br>//初始化插件类实例<br><br>if not InitializeMyPlugins(APlugin,sAlias) then<BR>begin<BR> showmessage('初始化插件类错误。');<BR> exit;<BR>end;<br><br>//获得当前权限值<br><br>APlugin.UserID := sUserID;<BR>//载入DLL窗口<br><br>if not LoadShowPluginForm(APlugin) then<BR>begin<BR> showmessage('载入中心插件出错。');<BR> exit;<BR> end;<BR>end;<br><br>//-----------------------------------------<BR>//Name: InitializeMyPlugins<BR>//Func: 初始化MYPLUGIN实例 (Caption | DllFileName | IsLoaded)<BR>//Para: APlugin-TMyPlugins<BR>//Rtrn: N/A<BR>//Auth: CST<BR>//Date: 2005-6-2<BR>//-----------------------------------------<br><br>function TFormHall.InitializeMyPlugins(APlugin:TMyPlugins; sAlias:String):Boolean;<BR>var<BR> strSQL:string;<BR> myDA:TMyDataAdapter;<BR>begin<BR> Result:=False;<BR> myDA:=TMyDataAdapter.Create;<BR> strSQL:='SELECT * FROM SystemModuleList WHERE modAlias='+QuotedStr(sAlias);<BR> try<BR>  myDA.RetrieveData(strSQL);<BR> except<BR>  on E:Exception do<BR>  begin<BR>   result:=false;<BR>   myDA.Free ;<BR>   exit;<BR>  end;<BR> end;<BR>try<BR> begin<BR>  with myDA.MyDataSet do<BR> begin<BR>  if Not IsEmpty then<BR> begin<BR>  APlugin.Caption:= FieldByName('modName').Value;<BR>  APlugin.DllFileName := FieldByName('modFile').Value;<BR>  APlugin.WndClass := FieldByName('modWndClass').Value ;<BR>  result:=True;<BR> end;<BR>Close;<BR> end; //end of with...do...<BR> end; //end of try<BR> except<BR>  on E:Exception do<BR>begin<BR> Result:=False;<BR> myDA.Free ;<BR> Exit;<BR> end; //end of exception<BR>end; //end of try...except<br><br> myDA.Free ; <BR>end;<br><br><br><br>//-----------------------------------------<br><br>//Name: LoadShowPluginForm<br><br>//Func: 载入DLL插件并显示窗口<br><br>//Para: APlugin-TMyPlugins<br><br>//Rtrn: true-创建成功<br><br>//Auth: CST<br><br>//Date: 2005-6-2<br><br>//-----------------------------------------<br><br>function TFormHall.LoadShowPluginForm (const APlugin:TMyPlugins):boolean;<br><br>var<BR> ShowDLLForm: TShowDLLForm; //DLL接口函数实例<BR> FreeDLLForm: TFreeDLLForm;<BR> sPath:string; //DLL文件的完整路径<BR>begin<BR> try<BR> begin<BR>  sPath:=ExtractFilepath(Application.ExeName)+ 'plugins\' + APlugin.DllFileName ;<BR>  APlugin.ProcAddr := LoadLibrary(PChar(sPath));<BR>  APlugin.FuncFreeAddr := GetProcAddress(APlugin.ProcAddr,'FreeDLLForm');<BR>  APlugin.FuncAddr := GetProcAddress(APlugin.ProcAddr ,'ShowDLLForm');<BR>  @ShowDLLForm:=APlugin.FuncAddr ;<BR>  @FreeDLLForm:=APlugin.FuncFreeAddr;<BR>  if ShowDllForm(Self.Handle, APlugin.Caption , APlugin.UserID) then<BR>   Result:=True<BR>  else<BR>   Result:=False;<BR>  end;<BR>  except<BR>   on E:Exception do<BR>  begin<BR>   Result:=False;<BR>   ShowMessage('载入插件模块错误,请检查PLUGINS目录里的文件是否完整。');<BR>  end;<BR> end;<BR>end; <br><br>…… <br><br>  4) DLL窗口控制<br><br>  正如3)中的代码说明的那样,DLL窗口的打开和关闭只是在表象层,关闭窗口并没有真正释放DLL窗口,只是调用API函数FindWindow根据窗口标识(就是Form.name)获得窗体句柄,用SHOWWINDOW函数的nCmdShow参数控制窗口显示/隐藏。<br><br>  其实这是我这个程序实现的不好的一个地方,如果在DLL窗口中使用Self.close方法会引起内存错误,实在能力有限没有办法解决,因此出此下策。所以每个DLL程序主窗口的关闭按钮都必须隐藏掉。 :-P<br><br>  5) DLL库的释放<br><br>  在程序退出时,必须根据插件信息实例逐一释放DLL库。释放DLL库的函数如下:<br><br>procedure TFormHall.ClosePlugin(aPLG:TMyPlugins);<BR>var<BR> FreeDLLForm:TFreeDLLForm;<BR>begin<BR> if aPLG.ProcAddr = 0 then exit;<BR> if aPLG.FuncFreeAddr = nil then exit;<BR> @FreeDLLForm:=aPLG.FuncFreeAddr;<BR> if not FreeDLLForm(Application.Handle,'','') then<BR>  showMessage('err');<BR>end; <br><br>  小结<br><br>  本实例程序运行效果如下:<br><br><BR><B></B><BR><IMG onclick="if(this.width>=550) window.open('/upload/20080315181507979.jpg');" src="/upload/20080315181507979.jpg" onload="if(this.width>'550')this.width='550';if(this.height>'1000')this.height='1000';" border=0> <br><br><BR>  我以上的方法中,因为有不少能力有限没有解决的问题,所以采用了一些看起来不太合理的掩饰方法,希望大家能在做了一点尝试后设计出更好的解决方法,我也希望能学到更多的好方法。
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>Delphi编程常用快捷键大全</li><li>C# 调用Delphi dll 实例代码</li><li>Delphi菜单组件TMainMenu使用方法详解</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: 初探Delphi中的插件编程