检测可编辑的 TWebBrowser 中的变化
Detecting changes in an editable TWebBrowser
我正在将 HTML 本地文件加载到 TWebBrowser
,如下所示:
procedure TForm1.FormCreate(Sender: TObject);
begin
WebBrowser1.Navigate('file:///C:\Tmp\input.html');
end;
在 TWebBrowser.OnDocumentComplete
事件处理程序中,我将其设置为可编辑:
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
(WebBrowser1.Document as IHTMLDocument2).designMode := 'on';
end;
我需要在用户通过 TWebBrowser
应用任何更改时立即得到通知(即:他写了一些东西...)但我看不到任何 OnChanged
或类似事件处理程序。
我已经尝试捕获 WM_PASTE
和 WM_KEYDOWN
但我的代码从未执行过:
TMyWebBrowser = class(TWebBrowser)
public
procedure WM_Paste(var Message: TWMPaste); message WM_PASTE;
procedure WM_KeyDown(var Message: TWMKeyDown); message WM_KEYDOWN;
end;
...
procedure TMyWebBrowser.WM_Paste(var Message: TWMPaste);
begin
inherited;
ShowMessage('Paste');
end;
procedure TMyWebBrowser.WM_KEYDOWN(var Message: TWMKeyDown);
begin
inherited;
ShowMessage('KeyDown');
end;
我也试过设置 WindowProc
属性 但没有成功。
要在设计模式下捕获对文档的更改,您应该使用其 IMarkupContainer2
interface to register an IHTMLChangeSink
via RegisterForDirtyRange
方法。这个过程非常简单 - 实现 IHTMLChangeSink
,从 WebBrowser1.Document
获取 IMarkupContainer2
并调用它的 RegisterForDirtyRange
方法,但是有一个问题。
当您更改 IHTMLDocument2
的 designMode
时,TWebBrowser
控件会重新加载当前文档并丢失所有已注册的更改接收器。因此,您应该在将文档置于设计模式后进行注册。之后,您会通过 IHTMLChangeSink.Notify
方法收到更改通知。
但是还有一个问题。由于进入设计模式会导致重新加载文档,进而导致将文档的 readyState
属性 更改为 'loading'
,然后连续更改为 'complete'
。您的更改接收器将收到那些 readyState
更改通知。请注意 TWebBrowser.OnDocumentComplete
不会在进入设计模式后被调用 。这就是为什么您应该忽略所有通知,直到文档在设计模式下完全重新加载。
另一个小问题是 RegisterForDirtyRange
创建了一个 cookie,您需要维护该 cookie 才能注销更改接收器。既然你需要一个class来实现IHTMLChangeSink
,它也可以封装设计模式状态和更改注册。
uses
System.SysUtils, SHDocVw, MSHTML;
const
DesignMode: array[Boolean] of string = ('off', 'on');
type
TWebBrowserDesign = class(TInterfacedObject, IHTMLChangeSink)
private
FDirtyRangeCookie: LongWord;
FDocumentComplete: Boolean;
FHTMLDocument2: IHTMLDocument2;
FMarkupContainer2: IMarkupContainer2;
FOnChange: TProc;
{ IHTMLChangeSink }
function Notify: HResult; stdcall;
public
constructor Create(WebBrowser: TWebBrowser; const AOnChange: TProc);
destructor Destroy; override;
end;
constructor TWebBrowserDesign.Create(WebBrowser: TWebBrowser; const AOnChange: TProc);
begin
inherited Create;
if not Assigned(WebBrowser) then
raise Exception.Create('Web browser control missing.');
if not Supports(WebBrowser.Document, IHTMLDocument2, FHTMLDocument2) then
raise Exception.Create('No HTML document loaded.');
FHTMLDocument2.designMode := DesignMode[True];
if Supports(WebBrowser.Document, IMarkupContainer2, FMarkupContainer2) then
begin
if FMarkupContainer2.RegisterForDirtyRange(Self, FDirtyRangeCookie) <> S_OK then
FDirtyRangeCookie := 0
else
_Release;
end;
FOnChange := AOnChange;
end;
destructor TWebBrowserDesign.Destroy;
begin
if Assigned(FMarkupContainer2) and (FDirtyRangeCookie <> 0) then
FMarkupContainer2.UnRegisterForDirtyRange(FDirtyRangeCookie);
if Assigned(FHTMLDocument2) then
FHTMLDocument2.designMode := DesignMode[False];
inherited;
end;
function TWebBrowserDesign.Notify: HResult;
begin
Result := S_OK;
if not FDocumentComplete then
FDocumentComplete := FHTMLDocument2.readyState = 'complete'
else if Assigned(FOnChange) then
FOnChange();
end;
注意注册更改接收器后对 _Release
的调用。这是为了 "prevent" 标记容器持有对 TWebBrowserDesign
实例的强引用。这允许您使用 TWebBrowserDesign
实例的生命周期来控制设计模式:
type
TForm1 = class(TForm)
{ ... }
private
FWebBrowserDesign: IInterface;
{ ... }
end;
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
{ enter design mode }
FWebBrowserDesign := TWebBrowserDesign.Create(WebBrowser1, procedure
begin
ButtonSave.Enabled := True;
end);
end;
procedure TForm1.ButtonSave(Sender: TObject);
begin
{ exit design mode }
FWebBrowserDesign := nil;
ButtonSave.Enabled := False;
end;
或者,您可以将更改接收器实现为一个组件。
type
TWebBrowserDesign = class(TComponent, IHTMLChangeSink)
private
FDirtyRangeCookie: LongWord;
FDocumentComplete: Boolean;
FHTMLDocument2: IHTMLDocument2;
FMarkupContainer2: IMarkupContainer2;
FOnChange: TNotifyEvent;
FWebBrowser: TWebBrowser;
procedure EnterDesignMode;
procedure ExitDesignMode;
function GetActive: Boolean;
procedure SetActive(const Value: Boolean);
procedure SetWebBrowser(const Value: TWebBrowser);
{ IHTMLChangeSink }
function Notify: HResult; stdcall;
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
destructor Destroy; override;
published
property Active: Boolean read GetActive write SetActive;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
property WebBrowser: TWebBrowser read FWebBrowser write SetWebBrowser;
end;
destructor TWebBrowserDesign.Destroy;
begin
ExitDesignMode;
inherited;
end;
procedure TWebBrowserDesign.EnterDesignMode;
begin
if not Assigned(FWebBrowser) then
raise Exception.Create('Web browser control missing.');
if not Supports(FWebBrowser.Document, IHTMLDocument2, FHTMLDocument2) then
raise Exception.Create('No HTML document loaded.');
try
FHTMLDocument2.designMode := DesignMode[True];
if Supports(FWebBrowser.Document, IMarkupContainer2, FMarkupContainer2) then
begin
if FMarkupContainer2.RegisterForDirtyRange(Self, FDirtyRangeCookie) <> S_OK then
FDirtyRangeCookie := 0;
end;
except
ExitDesignMode;
raise;
end;
end;
procedure TWebBrowserDesign.ExitDesignMode;
begin
if Assigned(FMarkupContainer2) then
begin
if FDirtyRangeCookie <> 0 then
begin
FMarkupContainer2.UnRegisterForDirtyRange(FDirtyRangeCookie);
FDirtyRangeCookie := 0;
end;
FMarkupContainer2 := nil;
end;
if Assigned(FHTMLDocument2) then
begin
FHTMLDocument2.designMode := DesignMode[False];
if not (csDestroying in ComponentState) then
FHTMLDocument2 := nil; { causes AV when its hosting TWebBrowser component is destroying; I didn't dig into details }
end;
FDocumentComplete := False;
end;
function TWebBrowserDesign.GetActive: Boolean;
begin
Result := Assigned(FHTMLDocument2);
end;
procedure TWebBrowserDesign.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if (Operation = opRemove) and (AComponent = FWebBrowser) then
WebBrowser := nil;
end;
function TWebBrowserDesign.Notify: HResult;
begin
Result := S_OK;
if not FDocumentComplete then
FDocumentComplete := FHTMLDocument2.readyState = 'complete'
else if Assigned(FOnChange) then
FOnChange(Self);
end;
procedure TWebBrowserDesign.SetActive(const Value: Boolean);
begin
if Active <> Value then
begin
if Value then
EnterDesignMode
else
ExitDesignMode;
end;
end;
procedure TWebBrowserDesign.SetWebBrowser(const Value: TWebBrowser);
begin
if Assigned(FWebBrowser) then
begin
ExitDesignMode;
FWebBrowser.RemoveFreeNotification(Self);
end;
FWebBrowser := Value;
if Assigned(FWebBrowser) then
FWebBrowser.FreeNotification(Self);
end;
如果您将这样的组件放在设计时包中并在 IDE 中注册它,那么您将能够 link 使用 TWebBrowser
这个组件并分配OnChange
表单设计器中的事件处理程序。在代码中使用 Active
属性 到 enter/exit 设计模式。
type
TForm1 = class(TForm)
{ ... }
WebBrowserDesign1: TWebBrowserDesign;
{ ... }
end;
procedure WebBrowserDesign1Change(Sender: TObject);
begin
ButtonSave.Enabled := True;
end;
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
{ enter design mode }
WebBrowserDesign1.Active := True;
end;
procedure TForm1.ButtonSave(Sender: TObject);
begin
{ exit design mode }
WebBrowserDesign1.Active := False;
ButtonSave.Enabled := False;
end;
注意:关于 C#/WinForms 已提出类似问题 - How do I detect when the content of a WebBrowser control has changed (in design mode)?
最后说明:我不相信在更改后启用保存按钮是最好的用户体验设计。如果您认为上面的代码值得实现您的目标,那么请继续。这只是概念验证,代码尚未经过全面测试。需要您自担风险使用它。
我正在将 HTML 本地文件加载到 TWebBrowser
,如下所示:
procedure TForm1.FormCreate(Sender: TObject);
begin
WebBrowser1.Navigate('file:///C:\Tmp\input.html');
end;
在 TWebBrowser.OnDocumentComplete
事件处理程序中,我将其设置为可编辑:
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
(WebBrowser1.Document as IHTMLDocument2).designMode := 'on';
end;
我需要在用户通过 TWebBrowser
应用任何更改时立即得到通知(即:他写了一些东西...)但我看不到任何 OnChanged
或类似事件处理程序。
我已经尝试捕获 WM_PASTE
和 WM_KEYDOWN
但我的代码从未执行过:
TMyWebBrowser = class(TWebBrowser)
public
procedure WM_Paste(var Message: TWMPaste); message WM_PASTE;
procedure WM_KeyDown(var Message: TWMKeyDown); message WM_KEYDOWN;
end;
...
procedure TMyWebBrowser.WM_Paste(var Message: TWMPaste);
begin
inherited;
ShowMessage('Paste');
end;
procedure TMyWebBrowser.WM_KEYDOWN(var Message: TWMKeyDown);
begin
inherited;
ShowMessage('KeyDown');
end;
我也试过设置 WindowProc
属性 但没有成功。
要在设计模式下捕获对文档的更改,您应该使用其 IMarkupContainer2
interface to register an IHTMLChangeSink
via RegisterForDirtyRange
方法。这个过程非常简单 - 实现 IHTMLChangeSink
,从 WebBrowser1.Document
获取 IMarkupContainer2
并调用它的 RegisterForDirtyRange
方法,但是有一个问题。
当您更改 IHTMLDocument2
的 designMode
时,TWebBrowser
控件会重新加载当前文档并丢失所有已注册的更改接收器。因此,您应该在将文档置于设计模式后进行注册。之后,您会通过 IHTMLChangeSink.Notify
方法收到更改通知。
但是还有一个问题。由于进入设计模式会导致重新加载文档,进而导致将文档的 readyState
属性 更改为 'loading'
,然后连续更改为 'complete'
。您的更改接收器将收到那些 readyState
更改通知。请注意 TWebBrowser.OnDocumentComplete
不会在进入设计模式后被调用 。这就是为什么您应该忽略所有通知,直到文档在设计模式下完全重新加载。
另一个小问题是 RegisterForDirtyRange
创建了一个 cookie,您需要维护该 cookie 才能注销更改接收器。既然你需要一个class来实现IHTMLChangeSink
,它也可以封装设计模式状态和更改注册。
uses
System.SysUtils, SHDocVw, MSHTML;
const
DesignMode: array[Boolean] of string = ('off', 'on');
type
TWebBrowserDesign = class(TInterfacedObject, IHTMLChangeSink)
private
FDirtyRangeCookie: LongWord;
FDocumentComplete: Boolean;
FHTMLDocument2: IHTMLDocument2;
FMarkupContainer2: IMarkupContainer2;
FOnChange: TProc;
{ IHTMLChangeSink }
function Notify: HResult; stdcall;
public
constructor Create(WebBrowser: TWebBrowser; const AOnChange: TProc);
destructor Destroy; override;
end;
constructor TWebBrowserDesign.Create(WebBrowser: TWebBrowser; const AOnChange: TProc);
begin
inherited Create;
if not Assigned(WebBrowser) then
raise Exception.Create('Web browser control missing.');
if not Supports(WebBrowser.Document, IHTMLDocument2, FHTMLDocument2) then
raise Exception.Create('No HTML document loaded.');
FHTMLDocument2.designMode := DesignMode[True];
if Supports(WebBrowser.Document, IMarkupContainer2, FMarkupContainer2) then
begin
if FMarkupContainer2.RegisterForDirtyRange(Self, FDirtyRangeCookie) <> S_OK then
FDirtyRangeCookie := 0
else
_Release;
end;
FOnChange := AOnChange;
end;
destructor TWebBrowserDesign.Destroy;
begin
if Assigned(FMarkupContainer2) and (FDirtyRangeCookie <> 0) then
FMarkupContainer2.UnRegisterForDirtyRange(FDirtyRangeCookie);
if Assigned(FHTMLDocument2) then
FHTMLDocument2.designMode := DesignMode[False];
inherited;
end;
function TWebBrowserDesign.Notify: HResult;
begin
Result := S_OK;
if not FDocumentComplete then
FDocumentComplete := FHTMLDocument2.readyState = 'complete'
else if Assigned(FOnChange) then
FOnChange();
end;
注意注册更改接收器后对 _Release
的调用。这是为了 "prevent" 标记容器持有对 TWebBrowserDesign
实例的强引用。这允许您使用 TWebBrowserDesign
实例的生命周期来控制设计模式:
type
TForm1 = class(TForm)
{ ... }
private
FWebBrowserDesign: IInterface;
{ ... }
end;
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
{ enter design mode }
FWebBrowserDesign := TWebBrowserDesign.Create(WebBrowser1, procedure
begin
ButtonSave.Enabled := True;
end);
end;
procedure TForm1.ButtonSave(Sender: TObject);
begin
{ exit design mode }
FWebBrowserDesign := nil;
ButtonSave.Enabled := False;
end;
或者,您可以将更改接收器实现为一个组件。
type
TWebBrowserDesign = class(TComponent, IHTMLChangeSink)
private
FDirtyRangeCookie: LongWord;
FDocumentComplete: Boolean;
FHTMLDocument2: IHTMLDocument2;
FMarkupContainer2: IMarkupContainer2;
FOnChange: TNotifyEvent;
FWebBrowser: TWebBrowser;
procedure EnterDesignMode;
procedure ExitDesignMode;
function GetActive: Boolean;
procedure SetActive(const Value: Boolean);
procedure SetWebBrowser(const Value: TWebBrowser);
{ IHTMLChangeSink }
function Notify: HResult; stdcall;
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
destructor Destroy; override;
published
property Active: Boolean read GetActive write SetActive;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
property WebBrowser: TWebBrowser read FWebBrowser write SetWebBrowser;
end;
destructor TWebBrowserDesign.Destroy;
begin
ExitDesignMode;
inherited;
end;
procedure TWebBrowserDesign.EnterDesignMode;
begin
if not Assigned(FWebBrowser) then
raise Exception.Create('Web browser control missing.');
if not Supports(FWebBrowser.Document, IHTMLDocument2, FHTMLDocument2) then
raise Exception.Create('No HTML document loaded.');
try
FHTMLDocument2.designMode := DesignMode[True];
if Supports(FWebBrowser.Document, IMarkupContainer2, FMarkupContainer2) then
begin
if FMarkupContainer2.RegisterForDirtyRange(Self, FDirtyRangeCookie) <> S_OK then
FDirtyRangeCookie := 0;
end;
except
ExitDesignMode;
raise;
end;
end;
procedure TWebBrowserDesign.ExitDesignMode;
begin
if Assigned(FMarkupContainer2) then
begin
if FDirtyRangeCookie <> 0 then
begin
FMarkupContainer2.UnRegisterForDirtyRange(FDirtyRangeCookie);
FDirtyRangeCookie := 0;
end;
FMarkupContainer2 := nil;
end;
if Assigned(FHTMLDocument2) then
begin
FHTMLDocument2.designMode := DesignMode[False];
if not (csDestroying in ComponentState) then
FHTMLDocument2 := nil; { causes AV when its hosting TWebBrowser component is destroying; I didn't dig into details }
end;
FDocumentComplete := False;
end;
function TWebBrowserDesign.GetActive: Boolean;
begin
Result := Assigned(FHTMLDocument2);
end;
procedure TWebBrowserDesign.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if (Operation = opRemove) and (AComponent = FWebBrowser) then
WebBrowser := nil;
end;
function TWebBrowserDesign.Notify: HResult;
begin
Result := S_OK;
if not FDocumentComplete then
FDocumentComplete := FHTMLDocument2.readyState = 'complete'
else if Assigned(FOnChange) then
FOnChange(Self);
end;
procedure TWebBrowserDesign.SetActive(const Value: Boolean);
begin
if Active <> Value then
begin
if Value then
EnterDesignMode
else
ExitDesignMode;
end;
end;
procedure TWebBrowserDesign.SetWebBrowser(const Value: TWebBrowser);
begin
if Assigned(FWebBrowser) then
begin
ExitDesignMode;
FWebBrowser.RemoveFreeNotification(Self);
end;
FWebBrowser := Value;
if Assigned(FWebBrowser) then
FWebBrowser.FreeNotification(Self);
end;
如果您将这样的组件放在设计时包中并在 IDE 中注册它,那么您将能够 link 使用 TWebBrowser
这个组件并分配OnChange
表单设计器中的事件处理程序。在代码中使用 Active
属性 到 enter/exit 设计模式。
type
TForm1 = class(TForm)
{ ... }
WebBrowserDesign1: TWebBrowserDesign;
{ ... }
end;
procedure WebBrowserDesign1Change(Sender: TObject);
begin
ButtonSave.Enabled := True;
end;
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
{ enter design mode }
WebBrowserDesign1.Active := True;
end;
procedure TForm1.ButtonSave(Sender: TObject);
begin
{ exit design mode }
WebBrowserDesign1.Active := False;
ButtonSave.Enabled := False;
end;
注意:关于 C#/WinForms 已提出类似问题 - How do I detect when the content of a WebBrowser control has changed (in design mode)?
最后说明:我不相信在更改后启用保存按钮是最好的用户体验设计。如果您认为上面的代码值得实现您的目标,那么请继续。这只是概念验证,代码尚未经过全面测试。需要您自担风险使用它。