本章要點:
n 為何要用包
n 為何不用包
n 包的類型
n 包文件
n 使用運行期包
n 把包安裝到Delphi IDE中
n 創(chuàng)建包
n 包的版本化
n 包編譯器指示符
n 包的命名約定
n 使用運行期(插件)
n 從包中導出函數(shù)
n 獲取包的信息
從Delph 3開始便引入了包的概念,它使我們能夠把應用程序放入獨立的模塊,然后讓多個應用程序共享這些模塊。包只是一種特殊的動態(tài)鏈接庫(DLLs),它包含了其他有關Delphi的詳細信息,它和DLL的不同之處在于使用方法。包主要用于共享獨立模塊(即Borland包庫,以.bpl為后綴的文件)中存儲組件集合。在開發(fā)Delphi應用程序時,應用程序通常是在運行期使用我們創(chuàng)建的包,而不是直接在編譯/鏈接期連接它們。因為這些單元的代碼都寄存在.bpl文件中,而不是exe或者.dll文件中,所以這些.exe或.d11文件可以非常小。
包是VCL專有的,也就是說,用其他語言編寫的應用程序不能使用Delphi創(chuàng)建的包(C++Builder是個例外)。提出包技術的原因是為了避開Delphi l和Delphi 2的限制。在以前的Delphi版本中,VCL向每個可執(zhí)行文件添加了最小150k~200k代碼。因此,即使把應用程序的—小部分分離到一個DLL巾,這個DLL和應用程序都還是包含冗余代碼。假如需要在一臺機器上提供一套應用程序集,必然非常頭痛。包使我們得以減小應用程序的大小,并為組件集合的發(fā)布提供便利途徑。
1.1 為何要用包
使用包的原因有多種。在接下來的小節(jié)中,我們將討論3個重要的原因:精簡代碼、分割應用程序和組件容器。
1.1.1 精簡代碼
使用包的一個土要原因是減小應用程序和DLL的大小。Delphi已經提供了幾個個預定義的包,這些包把VCL分割成邏輯止的分組。事實上,我們能夠有選擇地編譯自己的應用程序,如同有多個Delphi包存在一樣。
1.1.2 發(fā)布更小的應用程序——應用程序分割
人家已經知道Internet上有很多可用的程序,也許是完整的應用程序、可下載的演示版或者是對現(xiàn)有應用程序的升級。對于用戶來說.假如在他們的本地系統(tǒng)上已經存在某個應用程序的一部分(比如預安裝),選擇下載更小的應用程序版本將更有利。
通過使用包把應用程序進行分割,用戶就可以僅僅對應用程序需要升級的那一部分進行升級。然而,注意必須考慮一些有關版本的問題。本章涉及到了這些版本問題。
1.1.3 組件容器
也許使用包最通常的原因之一是第三方組件的發(fā)朽。假如你是一個組件賣主,你必須知道如何創(chuàng)建包,因為諸如組件和屬性編輯器、向導和專家工具等設計期元素,都是中包提供的。
包和DLL的對比
使用DLL來為它們的服務器應用程序存放管理窗體會導致DLL擁有自己的Forms.pas文件副本。這將會引起一個不可思議的錯誤,該錯誤與Windows的窗口句柄處理有關。Windows窗口句柄處理產生于DLL中——當DLL被卸載時,窗口句柄卻不能被操作系統(tǒng)解除參照。下一個穿過隊列被發(fā)往頂層窗口的消息會導致應用程序出錯,這樣操作系統(tǒng)就會因為應用程序處于非法狀態(tài)而將它關閉。使用包代替DLL可以克服這個問題,因為包引用了主應用程序的Forms.pas副本,因此消息隊列能夠成功地傳到應用程序。
1.2 為何不用包
最好不要使用運行期包,除非其他應用程序也需要使用這些包。因為,與把源代碼編譯為最終可執(zhí)行文件相比,包消耗的磁盤空間會更多。原因何在?假如創(chuàng)建一個使用包的應用程序使得代碼尺寸包從200k減小到30k,看上去好像是節(jié)約了不小的空間。然而,當需要發(fā)布包,甚至是Vcl60.dcp包時,大概需要2M空間??梢钥闯鰜磉@樣的“節(jié)約”并不是我們所期望的。我們的目的是,當某代碼被多個應用程序共享時,使用包來共享代碼。注意這就是在運行期使用包的惟一原出。對于一個組件作者來說,提供一個設計包,其中必須包含了希望用于Delphi IDE的組件。
1.3 包的類型
我們可以創(chuàng)建和使用的包有4種類型
n 運行期包——運行期包包含—個應用在運行期所需的代碼、組件等。一個依賴于某個特定運行期包的應用程序,在沒有包的情況下就不能運行。
n 設計包——設計包包含了在Delphi IDE中設計應用程序所必需的組件、屬性/組件編輯器、專家工具等。這種類型的包只能用于Delphi,而且絕不要和應用程序一起發(fā)布。
n 運行期設計包——一個可同時在設計和運行期被激活的包,通常用在沒有具體的設計元素(比如屬性/組件編輯器和專家工具)時。通過創(chuàng)建這種包,可以簡化應用程序的開發(fā)和配置。然而,假如這種包中含有設計元素,那么在運行期使用它將會帶來額外的設計支持負擔。如果有許多設計期元素的話,我們推薦創(chuàng)建設計包或運行期包,當出現(xiàn)多個特定設計元素時,通過這些包來分割它們。
n 非運行期、非設計包——這是種不太常見的包,只供其他包使用,不能直接供應用程序引用,也不能用于設計環(huán)境。
1.4 包文件
表1.1根據包的文件擴展名列出并描述了特定包文件的類型。
表 1.1 包文件
文件擴展名
文件類型
描述
.dbk
包源文件
當調用包編譯器時創(chuàng)建.dpk文件。正如大家所想到的,這有點類似于Delphi項目創(chuàng)建一個.dpr文件
.dcp
運行期/設計期包標識文件
這是已編譯的包,它包含了包及其單元的標識信息。另外,還有Delphi IDE所需的報頭信息
.dcu
編譯過的單元
一個包含在某個包中的已編譯單元,包中的每一個單元都一個相應的.dcu文件
.bpl
運行期/設計期
這是運行期包或設計期包的庫類型的包文件,相當于Windows中的DLL。對于一個運行期包,應該和應用程序(如果它們已經被激活)一起發(fā)布這個文件。假如這個文件代表的是一個設計期包,應該和運行期包一起發(fā)布給使用它們寫程序的程序員。注意如果所發(fā)布的不是源代碼,就必須發(fā)布相應的.dcp文件
1.5使用運行期包
為了在Delphi應用程序中使用運行期包,只要打開Project Options對話框上的Packages頁,選中其中的Build wlth Runtime Package核查框就行了。 下一次在選中這個選項之后再編譯應用程序,這個應用程序會被動態(tài)地連接到運行期包,而不是讓單元靜態(tài)的連入.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\目錄下創(chuàng)建\PKG目錄,可以把設計期包放在這里
包標志文件(*.dcp)
可以把包標識文件放在和設計期包文件(*.bpl)相同的地方
編譯單元(*.dcu)
假如正在發(fā)布沒有源代碼的包,必須同時發(fā)布已編譯的單元,我們推薦把DCU文件存放在類似\Delphi 6\Lib的目錄下,不要把自己的DCU文件和第三方賣主的DCU文件放在一起。比如,我們可以創(chuàng)建目錄\Delphi 6\3PrtyLib來存放第三方組件的*.dcu。必須要把這個目錄包括到搜索路徑中去。
要安裝一個包,只要在Delphi 6菜單中選擇Component | Install Packages,調出Project Options對話框上的Packages頁。
單擊Add按鈕,選擇某個.bpl文件。這樣做之后,這個文件將成為Project頁上被選定的文件。按下OK,這個新包就被安裝列Dclphi IDE中了。假如這個包中包含組件,我們在組件面板上會看到新的組件頁及新安裝的組件。
1.7 創(chuàng)建包
在創(chuàng)建包之前,需要就一些事情做出決策。首先,需要知道將要創(chuàng)建的包的類型(運行期包、設計期包等)。需要根據不同情況選樣包的類型,這一點我們馬上就會說明。第二,要知道給新創(chuàng)建的包取個什么樣的名寧,以及把這個也項目存放在什么地方,記住存放配置好的包的目錄也許并不是創(chuàng)建包的白錄。最后,需要知道這個包包含哪些單元以及它還需堅哪些其他的包。
1.7.1 包編輯器
最常見的創(chuàng)建包的方法是使用包編輯器,從Object Repository(對象倉庫)中選擇Packages圖標就可以調出包編輯器(在 Delphi主菜單中選擇File | New | Other,可以找到Object Repository)。注意包編輯器包含兩個文件夾:Contains和Requires。
1. Contains文件夾
在Contains文件夾中,我們指定需要編譯的單元放入新的包中。在把單元放入一個包的Contains時,要遵循如下幾個規(guī)則:
n 絕對不能把這個單元列于另一個包的contains子句中,也不能把它列于另一個包中的某個單元的uses子句中,這個包將和此單元應該放入的包同時被載入。
n 列于contains子句中的單元,不管直接地還是間接地(間接地就是指它們存在于其他單元的uses于句下,而這些單元列于某個包的contains子句中),都不能被列于此包的requires子句中。這是因為重編譯時,這些單元已經和包綁定起來了。
n 假如某個單元已經于某個包的contains子句里,就不能再把這個單元列于同—個應用程序中的另一個包的contains子句里。
2. Requires文件夾
在Requires文件夾中,可以指定新包所需的其他包。這類似于Delphi單元uses子句。在大多數(shù)情況下,我們創(chuàng)建的任何包中的requires子句下都會有VCL60——存放Delphi標準VCL組件的包。例如,典型的方案是,首先把所有的組件都放入一個運行期包,然后創(chuàng)建一個設計期包,并把那個運行期包放在這個設計期包的requires子句下。關于把包放在另一個包Requires文件夾中,也有一些規(guī)則:
n 避免循環(huán)引用——比如包Packagel不能放在自身的叫requires子句下,它也不能包含另一個requires子句下存有Package1包的包。
n 引用鏈接不能向反方向引用先前已經在鏈中引用過的包。
包編輯器擁合一個工具條和一個環(huán)境敏感菜單。參考Delphi 6的聯(lián)機幫助,在“Package Editor”下面列出了這些按鈕的用途,這里就不再重復了。
1.7.2 包設計方案
以前曾經說過,必須知道想創(chuàng)建的包是什么類型。在這一節(jié),將給出4種可能的情形,根據這些情形來選擇使用設計期包或運行期包。
方案1:使用組件的設計和運行期包
組件的設計和運行期包是針對這樣一種情形,你是一個組件作者并且適于以下任意一種情:
n 希望Delphi程序員能夠正確編譯/連接我們的組件到他們的應用程序,或者能夠單獨地和他們的應用程序一起發(fā)布。
n 有一個組件包,并且不想強迫我們的用戶把設計特性(組件/屬性編輯器等)編譯到他們的應制程序代碼中去。
對于這種情形,我們既要創(chuàng)建設計期包,也要創(chuàng)建運行期包,圖1.1說明了這種方案。正如圖中所示,設計期包(ddgDT60.dpk)同時封裝了設計特性(屬性和組件編輯器)和運行期包(ddgRT60.dpk)。運行期包(ddgRT60.dpk)所包括的僅僅只有組件。把運行包列表到設計期包的requires區(qū)域,這個方案就算完成了,如圖1.1所示。
圖1.1 設計期包包含了設計元素和運行期包
在編譯包之前,還必須為每一個包設置合適的使用選項。可以在Package Options對話框中做這項工作(在包編輯器中單擊右鍵彈出快捷菜單,選擇Options選項即可打開對話框)。對于運行期包DdgRT60.dpk,應該把使用選項設置為Runtime only 。這樣就可以保證這個包不會作為一個設計期包安裝到IDE(見本草后面的補充“組件安全”)。對于設計期包DdgRT60.dpk,應該選擇Design time only作為使用選項。這能夠確保用戶把包安裝到IDE,卻不會把它們作為運行期包使用。
把運行期包加入設計期包還不能使包含在此運行包中的組件能用于Delphi的IDE,必須向IDE注冊這些組件。正如大家所知道的,無論何時創(chuàng)建一個組件,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開發(fā)向導中包含的組件注冊單元
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后,組件將出現(xiàn)在組件面板中。然而,他不可能用我們的組件來編譯任何應用程序,因為在組件單元中沒有必需的*.dcu文件
包的發(fā)布
假如向組件作者發(fā)布包時沒有附上源代碼,就必須同時發(fā)布已編譯的DdgDT60.bpl和DdgRT60.bpl包、*.dcp文件以及編譯組件所需的任何已編譯的單元(*.dcu文件)。使用我們組件的程序員,假如想要激活他們的應用程序的運行期包的話,就必須和他們的應用程序一起發(fā)布DdgRT60.bpl包,以及其他可能使用的運行期包。
方案2:只用組件的設計期包
我們想發(fā)布不打算在運行期包中發(fā)布的組件時,只用組件的設計期包。這時,應該把組件、組件編輯器、屬性編輯器、組件注冊單元等都包括在一個包文件中。
包的發(fā)布
假如向組件作名發(fā)布包時沒有附上源代碼,就必須同時發(fā)布己編譯的包DdgDT6.bp1、DdgDT6.dcp文件和編譯組件所需的任何已編譯的單元(*.dcu文件),使用我們所編組件的程序員必須在他們的應用程序中編譯我們的組件,但他們不能把我們的組件作為運行期包發(fā)布。
方案3:只用(沒有組件)IDE增強工縣的設計特性
共用(沒有組件)IDE增強工具的設計特性是在我們想要為Delphi IDE提供諸如專家之類的增強工具時。對于這種情況,首先在注冊單元中注冊專家到IDE這種情況下的發(fā)布也比較簡單,只要發(fā)布已編譯的*.bpl文件就行了。
方案4:應用程序分割
應用程序分割是這樣一種情形,我們想分割我們的應用程序成為幾個邏輯上的塊,每一個塊都可以單獨發(fā)布。我們要這樣做是出于以下幾個別由:
n 這種方案易于維護。
n 當用戶需要這個應用程序時, 可以只購買所需的功能。然后假如用戶需要增加功能,只要下載必需的包就行了,這比下載整個應用程序小得多。
n 能夠更容易地提供部分程序的修正(補?。灰笥脩臬@取一個應用程序的全新版本。
采用這種方案時,只需提供應用程序所需的*.bpl文件即可。這種方案與前一種方案類似,只不過沒有為Delphi IDE提供包,而是為自己編寫的應用程序提供包。分割應用程序時,必須注意包的版本化所帶來的問題,詳情參見下一節(jié)的描述。
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}允許一個包中包括單元,它的代碼是靜態(tài)的和應用程序相連接
{$DENYPACKAGEUNIT}
含義同{$G}
{$WEAKPACKAGEUNIT}
參見“關于{$WEAKPACKAGEUNIT}的更多信息”小節(jié)
表1.4 Package.dpk文件的編譯器指示符
指示符
含義
{$DESIGNONLY ON}
把包作為設計期包編譯
{$RUNONLY ON}
把包作為運行期包編譯
{$IMPLICITBUILD OFF}
防止包被重編譯。當不經常改變包時使用這個選項
關于{$WEAKPACKAGEUNIT}的更多信息
弱包(weak package)的概念比較簡單,它主要指包正在引用并不存在的庫(DLL)。比如,Vcl60調用Windows操作系統(tǒng)的核心Winn32 API。DLL中有很多這樣的調用,而并不是每臺計算機中部有這些DLL。這些調用由包含{$WEAKPACKAGEUNIT}指示符的單元顯露。通過包括這個指示符,保持這個單元的源代碼在包中,但要放在DCP文件而不是BPL文件里(把DCP看作DCU,BPL看作DLL)。因此,任何對這些弱打包單元的引用都會靜態(tài)的鏈接到應用程序,而不是動態(tài)地通過這個包引用。
我們很少使用{$WEAKPACKAGEUNIT}指示符。Delphi程序員創(chuàng)建它來處理一些特殊的情況,雖然這不是必需的。假如有兩個組件,每一個都在一個單獨的包個,并且正在引用某個DLL的同一接口祖先,這時就會存在問題。當某個應用程序要同時使用這兩個組件時,就會導致那個DLL的實例被載入,這樣將會引發(fā)有關初始比和全局變量引用的巨大災難。解決的方法是把這個接口單元提供給某個標準的Delphi包,比如Vcl60.bpl。然而,這又會導致其他有關專門的DLL問題,這些DLL也許并不存在,比如PENWIN.DLL。假如Vcl60.bpl包含有一個不存在的DLL的接口單元,這將導致Vcl60.bpl和Delphi不能使用。Delphi程序員通過讓Vcl60把接口單元包含進一個單一的包中來解決這個問題。但只有和Delphi IDE一起使用Vcl60.bpl時,是使接口單元被靜態(tài)的鏈接,而不是動態(tài)載入。
一般情況下當然沒有必要使用這個指示符,除非出現(xiàn)前面的類似情形,或者想確保包中包含某個特定的單元,而且又是靜態(tài)的鏈接到正在運行的應用程序中。對于后者,這樣做的一個原因是為了優(yōu)化。注意弱打包的任何單元在它們的初始化部分和結束部分部不能有全局變量或代碼。在和包一起發(fā)布時,也必須要發(fā)布弱打包單元的*.dcu文件。
1.10 包的命名約定
以前我們說過包的版本化問題將合影響包的命名。對于怎么命名包沒有一套規(guī)則,但是我們建議使用某種把代碼基礎融入包的名稱中的命名習慣。舉個例子,本書中的組件都包含在一個運行期包中,它的名字包含了6個Delphi限定詞(DdgRT6.dpk)。設計期包(DdgDT6.dpk)也是這樣。上一個版在的包是用DdgRT5.dpk。這種命名習慣能夠防止用戶產生混淆,比如他們正在應用哪個版本的包、哪個版本的Delphi編譯器適合它們。注意,包名稱要以3個字符的作者/公司標識符開頭,接著用RT指示出這是一個運行期包,或者用DT表示它是一個設計期包。最后就是所適用的Delphi的版本號。
1.11 使用運行期(插件)包的可擴展應用程序
插件包使得我們可以把應用程序隔成不同的模塊.并且可以獨立于主程序發(fā)布這些模塊。這種方式是很有好處的,它允許我們擴展應用程序的功能,卻不用重編譯/重設計整個應用程序。然而,這就要求有—個精心的體系設計計劃。盡管深入研究這種設計問題已經超出水書的范圍,但我們還是要做些討論,以使大家知道如何利用這個強大的功能。
生成插件窗體
這個應用程序被分割為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的派生類可用于主程序的流系統(tǒng),就必須調用RegisterClass()。這類似于RegisterComponents()怎樣使得組件能夠可用于Delphi IDE中。當包被載入時,必須調用UnRegisterlass()來刪除注冊類。然而,要注意RegisterClass()僅僅只是使類可用于主程序,主程序卻仍然不知道類名。主程序要怎樣才能創(chuàng)建一個類名未知的類實例呢?我們的意圖難道不就是使得這些窗體可用在主程序中,而不必辛苦的編與它們的類名并放入主程序的源代碼嗎?程序1.2所示的就是主程序的主窗體的源代碼,其中我們要強調的是如何用插件包實現(xiàn)插件窗體。
程序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.
這個應用程序的邏輯其實是很簡單的。它使用系統(tǒng)注冊來確定哪一個包是可用的;當為正在載入的包構建菜單時,確定菜單的標題和包中窗體的類名。
大多數(shù)丁作都在LoadChildFormOnClick()事件處理程序中完成。在確定包的文件名以后,這個方法使用LoadPackage()載入這個包。LoadPackage()函數(shù)基本上和DLL中的LoadLibrary()相向。LoadChildFormOnClick()然后又為被載入的包個的窗體確定類名。
為了創(chuàng)建一個類,需要一個類似Tbutton或Tform1的類引用。然而,這個應用主程序卻沒有實體TchildForms的類名,這就是為什么要從系統(tǒng)注冊表中找回類名的原因。主應用程序可以向FindClass()函數(shù)傳遞類名,以返回一個關于特定的類的類引用,這個類已經用流系統(tǒng)注冊。記住,當包被載入時,我們是在實體窗體單元的初始化部分做這個工作。接著用下面兩行代碼來創(chuàng)建類:
ChildFormClass := TchildFormClass(FindClass(ChildFormClassName));
FchildForm := ChildFormClass.Create(self, pnlParent);
說明:類引用不過是內存中的某個區(qū)域,其中包含了相關類的信息,這和類的類型定義是一回事情。當用VCL流系統(tǒng)或RegisterClass()函數(shù)注冊這個類時,類引用就會進入內存,F(xiàn)indClass()函數(shù)查找內存區(qū)域,定位某個指定類名的類,并返回一個指向那個位置的指針,這不同于類實例。類實例是創(chuàng)建于調用構造函數(shù)時。
變量ChildFormClass是對TchildForm預聲明的類引用,并且能夠多態(tài)地訪問TchildForm派生類的類引用。
CloseFormOnClick()事件處理程序只用來關閉子窗體并卸載它的包。剩下的基本上是一些設置性代碼,用來創(chuàng)建包菜單以及從從INI件中讀信息。
使用這個技術,我們就能創(chuàng)建高擴展性和松散耦合的應用程序框架。
1.12 從包中導出函數(shù)
考慮到包不過是增強的DLL,因此可以像在DLL中那樣,從包中導出函數(shù)和過程。在這一節(jié)我們將向大家介紹這種使用包的方式。
從包函數(shù)中運行窗體
程序1.3足一個包含在包的內部的單元。
程序1.3 有兩個導出函數(shù)的包單元
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()簡單地把在這個單元中聲明的窗體作為一個模塊窗體顯示。函數(shù)AdEm()獲取兩個操作數(shù)作為參數(shù)并返問它們的和。注意這個函數(shù)是通過StdCall調用協(xié)定在此單元的接口部分聲明的。
程序1.4是演示如何從包中調用一個函數(shù)的應用程序。
程序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,而且必須是當它們在包中出現(xiàn)時聲明。
首先我們討論了btnPkgFormClick()事件處理程序,它的代碼看上去似曾相識。不同的是我們沒有調用LoadLibrary(),而是使用了LoadPackage(),其實LoadPackage()的出現(xiàn)就終結了LoadLibrary()的使用。接下來,我們使用GetProcAddress()函數(shù)來接受對這個過程的引用。CFunkForm常量的名稱和包中的函數(shù)名稱相同。
我們可以發(fā)現(xiàn),從包中導出函數(shù)和過程的方法幾乎和從動態(tài)鏈接庫中導出的方法完全相同。
1.13 獲取包的信息
可以查詢一個包以獲得有關信息,比如它包含多少個單元、需要另外哪些包等??梢允褂脙蓚€函數(shù)來做這件工作:EnumModules()和GetPackageInfo()。
這兩個函數(shù)都需要回調函數(shù)(callback functions),程序1.5演示了這些函數(shù)的使用。
程序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(),它枚舉可執(zhí)行文件和在此可執(zhí)行文件個的任何包。傳遞給EnumModuls()的回調函數(shù)是EnumModuleProc(),這個函數(shù)把有關這個應用程序中的每一個包的信息都填充到—個TTreeView組件中。大多數(shù)代碼都是針對TTreeView組件的設置代碼。函數(shù)GetPackDescription()返回包含在包子源文件中的描述性字符串。調用GetPackageInfo()來傳遞回調函數(shù)PackageInfoProc()。
在PackageInfoProc()中,我們能夠處理包信息表中的信息。包中的每個單元和這個包所需要的每個包都要調用這個函數(shù)。這里,我們通過檢查Flaqs參數(shù)和NameType參數(shù)的值,再一次把這些(包信息表中的)信息填充TTreeView組件。對于其他信息,聯(lián)機幫助的“TpackageInfoProc”中都有解釋。
1.14 小結
包是Delphi/VCL體系的一個關鍵組成部分。通過學習如何使用包(不僅僅只是作為組件容器),就能夠開發(fā)出設計優(yōu)雅、領域寬泛的體系結構。