2

I'm using a TWebBrowser to display a WYSIWYG HTML editor and I've added some handlers to catch keyboard and mouse events so I can integrate this editor into my application flow. This browser is integrated in a custom TPanel, TPanelEditorHTML.

This is the way I'm doing it, following some tips from this answer:

  //Create the procedure type to assign the event
  THTMLProcEvent = procedure(Sender: TObject; Event: IHTMLEventObj) of object;

  //Create a  new class for manage the event from the twebbrowser
  THTMLBrowserEventLink = class(TInterfacedObject, IDispatch)
  private
    FOnEvent: THTMLProcEvent;
  private
    constructor Create(Handler: THTMLProcEvent);
    function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
    function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
    function GetIDsOfNames(const IID: TGUID; Names: Pointer;
      NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
    function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
      Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
  public
    property OnEvent: THTMLProcEvent read FOnEvent write FOnEvent;
  end;

On my container for the TWebBrowser I have this:

FOnKeyDownConnector:  IDispatch; 
FOnClickConnector:  IDispatch;
FOnKeyDownConnectorIFrame:  IDispatch;
FOnClickConnectorIFrame:  IDispatch;    

procedure BrowserIHTMLDocument2OnKeyDown(Sender: TObject; Event: IHTMLEventObj);
procedure BrowserIHTMLDocument2OnClick(Sender: TObject; Event: IHTMLEventObj); 
procedure IframeIHTMLDocument2OnKeyDown(Sender: TObject; Event: IHTMLEventObj);
procedure IframeIHTMLDocument2OnClick(Sender: TObject; Event: IHTMLEventObj);

Where BrowserIHTMLDocument2OnKeyDown, etc are the procedures where I do all the work integrating the HTML editor data into my application

I create the handlers on startup

constructor TPanelEditorHTML.Create(AOwner: TComponent);
begin
  inherited;
  // ...
  FNavegador := TGENBrowser.Create(self);
  FOnKeyDownConnector := THTMLBrowserEventLink.Create(BrowserIHTMLDocument2OnKeyDown);
  FOnClickConnector := THTMLBrowserEventLink.Create(BrowserIHTMLDocument2OnClick);
  FOnKeyDownConnectorIFrame := THTMLBrowserEventLink.Create(IFrameIHTMLDocument2OnKeyDown);
  FOnClickConnectorIFrame := THTMLBrowserEventLink.Create(IFrameIHTMLDocument2OnClick);
end;    

And when I load the HTML editor I assign this handlers to a couple of elements in the DOM tree:

procedure TPanelEditorHTML.AsignarManejadores;
var
  HTMLDocument2_A, HTMLDocument2_B: IHTMLDocument2;
begin
  HTMLDocument2_A := ExtraerIframeEditor;
  HTMLDocument2_B := (FNavegador.Document AS IHTMLDocument2);
  if (HTMLDocument2_A = nil) or (HTMLDocument2_B = nil) then
    Exit;

  if (FOnKeyDownConnectorIFrame <> nil) then
    HTMLDocument2_A.onkeydown := FOnKeyDownConnectorIFrame; 
  if (FOnClickConnectorIFrame <> nil) then
    HTMLDocument2_A.onclick := FOnClickConnectorIFrame; 

  if (FOnKeyDownConnector <> nil) then
    HTMLDocument2_B.onkeydown := FOnKeyDownConnector; 
  if (FOnClickConnector <> nil) then
    HTMLDocument2_B.onclick := FOnClickConnector;   
end;

When the user ends the editing I remove this handlers

procedure TPanelEditorHTML.DesconectarManejadores;
var
  HTMLDocument2      : IHTMLDocument2;
begin
  HTMLDocument2 := ExtraerIframeEditor;
  if (HTMLDocument2 <> nil) then
  begin
    HTMLDocument2.onkeydown := Unassigned; //assign the event handler
    HTMLDocument2.onclick := Unassigned; //assign the event handler
  end;

  HTMLDocument2:=(FNavegador.Document AS IHTMLDocument2);
  if (HTMLDocument2 <> nil) then
  begin
    HTMLDocument2.onkeydown := Unassigned; //assign the event handler
    HTMLDocument2.onclick := Unassigned; //assign the event handler
  end;
end;

My problem is on the TPanelEditorHTML destructor. This leads to a memory leak of the four THTMLBrowserEventLink. If I try to FreeAndNil the handlers I get a runtime error.

destructor TPanelEditorHTML.Destroy;
begin
  FDataLink.Free;
  FOnKeyDownConnector := Unassigned;
  FOnClickConnector := Unassigned;
  FOnKeyDownConnectorIFrame := Unassigned;
  FOnClickConnectorIFrame := Unassigned;
  inherited;
end;

I found this article about memory leaks and I tried to replace both methods that were making copies to no avail.

Am I missing something?

Héctor C.
  • 433
  • 4
  • 17
  • Could you produce a small example, the shortest possible, which reproduce your problem? Edit your post with complete source (.pas and .dfm). If we can reproduce, we can hopefully build a good answer. – fpiette Apr 08 '21 at 09:38
  • 4
    There is superfluous _AddRef in THTMLEventLink.Create and this will cause the memory leak. – Dalija Prasnikar Apr 08 '21 at 09:42
  • @DalijaPrasnikar thanks, that solved my problem! – Héctor C. Apr 08 '21 at 09:46

1 Answers1

3

As @DalijaPrasnikar said there is a superfluous _AddRef in THTMLEventLink.Create, wich I copied from the solution proposed in this answer.

Changing the constructor of THTMLBrowserEventLink to this:

constructor THTMLBrowserEventLink.Create(Handler: THTMLProcEvent);
begin
  inherited Create;
  FOnEvent := Handler;
end;

Avoids the memory leak.

Héctor C.
  • 433
  • 4
  • 17
  • 4
    Additional (or should I say core) issue in the original answer (I added another comment there) is that FOnKeyDownConnector is declared as THTMLEventLink and because of that reference counting was broken which was "fixed" with superfluous _AddRef call. Your approach to declare variables as IDispatch was correct one and with removal of _AddRef you now have good code from the memory management aspect :) – Dalija Prasnikar Apr 08 '21 at 10:07