1. 程式人生 > >利用瀏覽器實現程式介面與實現的分離

利用瀏覽器實現程式介面與實現的分離

關鍵字 WebBrowser,IDocHostUIHandler,GetExternal

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來完成的。

Internet Explorer 4.0 架構


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 總結
從上面的例子可以看到,我們以及其簡單的方式實現了程式介面與實現的分離,這有利於程式的維護和擴充套件。傳統方式下,介面設計和編碼通常都由程式設計師來完成,一來造成程式設計師負擔較重,二來難以保證介面質量。實用上述方法,程式介面可以由專業美工人員來設計,他可以在完全不知道程式如何實現的情況下設計出完整的介面,而程式設計師只需專注於程式碼的編寫,並將必要的方法和屬性通過一個自動化介面暴露出來。合併的時候,在網頁中合適的位置放入所需的按鈕或其它網頁元素,並賦予簡單的指令碼呼叫即可。

(以上程式碼均在WindowsXP+Delphi 7環境下除錯通過)

5 參考文獻
《MSDN Library - July 2003》