為什麼要使用套件?
答案很簡單:因為包的功能強大。設計期套件(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、 最後儲存並編譯套件。
現在我們完成了這個包。在你的Project/BPL目錄中應該有一個名叫「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) 或 (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的套件為我們工作。