揭開Outlook Express編輯器的奧祕

揭開Outlook Express編輯器的奧祕

  【前言】
  Outlook Express是一款大家比較熟悉的郵件工具,其HTML編輯器一直是衆多程序員競相模仿的目標。作者最近在一個項目的開發中,開始接觸HTML編輯器的設計,並遇到了很多的難題。目前網絡上關於IE編程的文章中,涉及MSHTML編輯器的部分,又大多集中在VC領域,用Delphi作爲解決方案的少之又少。在經過一番艱難的摸索之後,作者積累了一些成功的經驗。並撰成此文,希望與大家共同探討。
  注:本文將涉及到COM編程,由於COM的複雜性,不免會有晦澀難懂之嫌。爲了讓閱讀不至於成爲一種折磨,作者將嘗試另一種寫作模式。文章將隨着一個叫做W的程序員的編程思路展開,以通俗易懂的敘事方式帶領讀者一起探討在MSHTML編輯器的開發過程中可能遇到的一些棘手問題。對於某些需要強調的關鍵術語,文中將適時的給出註解,以便讀者更好的領會。
  【學習目標】
  通過本文的閱讀,讀者將可以學習到以下內容:
  l 掌握TWebBrowser控件的用法;
  l 理解IHTMLDocument2和IDocHostUIHandle接口;
  l 探討在MSHTML中如何加載字符流;
  l 找回在MSHTML編輯器中丟失的回車鍵;
  l 實現工具欄的自動感應;
  l 自定義MSHTML編輯器強大的粘貼功能;
  本文假定讀者已經具備初步的COM知識和Delphi接口的編程經驗,如果您需要對COM和接口知識作進一步的深入瞭解,請參考其它相關文章。
  【關鍵字】
  TWebBrowser、MSHTML、自動化對象、IHTMLDocument2、IDocHostUIHandler、
  FilterDataObject、IDataObject、剪貼板、粘貼
  【正文】
  一個笑話的啓示
   在一次程序員大會上,主持人爲了活躍氣氛,做了個小遊戲。他問臺下的程序員:如果有誰在小的時候拆過鬧鐘的請舉手。臺下的程序員們全都舉起了手。主持人又問:那麼又有誰後來把鬧鐘裝回去的請舉手。舉起手來的程序員們又都把手放下了。
   這個笑話從側面說明了一個問題,追根求源正是大多數程序員的天性。缺少追根求源的精神,軟件設計就會缺少創新的動力。也正是由於有了追根求源的精神,越來越多的軟件新手跨越了初期的彷徨,走上了軟件高手的道路。
  未來的一天,程序員W所在的軟件公司接到一個信息管理系統的設計項目。由於最近W剛參加完公司組織的爲期一週的COM培訓,於是項目經理Y便把項目中最具挑戰的編輯器部分交給他來完成。用戶要求實現一個類似Outlook Express(以下簡稱OE)那樣所見即所得式的編輯器,並可以支持多種來源的粘貼操作。儘管W此前對OE編輯器的原理一無所知,但他還是面帶微笑並充滿自信的接受了挑戰。
  揭開OE編輯器的面紗
  在以往的使用過程中,W發現OE編輯器確實是一款強大的編輯工具。無論是編輯還是粘貼,OE編輯器都能完美的實現所見即所得的效果。OE編輯器本質上是一款HTML編輯器,其中的數據和格式都以HTML代碼的形式來保存。W以前曾研究過網頁上的HTML編輯器,該編輯器是通過DHTML技術來實現的。那麼,OE編輯器和IE瀏覽器之間是否有什麼關係呢?
  爲了搞清楚這個問題,W調出VC的Spy++探個究竟。拖動Spy++那個神奇的雷達指向OE編輯窗口,Spy++迅速的找到了窗口的類型:“Internet Explorer_Server”,這是個IE服務器窗口類型,這究竟是什麼意思呢?
   微軟的IE瀏覽器的核心部分是SHDOCVW.DLL和MSHTML.DLL。從下面的Internet Explorer的架構圖可以看到,IE其實只是一個外殼程序,真正的瀏覽網頁、記錄歷史等工作都是由封裝在SHDOCVW.DLL中的WebBrowser Control來完成的。而HTML的解析、腳本引擎、java虛擬機、插件宿主等,則由SHDOCVW.DLL通過調用MSHTML.DLL來完成。通過SHDOCVW.DLL提供的豐富接口,網頁中的元素可以訪問外殼應用程序提供的屬性和方法;而通過MSHTML.DLL提供的接口,外殼應用程序則反過來可以訪問網頁中元素的屬性、方法、行爲、事件等等。
  
  毫無疑問,OE編輯器正是通過對WebBrowser控件和MSHTML的封裝實現了HTML的編輯功能。由於WebBrowser屬於ActiveX控件,所以,利用Delphi的ActiveX導入嚮導,可以輕鬆的實現對WebBrowser控件的封裝。導入後將在Delphi的Import文件夾下自動生成兩個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:(似懂非懂,不過想想反正以後還可以再學)嗯,知道了。然後呢?
  D:OK。由於我們並不需要自動化機制,IDispatch接口對我們來說用處不大。但我們可以利用它通過Delphi中的as運算符查詢到其它我們想要的接口。例如,IHTMLDocument2接口在編程中用的比較多,用它可以實現大多數的DHTML功能。
  W:哦~IHTMLDocument2接口(自言自語)。那如何進入編輯狀態呢?
  D:答案就在這個IHTMLDocument2接口中。這個接口中有一個disignMode屬性,在運行時置爲“On”就可以從瀏覽模式轉變爲編輯模式了。當然了,前提是必須保證Document不能爲空。有個簡單的辦法,在初始化時,通過TWebBrowser的Navigate方法導航到一個空白頁面,有一個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的幫助下,W對TWebBrowser的用法有了一個初步的瞭解。很顯然,接口在TWebBrowser的編程中至關重要。此時,爲了加深對接口的瞭解,W決定對IHTMLDocument2接口做一個深入的瞭解。
  小知識:
   在執行TWebBrowser的某個方法以進行某些期望的操作如ExecWB等時候,可能會碰到如“試圖激活未註冊的丟失目標”或“OLE對象未註冊”等錯誤提示,或者並沒有任何出錯信息但卻得不到希望的結果。這是因爲TWebBrowser本身是一個OLE類型的COM組件,你需要在使用TWebBrowser前對OLE進行一些初始化工作,這個工作可以放到單元的initialization和finalization段中來完成。
  {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編輯器竟然經常不響應回車事件。也就是說,當在編輯器中按下回車時不能產生一個換行——編輯器面對回車按鍵毫無反應,就好像回車鍵被它吃掉一樣。類似的,象TAB、Delete、BACKSPACE、→、←等快捷鍵上也會出現這種情況。這真是一件令人匪夷所思的事。
  從本質上說,TWebBrowser是一個特殊的OLE控件。雖然Delphi在封裝過程中使它繼承了TWinControl,但它似乎並沒有由此取得TWinControl的自動獲得焦點的能力。看來,極有可能是由於Delphi的VCL消息處理機制同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;
  [提示:組件需要重新編譯並安裝(下同)]
  在響應wbEditor的OnShowContextMenu事件代碼中,返回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留下的印象很深刻,所以,W對MSHTML編輯器的粘貼能力同樣寄予了厚望。但不管如何,他需要做一些測試以驗證自己的想法。他設想了一些測試來源:網頁、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首先想到的是把修改後的數據重新賦值給剪貼板。但如果用戶希望多次粘貼,這個方法將顯然行不通。如果有這樣一個粘貼事件,在這個粘貼事件中提供In和Out兩種類型的參數,問題不就解決了嗎?那麼如何實現這個粘貼事件呢?答案依然在IDocHostUIHandle接口中,W找到了FilterDataObject方法,該方法恰好有兩個In和Out類型的參數:
  HRESULT FilterDataObject(
   IDataObject *pDO,
   IDataObject **ppDORet
  );
  當粘貼操作發生時,MSHTML組件將調用IDocHostUIHandle接口的FilterDataObject方法,並把內存中的數據對象通過pDO參數傳入,如果該函數返回S_OK並且ppDORet參數不爲NULL,MSHTML組件將嘗試從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參數。由於pDO和ppDORet兩個都是IDataObject接口類型的參數,那麼有必要先考察一下IDataObject接口的用法。
  IDataObject接口:
   在OLE對象的數據操作方法中,IDataObject接口在傳輸和轉換數據的過程中起到了關鍵的作用。OLE對象通過調用IDataObject接口對象的相關方法保存需要操作的數據格式、存儲媒介等信息。
   在傳送和接收數據前,OLE對象會根據需要分別填充FORMATETC和STGMEDIUM結構中的相關字段。傳送數據時,OLE對象通過多次調用SetData方法來設置要傳輸數據的多種格式。相反,在獲取數據時,OLE對象將先調用QueryGetData方法來查詢是否存在指定格式,然後通過GetData方法來取得數據。
   在IDataObject接口中,FORMATETC和STGMEDIUM這兩個結構類型尤爲重要,它們在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接口中的QueryGetData、GetData兩個方法即可,其它暫時不需要的方法設置成抽象(abstract)即可。
  W的設計思路如下:在這個類的構造方法中讀入pDO參數對象並保存在私有變量FDataSource中。在QueryGetData方法的實現中只簡單的返回S_OK,表示支持任何的格式;最後在GetData方法中先取得FDataSource的數據,並在實現過濾後把數據保存到方法的medium(out類型)參數中。這樣,當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文檔:儘管可以通過調用IDataObject的EnumFormatEtc方法來枚舉格式類型,
   //但通過剪貼板查詢的效率更高,也更有效
   Result := IsClipboardFormatAvailable(dataFormat)
  end;
  function TDataObject.QueryGetData(const formatetc: TFormatEtc): HResult;
  begin
   //在這裏可以定製哪些類型的數據將被過濾
   Result := S_OK;
  end;
  回到主窗口,重新編譯TWebEditor組件並安裝,然後給wbEditor的OnPaste事件添加如下代碼,一切就大功告成了。
  function TForm1.wbEditorPaste(const pDO: IDataObject;
   var ppDORet: IDataObject): HRESULT;
  begin
   ppDoRet := TDataObject.Create(pDO);
   Result := S_OK;
  end;
  感謝
  在編輯器的實現過程中,作者參考了網絡上很多技術文章和網友的智慧,由於行文倉促,沒有記住來源,無法在此一一列出,僅此向他們表示深深的謝意。希望此文能起到畫龍點睛的作用,給更多的程序員新手們提供學習的捷徑。(2005年新年 全文完)

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