Delphi: TOleControl puts ActiveControl in wrong state?
11:16 13 May 2009

Short Version

Using a TWebBrowser embedded inside Delphi, the user has to click a hyperlink twice in order to make it register the mouse click:

  • first click gets eaten
  • second click is actually registered by the browser

How do i stop the first click from getting eaten?

Long Version

We have a TWebBrowser (or TEmbeddedWB if you prefer that improved control) that powers a portion of the user-interface on a form in Delphi. There are hyperlinks in the embedded HTML that are surfaced to Delphi using Javascript:


This calls the standard IDispatch interface of the browser. First it uses the standard IDispatch.GetIDsOfNames to perform late binding of a method name to a dispid:

function TfrmGrobber.wbTasksGetIDsOfNames(const IID: TGUID;
  Names: Pointer; NameCount, LocaleID: Integer; DispIDs: Pointer): HRESULT;
var
    oslNames: POleStrList;
    pdidDispIds: PDispIDList;
begin
    OslNames := Names;
    pdidDispIDs := DispIds;
    if SameText(oslNames[0], 'FrobTheGrobber') then
    begin
        pdidDispIDs[0] := DISPID_FrobTheGrobber;
        Result := S_OK;
    end
end;

And the it calls the standard IDispatch.Invoke with the dispid:

function TfrmGrobber.wbTasksInvoke(DispID: Integer; const IID: TGUID;
  LocaleID: Integer; Flags: Word; var Params: tagDISPPARAMS; VarResult,
  ExcepInfo, ArgErr: Pointer): HRESULT;
begin
    case DispId of
    DISPID_FrobTheGrobber: 
       begin
            //...here we frob the grobber
            Result := S_OK;
        end;
    end;
end;

And that all works.

That sorta works

When the user clicks the hyperlink, the first click is always eaten—nothing happens. The user thinks they're crazy, and they click it again, and then it works.

And it's only an issue the first time you try to interact with the embedded web-browser. Once you've clicked it once, you can click elsewhere (so it loses focus), and you correctly only have to click the link once.

And it's been this way for...23 years ago.

And i'd really like to fix it.

Research Effort

In Mike Lischke's Virtual Treeview, there was workaround code added to fix a bug when using a TWebBrowser control on the same form.

The problem was that if the user tries to interact with a TOleControl (from which TWebBrowser descends), the first mouse click is eaten. They have to then click again to give the control focus. Then they can interact with the control.

He has comments to explain:

Every control derived from TOleControl has potentially the focus problem.

In order to avoid including the OleCtrls unit (which will, among others, include Variants), which would allow to test for the TOleControl class, the IOleClientSite interface is used for the test, which is supported by TOleControl and a good indicator.

From the full snippit:

procedure TBaseVirtualTree.WMKillFocus(var Msg: TWMKillFocus);
var
  Form: TCustomForm;
  Control: TWinControl;
  Pos: TSmallPoint;
  Unknown: IUnknown;
begin
  inherited;

  [snip]

  {
    Workaround for wrapped non-VCL controls (like TWebBrowser), 
    which do not use VCL mechanisms and 
    leave the ActiveControl property in the wrong state, 
    which causes trouble when the control is refocused.
  }
  Form := GetParentForm(Self);
  if Assigned(Form) and (Form.ActiveControl = Self) then
  begin
    Cardinal(Pos) := GetMessagePos;
    Control := FindVCLWindow(SmallPointToPoint(Pos));
    {
      Every control derived from TOleControl has potentially 
      the focus problem. In order to avoid including 
      the OleCtrls unit (which will, among others, include Variants),  
      which would allow to test for the TOleControl
      class, the IOleClientSite interface is used for the test, 
      which is supported by TOleControl and a good indicator.
    }
    if Assigned(Control) and Control.GetInterface(IOleClientSite, Unknown) then
      Form.ActiveControl := nil;

    // For other classes the active control should not be modified. 
    // Otherwise you need two clicks to select it.
  end;
end;

That is the exact problem i'm experiencing. I don't understand what he's saying, but he's saying it with authority, and apparently it worked. Problem is that the workaround is not working for me. And to be honest i have no idea what the problem really was, and how his solution fixed it.

Workaround for wrapped non-VCL controls (like TWebBrowser), which do not use VCL mechanisms and leave the ActiveControl property in the wrong state, which causes trouble when the control is refocused. Every control derived from TOleControl has potentially the focus problem.

For other classes the active control should not be modified.
Otherwise you need two clicks to select it.

(emphasis mine)

Is there anyone who:

  • knows what his comments mean
  • or understand what he's talking about
  • can explain what the problem is
  • how his fix was supposed to fix it

The code is reaching the intended statement:

Form.ActiveControl := nil; 

but it just isn't doing the trick.

I'd fix it, but i have no idea how he found it, or how it can come about that TOleControl doesn't "use VCL mechanisms and leaves the ActiveControl property in the wrong state."

I know what individually what all those words mean, but in that order i don't know what they're saying.

Bonus Reading

I originally asked this question on borland.public.delphi.nativeapi.win32 newsgroup in 2008

Question on Soft-Gems forum

delphi focus ole delphi-xe6 twebbrowser