為什麼要使用包包?
答案很簡單:因為包的功能強大。設計期套件(design-time package)簡化了自訂元件的發布和安裝;而運行期套件(run-time package) 則更是為傳統的程式設計注入了新鮮的力量。一旦把可重複使用的程式碼編譯為運行期程式庫中,你就可以在多個應用程式中共用它們。所有應用程式都可以透過套件存取標準組 件,Delphi本身就是這麼幹的。因為應用程式不必在可執行檔中單獨複製一份元件庫,這樣就大 大節省了系統資源和磁碟空間。此外,套件還可以減少花費在編譯上的時間,因為你只需編譯應用程式特有的程式碼。
如果可以動態的使用包,那麼我們還可以獲得更多的好處。包提供了一種新穎的模組化方法來開發應用程式。有些時候你也 許想把某些模組當作應用程式的選用零件,例如一個記帳系統附帶一個可選的HR模組。某些情況下,你 只需安裝基本的應用程序,而在另外一些情況下你可能需要額外安裝HR模組。這種模組化的方法可以 透過包技術很容易的實現。在過去,這只能透過動態裝載DLL實現,但是使用Delphi的套件技術,你就可以把應用程式的各個模組類型分別打「包」成捆。特別是從套件中建立的類別物件則屬於應 用程式所有,因此可以與應用程式中的物件互動。
運行期套件與應用程式
許多開發者只把Delphi套件看作放組件的地方,事實上包可 以(而且也應該)應用於模組化應用程式設計。
為了示範如何用套件來模組化你的應用程序,我們建立一個例子:
1、 新建一個具有兩個窗體的Delphi程式:Form1和Form2;
2、 將Form2從自動建立窗體清單中移除(Project |Options | Forms);
3、 在Form1上放一個按鈕,並且在按鈕的OnClick事件處理器中輸入如下代碼:
with TForm2.Create(Application) do begin ShowModal; Free; End;
4、記住添加Unit2到Unit1的uses子句中;
5、 保存並運作工程。
我們創建了一個簡單的應用程序,它顯示一個帶有按鈕的窗體,點擊這個按鈕則會創建並顯示出另一個窗體。
但是如果想將上述範例中的Form2包含在一個可重複使用模組 中,並使它仍然可以正常工作,我們該怎麼辦呢?
答案是:包包!
要為Form2建立套件需要以下工作:
1、 開啟工程管理器(View | Project Manager);
2 、右鍵Project Group,選擇「Add NewProject...」;
3、在「「 New」項目清單中選擇「Package」;
4、 現在你應該可以見到包編輯器;
5、選擇「Contains」項目,然後點擊「Add」按鈕;
6、 然後點擊「Browse. ..」按鈕,並選擇「Unit2.pas」;
7、現在套件中應該包含了「Unit2.pas」單元;
8、 最後儲存並編譯套件。
現在我們完成了這個包。在你的ProjectBPL目錄中 應該會有一個名叫「package1.bpl」的檔案。 (BPL是Borland Package Library的縮寫,DCP是Delphi CompiledPackage 的縮寫。)
這個包已經完成了。現在我們需要打開套件選項開關
並重新編譯原先的應用程式。
1、 在工程管理器中雙擊「Project1.exe」以選取該工程;
2、 右鍵並選擇「Options...」(你也可以從選單中選擇Project | Options...);
3、 選取「Packages」選項頁;
4、 選取「Build with runtime packages」 檢查框;
5、 編輯「Runtime packages」編輯框:「Vcl50;Package1」,並點選「OK」按鈕」;
6、 注意:不要從應用程式移除Unit2;
7、 儲存並執行應用程式。
應用程式會像從前一樣運行,不過區別可以從檔案的大小上看出來。
Project1.exe現在只有14K大 小,而從前則是293K。如果你用資源瀏覽器查看EXE和BPL檔案的內容,你會發現Form2的DFM和程式碼現在都保存在套件中。
Delphi在編譯期完成對套件的靜態連線。 (這就是為什麼你不能從EXE工程中移除Unit2。)
想想你可以由此得到什麼:你可以在套件中創建一個資料存取模組,並且在更改資料存取規則時(例如從BDE連接轉為ADO連接),稍作修改並重新發布這個包。或者,你可以在某個套件中建立一個顯示「此選項在目前版本中不可用」資訊的窗體,然後在另一個同名的套件中建立一個具有完整功能的窗體。現在我們不費吹灰 之力就有了「Pro」和「Enterprise」 兩個版本的產品。
包的動態裝載和卸載
在大多数情况下,静态连接的DLL或BPL已经可以满足要求了。但是如果我们不想发布BPL呢? “在指定目录中找不到动态链接库Package1.bpl”,这是在应用程序终止前,我们所能得到 的唯一消息。或者,在模块化应用程序程序中,我们是否可以使用任意数量的插件?
我们需要在运行期动态连接到BPL。
对于DLL 来说,有一个简单的方法,就是使用LoadLibrary函数:
function LoadLibrary(lpLibFileName: Pchar): HMODULE;stdcall;
装载了DLL之后,我们可以使用GetProcAddress函数来调用DLL的导出函 数和方法:
function GetProcAddress(hModule: HMODULE; lpProcName:LPCSTR): FARPROC; stdcall;
最后,我们使用FreeLibrary卸载DLL:
function FreeLibrary(hLibModule: HMODULE): BOOL;stdcall;
下面这个例子中我们动态装载Microsoft的HtmlHelp库:
function TForm1.ApplicationEvents1Help(Command: Word; Data: Integer; var CallHelp: Boolean):Boolean; type TFNHtmlHelpA = function(hwndCaller: HWND; pszFile: PansiChar; uCommand: UINT;dwData: Dword): HWND; stdcall; var HelpModule: Hmodule; HtmlHelp: TFNHtmlHelpA; begin Result := False; HelpModule := LoadLibrary('HHCTRL.OCX'); if HelpModule <> 0 then begin @HtmlHelp := GetProcAddress(HelpModule, 'HtmlHelpA'); if @HtmlHelp <> nil then Result := HtmlHelp(Application.Handle,Pchar(Application.HelpFile), Command,Data) <> 0; FreeLibrary(HelpModule); end; CallHelp := False; end;
动态装载BPL
我们可以用同样简单的方法来对付BPL,或者应该说基本上同 样简单。
我们可以使用LoadPackage函数动态装载包:
function LoadPackage(const Name: string): HMODULE;
然后使用GetClass 函数创建一个TPersistentClass类型对象:
function GetClass(const AclassName: string):TPersistentClass;
完成所有操作后,使用UnLoadPackage(Module:HModule);
让我们对原来的代码作一些小小的改动:
1、 在工程管理器中选中“Project1.exe”;
2、 右击之并选择“Options...”;
3、 选中“Packages”选项页;
4 、 从“Runtime packages”编辑框中移除“Package1”,并点击OK按钮;
5、 在Delphi的工具栏中,点击“Remove file from project”按钮;
6、 选择“Unit2 | Form2”,并点击OK;
7、 现在在“Unit1.pas”的源代码中,从uses子句中移除Unit2;
8、 进入Button1 的OnClick时间代码中;
9、 添加两个HModule和TPersistentClass类型的变量:
var
PackageModule: HModule;
AClass: TPersistentClass;
10、使用LoadPackage 函数装载Pacakge1包:
PackageModule := LoadPackage('Package1.bpl');
11、检查PackageModule是否为0;
12、使用GetClass函数创建一个持久类型:
AClass := GetClass('TForm2');
13、如果这个持久类型不为nil,我们就可以向从前
一样创建并使用该类型的对象了:
with TComponentClass(AClass).Create(Application) as TcustomForm do begin ShowModal; Free; end;
14、最后,使用UnloadPackage 过程卸载包:
UnloadPackage(PackageModule);
15、保存工程。
下面是OnClick事件处理器的完整清单:
procedure TForm1.Button1Click(Sender: Tobject); var PackageModule: HModule; AClass: TPersistentClass; begin PackageModule := LoadPackage('Package1.bpl'); if PackageModule <> 0 then begin AClass := GetClass('TForm2'); if AClass <> nil then with TComponentClass(AClass).Create(Application) as TcustomForm do begin ShowModal; Free; end; UnloadPackage(PackageModule); end; end;
不幸的是,并不是这样就万事大吉了。
问题在于,GetClass函数只能搜索到已经注册的类型。 通常在窗体中引用的窗体类和组件类会在窗体装载时自动注册。但是在我们的例子中,窗体无法提前装载。那么我们在哪里注册类型呢?答案是,在包中。包中的每 个单元都会在包装载的时候初始化,并在包卸载时清理。
现在回到我们的例子中:
1、 在工程管理器双击“Package1.bpl”;
2、 点击“Contains”部分“Unit2”旁的+号;
3、 双击“Unit2.pas”激活单元源代码编辑器;
4、 在文件的最后加入initialization部分;
5、 使用RegisterClass过程注册窗体的类型:
RegisterClass(TForm2);
6、 添加一个finalization部分;
7、 使用UnRegisterClass过程反注册窗体的类 型:
UnRegisterClass(TForm2);
8、 最后,保存并编译包。
现在我们可以安全的运行“Project1”,它还会像从前 一样工作,但是现在你可以随心所欲的装载包了。
尾声
记住,无论你是静态还是动态的使用包,都要打开Project | Options | Packages | Build with runtime packages 选项。
在你卸载一个包之前,记得销毁所有该包中的类对象,并反注册所有已注册的类。下面的过程可能会对你有所帮助:
procedure DoUnloadPackage(Module: 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 (Module = 0) or (HMODULE(M.AllocationBase) = Module) then Application.Components[i].Free; end; UnregisterModuleClasses(Module); UnloadPackage(Module); end;
在装载包之前,应用程序需要知道所有已注册类的名字。改善这一情况的方法是建立一个注册机制,以便告诉应用程序所有 由包注册的类的名字。
实例
多重包:包不支持循环引用。也就是说,一个单元不能引用一个已经引用了该单元的单元(嘿嘿)。这使得调用窗体中的某 些值难以由被调用的方法设置。
解决这个问题的方法是,创建一些额外的包,这些包同时由调用对象和包中的对象引用。设想一下我们如何使Application成为所有窗体的拥有者?变量Application创 建于Forms.pas 中,并包含在VCL50.bpl包 中。你大概注意到了你的应用程序既要将VCL50.pas编译进来,也同时你的包也需要(require) VCL50。
在我們第三個例子中,我們設計一個應用程式來顯示客戶訊息,並且可根據需要(動態)顯示客戶訂單。
那我們可以從哪裡開始呢?像所有的資料庫應用程式
程式一樣,我們需要連接。我們建立一個主資料模組,包含一個TDataBase連 接。然後我們將這個資料模組封裝在一個套件中(cst_main)。
現在在應用程式中,我們建立一個客戶窗體,並引用DataModuleMain(我 們靜態的連結VCL50 和cst_main)。
然後我們建立一個新的套件(cst_ordr),包中包含客戶 訂單窗體,並require cst_main。現在我們可以在應用程式中動態的裝載cst_ordr了。既然在動態套件裝載以前主資料模組已經存在,cst_ordr就 可以直接使用應用程式的主資料模組實例了。
上圖是此應用程式的功能示意圖:
可換套件:套件的另一個應用實例是建立可更換套件。實現這個功能並不需要套件的動態裝載能力。假設我們要發布一個有時間限制 的試用版的程序,如何實現這一點?
首先我們創建一個「Splash」窗體,通常情況下是一幅帶有「試用」字樣的圖片,並在應用程式啟動的過程中顯示它。然後我們建立一個「About」窗體,提 供一些關於應用程式的資訊。最後,我們建立一個用於測試軟體是否過期的函數。我們把這兩個窗體和這個函數封裝到一個包中,並將它隨試用版軟體發布。
對於付費版軟體,我們也創建一個「Splash」窗體和一個「About」窗體——要和前面的兩個窗體類名相同——以及一個測試函數(什麼也不做),並將它們封裝到同名的包中。
什麼什麼?你問這有用麼?好吧,我們可以公開的發布一個試用版軟體。如果某個客戶購買了該應用程序,我們只需要發送 非試用版的包。這就大大簡化了軟體的發布過程,因為只需要一次安裝和一次註冊包升級。
套件為Delphi和C++ Builder開發社群打開了另一扇通往模組化設計的大門。透過包你不再需要到處傳遞窗體句柄,不再需要回呼函數,不再需要其它DLL技術。由此也縮短了模組化程式設計的開發週期。我們所要做的僅僅是讓Delphi的套件為我們工作。