揭開Outlook Express編輯器的奧祕

 

【前言】

Outlook Express是一款大家比較熟悉的郵件工具,其HTML編輯器一直是衆多程序員競相模仿的目標。作者最近在一個項目的開發中,開始接觸HTML編輯器的設計,並遇到了很多的難題。目前網絡上關於IE編程的文章中,涉及MSHTML編輯器的部分,又大多集中在VC領域,用Delphi作爲解決方案的少之又少。在經過一番艱難的摸索之後,作者積累了一些成功的經驗。並撰成此文,希望與大家共同探討。

 

注:本文將涉及到COM編程,由於COM的複雜性,不免會有晦澀難懂之嫌。爲了讓閱讀不至於成爲一種折磨,作者將嘗試另一種寫作模式。文章將隨着一個叫做W的程序員的編程思路展開,以通俗易懂的敘事方式帶領讀者一起探討在MSHTML編輯器的開發過程中可能遇到的一些棘手問題。對於某些需要強調的關鍵術語,文中將適時的給出註解,以便讀者更好的領會。

 

【學習目標】

通過本文的閱讀,讀者將可以學習到以下內容:

 

l        掌握TWebBrowser控件的用法;

l        理解IHTMLDocument2IDocHostUIHandle接口;

l        探討在MSHTML中如何加載字符流;

l        找回在MSHTML編輯器中丟失的回車鍵;

l        實現工具欄的自動感應;

l        自定義MSHTML編輯器強大的粘貼功能;

 

本文假定讀者已經具備初步的COM知識和Delphi接口的編程經驗,如果您需要對COM和接口知識作進一步的深入瞭解,請參考其它相關文章。

 

【關鍵字】

TWebBrowserMSHTML、自動化對象、IHTMLDocument2IDocHostUIHandler

FilterDataObjectIDataObject、剪貼板、粘貼

 

【正文】

 

一個笑話的啓示

       在一次程序員大會上,主持人爲了活躍氣氛,做了個小遊戲。他問臺下的程序員:如果有誰在小的時候拆過鬧鐘的請舉手。臺下的程序員們全都舉起了手。主持人又問:那麼又有誰後來把鬧鐘裝回去的請舉手。舉起手來的程序員們又都把手放下了。

 

       這個笑話從側面說明了一個問題,追根求源正是大多數程序員的天性。缺少追根求源的精神,軟件設計就會缺少創新的動力。也正是由於有了追根求源的精神,越來越多的軟件新手跨越了初期的彷徨,走上了軟件高手的道路。

 

未來的一天,程序員W所在的軟件公司接到一個信息管理系統的設計項目。由於最近W剛參加完公司組織的爲期一週的COM培訓,於是項目經理Y便把項目中最具挑戰的編輯器部分交給他來完成。用戶要求實現一個類似Outlook Express(以下簡稱OE)那樣所見即所得式的編輯器,並可以支持多種來源的粘貼操作。儘管W此前對OE編輯器的原理一無所知,但他還是面帶微笑並充滿自信的接受了挑戰。

 

揭開OE編輯器的面紗

在以往的使用過程中,W發現OE編輯器確實是一款強大的編輯工具。無論是編輯還是粘貼,OE編輯器都能完美的實現所見即所得的效果。OE編輯器本質上是一款HTML編輯器,其中的數據和格式都以HTML代碼的形式來保存。W以前曾研究過網頁上的HTML編輯器,該編輯器是通過DHTML技術來實現的。那麼,OE編輯器和IE瀏覽器之間是否有什麼關係呢?

 

爲了搞清楚這個問題,W調出VCSpy++探個究竟。拖動Spy++那個神奇的雷達指向OE編輯窗口,Spy++迅速的找到了窗口的類型:“Internet Explorer_Server”,這是個IE服務器窗口類型,這究竟是什麼意思呢?

 

       微軟的IE瀏覽器的核心部分是SHDOCVW.DLLMSHTML.DLL。從下面的Internet Explorer的架構圖可以看到,IE其實只是一個外殼程序,真正的瀏覽網頁、記錄歷史等工作都是由封裝在SHDOCVW.DLL中的WebBrowser Control來完成的。而HTML的解析、腳本引擎、java虛擬機、插件宿主等,則由SHDOCVW.DLL通過調用MSHTML.DLL來完成。通過SHDOCVW.DLL提供的豐富接口,網頁中的元素可以訪問外殼應用程序提供的屬性和方法;而通過MSHTML.DLL提供的接口,外殼應用程序則反過來可以訪問網頁中元素的屬性、方法、行爲、事件等等。

      

 

毫無疑問,OE編輯器正是通過對WebBrowser控件和MSHTML的封裝實現了HTML的編輯功能。由於WebBrowser屬於ActiveX控件,所以,利用DelphiActiveX導入嚮導,可以輕鬆的實現對WebBrowser控件的封裝。導入後將在DelphiImport文件夾下自動生成兩個TLB文件:SHDocVw_TLB MSHTML_TLB

 

Delphi自帶的TWebBrowser

       Delphi4開始,Delphi就在Internet組件面板上提供TWebBrowser組件,作爲對WebBrowser控件的封裝。由於Delphi的封裝並不能保證和最新的WebBrowser控件版本相一致,建議Delphi7以前的讀者先卸載該組件並重新導入Shdocvw.dll,以便使用最新的接口功能,

 

進入TWebBrowser的神奇世界

感謝Delphi,使得一切都變得如此輕鬆。W啓動Delphi,新建一個項目,在Internet組件面板上找到TWebBrowser組件,然後拖放到窗體上,並重命名爲“wbEditor”。由於對TWebBrowser組件缺乏瞭解,W決定先請教一下公司的Delphi高手老D

 

W:老D,你知道TWebBrowser組件的用法嗎?

D:這個簡單。TWebBrowser有一個Document屬性,你看一下,這是個IDispatch接口類型的屬性。對了,IDispatch接口你知道嗎?

W:(支支吾吾)剛學過,不過沒弄懂……

D:簡單點說吧。爲了給解釋型語言——例如Javascript腳本語言——提供調用COM對象服務的能力,於是出現了COM自動化對象。由於解釋型語言無法象編譯型語言那樣實現和COM對象的早期綁定,所以,COM自動化對象便提供了IDispatch接口供自動化客戶端調用。通過IDispatch接口,自動化機制中的客戶端就可以動態的調用COM自動化對象中的方法了……總之啊,IDispatch接口是實現COM自動化對象機制的關鍵。你明白嗎?

W:(似懂非懂,不過想想反正以後還可以再學)嗯,知道了。然後呢?

DOK。由於我們並不需要自動化機制,IDispatch接口對我們來說用處不大。但我們可以利用它通過Delphi中的as運算符查詢到其它我們想要的接口。例如,IHTMLDocument2接口在編程中用的比較多,用它可以實現大多數的DHTML功能。

W:哦~IHTMLDocument2接口(自言自語)。那如何進入編輯狀態呢?

D:答案就在這個IHTMLDocument2接口中。這個接口中有一個disignMode屬性,在運行時置爲“On”就可以從瀏覽模式轉變爲編輯模式了。當然了,前提是必須保證Document不能爲空。有個簡單的辦法,在初始化時,通過TWebBrowserNavigate方法導航到一個空白頁面,有一個busy屬性可以用來監測是否加載完畢……

W:哦……哦……(一邊聽一邊敲出下面的代碼)運行成功!太感謝了。

 

procedure TForm1.FormCreate(Sender: TObject);

begin

  wbEditor.Navigate('about:blank');

  while wbEditor.busy do Application.ProcessMessages;

  (wbEditor.Document as IHTMLDocument2).designMode := 'On';

end;

 

在老D的幫助下,WTWebBrowser的用法有了一個初步的瞭解。很顯然,接口在TWebBrowser的編程中至關重要。此時,爲了加深對接口的瞭解,W決定對IHTMLDocument2接口做一個深入的瞭解。

 

小知識:

       在執行TWebBrowser的某個方法以進行某些期望的操作如ExecWB等時候,可能會碰到如“試圖激活未註冊的丟失目標”或“OLE對象未註冊”等錯誤提示,或者並沒有任何出錯信息但卻得不到希望的結果。這是因爲TWebBrowser本身是一個OLE類型的COM組件,你需要在使用TWebBrowser前對OLE進行一些初始化工作,這個工作可以放到單元的initializationfinalization段中來完成。

 

{uses ActiveX}

initialization

  OleInitialize(nil);

finalization

  try

    OleUninitialize;

  except

  end;

 

淺談IHTMLDocument2接口

 

背景知識:

       爲了使用IHTMLDocument2接口,你必須包含MSHTML.pas單元(如果你採用ActiveX導入的方式,這個單元就是MSHTML_TLB.pas

 

MSHTML控件的Document對象實現了包括IHTMLDocument2接口在內的多個接口。其中Document對象的常用屬性、子集合、方法等都集中在IHTMLDocument2接口中。通過IHTMLDocument2接口,可以利用DHTML的強大功能對網頁對象進行各種增刪操作和屬性的動態改變。

 

IHTMLDocument2的接口方法中,有一個特殊的方法引起了W的注意,這就是execCommand方法。很顯然,這個方法與命令的調用有關。execCommand方法聲明如下:

 

//對當前文檔、選定內容或指定範圍執行特定的操作

HRESULT execCommand(      
    BSTR cmdID,

    VARIANT_BOOL showUI,

    VARIANT value,

    VARIANT_BOOL *pfRet

);

 

其中,cmdID參數定義了大多數常用的格式化命令。這樣,OE編輯器工具欄上的大多數編輯功能完全可以通過這個方法來實現。爲了驗證自己的想法,W在窗體上新建一個按鈕,並寫了一些測試代碼。運行結果完全符合W的猜測。

 

procedure TForm1.Button1Click(Sender: TObject);

begin

  with wbEditor.Document as IHTMLDocument2 do

begin

//改變字體的前景色

execCommand('ForeColor', False, 'red');

//改變字體的粗細

execCommand('Bold', False, 1);

//打開插入圖片對話框,插入圖片

execCommand('InsertImage', True, '');

//文本居中

execCommand('JustifyCenter', False, 0);

//執行撤銷上一步操作

execCommand('Undo', False, 0);

  end;

end;

 

注:爲了確保execCommand調用成功,你必須保證當前頁面已經完全加載。

 

如果考慮效率問題,IOleCommandTarget::Exec方法則可以提供更好的性能。事實上,execCommand命令正是對IOleCommandTarget::Exec方法的一個封裝,其目的主要是爲了給Script類型的語言提供一個方便的調用入口。通過以下示例學習如何獲得對IOleCommandTarget接口的訪問並調用Exec方法:

 

IOleCommandTarget::Exec方法

      

       procedure TForm1.Button2Click(Sender: TObject);

       const

              CGID_MSHTML: TGUID = '{DE4BA900-59CA-11CF-9592-444553540000}';

       begin

               (wbEditor.Document as IOleCommandTarget).Exec(

@CGID_MSHTML,

IDM_BOLD,     //Bold命令的ID,請參考MSDN有關幫助

OLECMDEXECOPT_DODEFAULT,

0,

POlevariant(nil)^);

       end;

 

 

再談Document對象的初始化和賦值

IHTMLDocument2接口中,Document對象是實現DHTML模型的核心。要實現對Document對象的任何操作,必須要等到Document對象的初始化操作結束之後才能進行。通過Navigate方法,可以實現對Document對象的初始化。需要注意的是,Navigate方法並不能識別常規方式下的相對路徑,如果需要導航到某個文件,必須指定絕對路徑

 

procedure TForm1.InitDocument;

begin

  wbEditor.Navigate('about:blank');

  while wbEditor.ReadyState <> READYSTATE_COMPLETE do

    Application.ProcessMessages;

end;

 

Document文檔對象完成初始化之後,就可以對Document對象進行賦值。賦值對象既可以是字符串,也可以是內存中的數據流。通過Document對象的IPersistStreamInit接口,就可以實現數據流的加載。

 

function TForm1.LoadFromStream(const AStream: TStream): HRESULT;

begin

  if not Assigned(wbEditor.Document) then

    InitDocument;

  AStream.seek(0, 0);

  Result := (wbEditor.Document as IPersistStreamInit).Load(TStreamadapter.Create(AStream));

end;

 

利用數據流的加載方法,可以進一步的實現字符串的加載。

function TForm1.LoadFromStrings(const AStrings: TStrings): HRESULT;

var

  M: TMemoryStream;

begin

  M := TMemoryStream.Create;

  try

    AStrings.SaveToStream(M);

    Result := LoadFromStream(M);

  except

    Result := S_FALSE;

  end;

  M.free;

end;

 

找回被編輯器吃掉的回車

有一個問題從一開始就引起了W的注意,那就是MSHTML編輯器竟然經常不響應回車事件。也就是說,當在編輯器中按下回車時不能產生一個換行——編輯器面對回車按鍵毫無反應,就好像回車鍵被它吃掉一樣。類似的,象TABDeleteBACKSPACE、→、←等快捷鍵上也會出現這種情況。這真是一件令人匪夷所思的事。

 

從本質上說,TWebBrowser是一個特殊的OLE控件。雖然Delphi在封裝過程中使它繼承了TWinControl,但它似乎並沒有由此取得TWinControl的自動獲得焦點的能力。看來,極有可能是由於DelphiVCL消息處理機制同OLE之間存在某種衝突,導致了OLE自己吃掉了部分鍵盤消息。

 

既然如此,W實現想不出什麼更好的辦法。要解決這個問題,一個比較合理的解決方案就是直接捕獲並處理Windows的消息映射。於是,他嘗試寫了一個消息處理方法並把這個方法句柄指定給了Application.OnMessage事件。這樣,丟失的回車鍵又回來了。

 

procedure TForm1.IEMessageHandler(var Msg: TMsg; var Handled: Boolean);

const

  StdKeys = [VK_TAB, VK_RETURN]; { 標準鍵 }

  ExtKeys = [VK_DELETE, VK_BACK, VK_LEFT, VK_RIGHT]; { 擴展鍵 }

  fExtended = $01000000; { 擴展鍵標誌 }

begin

  Handled := False;

  with Msg do

    if ((Message >= WM_KEYFIRST) and (Message <= WM_KEYLAST)) and

      ((wParam in StdKeys) or (GetKeyState(VK_CONTROL) < 0) or

      (wParam in ExtKeys) and ((lParam and fExtended) = fExtended)) then

    try

      if IsChild(wbEditor.Handle, hWnd) then

        { 處理所有的瀏覽器相關消息 }

      begin

        with wbEditor.Application as IOleInPlaceActiveObject do

          Handled := TranslateAccelerator(Msg) = S_OK;

        if not Handled then

        begin

          Handled := True;

          TranslateMessage(Msg);

          DispatchMessage(Msg);

        end;

      end;

    except

    end;

end; // IEMessageHandler

 

procedure TForm1.FormCreate(Sender: TObject);

begin

   ……

   Application.OnMessage := IEMessageHandler;

end;

       

工具欄的動態感應

工具欄已經設計完畢,下一個問題是,如何讓工具欄能自動反映編輯器當前選定部分的編輯狀態。也就是說,如果當前文本是粗體居中,那麼粗體和居中按鈕應該處於選中狀態。很顯然,只要能寫出編輯器的OnDisplayChanged事件,一切都將迎刃而解。那麼必須得有一個接口方法,當編輯器狀態發生改變時,在這個方法中調用OnDisplayChanged事件即可。在MSDN的幫助下,W找到了IDocHostUIHandle接口,並鎖定了其中的UpdateUI方法。問題思路已經很清晰:當MSHTML組件的狀態發生改變時,該接口中的UpdateUI方法將被調用。

 

IDocHostUIHandle工作原理:

    MSHTML組件被加載到內存並執行初始化時,MSHTML開始在宿主客戶端查詢一個叫IDocHostUIHandle的接口實現。如果找到這樣的接口實現,MSHTML將在其運行期間,根據需要動態的調用IDocHostUIHandle中的對應方法。

 

    通過對IDocHostUIHandle接口的實現,MSHTML組件將能直接和用戶接口界面(UI)進行通信。這樣,宿主程序將有機會修改用戶界面中的菜單、工具條、以及其它的用戶接口元素。

 

爲了實現UpdateUI方法,W決定從TWebBrowser繼承併產生一個新的組件,新組件將實現對IDocHostUIHandle的封裝,新組件的名字就叫TWebEditor。在UpdateUI的實現中將調用OnDisplayChanged事件句柄——Delphi事件的實現思想實在太妙了。

 

TWebEditor = class(TWebBrowser, IDocHostUIHandle)

    ……

private

    FOnDisplayChanged: TNotifyEvent; //聲明私有事件變量

    function UpdateUI: HRESULT; stdcall;

publish

    property OnDisplayChanged: TNotifyEvent

                read FOnDisplayChanged write FOnDisplayChanged; //聲明屬性事件

end;

 

function TEmbeddedED.UpdateUI: HRESULT;

begin

  //在編輯器狀態改變時,通知宿主程序

  if Assigned(FOnDisplayChanged) then

    FOnDisplayChanged(self);

  Result := S_OK; //表示已經做了處理

end;

 

編譯並安裝TWebEditor組件到Internet面板,然後替換TWebBrowser組件。OK,雙擊OnDisplayChanged事件並編寫代碼吧。

 

自定義上下文菜單

很多時候,上下文菜單在編輯器的使用中可以爲用戶提供更方便的功能調用。但MSHTML編輯器提供的上下文菜單並不是W所希望的。所以,爲了讓自己的編輯器看起來更專業一點,W需要一個自己的上下文菜單。有了上面對IDocHostUIHandle編程的經歷,W很快發現ShowContextMenu接口方法正是自己所需要的。W設計了一個PopupMenu菜單,重命名爲ppMenu,然後在新TWebEditor組件中添加以下代碼:

 

……

//聲明一個顯示上下文事件類型

TShowContextMenuEvent = function(const dwID: DWORD; const ppt: PPOINT;

    const pcmdtReserved: IUnknown; const pdispReserved: IDispatch): HRESULT of object;

 

TWebEditor = class(TWebBrowser, IDocHostUIHandle)

    ……

private

    FOnShowContextMenu: TShowContextMenuEvent; //聲明響應上下文菜單私有事件變量

    ……

    function ShowContextMenu(const dwID: DWORD; const ppt: PPOINT;

              const pcmdtReserved: IUnknown;

              const pdispReserved: IDispatch): HRESULT; stdcall;

publish

    property OnShowContextMenu: TShowContextMenuEvent

              read FOnShowContextmenu write FOnShowContextmenu; //聲明屬性事件

end;

 

function TWebEditor.ShowContextMenu(const dwID: DWORD; const ppt: PPOINT;

  const pcmdtReserved: IUnknown; const pdispReserved: IDispatch): HRESULT;

begin

//MSHTML組件企圖顯示上下文菜單時,通知宿主程序

  if Assigned(FOnShowContextMenu) then

    RESULT := FOnShowContextMenu (dwID, ppt, pcmdtreserved, pdispreserved)

  else

    RESULT := S_FALSE;

end;

 

[提示:組件需要重新編譯並安裝(下同)]

在響應wbEditorOnShowContextMenu事件代碼中,返回S_OK將告訴MSHTML組件你將使用自定義菜單,否則返回S_FALSE

 

function TForm1.wbEditorShowContextMenu(const dwID: Cardinal;

  const ppt: PPoint; const pcmdtReserved: IInterface;

  const pdispReserved: IDispatch): HRESULT;

begin

  ppMenu.Popup(ppt.X, ppt.Y); //顯示自定義菜單

  Result := S_OK; //告訴MSHTML組件將顯示自定義菜單

end;

 

剪貼板中的玄機

OE編輯器支持多來源的粘貼功能給W留下的印象很深刻,所以,WMSHTML編輯器的粘貼能力同樣寄予了厚望。但不管如何,他需要做一些測試以驗證自己的想法。他設想了一些測試來源:網頁、Word文檔、Excel表格、圖片……

網頁的粘貼很順利,幾乎保持原貌;Excel表格也完全正常;圖片的粘貼則完全不被編輯器支持——不過可以利用插入圖片功能來替代——然而,Word的粘貼卻遇到了一些麻煩:如果Word中包含圖片或自定義對象,這部分內容將無法顯示。是Word的問題嗎?W回到OE中,粘貼同樣的內容,顯示正常。看來,問題還在MSHTML編輯器中。

 

通過上下文菜單,W打開源文件查看。Word粘貼過來的HTML代碼中,充斥了大量的XML代碼,圖片和對象則被一些奇怪的XML標籤所包圍,而同樣的內容在OE編輯器中卻顯示爲正常的HTML代碼。怎麼回事?W腦子第一時間蹦出一個很COOL的想法:跟蹤剪貼板。

 

根據剪貼板的原理,在獲取剪貼板內容之前,必須指定要獲取內容的格式。由於剪貼板中的數據可能存在多種格式,所以有必要對剪貼板的格式類型先做一些瞭解。W寫下了以下的測試代碼:

 

procedure TForm1.Button3Click(Sender: TObject);

var

  i: integer;

  Buffer: PChar;

  s: string;

begin

  Memo1.Lines.Clear;  //增加了一個Memo控件來跟蹤數據

  with TClipboard.Create do  //利用TClipboard追蹤剪貼板

  begin

    GetMem(Buffer, 20);

    for i:=0 to FormatCount - 1 do

    begin

      GetClipboardFormatName(Formats[i], Buffer, 20);

      s := StrPas(Buffer);

      Memo1.Lines.Add(Format('%s:%d', [s, Formats[i]]));

    end;

FreeMem(Buffer);

Free;

  end;

end;

 

點擊Button3,在Memo1文本框中顯示出以下的內容:

DataObject:49161

Object Descriptor:49166

Rich Text Format:49312

HTML Format:49394

HTML Format:14

HTML Format:3

PNG:49672

GIF:49536

JFIF:49538

……

 

很明顯,第4行的“HTML Format:49394應該就是HTML編輯器真正需要的格式。由於“HTML Format”並不是剪貼板默認支持的格式,所以W需要使用API函數RegisterClipboardFormat先進行註冊。

 

procedure TForm1.Button4Click(Sender: TObject);

var

  s: string;

  hMem: DWORD;

  CF_HTML: DWORD; // 聲明一個CF_HTML剪貼板格式

  txtPtr: PChar;

begin

  CF_HTML := RegisterClipboardFormat('HTML Format');  //註冊HTML Format格式

  with TClipboard.Create do

  begin

    hMem := GetAsHandle(CF_HTML);

    txtPtr := GlobalLock(hMem);

    s := StrPas(txtPtr);

    GlobalUnlock(hMem);

 

Memo1.Lines.Add(s);

Free;

  end;

end;

 

這回終於水落石出。在Memo1中,W見到了Word文檔被拷貝以後在剪貼板中以HTML格式存在的真實內容:

 

Version:1.0

StartHTML:0000000105

EndHTML:0000005323

StartFragment:0000003873

EndFragment:0000005283

 

<html xmlns:v="urn:schemas-microsoft-com:vml"

xmlns:o="urn:schemas-microsoft-com:office:office"

xmlns:w="urn:schemas-microsoft-com:office:word"

xmlns="http://www.w3.org/TR/REC-html40">

……

<body ……>

<!--StartFragment--><span ……><!--[if gte vml 1]>

<v:shapetype ……>

 <v:imagedata src="file:///C:/DOCUME~1/tttk/LOCALS~1/Temp/msohtml1/01/clip_image001.gif"

  o:title="el2"/>……

</v:shape><![endif]--><![if !vml]><img width=128 height=128

src="file:///C:/DOCUME~1/tttk/LOCALS~1/Temp/msohtml1/01/clip_image001.gif"

v:shapes="_x0000_i1025"><![endif]></span><!--EndFragment-->

……

(爲了節省篇幅,作者刪去了大量無用的信息)

 

W欣喜的發現,HTML中熟悉的<img>標籤出現在剪貼板中,雖然在最後的粘貼結果中沒有看到img元素,那也一定是裏面的<![if]>語句搗的鬼。現在只要把裏面的HTML部分提取出來不就行了嗎?通過規則表達式的幫助,一切都輕鬆搞定!

 

procedure TForm1.FilterData(var S: string);

var

  isOffice: Boolean;

begin 

  with TRegExpr.Create do

  begin

    isOffice := ExecRegExpr('(?i)xmlns:o="urn:schemas-microsoft-com:office:office"', S);

 

    Expression := '(?i)<!--StartFragment-->(.*)<!--EndFragment-->';

    if Exec(S) then S := Match[1];

 

    if isOffice then //trip office document

    begin

      S := ReplaceRegExpr('(?i)<!--[^>]+?>.+?<[^>]+?-->', S, '');

      S := ReplaceRegExpr('(?i)<[^/]|>]*/[[if|endif][^>]+>', S, '');

      S := ReplaceRegExpr('(?i)</?[v|o|w]:[^>]+>', S, '');

      S := ReplaceRegExpr('(?i)[/r|/n]{2,}', S, ' ');

    end;

  end;

 

  S := UTF8Decode(S);

end;

 

注:TRegExpr類是第三方開源的規則表達式類,感興趣的讀者請到http://regexpstudio.com/下載試用。

 

如何更新修改後的數據?

W沒高興多久。因爲他發現,雖然掌握了剪貼板的實際內容,但他依然不知道如何把修改後的數據更新到編輯器中。因爲編輯器沒有直接的粘貼方法可以調用,W首先想到的是把修改後的數據重新賦值給剪貼板。但如果用戶希望多次粘貼,這個方法將顯然行不通。如果有這樣一個粘貼事件,在這個粘貼事件中提供InOut兩種類型的參數,問題不就解決了嗎?那麼如何實現這個粘貼事件呢?答案依然在IDocHostUIHandle接口中,W找到了FilterDataObject方法,該方法恰好有兩個InOut類型的參數:

 

HRESULT FilterDataObject(
    IDataObject *pDO,

    IDataObject **ppDORet

);

 

當粘貼操作發生時,MSHTML組件將調用IDocHostUIHandle接口的FilterDataObject方法,並把內存中的數據對象通過pDO參數傳入,如果該函數返回S_OK並且ppDORet參數不爲NULLMSHTML組件將嘗試從ppDORet接口讀出修改後的數據。根據這個想法,W很快的實現了下面的OnPaste事件:

 

//聲明粘貼事件類型

TPasteEvent = function(const pDO: IDataObject;

                        var ppDORet: IDataObject): HRESULT of object;

TWebEditor = class(TWebBrowser, IDocHostUIHandle, IOleCommandTarget, …)

    ……

private

    ……

    FOnPaste: TPasteEvent;

    function FilterDataObject(const pDO: IDataObject;

                              var ppDORet: IDataObject): HRESULT; stdcall;

    ……

publish

//聲明粘貼事件屬性

    property OnPaste: TPasteEvent read FOnPaste write FOnPaste;

    ……

end;

 

implementation

 

function TWebEditor.FilterDataObject(const pDO: IDataObject;

                                   out ppDORet: IDataObject): HRESULT;

begin

  { 如果數據對象被替換,返回 S_OK,否則返回S_FALSE

    雖然文檔沒有明顯指出, 這個方法只能用在處理粘貼事件}

 

  if Assigned(FOnPaste) then

    Result := FOnPaste(pDO, ppDORet)

  else

    Result := E_NOTIMPL;

end;

 

在處理OnPaste事件時,W遇到了新的麻煩。他需要知道兩件事,一是如何從pDO參數取得數據;第二是如何把修改後的數據送給ppDORet參數。由於pDOppDORet兩個都是IDataObject接口類型的參數,那麼有必要先考察一下IDataObject接口的用法。

 

IDataObject接口:

    OLE對象的數據操作方法中,IDataObject接口在傳輸和轉換數據的過程中起到了關鍵的作用。OLE對象通過調用IDataObject接口對象的相關方法保存需要操作的數據格式、存儲媒介等信息。

    在傳送和接收數據前,OLE對象會根據需要分別填充FORMATETCSTGMEDIUM結構中的相關字段。傳送數據時,OLE對象通過多次調用SetData方法來設置要傳輸數據的多種格式。相反,在獲取數據時,OLE對象將先調用QueryGetData方法來查詢是否存在指定格式,然後通過GetData方法來取得數據。

 

    IDataObject接口中,FORMATETCSTGMEDIUM這兩個結構類型尤爲重要,它們在Delphi中的定義如下:

  tagFORMATETC = record

    cfFormat: TClipFormat;

    ptd: PDVTargetDevice;

    dwAspect: Longint;

    lindex: Longint;

    tymed: Longint;

  end;

  TFormatEtc = tagFORMATETC;

  FORMATETC = TFormatEtc;

 

  tagSTGMEDIUM = record

    tymed: Longint;

    case Integer of

      0: (hBitmap: HBitmap; unkForRelease: Pointer{IUnknown});

      1: (hMetaFilePict: THandle);

      2: (hEnhMetaFile: THandle);

      3: (hGlobal: HGlobal);

      4: (lpszFileName: POleStr);

      5: (stm: Pointer{IStream});

      6: (stg: Pointer{IStorage});

  end;

  TStgMedium = tagSTGMEDIUM;

  STGMEDIUM = TStgMedium;

   

 

瞭解了IDataObject接口的原理之後,接下來的問題變得很清晰。很顯然,要實現對數據的過濾,必須要把修改後的數據通過一個IDataObject類型的對象傳遞給ppDORet參數。因爲在Delphi中並沒有這樣一個實現了IDataObject接口的類可供使用,所以這個類必須自己來實現。根據IDataObject接口的工作原理,這個類中只需要實現IDataObject接口中的QueryGetDataGetData兩個方法即可,其它暫時不需要的方法設置成抽象(abstract)即可。

 

W的設計思路如下:在這個類的構造方法中讀入pDO參數對象並保存在私有變量FDataSource中。在QueryGetData方法的實現中只簡單的返回S_OK,表示支持任何的格式;最後在GetData方法中先取得FDataSource的數據,並在實現過濾後把數據保存到方法的mediumout類型)參數中。這樣,當MSHTML調用ppDORet參數並調用其GetData方法時,將能夠從medium參數中取得想要的數據。

 

type

  TDataObject = class(TInterfacedObject, IDataObject)

  private

    FDataSource: IDataObject;

    function GetGlobalData(dataFormat: DWORD): string;

  public

constructor Create(pDO: IDataObject);

{ 接口未實現部分全部聲明爲abstract即可 }

function DAdvise(const formatetc: TFormatEtc; advf: Longint;

      const advSink: IAdviseSink;

      out dwConnection: Longint): HResult; virtual; stdcall; abstract;

function DUnadvise(dwConnection: Longint): HResult; virtual; stdcall; abstract;

function EnumDAdvise(out enumAdvise: IEnumStatData): HResult;

      virtual; stdcall; abstract;

function EnumFormatEtc(dwDirection: Longint; out enumFormatEtc:

      IEnumFormatEtc): HResult; virtual; stdcall; abstract;

function GetCanonicalFormatEtc(const formatetc: TFormatEtc;

      out formatetcOut: TFormatEtc): HResult; virtual; stdcall; abstract;

function GetDataHere(const formatetc: TFormatEtc; out medium: TStgMedium):

      HResult; virtual; stdcall; abstract;

function GetObjectDescriptor: HGlobal; virtual; stdcall; abstract;

function SetData(const formatetc: TFormatEtc; var medium: TStgMedium;

      fRelease: BOOL): HResult; virtual; stdcall; abstract;

{ 需要實現的部分 }

function GetData(const formatetcIn: TFormatEtc; out medium: TStgMedium):

      HResult; stdcall;

function IsDataAvailable(dataFormat: DWORD): Boolean;

function QueryGetData(const formatetc: TFormatEtc): HResult; virtual; stdcall;

  end;

 

implementation

……

constructor TDataObject.Create(pDO: IDataObject);

begin

  //在初始化過程中保存數據源

  FDataSource := pDO;

end;

 

function TDataObject.GetData(const formatetcIn: TFormatEtc; out medium: TStgMedium): HResult;

var

  s: string;

  hMem: DWORD;

  txtPtr: PChar;

begin

  //只處理CF_HTML格式

  if formatetcIn.cfFormat = CF_HTML then

  begin

    s := GetGlobalData(formatetcIn.cfFormat);

    FilterData(s); //過濾數據

    hMem := GlobalAlloc(GMEM_MOVEABLE, Length(s)); //分配全局內存

    txtPtr := GlobalLock(hMem);

    Move(PChar(s)^, txtPtr^, Length(s));

    GlobalUnlock(hMem);

 

    with medium do

    begin

      tymed := TYMED_HGLOBAL;  //指定要存儲數據的存儲格式爲全局內存

      hGlobal := hMem;

      unkForRelease := nil;  //指定由調用者負責釋放內存

    end;

  end;

 

  Result := S_OK; 

end;

 

//通過此函數取得全局內存中的數據

function TDataObject.GetGlobalData(dataFormat: DWORD): string;

var

  stgMedium: TStgMedium;

  formatEtc: TFormatEtc;

  txtPtr: PChar;

begin

  with formatEtc do

  begin

    cfFormat := dataFormat;  //設置數據格式

    ptd := nil;

    dwAspect := DVASPECT_CONTENT;  //指定數據類型爲CONTENT

    lindex := -1;

    tymed := TYMED_HGLOBAL;  //指定要獲取數據的存儲格式爲全局內存

  end;

 

  //調用源數據的QueryGetData方法來查詢指定格式是否存在

  if FDataSource.QueryGetData(formatEtc) <> S_OK then Exit;

 

  //調用源數據的GetData方法取得數據,並存放在stgMedium結構中

  OleCheck(FDataSource.GetData(formatEtc, stgMedium));

  with stgMedium do

  begin

    txtPtr := GlobalLock(hGlobal); //hGlobal全局句柄中獲取數據

    Result := StrPas(txtPtr);

    GlobalUnlock(hGlobal);

 

    if unkForRelease = nil then

      ReleaseStgMedium(stgMedium); //調用者負責釋放內存

  end;

end;

 

function TDataObject.IsDataAvailable(dataFormat: DWORD): Boolean;

begin

  //MFC文檔:儘管可以通過調用IDataObjectEnumFormatEtc方法來枚舉格式類型,

  //但通過剪貼板查詢的效率更高,也更有效

  Result := IsClipboardFormatAvailable(dataFormat)

end;

 

function TDataObject.QueryGetData(const formatetc: TFormatEtc): HResult;

begin

  //在這裏可以定製哪些類型的數據將被過濾

  Result := S_OK;

end;

 

回到主窗口,重新編譯TWebEditor組件並安裝,然後給wbEditorOnPaste事件添加如下代碼,一切就大功告成了。

 

function TForm1.wbEditorPaste(const pDO: IDataObject;

                              var ppDORet: IDataObject): HRESULT;

begin

  ppDoRet := TDataObject.Create(pDO);

  Result := S_OK;

end;

 

感謝

在編輯器的實現過程中,作者參考了網絡上很多技術文章和網友的智慧,由於行文倉促,沒有記住來源,無法在此一一列出,僅此向他們表示深深的謝意。希望此文能起到畫龍點睛的作用,給更多的程序員新手們提供學習的捷徑。(2005年新年 全文完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章