检测可编辑 TWebBrowser 中的更改

Posted

技术标签:

【中文标题】检测可编辑 TWebBrowser 中的更改【英文标题】:Detecting changes in an editable TWebBrowser 【发布时间】:2020-10-02 06:44:32 【问题描述】:

我正在将一个 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_PASTEWM_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 属性,但没有任何成功。

【问题讨论】:

为什么需要这样做? @Olivier:因为我想启用/禁用保存按钮 【参考方案1】:

要在设计模式下捕获对文档的更改,您应该使用其IMarkupContainer2 接口通过RegisterForDirtyRange 方法注册IHTMLChangeSink。过程很简单——实现IHTMLChangeSink,从WebBrowser1.Document获取IMarkupContainer2并调用它的RegisterForDirtyRange方法,但是有一个问题。

当您更改IHTMLDocument2designMode 时,TWebBrowser 控件会重新加载当前文档,并且它会丢失所有已注册的更改接收器。因此,您应该在将文档置于设计模式后进行注册。之后您会通过IHTMLChangeSink.Notify 方法收到更改通知。

但还有另一个问题。由于进入设计模式会导致重新加载文档,进而导致将文档的readyState 属性更改为'loading',然后连续更改为'complete'。您的更改接收器将收到那些readyState 更改通知。请注意,进入设计模式后,TWebBrowser.OnDocumentComplete 不会调用。这就是为什么您应该忽略任何通知,直到文档在设计模式下完全重新加载。

另一个小麻烦是RegisterForDirtyRange 创建了一个 cookie,您需要维护该 cookie 才能取消注册更改接收器。既然你无论如何都需要一个类来实现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 的调用。这是为了“防止”标记容器持有对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 中注册它,那么您将能够将此组件与TWebBrowser 链接并在表单设计器中分配OnChange 事件处理程序。在代码中使用Active属性进入/退出设计模式。

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)?

最后一点:我不相信在更改后启用保存按钮是最好的 UX 设计。如果您认为上面的代码值得实现您的目标,请继续。这只是一个概念证明,代码尚未经过彻底测试。需要您自担风险使用它。

【讨论】:

很好的答案!远远超出我的预期!我已经尝试了两种方法,似乎两者都很好用。我只发现了两个问题:1) 在 Delphi XE7 上我找不到DesignMode[True]/DesignMode[False],所以我将它们切换到'on'/'off'。 2)在第一次更改时,OnChange 没有执行,因为它将FDocumentComplete 设置为True 并且没有执行OnChange,我稍微修改了Notify 方法:Result := S_OK; if not FDocumentComplete then FDocumentComplete := FHTMLDocument2.readyState = 'complete'; if FDocumentComplete and Assigned(FOnChange) then FOnChange(Self);跨度> @Fabrizio 我添加了缺失的代码 - 请参阅第一个代码块中的 const。至于FDocumentComplete 标志 - 在进入设计模式后我收到了 2 条通知,而没有用户修改。我的第一个通知readyState'loading',第二个通知是'complete'。这可能因 IE 版本/IE 文档模式而异。 谢谢,我有 IE 11.900.18362.0。 “IE 文档模式”是什么意思,我如何检查TWebBrowser 使用的是哪一个? Fix web compatibility issues using document modes and the Enterprise Mode site list。 Web浏览器控制和文档模式相关话题:WebBrowser control set Document Mode.

以上是关于检测可编辑 TWebBrowser 中的更改的主要内容,如果未能解决你的问题,请参考以下文章

使用 TWebBrowser 时查看 Web 控制台

如何在 TWebBrowser IDispatchEvent 中获取自定义事件参数

如何从“更多”标签栏项目中的编辑检测标签栏项目更改?

用Twebbrowser做可控编辑器与MSHTML(调用js)

使用带有 TWebBrowser 的 IHTMLEventObj 处理程序的内存泄漏

TWebBrowser 中的方向键切换控件