延伸父應用
這個簡單的插件不錯,不過它不能做什麼有用的事情。第二個例子就是糾正這個問題。
這個插件的目標就是在父應用程序的主菜單中加入一個項目。這個菜單項目,當被單
擊時,就會執行插件內的一些代碼。圖6顯示外殼程序的改進版,兩個插件都已經加
載。在這個版本的外殼程序中,一個名為Plug-in的新菜單項目,被添加到主菜單中。
插件會在運行時加入一個菜單項。
圖6:加載了兩個插件的外殼程序的改進版
為了實現這個目的,我們必須在插件DLL中定義第二個接口。現有的DLL只導出了一
個過程,DescribePlugin。第二個插件將聲明一個叫做InitPlugin的過程。不過,在
這個過程可以在主應用程序中看到以前,必須修改LoadPlugin來配合它。
圖7所示的代碼展示了改進的過程。
procedure TfrmMain.LoadPlugin(sr: TSearchRec);
var
Description: string;
LibHandle: Integer;
DescribeProc: TPluginDescribe;
InitProc: TPluginInit;
begin
LibHandle := LoadLibrary(Pchar(sr.Name));
if LibHandle <> 0 then
begin
// 查找 DescribePlugin.
DescribeProc := GetProcAddress(LibHandle,
cPLUGIN_DESCRIBE);
if Assigned(DescribeProc) then
begin
// 調用 DescribePlugin.
DescribeProc(Description);
memPlugins.Lines.Add(Description);
// 查找 InitPlugin.
InitProc := GetProcAddress(LibHandle, cPLUGIN_INIT);
if Assigned(InitProc) then
begin
// 調用 InitPlugin.
InitProc(mnuMain);
end;
end
else
begin
MessageDlg(File " + sr.Name +
" is not a valid plugin.,
mtInformation, [mbOK], 0);
end;
end
else
begin
MessageDlg(An error occurred loading the plugin " +
sr.Name + "., mtInformation, [mbOK], 0);
end;
end;
圖 7: 改進過的LoadPlugin方法
如你所見,當GetProcAddress第一次查找調用描述過程之後,又調用了一次
GetProcAddress。這一次,我們要尋找的是常量cPLUGIN_INIT,定義如下:
const
cPLUGIN_INIT = InitPlugin;
返回值存儲在TpluginInit類型的變量中,定義如下:
type
TPluginInit = procedure(ParentMenu: TMainMenu); stdcall;
當InitPlugin方法被執行時,父應用程序的主菜單被當作一個參數傳遞給它。這個
過程可以按照自己的意願修改菜單。由於所有GetProcAddress的返回值都用assigned
測試,新版本的LoadPlugin過程仍然會加載不包含InitPlugin過程的第一個插件。在
這個過程中第一次調用尋找DescribePlugin方法會通過,第二次尋找InitPlugin會
無響應失敗。
現在新的接口已經定義好了,可以為新的InitPlugin方法編寫代碼了。像原先一樣,
新插件的實現代碼存在於一個單獨的單元中。圖8顯示了修改過的包含InitPlugin方法
的main.pas。
unit main;
interface
uses Dialogs, Menus;
type
THolder = class
public
procedure ClickHandler(Sender: TObject);
end;
procedure DescribePlugin(var Desc: string);
export; stdcall;
procedure InitPlugin(ParentMenu: TMainMenu);
export; stdcall;
var
Holder: THolder;
implementation
procedure DescribePlugin(var Desc: string);
begin
Desc := Test plugin 2 - Menu test;
end;
procedure InitPlugin(ParentMenu: TMainMenu);
var
i: TMenuItem;
begin
// 創建新菜單項.
i := NewItem(Plugin &Test, scNone, False, True,
Holder.ClickHandler, 0, mnuTest);
ParentMenu.Items[1].Add(i);
end;
procedure THolder.ClickHandler;
begin
ShowMessage(Clicked!);
end;
initialization
Holder := THolder.Create;
finalization
Holder.Free;
end.
圖 8: 第二個插件的代碼
很明顯,對原始插件的第一個改變就是增加了InitPlugin過程。像原先一樣,帶有
export關鍵字的原型被加入到單元頂端的列表中,過程名也被加入到工程源代碼的
exports子句列表中。這個過程使用NewItem函數創建一個新的菜單項,返回值是
TmenuItem對象。新菜單項通過下列語句被加入到應用程序主菜單中:
ParentMenu.Items[1].Add(I);
在測試外殼主菜單上的Items[1]是菜單項Plug-in,所以這個語句在Plugin菜單條
上添加一個叫Plug-in Test的菜單項。
為了處理對新菜單項的響應,作為它的第五個參數,NewItem可以接受一個
TNotifyEvent類型的過程,這個過程將在菜單項被點擊時調用。不幸的是,按照定
義,這種類型的過程是一個對象方法,然而在我們的插件中並沒有對象。如果我們想
用通常的指針來指向函數,那麼得到的將只會是Delphi編譯器的抱怨。所以,唯一的
解決辦法就是創建一個處理菜單點擊的對象。這就是Tholder類的用處。它只有一個方
法,是一個叫做ClickHandler的過程。一個叫做Holder的全局變量,在修改過的main.pas
的var段中被聲明為Tholder類型,並且在單元的initialization段中被創建。現在我們就
有一個對象了,我們可以拿它的方法(Holder.ClickHandler)當作NewItem函數的參數。
搞了這一通,ClickHandler除了顯示一個"Clicked!"消息對話框以外什麼以沒干。
也許這不怎麼有趣,不過它仍然證明了一點:插件DLL成功的修改了父應用的主菜單,
表現了它的新用途。並且如同第一個例子一樣,不管這個插件在不在應用程序都能執行。
由於我們創建了一個對象來處理菜單點擊,那麼在不再需要這個插件時,就要釋放這
個對象。修改後的單元中會在finalization段中處理這件事情。Finalization端時與
initialization段相對應的,如果前面有一個initialization段,那麼在應用程序終
止時finalization段一定會得到執行。把下面的語句
Holder.Free
加到finalization段中,以確保Holder對象會被正確的釋放。
顯而易見,雖然這個插件只是修改了外殼應用的主菜單,但是它可以輕易地操縱傳
遞到InitPlugin過程中的任何其他對象。如果有必要,插件也可以打開自己的對話框,
向列表框(List boxes)和樹狀視圖(tree views)中添加項目,或者在畫布(canvas)
中繪畫。
事件驅動的插件
到現在為止我們所描述的技術可以產生一種通用的擴展應用程序的方法。通過增加
新菜單、窗體和對話框,就可以實現全新的功能而不必對父應用做任何修改。不過仍
然有一個限制:這只是一種單側(one-sided)機制。正如所看到的,系統依賴用戶的
某些操作才能啟動插件代碼,比如點擊菜單或者類似的動作。代碼運行起來以後,又要
依靠另外一個用戶動作來停止它,例如,關閉插件可能已經打開的窗體。克服這種缺
陷的一種可行的方法就是使插件可以響應父應用中的動作--模仿在Delphi中工作地
很好的事件驅動編程模型的確有效。
在最後一個例子插件中,我們將創建一種機制,插件可以藉此響應父應用中產生的事
件。通常情況下,可以通過判定需要觸發哪些事件、在父應用中為每個事件創建一個
Tlist對象來實現。然後每個Tlist對象都被傳遞到插件的初始化過程中,如果插件想
在某個事件中執行動作,它就把負責執行的函數地址加入到對應的TList中。父應用在
適當的時刻循環這些函數指針的列表,按次序調用每個函數。通過這種方法,就為多
個插件在同一事件中執行動作提供了可能。
應用程序產生的事件完全依賴於程序已確定的功能。例如,一個TCP/IP網絡應用程序
可能希望通過TclientSocket的onRead事件通知插件數據抵達,而一個圖形應用程序可
能對調色板的變化更感興