本章要点:
n 为何要用包
n 为何不用包
n 包的类型
n 包文件
n 使用运行期包
n 把包安装到Delphi IDE中
n 创建包
n 包的版本化
n 包编译器指示符
n 包的命名约定
n 使用运行期(插件)
n 从包中导出函数
n 获取包的信息
从Delph 3开始便引入了包的概念,它使我们能够把应用程序放入独立的模块,然后让多个应用程序共享这些模块。包只是一种特殊的动态链接库(DLLs),它包含了其他有关Delphi的详细信息,它和DLL的不同之处在于使用方法。包主要用于共享独立模块(即Borland包库,以.bpl为后缀的文件)中存储组件集合。在开发Delphi应用程序时,应用程序通常是在运行期使用我们创建的包,而不是直接在编译/链接期连接它们。因为这些单元的代码都寄存在.bpl文件中,而不是exe或者.dll文件中,所以这些.exe或.d11文件可以非常小。
包是VCL专有的,也就是说,用其他语言编写的应用程序不能使用Delphi创建的包(C++Builder是个例外)。提出包技术的原因是为了避开Delphi l和Delphi 2的限制。在以前的Delphi版本中,VCL向每个可执行文件添加了最小150k~200k代码。因此,即使把应用程序的—小部分分离到一个DLL巾,这个DLL和应用程序都还是包含冗余代码。假如需要在一台机器上提供一套应用程序集,必然非常头痛。包使我们得以减小应用程序的大小,并为组件集合的发布提供便利途径。
1.1 为何要用包
使用包的原因有多种。在接下来的小节中,我们将讨论3个重要的原因:精简代码、分割应用程序和组件容器。
1.1.1 精简代码
使用包的一个土要原因是减小应用程序和DLL的大小。Delphi已经提供了几个个预定义的包,这些包把VCL分割成逻辑止的分组。事实上,我们能够有选择地编译自己的应用程序,如同有多个Delphi包存在一样。
1.1.2 发布更小的应用程序——应用程序分割
人家已经知道Internet上有很多可用的程序,也许是完整的应用程序、可下载的演示版或者是对现有应用程序的升级。对于用户来说.假如在他们的本地系统上已经存在某个应用程序的一部分(比如预安装),选择下载更小的应用程序版本将更有利。
通过使用包把应用程序进行分割,用户就可以仅仅对应用程序需要升级的那一部分进行升级。然而,注意必须考虑一些有关版本的问题。本章涉及到了这些版本问题。
1.1.3 组件容器
也许使用包最通常的原因之一是第三方组件的发朽。假如你是一个组件卖主,你必须知道如何创建包,因为诸如组件和属性编辑器、向导和专家工具等设计期元素,都是中包提供的。
包和DLL的对比 使用DLL来为它们的服务器应用程序存放管理窗体会导致DLL拥有自己的Forms.pas文件副本。这将会引起一个不可思议的错误,该错误与Windows的窗口句柄处理有关。Windows窗口句柄处理产生于DLL中——当DLL被卸载时,窗口句柄却不能被操作系统解除参照。下一个穿过队列被发往顶层窗口的消息会导致应用程序出错,这样操作系统就会因为应用程序处于非法状态而将它关闭。使用包代替DLL可以克服这个问题,因为包引用了主应用程序的Forms.pas副本,因此消息队列能够成功地传到应用程序。 |
1.2 为何不用包
最好不要使用运行期包,除非其他应用程序也需要使用这些包。因为,与把源代码编译为最终可执行文件相比,包消耗的磁盘空间会更多。原因何在?假如创建一个使用包的应用程序使得代码尺寸包从200k减小到30k,看上去好像是节约了不小的空间。然而,当需要发布包,甚至是Vcl60.dcp包时,大概需要2M空间。可以看出来这样的“节约”并不是我们所期望的。我们的目的是,当某代码被多个应用程序共享时,使用包来共享代码。注意这就是在运行期使用包的惟一原出。对于一个组件作者来说,提供一个设计包,其中必须包含了希望用于Delphi IDE的组件。
1.3 包的类型
我们可以创建和使用的包有4种类型
n 运行期包——运行期包包含—个应用在运行期所需的代码、组件等。一个依赖于某个特定运行期包的应用程序,在没有包的情况下就不能运行。
n 设计包——设计包包含了在Delphi IDE中设计应用程序所必需的组件、属性/组件编辑器、专家工具等。这种类型的包只能用于Delphi,而且绝不要和应用程序一起发布。
n 运行期设计包——一个可同时在设计和运行期被激活的包,通常用在没有具体的设计元素(比如属性/组件编辑器和专家工具)时。通过创建这种包,可以简化应用程序的开发和配置。然而,假如这种包中含有设计元素,那么在运行期使用它将会带来额外的设计支持负担。如果有许多设计期元素的话,我们推荐创建设计包或运行期包,当出现多个特定设计元素时,通过这些包来分割它们。
n 非运行期、非设计包——这是种不太常见的包,只供其他包使用,不能直接供应用程序引用,也不能用于设计环境。
1.4 包文件
表1.1根据包的文件扩展名列出并描述了特定包文件的类型。
表 1.1 包文件
文件扩展名 |
文件类型 |
描述 |
.dbk |
包源文件 |
当调用包编译器时创建.dpk文件。正如大家所想到的,这有点类似于Delphi项目创建一个.dpr文件 |
.dcp |
运行期/设计期包标识文件 |
这是已编译的包,它包含了包及其单元的标识信息。另外,还有Delphi IDE所需的报头信息 |
.dcu |
编译过的单元 |
一个包含在某个包中的已编译单元,包中的每一个单元都一个相应的.dcu文件 |
.bpl |
运行期/设计期 |
这是运行期包或设计期包的库类型的包文件,相当于Windows中的DLL。对于一个运行期包,应该和应用程序(如果它们已经被激活)一起发布这个文件。假如这个文件代表的是一个设计期包,应该和运行期包一起发布给使用它们写程序的程序员。注意如果所发布的不是源代码,就必须发布相应的.dcp文件 |
1.5使用运行期包
为了在Delphi应用程序中使用运行期包,只要打开Project Options对话框上的Packages页,选中其中的Build wlth Runtime Package核查框就行了。 下一次在选中这个选项之后再编译应用程序,这个应用程序会被动态地连接到运行期包,而不是让单元静态的连入.exe或.d11文件。这样将会得到一个苗条得多的应用程序(当然,要牢牢记住必须和应用程序—起配置必需的包)。
1.6 把包安装到DeIphi IDE中
有时,还必须把包安装到DeIphi IDE中。例如在获得个第三方组件集或Delphi插件时,就有必要这样做。
在这种情况下,必须首先把包文件放置到合适的位置。表1.2所示的就是通常放置包文件的地方。
表1.2 包文件的存放位置
包文件 |
位置 |
运行期包(*.bpl) |
运行期包应该被放到Windows\system\目录下(Windows95/98)或WinNT\System32\目录下(Windows NT/2000) |
设计期包(*.bpl) |
因为从不同的卖主得到几个包是可能的,所以应该把设计期包放在公共的目录下,以便于管理。比如,在\Delphi 6\目录下创建\PKG目录,可以把设计期包放在这里 |
包标志文件(*.dcp) |
可以把包标识文件放在和设计期包文件(*.bpl)相同的地方 |
编译单元(*.dcu) |
假如正在发布没有源代码的包,必须同时发布已编译的单元,我们推荐把DCU文件存放在类似\Delphi 6\Lib的目录下,不要把自己的DCU文件和第三方卖主的DCU文件放在一起。比如,我们可以创建目录\Delphi 6\3PrtyLib来存放第三方组件的*.dcu。必须要把这个目录包括到搜索路径中去。 |
要安装一个包,只要在Delphi 6菜单中选择Component | Install Packages,调出Project Options对话框上的Packages页。
单击Add按钮,选择某个.bpl文件。这样做之后,这个文件将成为Project页上被选定的文件。按下OK,这个新包就被安装列Dclphi IDE中了。假如这个包中包含组件,我们在组件面板上会看到新的组件页及新安装的组件。
1.7 创建包
在创建包之前,需要就一些事情做出决策。首先,需要知道将要创建的包的类型(运行期包、设计期包等)。需要根据不同情况选样包的类型,这一点我们马上就会说明。第二,要知道给新创建的包取个什么样的名宁,以及把这个也项目存放在什么地方,记住存放配置好的包的目录也许并不是创建包的白录。最后,需要知道这个包包含哪些单元以及它还需坚哪些其他的包。
1.7.1 包编辑器
最常见的创建包的方法是使用包编辑器,从Object Repository(对象仓库)中选择Packages图标就可以调出包编辑器(在 Delphi主菜单中选择File | New | Other,可以找到Object Repository)。注意包编辑器包含两个文件夹:Contains和Requires。
1. Contains文件夹
在Contains文件夹中,我们指定需要编译的单元放入新的包中。在把单元放入一个包的Contains时,要遵循如下几个规则:
n 绝对不能把这个单元列于另一个包的contains子句中,也不能把它列于另一个包中的某个单元的uses子句中,这个包将和此单元应该放入的包同时被载入。
n 列于contains子句中的单元,不管直接地还是间接地(间接地就是指它们存在于其他单元的uses于句下,而这些单元列于某个包的contains子句中),都不能被列于此包的requires子句中。这是因为重编译时,这些单元已经和包绑定起来了。
n 假如某个单元已经于某个包的contains子句里,就不能再把这个单元列于同—个应用程序中的另一个包的contains子句里。
2. Requires文件夹
在Requires文件夹中,可以指定新包所需的其他包。这类似于Delphi单元uses子句。在大多数情况下,我们创建的任何包中的requires子句下都会有VCL60——存放Delphi标准VCL组件的包。例如,典型的方案是,首先把所有的组件都放入一个运行期包,然后创建一个设计期包,并把那个运行期包放在这个设计期包的requires子句下。关于把包放在另一个包Requires文件夹中,也有一些规则:
n 避免循环引用——比如包Packagel不能放在自身的叫requires子句下,它也不能包含另一个requires子句下存有Package1包的包。
n 引用链接不能向反方向引用先前已经在链中引用过的包。
包编辑器拥合一个工具条和一个环境敏感菜单。参考Delphi 6的联机帮助,在“Package Editor”下面列出了这些按钮的用途,这里就不再重复了。
1.7.2 包设计方案
以前曾经说过,必须知道想创建的包是什么类型。在这一节,将给出4种可能的情形,根据这些情形来选择使用设计期包或运行期包。
方案1:使用组件的设计和运行期包
组件的设计和运行期包是针对这样一种情形,你是一个组件作者并且适于以下任意一种情:
n 希望Delphi程序员能够正确编译/连接我们的组件到他们的应用程序,或者能够单独地和他们的应用程序一起发布。
n 有一个组件包,并且不想强迫我们的用户把设计特性(组件/属性编辑器等)编译到他们的应制程序代码中去。
对于这种情形,我们既要创建设计期包,也要创建运行期包,图1.1说明了这种方案。正如图中所示,设计期包(ddgDT60.dpk)同时封装了设计特性(属性和组件编辑器)和运行期包(ddgRT60.dpk)。运行期包(ddgRT60.dpk)所包括的仅仅只有组件。把运行包列表到设计期包的requires区域,这个方案就算完成了,如图1.1所示。
图1.1 设计期包包含了设计元素和运行期包
在编译包之前,还必须为每一个包设置合适的使用选项。可以在Package Options对话框中做这项工作(在包编辑器中单击右键弹出快捷菜单,选择Options选项即可打开对话框)。对于运行期包DdgRT60.dpk,应该把使用选项设置为Runtime only 。这样就可以保证这个包不会作为一个设计期包安装到IDE(见本草后面的补充“组件安全”)。对于设计期包DdgRT60.dpk,应该选择Design time only作为使用选项。这能够确保用户把包安装到IDE,却不会把它们作为运行期包使用。
把运行期包加入设计期包还不能使包含在此运行包中的组件能用于Delphi的IDE,必须向IDE注册这些组件。正如大家所知道的,无论何时创建一个组件,Delphi都会自动地在组件单元中插入一个Register()过程,接着再调用RegisterComponents()过程。当我们安装组件时,实际上向Delphi IDE注册这个组件的是RegisterComponents()过程。在使用包时,推荐的方法是把Register ()过程从组件单元转移到一个单独的注册单元,这个注册单元调用RegisterComponents()来注册所有的组件。这不仅使我们能够更加容易的管理组件注册,考虑到还不能在Delphi IDE中使用这些组件,这样做也能够防止别人非法的安装和使用我们的运行期包。
作为例子,本书中的组件都被存放在运行期包DdgRT60.dpk中。有关这些组件的属性编辑器、组件编辑器和注册单元(DdgReg.pas)都存放在设计包DdgDT60.dpk中。DdgDT60.dpk还把DdgRT60.dpk放在了它的requires子句下。程序1.1所示的是注册单元。
程序1.1 Delphi6开发向导中包含的组件注册单元
Unit DDGReg Interface Procedure Register; Implementation uses Classes, ExptIntf, DsgnIntf, TrayIcon, AppBars, ABExpt, Worthless, RunBtn, PwDlg, Planets, LbTab, HalfMin, DDGClock, ExMemo, MenView, Marquee, PlanetPE, RunBtnPE, CompEdit, DefProp, Wavez, WavezEd, LnchPad, LPadPE, Cards, ButtonEdit, Planet, DrwPnel; Procedure Register; begin //Register the components RegisterComponents('DDG',[TddgTrayNotifyIcon, TddDigitalClock, TddgHalfMinute, TddgButtonEdit, TddgExtendedMemo, TddgTabListbox, TddgRunButton, TddgLaunchPadm, TddgMenView, TddgMarquee, TddgWaveFile, TddgCard, TddgPasswordDialog, TddgPlanet, TddgPlanets, TddgWorthLess, TddgDrawPanel, TComponentEditorSample, TDefinePropTest]); //Register any property editor RegisterPropertyEditor(TypeInfo(TRunButton),TddgLaunchPad,'',TRunButtonProperty); RegisterpropertyEditor(TypeInfo(TWaveFileString), TddgWaveFile, 'WaveName',TWaveFileStringProperty); RegisterpropertyEditor(TddgWaveFile, TWaveEditor); RegisterpropertyEditor(TComponentEdtiorSample, TSampleEditor); RegisterpropertyEditor(TypeInfo(TPlanetName), TddgPlanet, 'PlanetName', TPlanetNameProperty); RegisterpropertyEditor(TypeInfo(TCommandLine), TddgRunButton,'', TCommandLineProperty); RegisterCustomModule(TAppBar, TCustomModule); RegisterLibraryExpert(TAappBarExpert.Create); end; end. |
件安全 即使某个人只有我们的运行期包,也能注册我们的组件。他可以调用他自己的注册单元来注册我们的组件。然后他把这个单元添加到一个单独的包中,此包的requires子句下有我们的运行期包。在他把这个新包安装到Delphi IDE后,组件将出现在组件面板中。然而,他不可能用我们的组件来编译任何应用程序,因为在组件单元中没有必需的*.dcu文件 |
包的发布
假如向组件作者发布包时没有附上源代码,就必须同时发布已编译的DdgDT60.bpl和DdgRT60.bpl包、*.dcp文件以及编译组件所需的任何已编译的单元(*.dcu文件)。使用我们组件的程序员,假如想要激活他们的应用程序的运行期包的话,就必须和他们的应用程序一起发布DdgRT60.bpl包,以及其他可能使用的运行期包。
方案2:只用组件的设计期包
我们想发布不打算在运行期包中发布的组件时,只用组件的设计期包。这时,应该把组件、组件编辑器、属性编辑器、组件注册单元等都包括在一个包文件中。
包的发布
假如向组件作名发布包时没有附上源代码,就必须同时发布己编译的包DdgDT6.bp1、DdgDT6.dcp文件和编译组件所需的任何已编译的单元(*.dcu文件),使用我们所编组件的程序员必须在他们的应用程序中编译我们的组件,但他们不能把我们的组件作为运行期包发布。
方案3:只用(没有组件)IDE增强工县的设计特性
共用(没有组件)IDE增强工具的设计特性是在我们想要为Delphi IDE提供诸如专家之类的增强工具时。对于这种情况,首先在注册单元中注册专家到IDE这种情况下的发布也比较简单,只要发布已编译的*.bpl文件就行了。
方案4:应用程序分割
应用程序分割是这样一种情形,我们想分割我们的应用程序成为几个逻辑上的块,每一个块都可以单独发布。我们要这样做是出于以下几个别由:
n 这种方案易于维护。
n 当用户需要这个应用程序时, 可以只购买所需的功能。然后假如用户需要增加功能,只要下载必需的包就行了,这比下载整个应用程序小得多。
n 能够更容易地提供部分程序的修正(补丁),而不要求用户获取一个应用程序的全新版本。
采用这种方案时,只需提供应用程序所需的*.bpl文件即可。这种方案与前一种方案类似,只不过没有为Delphi IDE提供包,而是为自己编写的应用程序提供包。分割应用程序时,必须注意包的版本化所带来的问题,详情参见下一节的描述。
1.8 包的版本化
包的版本化不太容易理解。在很大程度亡,包的版本化和单元版本化的方式是相同的。也就是说,为应用程序提供的任何包必须使用和编译应用程序相同的版本的Delphi来编译。因此不能把在Delphi 6中编写的包用于在Delphi 5中编写的应用程序里。Borland程序员把包的版本看作是代码基础(code base),所以Delphi 6中编写的包有6.0版的代码基础。这个概念会影响包文件命名约定。
1.9 包编译器指示符
有—些特别的编译器指示符可以插入包的源代码。在这些指示符中,有的是特定用于正在被打包的单元,其他的用于包文件。表1.3和表1.4列出了这些指示符及其说明。
表1.3 被打包的单元的编译器指示符
指示符 |
含义 |
{$G} or {IMPORTEDDATA OFF} |
如果不想使单元被打包,而是想直接和应用程序相连接,就要使用这个指示符。把它和{$WEAKPACKAGEUNIT}比较一下,{$WEAKPACKAGEUNIT}允许一个包中包括单元,它的代码是静态的和应用程序相连接 |
{$DENYPACKAGEUNIT} |
含义同{$G} |
{$WEAKPACKAGEUNIT} |
参见“关于{$WEAKPACKAGEUNIT}的更多信息”小节 |
表1.4 Package.dpk文件的编译器指示符
指示符 |
含义 |
{$DESIGNONLY ON} |
把包作为设计期包编译 |
{$RUNONLY ON} |
把包作为运行期包编译 |
{$IMPLICITBUILD OFF} |
防止包被重编译。当不经常改变包时使用这个选项 |
关于{$WEAKPACKAGEUNIT}的更多信息
弱包(weak package)的概念比较简单,它主要指包正在引用并不存在的库(DLL)。比如,Vcl60调用Windows操作系统的核心Winn32 API。DLL中有很多这样的调用,而并不是每台计算机中部有这些DLL。这些调用由包含{$WEAKPACKAGEUNIT}指示符的单元显露。通过包括这个指示符,保持这个单元的源代码在包中,但要放在DCP文件而不是BPL文件里(把DCP看作DCU,BPL看作DLL)。因此,任何对这些弱打包单元的引用都会静态的链接到应用程序,而不是动态地通过这个包引用。
我们很少使用{$WEAKPACKAGEUNIT}指示符。Delphi程序员创建它来处理一些特殊的情况,虽然这不是必需的。假如有两个组件,每一个都在一个单独的包个,并且正在引用某个DLL的同一接口祖先,这时就会存在问题。当某个应用程序要同时使用这两个组件时,就会导致那个DLL的实例被载入,这样将会引发有关初始比和全局变量引用的巨大灾难。解决的方法是把这个接口单元提供给某个标准的Delphi包,比如Vcl60.bpl。然而,这又会导致其他有关专门的DLL问题,这些DLL也许并不存在,比如PENWIN.DLL。假如Vcl60.bpl包含有一个不存在的DLL的接口单元,这将导致Vcl60.bpl和Delphi不能使用。Delphi程序员通过让Vcl60把接口单元包含进一个单一的包中来解决这个问题。但只有和Delphi IDE一起使用Vcl60.bpl时,是使接口单元被静态的链接,而不是动态载入。
一般情况下当然没有必要使用这个指示符,除非出现前面的类似情形,或者想确保包中包含某个特定的单元,而且又是静态的链接到正在运行的应用程序中。对于后者,这样做的一个原因是为了优化。注意弱打包的任何单元在它们的初始化部分和结束部分部不能有全局变量或代码。在和包一起发布时,也必须要发布弱打包单元的*.dcu文件。
1.10 包的命名约定
以前我们说过包的版本化问题将合影响包的命名。对于怎么命名包没有一套规则,但是我们建议使用某种把代码基础融入包的名称中的命名习惯。举个例子,本书中的组件都包含在一个运行期包中,它的名字包含了6个Delphi限定词(DdgRT6.dpk)。设计期包(DdgDT6.dpk)也是这样。上一个版在的包是用DdgRT5.dpk。这种命名习惯能够防止用户产生混淆,比如他们正在应用哪个版本的包、哪个版本的Delphi编译器适合它们。注意,包名称要以3个字符的作者/公司标识符开头,接着用RT指示出这是一个运行期包,或者用DT表示它是一个设计期包。最后就是所适用的Delphi的版本号。
1.11 使用运行期(插件)包的可扩展应用程序
插件包使得我们可以把应用程序隔成不同的模块.并且可以独立于主程序发布这些模块。这种方式是很有好处的,它允许我们扩展应用程序的功能,却不用重编译/重设计整个应用程序。然而,这就要求有—个精心的体系设计计划。尽管深入研究这种设计问题已经超出水书的范围,但我们还是要做些讨论,以使大家知道如何利用这个强大的功能。
生成插件窗体
这个应用程序被分割为3个逻辑上的块:主程序(ChildTest.exe)、TchildForm包(AIChildFrm6.bpl)和具体的TChildForM的派生类,每一个都存放在它日己的包里。
AIChildFrm6.bpl包包含基类ChildForm。其他的包包含TchildForm类的派生类或具体的(concrete)TchildForm类。我们分别将把这些包称为基类包和实体包。
主应用程序使用抽象包〔AIChildFrm6.bpl〕,每一个实体包也使用抽象包。这样,为了工常上作,这个主应用程序必须和包括了AIChildFm6.dcp包的运行期包一起编译。同样,每—个实体包都必须要求有AIChildFrm6.dcp包。我们不准备列出TChildForm的源代码,也不列出对每一个TchildForm派生单元的实体派生类的源代码。它们必须包括类似下面的初姑化(initialization)部分和结束(finalization)部分:
Initialization RegisterClass(TCF2Form); Finalization UnRegisterClass(TCF2Form); |
当主程序载入自身的包时,要使TChildForm的派生类可用于主程序的流系统,就必须调用RegisterClass()。这类似于RegisterComponents()怎样使得组件能够可用于Delphi IDE中。当包被载入时,必须调用UnRegisterlass()来删除注册类。然而,要注意RegisterClass()仅仅只是使类可用于主程序,主程序却仍然不知道类名。主程序要怎样才能创建一个类名未知的类实例呢?我们的意图难道不就是使得这些窗体可用在主程序中,而不必辛苦的编与它们的类名并放入主程序的源代码吗?程序1.2所示的就是主程序的主窗体的源代码,其中我们要强调的是如何用插件包实现插件窗体。
程序1.2 使用插件包的主程序的主窗体
unit MainFrm; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, ChildFrm, Menus; const { Child form registration location in the Windows Registry. } cAddInIniFile = 'AddIn.ini'; cCFRegSection = 'ChildForms'; // Module initialization data section FMainCaption = 'Delphi 6 Developer''s Guide Child Form Demo'; type TChildFormClass = class of TChildForm; TMainForm = class(TForm) pnlMain: TPanel; Splitter1: TSplitter; pnlParent: TPanel; mmMain: TMainMenu; mmiFile: TMenuItem; mmiExit: TMenuItem; mmiHelp: TMenuItem; mmiForms: TMenuItem; procedure mmiExitClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); private // reference to the child form. FChildForm: TChildForm; // a list of available child forms used to build a menu. FChildFormList: TStringList; // Index to the Close Form menu which shifts position. FCloseFormIndex: Integer; // Handle to the currently loaded package. FCurrentModuleHandle: HModule; // method to create menus for available child forms. procedure CreateChildFormMenus; // Handler to load a child form and its package. procedure LoadChildFormOnClick(Sender: TObject); // Handler to unload a child form and its package. procedure CloseFormOnClick(Sender: TObject); // Method to retrieve the classname for a TChildForm descendant function GetChildFormClassName(const AModuleName: String): String; public { Public declarations } end; var MainForm: TMainForm; implementation uses IniFiles; {$R *.DFM} function RemoveExt(const AFileName: String): String; { Helper function to removes the extension from a file name. } begin if Pos('.', AFileName) <> 0 then Result := Copy(AFileName, 1, Pos('.', AFileName)-1) else Result := AFileName; end; procedure TMainForm.mmiExitClick(Sender: TObject); begin Close; end; procedure TMainForm.FormCreate(Sender: TObject); begin FChildFormList := TStringList.Create; CreateChildFormMenus; end; procedure TMainForm.FormDestroy(Sender: TObject); begin FChildFormList.Free; // Unload any loaded child forms. if FCurrentModuleHandle <> 0 then CloseFormOnClick(nil); end; procedure TMainForm.CreateChildFormMenus; { All available child forms are registered in the Windows Registry. Here, we use this information to creates menu items for loading each of the child forms. } var IniFile: TIniFile; MenuItem: TMenuItem; i: integer; begin inherited; { Retrieve a list of all child forms and build a menu based on the entries in the registry. } IniFile := TIniFile.Create(ExtractFilePath(Application.ExeName)+cAddInIniFile); try IniFile.ReadSectionValues(cCFRegSection, FChildFormList); finally IniFile.Free; end; { Add Menu items for each module. NOTE THE mmMain.AutoHotKeys property must bet set to maAutomatic } for i := 0 to FChildFormList.Count - 1 do begin MenuItem := TMenuItem.Create(mmMain); MenuItem.Caption := FChildFormList.Names[i]; MenuItem.OnClick := LoadChildFormOnClick; mmiForms.Add(MenuItem); end; // Create Separator MenuItem := TMenuItem.Create(mmMain); MenuItem.Caption := '-'; mmiForms.Add(MenuItem); // Create Close Module menu item MenuItem := TMenuItem.Create(mmMain); MenuItem.Caption := '&Close Form'; MenuItem.OnClick := CloseFormOnClick; MenuItem.Enabled := False; mmiForms.Add(MenuItem); { Save a reference to the index of the menu item required to close a child form. This will be referred to in another method. } FCloseFormIndex := MenuItem.MenuIndex; end; procedure TMainForm.LoadChildFormOnClick(Sender: TObject); var ChildFormClassName: String; ChildFormClass: TChildFormClass; ChildFormName: String; ChildFormPackage: String; begin // The menu caption represents the module name. ChildFormName := (Sender as TMenuItem).Caption; // Get the actual Package file name. ChildFormPackage := FChildFormList.Values[ChildFormName]; // Unload any previously loaded packages. if FCurrentModuleHandle <> 0 then CloseFormOnClick(nil); try // Load the specified package FCurrentModuleHandle := LoadPackage(ChildFormPackage); // Return the classname that needs to be created ChildFormClassName := GetChildFormClassName(ChildFormPackage); { Create an instance of the class using the FindClass() procedure. Note, this requires that the class already be registered with the streaming system using RegisterClass(). This is done in the child form initialization section for each child form package. } ChildFormClass := TChildFormClass(FindClass(ChildFormClassName)); FChildForm := ChildFormClass.Create(self, pnlParent); Caption := FChildForm.GetCaption; { Merge child form menus with the main menu } if FChildForm.GetMainMenu <> nil then mmMain.Merge(FChildForm.GetMainMenu); FChildForm.Show; mmiForms[FCloseFormIndex].Enabled := True; except on E: Exception do begin CloseFormOnClick(nil); raise; end; end; end; function TMainForm.GetChildFormClassName(const AModuleName: String): String; { The Actual class name of the TChildForm implementation resides in the registry. This method retrieves that class name. } var IniFile: TIniFile; begin IniFile := TIniFile.Create(ExtractFilePath(Application.ExeName)+cAddInIniFile); try Result := IniFile.ReadString(RemoveExt(AModuleName), 'ClassName', EmptyStr); finally IniFile.Free; end; end; procedure TMainForm.CloseFormOnClick(Sender: TObject); begin if FCurrentModuleHandle <> 0 then begin if FChildForm <> nil then begin FChildForm.Free; FChildForm := nil; end; // Unregister any classes provided by the module UnRegisterModuleClasses(FCurrentModuleHandle); // Unload the child form package UnloadPackage(FCurrentModuleHandle); FCurrentModuleHandle := 0; mmiForms[FCloseFormIndex].Enabled := False; Caption := FMainCaption; end; end; end. |
这个应用程序的逻辑其实是很简单的。它使用系统注册来确定哪一个包是可用的;当为正在载入的包构建菜单时,确定菜单的标题和包中窗体的类名。
大多数丁作都在LoadChildFormOnClick()事件处理程序中完成。在确定包的文件名以后,这个方法使用LoadPackage()载入这个包。LoadPackage()函数基本上和DLL中的LoadLibrary()相向。LoadChildFormOnClick()然后又为被载入的包个的窗体确定类名。
为了创建一个类,需要一个类似Tbutton或Tform1的类引用。然而,这个应用主程序却没有实体TchildForms的类名,这就是为什么要从系统注册表中找回类名的原因。主应用程序可以向FindClass()函数传递类名,以返回一个关于特定的类的类引用,这个类已经用流系统注册。记住,当包被载入时,我们是在实体窗体单元的初始化部分做这个工作。接着用下面两行代码来创建类:
ChildFormClass := TchildFormClass(FindClass(ChildFormClassName)); FchildForm := ChildFormClass.Create(self, pnlParent); |
说明:类引用不过是内存中的某个区域,其中包含了相关类的信息,这和类的类型定义是一回事情。当用VCL流系统或RegisterClass()函数注册这个类时,类引用就会进入内存,FindClass()函数查找内存区域,定位某个指定类名的类,并返回一个指向那个位置的指针,这不同于类实例。类实例是创建于调用构造函数时。 |
变量ChildFormClass是对TchildForm预声明的类引用,并且能够多态地访问TchildForm派生类的类引用。
CloseFormOnClick()事件处理程序只用来关闭子窗体并卸载它的包。剩下的基本上是一些设置性代码,用来创建包菜单以及从从INI件中读信息。
使用这个技术,我们就能创建高扩展性和松散耦合的应用程序框架。
1.12 从包中导出函数
考虑到包不过是增强的DLL,因此可以像在DLL中那样,从包中导出函数和过程。在这一节我们将向大家介绍这种使用包的方式。
从包函数中运行窗体
程序1.3足一个包含在包的内部的单元。
程序1.3 有两个导出函数的包单元
unit FunkFrm; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TFunkForm = class(TForm) Label1: TLabel; Button1: TButton; private { Private declarations } public { Public declarations } end; // Declare the package functions using the StdCall calling convention procedure FunkForm; stdcall; function AddEm(Op1, Op2: Integer): Integer; stdcall; // Export the functions. exports FunkForm, AddEm; implementation {$R *.dfm} procedure FunkForm; var FunkForm: TFunkForm; begin FunkForm := TFunkForm.Create(Application); try FunkForm.ShowModal; finally FunkForm.Free; end; end; function AddEm(Op1, Op2: Integer): Integer; begin Result := Op1+Op2; end; end. |
很明显,过程FunkForm()简单地把在这个单元中声明的窗体作为一个模块窗体显示。函数AdEm()获取两个操作数作为参数并返问它们的和。注意这个函数是通过StdCall调用协定在此单元的接口部分声明的。
程序1.4是演示如何从包中调用一个函数的应用程序。
程序1.4 应用程序演示
unit MainFrm; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Mask; const cFunkForm = 'FunkForm'; cAddEm = 'AddEm'; type TForm1 = class(TForm) btnPkgForm: TButton; meOp1: TMaskEdit; meOp2: TMaskEdit; btnAdd: TButton; lblPlus: TLabel; lblEquals: TLabel; lblResult: TLabel; procedure btnAddClick(Sender: TObject); procedure btnPkgFormClick(Sender: TObject); private { Private declarations } public { Public declarations } end; // Defined the method signatures TAddEmProc = function(Op1, Op2: Integer): integer; stdcall; TFunkFormProc = procedure; stdcall; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.btnAddClick(Sender: TObject); var PackageModule: THandle; AddEmProc: TAddEmProc; Rslt: Integer; Op1, Op2: integer; begin PackageModule := LoadPackage('ddgPackFunk.bpl'); try @AddEmProc := GetProcAddress(PackageModule, PChar(cAddEm)); if not (@AddEmProc = nil) then begin Op1 := StrToInt(meOp1.Text); Op2 := StrToInt(meOp2.Text); Rslt := AddEmProc(Op1, Op2); lblResult.Caption := IntToStr(Rslt); end; finally UnloadPackage(PackageModule); end; end; procedure TForm1.btnPkgFormClick(Sender: TObject); var PackageModule: THandle; FunkFormProc: TFunkFormProc; begin PackageModule := LoadPackage('ddgPackFunk.bpl'); try @FunkFormProc := GetProcAddress(PackageModule, PChar(cFunkForm)); if not (@FunkFormProc = nil) then FunkFormProc; finally UnloadPackage(PackageModule); end; end; end. |
首先要注意的是必须声明两个过程类型——TAddEmProc和TFunkFormProc,而且必须是当它们在包中出现时声明。
首先我们讨论了btnPkgFormClick()事件处理程序,它的代码看上去似曾相识。不同的是我们没有调用LoadLibrary(),而是使用了LoadPackage(),其实LoadPackage()的出现就终结了LoadLibrary()的使用。接下来,我们使用GetProcAddress()函数来接受对这个过程的引用。CFunkForm常量的名称和包中的函数名称相同。
我们可以发现,从包中导出函数和过程的方法几乎和从动态链接库中导出的方法完全相同。
1.13 获取包的信息
可以查询一个包以获得有关信息,比如它包含多少个单元、需要另外哪些包等。可以使用两个函数来做这件工作:EnumModules()和GetPackageInfo()。
这两个函数都需要回调函数(callback functions),程序1.5演示了这些函数的使用。
程序1.5 包信息演示
unit MainFrm; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, DBXpress, DB, SqlExpr, DBTables; type TForm1 = class(TForm) Button1: TButton; TreeView1: TTreeView; Table1: TTable; SQLConnection1: TSQLConnection; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} type TNodeHolder = class ContainsNode: TTreeNode; RequiresNode: TTreeNode; end; procedure RealizeLength(var S: string); begin SetLength(S, StrLen(PChar(S))); end; procedure PackageInfoProc(const Name: string; NameType: TNameType; Flags: Byte; Param: Pointer); var NodeHolder: TNodeHolder; TempStr: String; begin with Form1.TreeView1.Items do begin TempStr := EmptyStr; if (Flags and ufMainUnit) <> 0 then TempStr := 'Main unit' else if (Flags and ufPackageUnit) <> 0 then TempStr := 'Package unit' else if (Flags and ufWeakUnit) <> 0 then TempStr := 'Weak unit'; if TempStr <> EmptyStr then TempStr := Format(' (%s)', [TempStr]); NodeHolder := TNodeHolder(Param); case NameType of ntContainsUnit: AddChild(NodeHolder.ContainsNode, Format('%s %s', [Name,TempStr])); ntRequiresPackage: AddChild(NodeHolder.RequiresNode, Name); end; // case end; end; function EnumModuleProc(HInstance: integer; Data: Pointer): Boolean; var ModFileName: String; ModNode: TTreeNode; ContainsNode: TTreeNode; RequiresNode: TTreeNode; ModDesc: String; Flags: Integer; NodeHolder: TNodeHolder; begin with Form1.TreeView1 do begin SetLength(ModFileName, 255); GetModuleFileName(HInstance, PChar(ModFileName), 255); RealizeLength(ModFileName); ModNode := Items.Add(nil, ModFileName); ModDesc := GetPackageDescription(PChar(ModFileName)); ContainsNode := Items.AddChild(ModNode, 'Contains'); RequiresNode := Items.Addchild(ModNode, 'Requires'); if ModDesc <> EmptyStr then begin NodeHolder := TNodeHolder.Create; try NodeHolder.ContainsNode := ContainsNode; NodeHolder.RequiresNode := RequiresNode; GetPackageInfo(HInstance, NodeHolder, Flags, PackageInfoProc); finally NodeHolder.Free; end; Items.AddChild(ModNode, ModDesc); if Flags and pfDesignOnly = pfDesignOnly then Items.AddChild(ModNode, 'Design-time package'); if Flags and pfRunOnly = pfRunOnly then Items.AddChild (ModNode, 'Run-time package'); end; end; Result := True; end; procedure TForm1.Button1Click(Sender: TObject); begin EnumModules(EnumModuleProc, nil); end; end. |
首先调用EnumModules(),它枚举可执行文件和在此可执行文件个的任何包。传递给EnumModuls()的回调函数是EnumModuleProc(),这个函数把有关这个应用程序中的每一个包的信息都填充到—个TTreeView组件中。大多数代码都是针对TTreeView组件的设置代码。函数GetPackDescription()返回包含在包子源文件中的描述性字符串。调用GetPackageInfo()来传递回调函数PackageInfoProc()。
在PackageInfoProc()中,我们能够处理包信息表中的信息。包中的每个单元和这个包所需要的每个包都要调用这个函数。这里,我们通过检查Flaqs参数和NameType参数的值,再一次把这些(包信息表中的)信息填充TTreeView组件。对于其他信息,联机帮助的“TpackageInfoProc”中都有解释。
1.14 小结
包是Delphi/VCL体系的一个关键组成部分。通过学习如何使用包(不仅仅只是作为组件容器),就能够开发出设计优雅、领域宽泛的体系结构。