■印象中的Packagehtml
在通常的AP开发时,咱们知道在Delphi7.0整合环境中将Project->Options->选到Packages卷标页,Builder with runtime packages选项打勾,就会让编译出来的执行文件Size变小不少(以空白的Form1为例,编译出来的Size由367kb变成20kb),由于它把一些VCL共享模块的Loding放到*.bpl中;换句话说这个变小的EXE文件在执行时是须要那些*.bpl的,而本来较大的执行文件执行时则不须要那些*.bpl,这样看来其实只是换汤不换药罢了。数据库
这种编译架构的差别有什么优势?试想,若是Project1.exe&Project2.exe都不使用Builder with runtime packages选项,Project1.exe跟Project2.exe编译出来的Size都是367kb,如有10个ProjectN.exe,则占用的Size将是所有exe加起来的总和,如果咱们将其中共享的部分(如VCL共享模块)独立出来,每一个EXE都将只有20K(以测试用的空白Form而言),缺点则只有每一个exe执行时都要在系统搜寻路径中找获得那些VCL的共享模块(以*.bpl文件名存在);因此这种架构适用于一个项目系统内有多个独立执行文件或独立业务模块存在的状况,多个执行文件就能够分散给多我的去开发,也可依照应用程序不一样的业务性质去区隔并分别设计。数组
将执行文件分散开来开发,这种作法跟一个主执行文件配合多个不一样业务别的DLL开发没有什么差异,而下降每支执行文件的Size也只是Package最普通的应用;使用Package有一个更强大的优势——能够共享变量,若是将数据模块当成一个共享变量的观念去看待,咱们不用在每次启动或关闭不一样的子系统业务模块时,从新链接或释放数据库的Connection。不过,要真正能共享数据库模块是须要每一个业务模块皆以*.bpl的方式存在,这也是实际应用上的情况。架构
项目架构种类模块化 |
Package编译方式函数 |
说明学习 |
类型一测试 Project0.exe(MIS主系统)ui Project1.exe(会计子系统)spa Project2.exe(人事子系统) Project3.exe(库存子系统) |
全部EXE文件编译选项 □Builder with runtime packages |
1.每一个子系统都是完整独立的EXE文件;每一个EXE文件至少都有数百KB以上 |
类型二 Project0.exe(MIS主系统) Project1.exe(会计子系统) Project2.exe(人事子系统) Project3.exe(库存子系统) |
VCL共享模块(*.bpl) 全部EXE文件编译选项 ■Builder with runtime packages(打勾) |
1.每一个子系统虽然都是EXE文件,但执行时须要VCL的共享模块存在;此种方式,每一个EXE文件SIZE都只有几十KB 2.此种架构只有节省档案SIZE的优势 |
类型三 Project0.exe(MIS主系统) Project1.bpl(会计子系统) Project2.bpl(人事子系统) Project3.bpl(库存子系统) VCL共享模块(*.bpl) DataMoudle(db.bpl) |
全部EXE文件编译选项 ■Builder with runtime packages(打勾) |
1.将各子系统中共享的程序(如链接数据库的模块)独立出来共享 2.除主系统外其他子系统皆为BPL型式存在 3.节省档案SIZE外,并有共 用数据库连结模块的优势 |
注:
1.MIS主系统(Project0.exe)可能只是一个选单(Menu),用来看成启动各子系统的Shell
2.VCL共享模块(*.bpl)是Delphi在安装时即安装至C:"WINNT"SYSTEM32"中的那些*.bpl档案,无论你EXE文件是否Builderwithruntimepackages,那些*.bpl早已存在,端看你要不要用它而已(Builderwithruntimepackages是否打勾)
3.当AP开发是以Builderwithruntimepackages(打勾)编译时,RELEASE到客户端同时也要将C:"WINNT"SYSTEM32"中的那些*.bpl档案COPY到客户端,由于客户端没有安装Delphi,因此没有那些VCL共享模块(*.bpl)
4.BPL跟EXE同样是能够“LOAD”其它的*.bpl;如Project1.bpl(会计子系统)会”LOAD”VCL共享模块(*.bpl)以及DataMoudle(db.bpl)
5.在类型三中,做为惟一的EXE文件,Project0.exe编译时必定要Builderwithruntimepackages(打勾),不然在动态加载其它子系统模块时,会出现找不到类别的错误
■Package架构的优缺点
使用Package的优势:
1.相似DLL-应用程序能够被高度的模块化
2.优于DLL之处-能够共享变量
使用Package的缺点:
1.架构较复杂,须要花比较多的心思在模块化的设计
2.须要程度较高的开发人员,若对package的使用不熟,容易出trouble
■哪些东西能够开发成为Package
前面讲过,Package其实就是相似DLL的一种架构,可是是专门属于Borland C++Builder/Delphi使用的一种DLL架构,举凡是有含Form的Unit文件、无Form的Unit文件(如本身写的函数库)、组件等等,均可以编译成为Package
■Package档案的类型
Package项目 |
至关于通常项目 |
至关于DLL项目 |
||
编译前 |
*.DPK |
Package项目文件 |
DPR文件 |
DPR文件 |
*.PAS |
PackageSource |
PAS文件(Source) |
PAS文件(Source) |
|
编译后(产出) |
*.DCP |
Package产出文件 |
LIB文件 |
|
*.BPL |
Package产出文件 |
EXE文件 |
DLL文件 |
Package中的DCP文件,跟DLL项目的LIB文件相似,DCP是提供为静态连结编译之用(注意:只是静态连结的“编译过程”用之),BPL则是直接提供为执行环境之用;DCP只有在提供给别的EXE或BPL静态连结的“编译过程”中会用到,因此不用RELEASE到客户端;在执行环境中,Package不论是被静态联结或是动态连结使用,一概以BPL型式存在。
例如Project0.exe以“静态连结”方式使用到Package1,Project0.exe在编译时须要Package1.dcp的档案存在,但编译后分发到客户端的档案则为roject0.exe以及Package1.bpl(Package1.dcp不用release),虽然它们之间是使用静态连结(加载)…
若是Project0.exe以“动态连结”方式使用到Package1,Project0在编译过程当中根本不须要任何Package1的档案,而分发到客户端同样仍然只有roject0.exe以及Package1.bpl。
■Package的载入(从EXE载入BPL的角度来看)
静态加载–无论用得用不到,该*.dcp是必定都要加载的,也就是在exe项目选项中的Builder with runtime packages->Add加入,如Project0.exe(MIS主系统)必定要加载VCL共享模块(*.dcp),不然没法产生GUI接口;因此就用静态加载的方式加载它;静态加载的*.dcp,是在编译时就已决定的
动态加载–使用到时,才透过LoadPackage()这个API来呼叫,如Project0.exe(MIS主系统)呼叫各个子系统的*.bpl,是透过Coding的方式去加载子系统的*.bpl,Project0.exe(MIS主系统)并不须要在编译时将全部子系统加载编译
■Package的载入(从BPL载入BPL的角度来看)
静态加载–放在Package项目之Requires区段中的*.dcp文件,反正是“必定要用到”的,不须要利用动态加载的就放在这儿
动态加载–跟EXE动态加载BPL方式相同,使用到时,才透过Coding方式利用LoadPackage()这个API来加载的
■Package项目的创建
在Delphi7.0整合环境中,主菜单->File->Close ALL关掉全部的Form及项目;而后再一次:主菜单->File->New->Other...->选择Package,创建一个全新的Package项目
一个全新的Package应该是长的以下图所示,分为两个部分:Contains内放的是项目的主题,也就是咱们要为这个Package开发的程序代码;Require内放的是要“静态连结” 的其它Package(*.dcp文件),也就是不须要利用LoadPackage()这个API来加载(动态加载)的Package就放到Require中;rtl.dcp是系统内建就已Require的Package,应该是VCL之类的东西吧
接下来咱们要开始设计项目内容,帮这个Package项目加入一个空白的Form,到Delphi主菜单->File->New->Form;咱们再回到Project Manager看看在Contains区段是否多了Unit1(Form1);前面说过,Contains内放的是项目的主题,也就是咱们要为这个Package开发的程序代码,接下来要在Form1中放什么组件,写什么程序代码,都跟通常的Form没什么两样,只是若这个Form是写在通常的项目中(Project1.dpr)它能够被编译成EXE文件,而在Package项目中(Package1.dpk),它只能被编译成Package1.dcp(给别的项目静态连结编译时用)和Package1.bpl
Package1.dcp以及Package1.bpl这两个编译后的结果,Default是放到Delphi7.0目录的."Projects"Bpl"中;若要改变,请至Delphi主菜单->Project->Options(以下图),修改项目的输出目录路径为目前路径(.")
还有,很重要的一点,在Package项目中的Form因为未来会被别人加载使用(无论被静态或被动态加载);它都须要先向系统注册它的类别;因此在前面的例子中,将Unit1(Form1)开发好后,最后要在end.以前插入注册(RegisterClass)/注销(UnRegisterClass)的程序代码,注册内容就是本身的Form类别(如TForm一、TForm2等等)
(其它程序代码) ...... ...... initialization RegisterClass(TForm1); finalization UnRegisterClass(TForm1); end. |
■Package的应用限制
若是只是一个EXE文件附带一个BPL文件,这种架构还算单纯,但若是如文章开始所述:一个MIS主系统(Project0.exe)带着多个子系统(*.bpl),那会有什么限制发生呢?
1.各个Package(*.bpl)在开发过程当中,彼此的Contains区段中不能有同名的Unit
2.共享的unit必定要放在package,也就是要把共享模块变成Package
咱们如今来想一想,若是是咱们来主导这个系统,咱们会如何设计呢?
1.虽然各项子系统是各自独立开发,甚至是交由不一样的开发TEAM来完成,但为了接口的风格一致及操做统一(如Button的大小及位置),咱们会有一个共通的BaseForm的雏形,让全部的子系统的主Form都由这个BaseForm继承而来,这样会让子系统(Package)的Contains区段都会有一个共同uses的BaseForm.pas
2.为了程序代码的一致性,也为了增长Coding速度,公司累积了程序代码经验,可能会有一个公用副函数集MySub供各个子系统呼叫,这样也会让子系统(Package)的Contains区段都会有一个共同uses的MySub.pas
为了避免让BaseForm.pas及MySub.pas成为Package开发的限制瓶颈,因此咱们要将BaseForm及MySub也变成Package(成为BaseForm.dcp及MySub.dcp),而后让各个子系统Package放在Requires中静态连结编译。
■Package的动态加载
前面已介绍了Package在静态加载以及编译的方法,如今要介绍Package应用的重头戏-动态加载;通常来讲,共通的副函数或共享分享的数据库模块以及共享的继承样板会先被制做成Package,而后被主程序(EXE文件)及各个子系统模块(BPL文件)做为静态连结之用;而整个系统开发的主角,也就是各个子系统模块会被主程序或子系统模块(BPL文件)间,当成动态加载的目标。
var ModuleInstance1:HMODULE; {$R*.dfm} //--------------------------------------------------------------- //动态加载Package //--------------------------------------------------------------- procedureTForm0.Button1Click(Sender:TObject); begin ModuleInstance1:=LoadPackage('Package1.bpl'); end; //--------------------------------------------------------------- //将Package中的Form1带出 //--------------------------------------------------------------- procedureTForm0.Button2Click(Sender:TObject); var frm : TcustomForm; begin frm :=CreateFormByClassName('TForm1'); try frm.ShowModal; finally frm.Release; end; end; //--------------------------------------------------------------- //释放Package //--------------------------------------------------------------- procedureTForm0.Button3Click(Sender:TObject); begin UnloadAddInPackage(ModuleInstance1); end; |
注:
1.在上列的程序代码中,加载Package的LoadPackage()是系统内建函数
2.带出Package中的Form资源或是释放Package的函数因为要多作一些处理,因此把它包在CreateFormByClassName()以及UnloadAddInPackage()两个自订函数中
//--------------------------------------------------------------- //自订函数–CreateFormByClassName(),创建Form //--------------------------------------------------------------- Function TForm0.CreateFormByClassName(const ClassName:string) : TCustomForm; var AClass:TPersistentClass; begin AClass:=GetClass(ClassName); If AClass=nil then exit; Result:=TComponentClass(AClass).Create(Application) as TCustomForm; //或Result:=TCustomForm(TComponentClass(AClass).Create(Application)); end; //--------------------------------------------------------------- //自订函数–CreateDataModuleByClassName(),创建数据模块 //--------------------------------------------------------------- Function TForm0.CreateDataModuleByClassName(const ClassName: string):TDataModule; var AClass:TPersistentClass; begin Result:=nil; AClass:=GetClass(ClassName); If AClass=nil then exit; Result:=TComponentClass(AClass).Create(Application) as TDataModule; end; //--------------------------------------------------------------- //自订函数–UnloadAddInPackage(),释放Package //--------------------------------------------------------------- Procedure TForm0.UnloadAddInPackage(ModuleInstance:HMODULE); var i:Integer; M:TMemoryBasicInformation; begin for i:=Application.ComponentCount-1 downto 0 do begin VirtualQuery(GetClass(Application.Components[i].ClassName),M,SizeOf(M)); if (ModuleInstance=0) or (HMODULE(M.AllocationBase)=ModuleInstance) then Application.Components[i].Free; end; //下面这两个函数应该是只要取其中一个呼叫便可 UnRegisterModuleClasses(ModuleInstance);//直接注销Package UnloadPackage(ModuleInstance);//间接注销,呼叫Package中的finalization区段 end; |
■完整的Package项目架构范例
子系统名称 |
内部主要对象 |
补充说明 |
Project0.exe(MIS主系统) |
Form0(Unit0.pas) |
整个项目中惟一的EXE文件Project的Option选项要设 ■Builderwithruntimepackages并加入BaseForm,MySub两个DCP (静态连结) |
Package1.bpl(会计子系统) |
Form1(Unit1.pas) |
Requires区段要加入 BaseForm,MySub两个DCP (静态连结) |
Package2.bpl(人事子系统) |
Form2(Unit2.pas) |
Requires区段要加入 BaseForm,MySub两个DCP (静态连结) |
Package3.bpl(库存子系统) |
Form3(Unit3.pas) |
Requires区段要加入 BaseForm,MySub两个DCP (静态连结) |
MySub.bpl共享函数库 |
无,只有函数程序代码 MySub2003.pas |
并提供MySub.dcp供其它系统静态连结之用 |
BaseForm.bpl共享继承Form雏形 |
FormBase (UnitFormBase.pas) |
并提供BaseForm.dcp供其它系统静态连结之用 |
注:在此仍不厌其烦的提醒:每一个Package的Source最后都要在end.以前插入注册(RegisterClass)/注销(UnRegisterClass)本身类别的程序代码
□Project0.exe(MIS主系統)
注:无论这个惟一的EXE主程序是否须要加入自订的共享模块(如本例的BaseForm,MySub两个DCP),没有的话,它仍然要将Builder with runtime packages打勾(就算只有使用那些系统内定的VCL、DCP),不然这个EXE文件执行后会没法动态加载其它的子系统模块,会出现找不到类别的错误,这个问题我花了一整个下午才找到问题原来在这里;但我不知缘由是否为Delphi Bug;由于在个人认知,若不使用自订的共享模块(有的话也是放到静态连接中),主程式跟其它的子系统间则只有动态加载的关系,那静态连接则只有决定是否要将系统的VCL静态连接进来的问题;Builder with runtime packages打不打勾,单纯也只决定跟系统VCL间的关系,为什么会影响其它子系统的动态加载??我不肯就看图说故事或凑答案的方式来牵强解释这个问题缘由….只有推论为Delphi bug
□Package1.bpl(会计子系统)
□Package2.bpl(人事子系統)
□Package3.bpl(库存子系统)
(略,以上类推)
注:
1.在Requires区段(静态连结)中,除了手动加入的BaseForm,MySub两个DCP外,其它DCP是由系统自动加入的,不用理会它
2.Package的Contains区段中,彼此间不能有相同名称的Unit,也就是说Package2中的Unit不能跟Package1中同样也叫Unit1(要更名如Unit2或其它)
□MySub.bpl共享函数库(无Form,只有函数库的程序代码MySub2003.pas)
□BaseForm.bpl共享继承Form雏形
■Package项目的测试环境
Package在测试及除错上因为BPL不能直接执行验证结果,因此在开发中的测试是个问题;不过前面说过,除了项目文件架构不一样外,Source开发式和通常Project项目是没什么两样的,之前面介绍过的Package1.bpl(会计子系统)为例,咱们来看一下如何开发及测试
1.创建一标准可编译成EXE的Project1.dpr项目,加入Unit1(Form1),将项目写好后直接编译为EXE文件测试
2.当测试好后没问题,新开启一Package项目叫作Package1.dpk,将Contains区段归入刚才开发的Project1.dpr项目中的Unit1(Form1);此时从新编译,会计子系统就变成一个BPL而不是EXE文件了
3.之后要修改会计子系统,就利用Project1.dpr项目来编译成EXE测试,再用Package1.dpk项目来编译成BPL
■Package项目网状加载问题
在先前介绍的系统中,各个子系统理应由Project0.exe(MIS主系统)这个惟一的EXE文件来切换进入或退出各子系统的时机;可是咱们在实际的应用中,也可能发生直接由Package1.bpl(会计子系统)加载(跳到)Package3.bpl(库存子系统)的状况(记得BPL也是能够加载BPL的吧?),整个系统RUN过一段时间后,谁加载谁,或谁已被退出,已经不是很清楚了,若此时忽然有个快捷方式直接回主程序中并结束程序,咱们要能保证全部曾经被加载的BPL都要被所有彻底释放干净;若是动态加载其它BPL的LoadPackage()函数是分散在各子系统中(人人均可以加载其它的BPL),咱们如何去统一管理那些PackageHandle变量呢(HMODULE)?因此理想的方法是将加载BPL的动做统一放在共同函数库(如MySub中),主程序或各个子系统要加载或释放BPL都透过在共通函数库中包装好的Package载入或释放函数,该函数内部有List数组来记录管理全部的PackageHandle的消长状况;参考资料“DelphiPackage学习笔记”及“DelphiPackage无痛使用”中有个写好的共享函数库,叫作PkgUtils.pas,您能够把它里面的函数合并到您公司开发的共享函数库中(如MySub中),全部主程序或各个子系统的Package加载/释放动做都透过这个公用函数库包装的函数去执行动做。