1 引言
在用Delphi、Visual Basic等可視化快速開發工具編寫Windows應用程序時,常會遇到這樣幾個問題:
1) 希望程序界面美觀。在Delphi中,開發人員通常使用各種控件來實現界面的風格化,但缺點是造成應用程序體積較大,且在升級時常會被控件版本與Delphi版本不兼容帶來的問題所困擾。
2) 希望應用程序在功能不變的情況下具有不同的界面風格。這常常通過換"皮膚"的技術來實現,但一般實現"換膚"功能的控件體積都較大,且界面反應速度比較慢,而且 "皮膚"的制作比較麻煩。
3) 程序界面的維護困難。為了使界面與代碼實現相分離而獲得"換膚"等靈活性,通常要用到一些設計模式的技術,這對於不熟悉設計模式的開發人員來說比較困難。
微軟公司預計將於2006年發布下一代操作系統(開發代號為Longhorn)中,應用程序的結構及部署將有重大變革,其中一項就是應用程序的界面完全以XML的一個擴展集XAML語言來描述,以便達到界面的高度可定制性。這無疑能夠方便地解決上述幾個問題。問題是在目前來說有沒有類似的方法呢?答案就是使用浏覽器控件。
微軟公司的網頁浏覽器Internet Explorer的核心被設計為可以嵌入到應用程序中重用的ActiveX組件,它有極強的可編程能力和與容器交互的能力,使得開發人員能夠快速地開發出功能強勁的應用程序。從下面的Internet Explorer的架構圖可以看到,我們平常運行的IExplorer.exe其實只是一個外殼程序,真正的浏覽網頁、記錄歷史等工作是由嵌入其窗口的封裝在shdocvw.dll中的WebBrowser Control來完成的。
Shdocvw.dll的功能則是調用mshtml.dll來解析網頁,以及在它的窗口中嵌入其它活動文檔組件(如Microsoft Office、Adobe Acrobat等應用程序的文檔都可以嵌入到浏覽器窗口中查看)。而mshtml.dll一方面處理Html解析以及作為腳本引擎、Java虛擬機、ActiveX控件、插件的宿主,另一方面,它實現了活動文檔服務器接口,允許應用程序以標准的COM接口來把它嵌入到程序中並通過它暴露的接口來訪問其中的網頁及網頁元素。
通過shdocvw.dll提供的豐富接口,網頁中的元素可以訪問外殼應用程序提供的屬性和方法(如window.external.AddFavorite(location.href, document.title)則是調用IE的AddFavorite方法把當前頁添加到收藏夾),而通過mshtml.dll提供的接口,外殼應用程序則反過來可以訪問網頁中元素的屬性、方法、行為、事件等等。解決文章開頭提出的幾個問題的方法就是基於shdocvw.dll和msHtml.dll實現的。一些著名軟件如:Microsoft Money、Microsoft Visual Studio .Net、Macromedia Dreamweaver MX 2004等都運用了這種技術。
2 原理
1) 程序的界面完全由制作網頁來完成。網頁在文字、圖像、聲音等方面具有強大的表現能力,運用所見即所得的網頁制作工具可以輕松制作出圖文並茂的網頁。以網頁作為程序的界面,其效果勝過任何界面控件。
2) "換膚"功能容易實現。只需制作不同風格的網頁,即可輕松實現樣式各異的程序界面。
3) 程序的功能在應用程序內部編寫代碼來實現,並通過一個自動化接口提供給網頁中的元素調用。這就實現了程序界面和代碼的分離,網頁布局及風格的改變不會影響到程序的實現。
3 從網頁調用外殼程序的屬性和方法
3.1 GetExternal接口方法
WebBrowser Control提供的接口使得外殼應用程序可以用自己的對象、方法和屬性等來擴展IE的對象模型(DOM),以達到個性化定制的目的。在網頁中訪問外殼應用程序的擴展則通過文檔的"external"對象來實現,如外殼程序提供了名為AddFavorite的方法,網頁中就通過window.external.AddFavorite()來調用。實現這一功能的核心是IDocHostUIHandler接口的GetExternal方法:
HRESULT GetExternal(IDispatch **ppDispatch);
在自定義的WebBrowser Control中實現IDocHostUIHandler接口,當網頁元素通過"external"對象訪問外殼擴展的屬性和方法時,GetExternal方法就會被調用,在此方法的中將實現外殼程序屬性和方法的自動化接口傳遞給ppDispatch即可。自定義的WebBrowser Control示例代碼如下,在其中將GetExternal包裝為OnGetExternal事件供外部程序調用。IDocHostUIHandler接口有15個方法,此處我們只關心GetExternal方法,故略去其余14個(省略號處為略去的代碼)。
unit ZoCWebBrowser;
interface
uses
Variants,IEConst, Windows, SysUtils, Classes, SHDocVw, ActiveX, shlObj, MSHtml, comobj;
type
……
TGetExternalEvent = function(out ppDispatch: IDispatch): HRESULT of object; //定義OnGetExternal事件類型
TZoCWebBrowser = class(TWebBrowser, IDocHostUIHandler)
private
……
FOnGetExternal: TGetExternalEvent;
protected
……
function GetExternal(out ppDispatch: IDispatch): HRESULT; stdcall;
published
……
property OnGetExternal: TGetExternalEvent read FOnGetExternal write FOnGetExternal;
end;
……
implementation
……
function TZoCWebBrowser.GetExternal(out ppDispatch: IDispatch): HRESULT;
begin
if Assigned(FOnGetExternal) then
Result := FOnGetExternal(ppDispatch)
else
Result := S_FALSE;
end;
initialization
OleInitialize(nil);
finalization
try
OleUninitialize;
except
end;
end.
3.2 實現外殼程序擴展自動化接口
在Delphi的"New Items"對話框中,切換到"ActiveX"頁,選擇"Automation Object",新建一個自動化對象,並在"CoClass Name"一欄中填入接口名"MyExternal","Instancing"選擇為"Internal",表示該對象只能在程序內部被創建,外部程序不能直接創建。點擊"OK"按鈕後在Type Library編輯對話框中為IMyExternal接口添加兩個方法ShowAboutBox和SwitchUI,此時代碼大致如下所示:
unit MyExternalImpl;
{$WARN SYMBOL_PLATFORM OFF}
interface
uses
ComObj, ActiveX, Project1_TLB, StdVcl;
type
TMyExternal = class(TAutoObject, IMyExternal)
protected
procedure ShowAboutBox; safecall;
procedure SwitchUI; safecall;
end;
implementation
uses ComServ;
procedure TMyExternal.ShowAboutBox;
begin
MessageBox(MainForm.Handle, 'GetExternal Demo', 'ZoCWebBrowser', MB_OK or MB_ICONASTERISK);
end;
procedure TMyExternal.SwitchUI;
begin
ShowSwitchUIForm; //顯示切換程序界面對話框
end;
initialization
TAutoObjectFactory.Create(ComServer, TMyExternal,
Class_MyExternal, ciInternal, tmApartment);
end.
3.3 從網頁中調用外殼程序接口
在程序主窗口中放置一個自定義的WebBrowser Control,命名為ZoCWebBrowser,編寫它的OnGetExternal事件(由網頁中的window.external調用觸發),代碼如下:
function TMainForm.ZoCWebBrowserGetExternal(
out ppDispatch: IDispatch): HRESULT;
var
MyExternal: TMyExternal;
begin
MyExternal:= TMyExternal.Create; //創建實現自動化接口的對象
ppDispatch :=MyExternal; //將對象接口傳遞給WebBrowser Control
//這樣當"external"對象被調用時,真正被調用的是我們實現的TMyExternal對象
Result :=S_OK;
end;
假設我們制作了兩個風格迥異的的網頁Style1.html和Style2.html作為程序界面,這兩個網頁中都有兩個按鈕(也可以是其它網頁元素),其Html代碼示例如下:
在程序開始運行時讓WebBrowser Control布滿整個Form,且顯示Style1.html頁面,則當點擊"關於"按鈕時程序將顯示一個關於信息對話框,而點擊"切換界面"按鈕時將顯示切換界面的對話框,在其中選擇Style2.html並讓WebBrowser Control顯示它即可獲得風格完全不同的界面,但在功能上與Style1.Html完全一樣。
4 總結
從上面的例子可以看到,我們以及其簡單的方式實現了程序界面與實現的分離,這有利於程序的維護和擴展。傳統方式下,界面設計和編碼通常都由程序員來完成,一來造成程序員負擔較重,二來難以保證界面質量。實用上述方法,程序界面可以由專業美工人員來設計,他可以在完全不知道程序如何實現的情況下設計出完整的界面,而程序員只需專注於代碼的編寫,並將必要的方法和屬性通過一個自動化接口暴露出來。合並的時候,在網頁中合適的位置放入所需的按鈕或其它網頁元素,並賦予簡單的腳本調用即可。