Delphi中的DLL封装和调用对象技术
- 格式:pdf
- 大小:124.91 KB
- 文档页数:17
第一章为什么要使用动态链接库(DLL) top提起DLL您一定不会陌生,在Windows中有着大量的以DLL为后缀的文件,它们是保证Windows正常运行和维护升级的重要保证。
(举个例子,笔者的Win95 System目录下尽有500多个DLL文件。
)其实,DLL 是一种特殊的可执行文件。
说它特殊主要是因为一般它都不能直接运行,需要宿主程序比如*.EXE程序或其他DLL的动态调用才能够使用。
简单的说,在通常情况下DLL是经过编译的函数和过程的集合。
使用DLL技术主要有以下几个原因:一、减小可执行文件大小。
DLL技术的产生有很大一部分原因是为了减小可执行文件的大小。
当操作系统进入Windows时代后,其大小已经达到几十兆乃至几百兆。
试想如果还是使用DOS时代的单执行文件体系的话一个可执行文件的大小可能将达到数十兆,这是大家都不能接受的。
解决的方法就是采用动态链接技术将一个大的可执行文件分割成许多小的可执行程序。
二、实现资源共享。
这里指的资源共享包括很多方面,最多的是内存共享、代码共享等等。
早期的程序员经常碰到这样的事情,在不同的编程任务中编写同样的代码。
这种方法显然浪费了很多时间,为了解决这个问题人们编写了各种各样的库。
但由于编程语言和环境的不同这些库一般都不能通用,而且用户在运行程序时还需要这些库才行,极不方便。
DLL的出现就像制定了一个标准一样,使这些库有了统一的规范。
这样一来,用不同编程语言的程序员可以方便的使用用别的编程语言编写的DLL。
另外,DLL还有一个突出的特点就是在内存中只装载一次,这一点可以节省有限的内存,而且可以同时为多个进程服务。
三、便于维护和升级。
细心的朋友可能发现有一些DLL文件是有版本说明的。
(查看DLL文件的属性可以看到,但不是每一个DL L文件都有)这是为了便于维护和升级。
举个例子吧,早期的Win95中有一个BUG那就是在闰年不能正确显示2月29日这一天。
后来,Microsoft发布了一个补丁程序纠正了这个BUG。
用Delphi编写及调用DLL的方法和技巧
蒋红龙
【期刊名称】《电脑开发与应用》
【年(卷),期】2003(016)012
【摘要】动态链接库是一种特殊的可执行文件,它是Microsoft Windows系列操作系统的一项新技术,阐述了动态链接库的基本概念及其优点,并举例讨论了用Delphi编写和调用DLL.的方法和技巧,由于DLL具有节省内存、共享代码、升级方便和独立于编程语言的优点,现已被广泛应用.
【总页数】3页(P22-24)
【作者】蒋红龙
【作者单位】大庆有限责任公司第四采油厂,大庆,163000
【正文语种】中文
【中图分类】TP3
【相关文献】
1.在C++Builder中编写和调用DLL [J], 黄明志
2.如何利用Delphi 4编写网上检索馆藏数据库的ISAPL DLL [J], 马林梓
3.使用Delphi编写DLL实现软件自动更新 [J], 云凤生
4.在VC中对Delphi所生成的DLLs的调用 [J], 李强;贾云霞
5.在Delphi中调用由VC++生成DLL的方法 [J], 赵福来;娄国焕
因版权原因,仅展示原文概要,查看原文内容请购买。
Delphi中控制Word,xml,dll等操作DLL的建立与调用[转]动态链接库是一个能够被应用程序和其它的DLL调用的过程和函数的集合体,它里面包含的是公共代码或资源。
由于DLL代码使用了内存共享技术,在某些地方windows也给了DLL一些更高的权限,因而DLL中可以实现一些一般程序所不能实现的功能,如实现windows的HOOK、ISAPI等。
同时,DLL还为不同语言间代码共享提供了一条方便的途径。
因而DLL在编程时应用较为广泛,本文将介绍如何在Delphi 中建立和使用DLL。
一.DLL 库内存共享机制从使用效果看,DLL和unit 很像,它们都可以被别的工程模块所调用,但二者在内部的实现机制上确存在着差别。
如果一个程序模块中用uses语句引用了某个unit,编译程序在编译该模块时,便会连同unit一起编译,并把编译后的可执行代码链接到本程序模块中,这就是一个程序模块能够调用所引用unit中过程和函数的原因。
当同一个unit被多个工程所引用时,则每个工程中都含有该unit的可执行代码,当含有该unit的多个工程同时执行时,unit的可执行代码会随不同工程而多次被调入内存,造成内存资源的浪费。
DLL则不同,它即使被某个工程调用,编译后仍是独立的。
也就是说编译后,一个DLL库形成一个单独的可执行文件,而不与任何其它的可执行文件连接在一起,因而DLL库并不从属于某个特定的工程,当多个工程调用同一个DLL库时只有第一个工程把DLL库调入内存,其余工程并不重复调入同一个DLL库到内存,而是到同一个共享内存区读取。
并且,DLL的执行代码是在程序运行期间动态调入的,而不是如unit在程序运行时就与整个工程一起调入内存。
这样便可消除unit带来的相同代码多处占用内存的弊病。
二 Delphi中DLL库的建立在Delphi环境中,编写一个DLL同编写一个一般的应用程序并没有太大的区别。
事实上作为DLL主体的DLL函数的编写,除了在内存、资源的管理上有所不同外,并不需要其它特别的手段。
在Delphi中封装对象到DLL方法一:基本封装 (1)原型 (1)原型说明 (1)封装TCar到DLL中 (2)在DLL中封装对象的限制 (2)封装步骤 (2)方法二:运用接口在DLL中封装对象 (3)原型 (3)封装步骤 (3)方法三:用COM/COM+封装对象 (4)方法一:基本封装原型原型说明TForm1和TCar分别在不同的单元当中;TForm1中有一个按钮Button1,点击Button1执行TCar.Drive;TForm1单元uses了TCar单元;TForm1的处理过程如下:varmycar:TCar;beginmycar:=TCar.Create;mycar.Drive;mycar.Free;封装TCar到DLL中在DLL中封装对象的限制调用DLL的应用程序只能使用DLL中对象得动态绑定方法。
也就是说方法后面必须有virtual关键字;封装对象的实例只能在DLL中创建;在DLL和调用DLL的应用程序中都需要对封装对象以及被调用的方法进行声明;封装步骤1、在Project Manager中添加新项目Add New Project,选择DLL Wizard,命名新项目为DemoDll。
2、将TCar单元加入到DemoDll项目中,并从原项目DemoExe中删除TCar单元。
3、在TCar单元做以下修改:在Drive方法后面加上virtual关键字。
4、在Library DemoDll中做以下修改:添加usesShareMem……function CrtCar:TCar;beginresult:=TCar.Create;end;exportsCrtCar;5、在Frm单元做以下修改:添加TCar = class(TObject)procedure Drive;virtual;abstract;end;……varfunction CrtCar:TCar;external 'DemoDll.dll';修改将mycar:=TCar.Create改为mycar:=CrtCar我们发现,在TCar单元中声明了类TCar,在Frm单元也声明了TCar,重复声明不是OO的正确思想。
标题:Delphi中使用DLL调用及结构参数传递的方法一、引言在Delphi中,我们经常需要使用动态信息库(DLL)来实现对外部功能的调用。
而在调用DLL时,有时需要传递结构参数,本文将介绍在Delphi中使用DLL进行结构参数传递的方法。
二、调用DLL1. 静态信息和动态信息在Delphi中,我们可以通过静态信息和动态信息两种方式调用DLL。
静态信息是将DLL文件直接嵌入到可执行文件中,而动态信息则是在程序运行时加载DLL文件。
一般来说,动态信息是更为常用的方式。
2. 使用动态信息调用DLL在Delphi中使用动态信息调用DLL,一般需要使用`LoadLibrary`函数加载DLL文件,然后使用`GetProcAddress`函数获取DLL中的函数位置区域,最后通过指针调用DLL函数。
示例代码如下:```delphivarhDLL: THandle; // DLL句柄addFunc: function(a, b: Integer): Integer; stdcall; // DLL函数指针beginhDLL := LoadLibrary('MyDll.dll'); // 加载DLLif hDLL <> 0 thenbegin@addFunc := GetProcAddress(hDLL, 'Add'); // 获取DLL中的Add函数位置区域if Assigned(addFunc) thenbegin// 调用DLL函数Result := addFunc(1, 2);end;FreeLibrary(hDLL); // 释放DLLend;end;```三、结构参数传递在DLL调用中,有时我们需要传递结构参数,即将自定义的结构体作为函数参数传递给DLL中的函数。
下面将介绍在Delphi中如何实现结构参数的传递。
1. 定义结构体我们需要在Delphi中定义需要传递的结构体。
Delphi动态与静态调用DLL(最好的资料)摘要:本文阐述了 Windows 环境下动态链接库的概念和特点,对静态调用和动态调用两种调用方式作出了比较,并给出了 Delphi 中应用动态链接库的实例。
一、动态链接库的概念动态链接库( Dynamic Link Library ,缩写为 DLL )是一个可以被其它应用程序共享的程序模块,其中封装了一些可以被共享的例程和资源。
动态链接库文件的扩展名一般是 dll ,也有可能是 drv 、 sys 和 fon ,它和可执行文件( exe )非常类似,区别在于 DLL 中虽然包含了可执行代码却不能单独执行,而应由 Windows 应用程序直接或间接调用。
动态链接是相对于静态链接而言的。
所谓静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。
换句话说,函数和过程的代码就在程序的 exe 文件中,该文件包含了运行时所需的全部代码。
当多个程序都调用相同函数时,内存中就会存在这个函数的多个拷贝,这样就浪费了宝贵的内存资源。
而动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。
仅当应用程序被装入内存开始运行时,在 Windows 的管理下,才在应用程序与相应的 DLL 之间建立链接关系。
当要执行所调用 DLL 中的函数时,根据链接产生的重定位信息, Windows 才转去执行 DLL 中相应的函数代码。
一般情况下,如果一个应用程序使用了动态链接库, Win32 系统保证内存中只有 DLL 的一份复制品,这是通过内存映射文件实现的。
DLL 首先被调入 Win32 系统的全局堆栈,然后映射到调用这个 DLL 的进程地址空间。
在 Win32 系统中,每个进程拥有自己的 32 位线性地址空间,如果一个 DLL 被多个进程调用,每个进程都会收到该 DLL 的一份映像。
Delphi中高级DLL的编写和调用2001年06月14日10:54:38 赛迪网苏涌根据Delphi提供的有关DLL编写和调用的帮助信息,你可以很快完成一般的DLL编写和调用的应用程序。
本文介绍的主题是如何编写和调用能够传递各种参数(包括对象实例)的DLL。
例如,主叫程序传递给DLL一个ADOConnection 对象示例作为参数,DLL中的函数和过程调用通过该对象实例访问数据库。
需要明确一些基本概念。
对于DLL,需要在主程序中包含exports子句,用于向外界提供调用接口,子句中就是一系列函数或过程的名字。
对于主叫方(调用DLL的应用程序或其它的DLL),则需要在调用之前进行外部声明,即external保留字指示的声明。
这些是编写DLL和调用DLL 必须具备的要素。
另外需要了解Object Pascal 中有关调用协议的内容。
在Object Pascal 中,对于过程和函数有以下五种调用协议:指示字参数传递顺序参数清除者参数是否使用寄存器register 自左向右被调例程是pascal 自左向右被调例程否cdecl 自右向左调用者否stdcall 自右向左被调例程否safecall 自右向左被调例程否这里的指示字就是在声明函数或过程时附加在例程标题之后的保留字,默认为register,即是唯一使用CPU寄存器的参数传递方式,也是传递速度最快的方式;pascal: 调用协议仅用于向后兼容,即向旧的版本兼容;cdecl: 多用于C和C++语言编写的例程,也用于需要由调用者清除参数的例程;stdcall: 和safecall主要用于调用W indows API 函数;其中safecall还用于双重接口。
在本例中,将使用调用协议cdecl ,因为被调用的DLL中,使用的数据库连接是由主叫方传递得到的,并且需要由主叫方处理连接的关闭和销毁。
下面是DLL完整源程序和主叫程序完整源程序。
包括以下四个文件:Project1.DPR {主叫程序}Unit1.PAS {主叫程序单元}Project2.DPR {DLL}Unit2.PAS {DLL单元}{---------- DLL 主程序Project2.DPR ----------}library Project2;usesSysUtils,Classes,Unit2 in 'Unit2.pas' {Form1};{$R *.RES}{ 下面的语句用于向调用该DLL的程序提供调用接口}exportsDoTest; { 过程来自单元Unit2 }beginend.{---------- DLL中的单元Unit2.PAS ----------}unit Unit2;interfaceusesWindows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Db, ADODB, StdCtrls, Menus;typeTForm1 = class(TForm)ADOConnection1: TA DOConnection;{ 本地数据库连接}Memo1: TMemo; { 用于显示信息}privatepublicend;{ 该过程向外提供}procedure DoTest(H: THandle; { 获得调用者的句柄}A Conn: TADOConnection;{ 获得调用者的数据库连接}S: string; { 获得一些文本信息}N: Integer); { 获得一些数值信息}cdecl; { 指定调用协议}implementation{$R *.DFM}procedure DoTest(H: THandle; AConn: TA DOConnection; S: string; N: Integer); beginApplication.Handle := H; { 将过程的句柄赋值为调用者的句柄}{ 上面语句的作用在于,DLL的句柄和调用者的句柄相同,在任务栏中就不会} { 各自出现一个任务标题了。
深入Delphi下的DLL编程作者:岑心引言相信有些计算机知识的朋友都应该听说过―DLL‖。
尤其是那些使用过windows操作系统的人,都应该有过多次重装系统的―悲惨‖经历——无论再怎样小心,没有驱动损坏,没有病毒侵扰,仍然在使用(安装)了一段时间软件后,发现windows系统越来越庞大,操作越来越慢,还不时的出现曾经能使用的软件无法使用的情况,导致最终不得不重装系统。
这种情况常常是由于dll文件的大量安装和冲突造成的。
这一方面说明DLL的不足,另一方面也说明DLL的重要地位,以至我们无法杜绝它的使用。
DLL(动态链接库,Dynamic Link Library)简单来说是一种可通过调用执行的已编译的代码模块。
DLL是windows系统的早期产物。
当时的主要目的是为了减少应用程序对内存的使用。
只有当某个函数或过程需要被使用时,才从硬盘调用它进入内存,一旦没有程序再调用该DLL了,才将其从内存中清除。
光说整个windows系统,就包括了成百上千个dll文件,有些dll文件的功能是比较专业(比如网络、数据库驱动)甚至可以不安装的。
假如这些功能全部要包括在一个应用程序(Application program)里,windows将是一个数百M大小的exe文件。
这个简单的例子很容易解释DLL的作用,而调用DLL带来的性能损失则变得可被忽略不计。
多个应用程序调用同一个DLL,在内存里只有一个代码副本。
而不会象静态编译的程序那样每一个都必须全部的被装入。
装载DLL时,它将被映射到进程的地址空间,同时使用DLL的动态链接并非将库代码拷贝,而仅仅记录函数的入口点和接口。
同时DLL还能带来的共享的好处。
一家公司开发的不同软件可能需要一些公用的函数/过程,这些函数/过程可能是直接的使用一些内部开发的DLL;一些常用的功能则可以直接使用windows 的标准DLL,我们常说的windows API就是包含在windows几个公用DLL文件里的函数/过程;理论上(如果不牵涉作者的版权),知道一个DLL的声明及作用(函数定义的输入参数及返回值),我们完全可以在不清楚其实现(算法或编译方式)的情况下直接使用它。
Delphi中的DLL封装和调用对象技术本文刊登2003年10月份出版的Dr.Dobb's 软件研发第3期刘艺摘要DLL是一种应用最为广泛的动态链接技术但是由于在DLL中封装和调用对象受到对象动态绑定机制的限制使得DLL在封装对象方面有一定的技术难度导致有些Delphi程序员误以为DLL只支持封装函数不支持封装对象本文着重介绍了DLL中封装和调用对象的原理和思路并结合实例给出了多种不同的实现方法关键字动态链接库DLL对象接口虚方法动态绑定类引用面向对象1物理封装与动态链接物理上的封装意味着将程序封装成若干个独立的物理组成部分各部分之间通过动态链接共同完成系统的功能而且各个物理组成部分可以单独维护和编译不影响其他部分要理解物理封装首先要搞清楚静态链接和动态链接在Delphi中如果程序的各个模块分别保存在不同的单元文件中并通过uses指令来互相调用这就是一个典型的静态链接于是各个静态的子例程编译之后连接器从Delphi 编译过的单元或静态库中取出子例程编译代码并添加到执行文件中最终EXE文件包括了程序及其所属单元的所有代码显然静态链接的单元或模块最终以一个独立的物理形式可执行文件存在除了自己编写的单元文件Delphi还自动uses了一些预设的单元如Windows Messages等这些都是静态链接静态链接无法实现物理上的切割和封装而且一旦其中某个单元或模块改动其他所有单元或模块都得随之重新编译和连接用于实现物理切割和封装的bpl包DLL动态链接库或COM+组件都是一种动态链接的形式在动态链接情况中连接器只使用子例程external声明中的信息在执行文件中产生一些数据表格当Windows向内存中装载执行文件时它首先装载所有必需的DLL然后程序才会启动在装载过程中Windows用函数在内存中的地址填充程序的内部表格每当程序调用一个外部函数时它就会使用该内部数据表格直接对DLL代码它当前装载在程序的地址空间中进行调用注意该模式不会涉及两个不同的应用程序DLL已经变成了应用程序的一部分并装载在同一地址空间所有参数的传递都发生在堆栈上与其它任何函数调用一样这里我们不打算讨论DLL的编译因为我们首先想重点介绍Delphi中的DLL封装和调用对象技术2用DLL封装对象DLL Dynamic Link Library动态链接库就目前来讲已经不再是什么新技术读者可以在书店过时的Delphi 书籍里随便找到讨论DLL编程的章节但这些涉及DLL编程的书中几乎都是谈论用DLL来封装函数的实际上大量的程序员也是在使用DLL来封装函数或面向过程的一个模块一个函数集合而在这里我只想讨论如何用DLL来封装对象这可能是读者未曾有过的DLL使用经验但这却是这本完全围绕面向对象编程的书中重要的部分之一或许你能从中发现一些与众不同的实用技巧参见考虑到目前关于DLL的现成资料很多这里我省略了DLL的基本知识和编写方法假设读者已经有了一定的DLL编程基础如果你没有这样的基础建议参阅拙作Delphi6企业级解决方案及应用剖析DLL编程技术一节P271一般来说使用DLL封装对象主要有以下好处y节约内存多个程序可以使用同一个DLL时该DLL只需加载一次而且可以只在使用时加载不用时销毁y使程序代码实现复用这就是说用DLL封装的对象可以重复使用甚至可以让不同的程序语言调用y使程序模块化组件化这样利于团队开发维护和更新方便然而DLL在封装对象方面却有一定的技术难度这方面资料极少甚至有的程序员误以为DLL只支持封装函数不支持封装对象通过研究我们发现DLL在封装对象上主要的限制在于y调用DLL的应用程序只能使用DLL中对象的动态绑定的方法y DLL封装对象的实例只能在DLL中创建y在DLL和调用DLL的应用程序中都需要对封装的对象及其被调用的方法进行声明下面我先通过一个简单的例子来演示如何使用DLL封装对象并在应用程序中调用该对象然后再讨论相关的技术细节3一个简单的例子读者一定还记得我们在前面章节中演示了车的继承关系和合成关系这个程序由逻辑单元的Demo和界面单元frmDemo组成我们现在就用DLL封装Demo单元的所有对象并在frmDemo单元实现调用读者可以通过这个具体的例子来学习如何使用DLL封装对象打开项目文件ObjDemo.dpr如图1所示在项目管理器Project Manager中鼠标右击ProjectGroup1然后在弹出菜单中选择Add New Project...菜单项此时弹出如图2所示的New Items对话框选择DLL Wizard Delphi的DLL向导将创建一个DLL项目我们将该项目重新命名为DemoSvr并保存在项目组同一目录下图1鼠标右击ProjectGroup1在弹出菜单中选择Add New Project...菜单项图2在New Items对话框中选择DLL Wizard修改DemoSvr中的代码如示例程序1所示示例程序 1 动态链接库DemoSvr的主程序library DemoSvr;{ Important note about DLL memory management: ShareMem must be thefirst 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. }usesShareMem,SysUtils,Classes,Demo in 'Demo.PAS';{$R *.res}function CarObj:TCar;beginResult:=TCar.create;end;function BicycleObj:TBicycle;beginResult:=TBicycle.create;end;exportsCarObj,BicycleObj;end.由此可见 DLL封装对象的实例是在DLL中创建的CarObj 和BicycleObj函数创建并输出了Car对象和Bicycle对象的引用这样DemoSvr动态链接库就可以通过CarObj 和BicycleObj函数输出Car对象和Bicycle对象了但是Car对象和Bicycle对象是在Demo.pas 文件中声明和实现的所以这里uses了Demo.PAS为了能够使用Demo.PAS在项目管理器中直接把Demo.pas文件从ObjDemo项目中拖放到DemoSvr项目中如图3所示图 3 将Demo.pas文件从ObjDemo项目中拖放到DemoSvr项目中打开Demo.pas修改TBicycle和TCar的声明如下TBicycle = class(TVehicle)publicconstructor create;destructor Destory;procedure ride;virtual;end;TCar = class(TVehicle)protectedFEngine: TEngine;publicconstructor create;destructor Destory;procedure drive;virtual;end;请注意这里我把应用程序中需要调用的对象方法ride和drive改成了虚方法显然这么做不是为了让TBicycle 和TCar的派生类来覆盖ride和drive方法这是因为编译连接应用程序时编译器无法知道也无需知道对象在DLL中的方法是如何实现的这就意味着对于应用程序来说要使用动态绑定晚绑定技术所以调用DLL的应用程序只能使用DLL中对象的动态绑定的方法前面我们讲过虚方法的动态绑定技术是把虚方法的入口放到虚方法表VMT中VMT是一块包含对象方法指针的内存区通过VMT调用程序可以得到虚方法的指针如果我们不把ride和drive声明为虚方法VMT中就不会有这些方法的入口指针因此调用程序也就无法得到这个方法的入口指针接下来回到frmDemo单元在调用DLL的应用程序中同步声明需要调用的的对象及其被调用的方法这里除了将ride和drive声明为虚方法外还要声明为抽象方法因为frmDemo单元不提供ride和drive方法的实现不把它们声明为抽象方法则编译时无法通过应用程序frmDemo单元的完整代码如示例程序2所示示例程序2调用DLL对象的应用程序unit frmDemo;interfaceusesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;type//---这里声明需要用到的DLL中对象的方法---TVehicle = class(TObject);TCar = class(TVehicle)publicprocedure drive;virtual;abstract;end;TBicycle = class(TVehicle)publicprocedure ride;virtual;abstract;end;//----------------------------TForm1 = class(TForm)Button1: TButton;Button2: TButton;procedure Button2Click(Sender: TObject);procedure Button1Click(Sender: TObject);private{ Private declarations }public{ Public declarations }end;varForm1: TForm1;//---这里导入DLL文件及其函数---function CarObj:TCar ;external 'DemoSvr.dll';function BicycleObj:TBicycle ;external 'DemoSvr.dll';implementation{$R *.dfm}procedure TForm1.Button2Click(Sender: TObject);var MyCar:TCar;beginMyCar:=CarObj;if Mycar=nil then exit;tryMyCar.drive;finallyMyCar.Free;end;end;procedure TForm1.Button1Click(Sender: TObject);var Bicycle:TBicycle;beginBicycle:=BicycleObj;tryBicycle.ride;finallyBicycle.Free;end;end;end.最后选择Build All Projects菜单项编译和连接所有的项目如图4所示我们就得到了需要的应用程序可执行文件以及DLL运行测试可以看到这个程序实现了和原先一样的功能但是我对这样的DLL封装对象实现不是太满意因为在DLL和应用程序中都需要声明封装的对象还要使用好virtual和abstract限定符很容易造成阅读程序理解上的错觉如果一旦对象发生变化就需要分别在两边修改对象声明以保持同步稍有不慎就会出错对此Steve Teixeira在Delphi6开发人员指南机械工业出版社2003年出版相关内容参见该书209页一书中提出了使用头文件的方法并通过加上编译指令来控制DLL和应用程序分别读到不同的头文件内容这个方法虽然可以通过只修改头文件来保持声明的同步但编译指令和头文件使得阅读程序更加困难在这里我有一个更好的方法供读者分享这就是使用接口的方法4利用Delphi接口实现DLL中对象的动态绑定前面我们分析过调用DLL的应用程序只能使用DLL中对象的动态绑定的方法理解这一点是实现DLL封装和使用对象的关键那么Delphi接口技术为我们提供了一个最佳选择图 4 选择Build All Projects菜单项编译和连接所有的项目为此我们创建一个接口单元IDemo分别声明ICar和IBicycle接口接口方法分别是应用程序要用到的Drive和Ride完整代码如示例程序3所示示例程序3接口单元IDemo的代码unit IDemo;interfacetypeICar = interface (IInterface)['{ED52E264-6683-11D7-B847-001060806215}']procedure Drive;end;IBicycle = interface (IInterface)['{ED52E264-6683-11D7-B847-001060806216}']procedure Ride;end;implementationend.注意接口单元IDemo中没有也不能有任何实现它同时被应用程序和DLL所用Use这样当需要修改应用程序调用的对象方法时只要在一个地方即该接口单元修改即可避免了可能出现的声明不一致错误使用接口还带来了更多的好处首先无需使用virtual和abstract限定符修改对象方法声明避免了程序阅读上的错觉其次利用接口实例计数器自动管理对象的生命期避免了程序员遗忘销毁对象造成的内存泄漏为了使用接口我将Demo单元的类型声明部分作了以下修改以便TBicycle和TCar 类能够实现接口方法值得高兴的是该单元仅仅需要修改声明部分而程序实现部分根本不需要做任何改动unit Demo;interfaceusesSysUtils, Windows, Messages, Classes, Dialogs,IDemo;typeTVehicle = class(TInterfacedObject)protectedFColor: string;FMake: string;FTopSpeed: Integer;FWheel: TWheel;FWheels: TList;procedure SlowDown;procedure SpeedUp;procedure Start;procedure Stop;end;TBicycle = class(TVehicle,IBicycle)publicconstructor create;destructor Destory;procedure ride;end;TCar = class(TVehicle,ICar)protectedFEngine: TEngine;publicconstructor create;destructor Destory;procedure drive;end;最后检查一下项目管理器确保在应用程序项目和DLL项目中都添加了IDemo单元如图5所示图5确保在应用程序项目和DLL项目中都添加了接口单元IDemo示例程序4使用接口技术调用DLL对象的应用程序unit frmDemo;interfaceusesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, IDemo;//在这里Use IDemo单元typeTForm1 = class(TForm)Button1: TButton;Button2: TButton;procedure Button2Click(Sender: TObject);procedure Button1Click(Sender: TObject);private{ Private declarations }public{ Public declarations }end;varForm1: TForm1;function CarObj:ICar ;external 'DemoSvr.dll';function BicycleObj:IBicycle ;external 'DemoSvr.dll';implementation{$R *.dfm}procedure TForm1.Button2Click(Sender: TObject);var MyCar:ICar;beginMyCar:=CarObj;MyCar.drive;Mycar:=nil;end;procedure TForm1.Button1Click(Sender: TObject);var Bicycle:IBicycle;beginBicycle:=BicycleObj;Bicycle.ride;Bicycle:=nil;end;end.在示例程序4中改动不是很多这里Use了IDemo单元而没有额外的声明实现部分通过接口调用了DLL中的接口方法也可以说是对象方法运行示例程序4和运行示例程序2实现的功能完全一样5使用抽象类实现DLL中对象的动态绑定既然DLL中封装和调用对象受到了对象动态绑定机制的限制那么除了利用Delphi接口技术外我们还可以考虑使用抽象类来实现DLL中对象的动态绑定机制图6显示了一个基于数据库应用的示例程序的面向对象设计我将界面部分设计成一个瘦客户机的形式这是一个供用户交互的可执行文件distributabel2.exe它封装了外观类TfrmUsers我把业务部分包括数据模块设计成提供服务的服务器这是一个动态链接库文件UserSvr.dll它封装了业务类TuserMaint和数据库访问类TuserDM这种设计体现了界面和业务分离的思想o reateTT T o create T o pe DataModuleCreate(..)T o pe btnExitClick(..)T Tf rmUsersu TIUserMaintu TUserDMu TUserMaintu TIUserMaintu objUsersUserDM图 6界面和业务的物理分离的设计由于原来的逻辑独立的类和代码存放在不同的单元文件中我们很容易重新将它们划分到不同的项目里如图 7所示图 7重新将逻辑独立的单元文件划分到不同的项目里瘦客户机其实上就是一个空壳只提供交互的界面它的外观类TfrmUsers 向TUserMaint 的实例对象请求服务该对象封装在DLL 中前面我已经讲过如何用DLL 来封装对象除了前面讲过的两种方法外这里我想介绍第三种方法即使用抽象类做接口的方法由于调用DLL的应用程序只能使用DLL中对象的动态绑定的方法我们不妨专门设计一个抽象类TIUserMaint作为提供对象方法的接口在抽象类TIUserMaint中有供应用程序使用的对象方法不过它们都是虚抽象方法目的是支持动态绑定而又无需提供实现我将新增的TIUserMaint放在抽象类接口单元uIUserMaint.pas文件中其源代码如示例程序5所示这个单元将作为接口文件分别包含在UserSvr和Distributable2项目中如图7所示示例程序5抽象类接口单元uIUserMaint代码unit uIUserMaint;interfaceusesClasses;typeTIUserMaint = class (TObject)publicfunction GetDepList: TStrings;virtual;abstract;function GetUserList(strName:String): OLEVariant;virtual;abstract;procedure UpdateUserData(UserData:OleVariant; out ErrCount: Integer);virtual;abstract;constructor create;virtual;abstract;end;TIUserMaintClass=class of TIUserMaint;implementation//没有实现代码end.在示例程序5中还定义了TIUserMaintClass 类型它是TIUserMaint的类引用这对于把实现类从DLL传递到进行调用的应用程序是必要的一般抽象类只定义接口它由虚抽象方法组成而没有实际的数据为了实现抽象类TIUserMaint的抽象方法原来的TUserMaint类需要继承TIUserMaint类并覆盖其所有的虚抽象方法新的TUserMaint类声明如下TUserMaint = class (TIUserMaint)privateUserDM:TUserDM;publicfunction GetDepList: TStrings;override;function GetUserList(strName:String): OLEVariant;override;procedure UpdateUserData(UserData:OleVariant; out ErrCount: Integer);override;constructor create;override;destructor Destroy;override;end;但实际上TUserMaint类原有的实现部分并不需要改动所以我们的工作量不大示例程序6是动态链接库UserSvr.dll的源代码这里我使用了TObjUsers函数该函数返回了一个类型为TIUserMaintClass的类引用而不是对象引用所以在应用程序中可以使用这样的代码来创建DLL封装的对象objUsers:=TObjUsers.Create;但这不意味着TObjUsers是一个类记住这里TObjUsers是一个DLL输出的函数它的返回类型是一个类引用类型示例程序 6 动态链接库UserSvr.dll的源代码library UserSvr;usesShareMem,SysUtils,Classes,uUserMaint in 'uUserMaint.pas',udmUser in 'udmUser.pas' {UserDM: TDataModule},uIUserMaint in 'uIUserMaint.pas';{$R *.res}function TObjUsers:TIUserMaintClass;beginresult:=TUserMaint;end;exportsTObjUsers;beginend.细心的读者可能已经发现既然TObjUsers函数的返回类型为TIUserMaintClassTIUserMaintClass在示例程序5中声明为:TIUserMaintClass=class of TIUserMaint;那么result:=TUserMaint会不会是写错了呢没有写错我们在应用程序中声明了TIUserMaint类型的对象 objUsers通过传递类引用那条objUsers:=TObjUsers.Create语句实现的是objUsers:= TUserMaint.Create功能这里面隐含了TUserMaint向TIUserMaint转型的过程当然TIUserMaint作为抽象类本身也无法直接创建自己的实例所以必须通过转型才行另外在TIUserMaint的派生类中可以随意改变方法的实现却不会影响到方法的接口这就是说你以后通过进一步修改DLL封装对象的实现方法来升级DLL无需重新修改和编译应用程序因为TIUserMaint作为抽象类提供的方法接口没有改变示例程序7是应用程序实现界面和业务的物理分离后的界面单元的源代码这里要注意几点y在interface部分要Uses抽象类接口单元uIUserMainty objUsers声明为TIUserMaint类型y声明DLL函数function TObjUsers:TIUserMaintClass; external 'UserSvr.dll';除此之外几乎不需要进行其他的改动由此可见从界面和业务的逻辑分离演化到界面和业务的物理分离实际上并不是想象的那样困难示例程序7物理分离后的界面单元代码unit ufrmUsers;interfaceusesWindows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, DB, DBClient, StdCtrls, DBCtrls, Grids, DBGrids, Mask, ExtCtrls, Buttons,uIUserMaint;typeTfrmUsers = class(TForm)btnExit: TButton;btnQryByName: TSpeedButton;Label1: TLabel;Label2: TLabel;Label3: TLabel;Label5: TLabel;Label6: TLabel;Label7: TLabel;edtQryByName: TLabeledEdit;DBEdit1: TDBEdit;DBEdit2: TDBEdit;DBEdit3: TDBEdit;DBEdit4: TDBEdit;DBGrid1: TDBGrid;dbcbSex: TDBComboBox;dbcbDep: TDBComboBox;DataSource1: TDataSource;cdsUserMaint: TClientDataSet;cdsUserMaintID: TWideStringField;cdsUserMaintNAME: TWideStringField;cdsUserMaintSEX: TWideStringField;cdsUserMaintJOB: TWideStringField;cdsUserMaintTEL: TWideStringField;cdsUserMaintCALL: TWideStringField;cdsUserMaintDEP: TWideStringField;cdsUserMaintGROUP_ID: TWideStringField;cdsUserMaintPASSWORD: TWideStringField;btnUpdate: TBitBtn;procedure btnUpdateClick(Sender: TObject);procedure btnQryByNameClick(Sender: TObject);procedure FormCreate(Sender: TObject);procedure btnExitClick(Sender: TObject);procedure FormDestroy(Sender: TObject);privateobjUsers:TIUserMaint;public{ Public declarations }end;varfrmUsers: TfrmUsers;constM_TITLE='操作提示';//所有提示对话框的标题implementation{$R *.dfm}function TObjUsers:TIUserMaintClass;external 'UserSvr.dll';procedure TfrmUsers.btnUpdateClick(Sender: TObject); varnErr:integer;beginif cdsUserMaint.State=dsEdit then cdsUserMaint.Post; if (cdsUserMaint.ChangeCount > 0) thenbeginobjUsers.UpdateUserData(cdsUserMaint.Delta,nErr);if nErr>0 thenapplication.MessageBox('更新失败',M_TITLE,MB_ICONWARNING)elsebeginapplication.MessageBox('更新成功',M_TITLE,MB_ICONINFORMATION) ;btnQryByNameClick(nil);end;end;end;procedure TfrmUsers.btnQryByNameClick(Sender: TObject);beginbtnUpdate.Enabled:=true;dbcbDep.Items.AddStrings(objUsers.GetDepList);cdsUserMaint.Active:=false;cdsUserMaint.Data:=objUsers.GetUserList(edtQryByName.Text);cdsUserMaint.Active:=True;end;procedure TfrmUsers.FormCreate(Sender: TObject);beginobjUsers:=TObjUsers.Create;end;procedure TfrmUsers.btnExitClick(Sender: TObject);beginclose;end;procedure TfrmUsers.FormDestroy(Sender: TObject);beginobjUsers.Free;end;end.作者简介刘艺海军工程大学副教授美国Borland公司授予的 Delphi产品专家计算机技术作家著有10多部计算机专著出版重点大学计算机教材2部这篇文章摘编自他的新书Delphi面向对象编程思想作者个人网站 。