揭開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年新年 全文完)

 
揭開NTFS下流的奧秘
  NTFS下,支持一個非凡概念,那就是'流'.怎麽個流法呢?先看'流'的定義: stream A sequence of bits, bytes, or other small strUCturally uniform units. B99v的序列,或者小的統一結構單元.當然,小的統一結構單元並...查看完整版>>揭開NTFS下流的奧秘
 
亮金:爲您揭開搜索引擎的奧秘
亮金:爲您揭開搜索引擎的奧秘
  跟A5結緣是從來到公司的第二星期,以前是不知道有那麽好的一個網站的,這是同事跟我介紹的,說A5是一個可以提升我自己知識水平的一個網站,就這樣我成爲A5的忠實“粉絲”,的確A5也給了我很多的幫助,...查看完整版>>亮金:爲您揭開搜索引擎的奧秘
 
番茄爲何有酸有甜?植物代謝奧秘終于被揭開
人生存所需的能量、營養是通過“代謝”獲取的;一旦某種“代謝途徑”出了問題,健康就會受到影響。同樣,由“代謝産物”決定的蔬菜、水果的營養、色澤、口感、抗病性等不同品質也是這樣。同樣是西紅柿,有的皮厚耐貯...查看完整版>>番茄爲何有酸有甜?植物代謝奧秘終于被揭開
 
《科學》雜志揭開昆蟲呼吸的奧秘 效率比人還高
蟋蟀、螞蟻、蝴蝶、蜻蜓以及蟑螂都沒有肺,那它們能呼吸嗎?美國科學家研究發現,這些昆蟲不僅具備呼吸能力,而且呼吸效率可能比人還高。   美國菲爾德博物館的韋斯特尼特認爲,發現昆蟲的呼吸機制,不僅有可能會“...查看完整版>>《科學》雜志揭開昆蟲呼吸的奧秘 效率比人還高
 
揭開毒蛙毒性奧秘:毒素可能來自被捕食動物 動物世界
揭開毒蛙毒性奧秘:毒素可能來自被捕食動物 動物世界
  馬達加斯加和美洲的小毒蛙將有毒的化合物作爲它們防身的化學武器,例如一些生物堿。一項新的研究發現,盡管這兩種生活在不同地區的毒蛙親緣關系很遠,但是它們都能夠從螞蟻和其他節肢動物那裏獲得同一種生物堿,...查看完整版>>揭開毒蛙毒性奧秘:毒素可能來自被捕食動物 動物世界