Skip over navigation

How to make a TWebBrowser become the active control when clicked

Introduction

When you have a form containing a TWebBrowser control and you click in the control it does not automatically become the active control of the parent form. However, tabbing around a form does make the web browser the active control. This behaviour is unlike other controls such as TEdit and TMemo and can cause some unexpected behaviour in your application, as I learned the hard way!

This article presents two methods of ensuring that the TWebBrowser control behaves like other controls by making it the active control when clicked. As we will see, there are problems with either approach, but the second method is the most robust.

Investigate the problem

To see the problem let's create a small application that shows the name of the parent form's active control in the status bar.

Start a new Delphi application and drop the following controls on the form, keeping the default names:

  • A TMemo – clear its Lines property and set its TabOrder property to 0.
  • A TWebBrowser – sets its TabOrder property to 1.
  • A TButton – set its Caption to 'Load HTML' and its TabOrder property to 2.
  • A TTimer – set its Interval property to 100.
  • A TOpenDialog – set its Filter property to display files with .html and .htm extensions.
  • A TStatusBar.

Arrange the form and set its Caption to look something like this:

Test application main form in Delphi form designer
Image 1: Test application main form in Delphi form designer

We will use the TTimer to update the status bar with details of the active control every 1/10th of a second. To do this create a OnTimer event handler for the TTimer as follows:

  1procedure TForm1.Timer1Timer(Sender: TObject);
  2begin
  3  if Assigned(ActiveControl) then
  4    StatusBar1.SimpleText := 'ActiveControl = '
  5      + ActiveControl.Name
  6  else
  7    StatusBar1.SimpleText := 'ActiveControl = nil';
  8end;
Listing 1

Compile and run the application and move around the controls using the Tab key. You will see the name of each control displayed in the status bar as it becomes active. To see the problem use the mouse and click in either the memo or edit control. The status bar will change to show its name. Now click in the web browser. Nothing will change.

Maybe this is because the web browser is not displaying any HTML – i.e. it has no document object? Let us fix this by adding the facility to display a HTML document. Double click the TButton to create an OnClick event handler and add the following code to it:

  1procedure TForm1.Button1Click(Sender: TObject);
  2begin
  3  if OpenDialog1.Execute then
  4  begin
  5    StatusBar1.SimpleText := ''; // fixes SB redraw problem
  6    if FileExists(OpenDialog1.FileName) then
  7    begin
  8      WebBrowser1.Navigate('file://' + OpenDialog1.FileName);
  9      Memo1.Lines.LoadFromFile(OpenDialog1.FileName);
 10    end        
 11    else
 12    begin
 13      WebBrowser1.Navigate('about:blank');
 14      Memo1.Text := 'about:blank';
 15    end;
 16  end;
 17end;
Listing 2

This method displays the open file dialog box. If the user OKs we check if the provided file exists. If so we display it in the web browser and show its source code in the memo control. If the file doesn't exist we navigate to about:blank and display that text in the memo.

Recompile the application and run it. Load a HTML file into the web browser (or create an about:blank document by entering an invalid file name). When you try clicking the various controls you will see that the problem persists. We have more work to do…

A solution

This solution relies in detecting a mouse click in the web browser control and, in reponse, setting the form's ActiveControl property to reference the web browser. In essence we wan't to do something like this:

  1// !!! Wrong !!!
  2procedure TForm1.WebBrowser1Click(Sender: TObject);
  3begin
  4  ActiveControl := Sender as TWebBrowser;
  5end;
Listing 3

Unfortunately, TWebBrowser does not have an OnClick or OnMouseDown event in which to do this, so we have to find an alternative way of detecting the mouse click.

The web browsers's OnCommandStateChange event is useful here. According to Delphi's help, this event is triggered when the ability to execute certain TWebBrowser methods changes. It happens that this event is triggered when, amongst other things, the user clicks the mouse in a document.

Note that I said "amongst other things". The OnCommandStateChange event is not only triggered when the mouse is clicked in the document – it is also triggered on other occasions. So we need to find when the event is triggered in response to such a click. Research and experimentation shows that we can reliably detect a mouse click in the following circumstances:

  • The event handler is called with a Command parameter of CSC_UPDATECOMMANDS.
  • The document's selection object is available and has a type of 'Text'.

Now, to use the selection object, we must access the browser's document object – and to access the document object we must have loaded a document into the browser. This means we can't use this code on the web browser control until a document has been loaded. The simplest way to ensure this is by navigating to about:blank. We can do this in the Form's OnShow event handler which we add now:

  1procedure TForm1.FormShow(Sender: TObject);
  2begin
  3  WebBrowser1.Navigate('about:blank');
  4end;
Listing 4

Having ensured there is a document present in the web browser we can use the following code in the browser's OnCommandStateChange event handler:

  1procedure TForm1.WebBrowser1CommandStateChange(Sender: TObject;
  2  Command: Integer; Enable: WordBool);
  3var
  4  Doc: IHTMLDocument2;       // document object
  5  Sel: IHTMLSelectionObject; // current selection
  6begin
  7  // Check we have a valid web browser triggering this event
  8  if not Assigned(Sender) or not (Sender is TWebBrowser) then
  9    Exit;
 10  // Check we have required command
 11  if TOleEnum(Command) <> CSC_UPDATECOMMANDS then
 12    Exit;
 13  // Get ref to document object and check not nil
 14  Doc := WebBrowser1.Document as IHTMLDocument2;
 15  if not Assigned(Doc) then
 16    Exit;
 17  // Get ref to current selection
 18  Sel := Doc.selection as IHTMLSelectionObject;
 19  // If selection is of correct type then we have a mouse click
 20  if Assigned(Sel) and (Sel.type_ = 'Text') then
 21    // Make the web browser the form's active control
 22    ActiveControl := Sender as TWebBrowser;
 23end;
Listing 5

Here we first make sure that the sending object is a valid TWebBrowser control. We then check the Command parameter and exit if it is not CSC_UPDATECOMMANDS. Next we try to get hold of the browser's Document object, exiting if it is nil. From a valid document object we get the document's current selection object, again checking it is not nil. We also check the selection type is 'Text'. If these tests succeed we finally set the form's ActiveControl property to the web browser and we are done.

Once again, recompile the project and tab and click about the form. You should now find the web browser becomes the active control when clicked or tabbed into.

«« Gotcha #1 »»

There's a gotcha here. I carefully used the word "document" rather than "control" in the above description. The OnCommandStateChange event only detects clicks in the current document, not necessarily in the control. This is because the browser, by default, places a border round the document. Clicking in this border is not detected because it is not part of the document.

The only (partial) solution I've heard of is to make the border disappear using CSS. This is OK if you have access to the document – you can use something like:

body {margin: 0;}

The technique is no good if you can't change or override the document's CSS. We can improve matters by setting the browser's default CSS (See article #18 for details of how to do this from code). However, if the document sets its own border it will override any default, so we're stuck.

Thanks to ANBe for pointing out the cause of this problem and suggesting the CSS fix. Anyone who has any other ideas please let me know.

«« Gotcha #2 »»

There's more. The solution requires the document's selection type to be "text". Unfortunately, if you click on a non-text item in the document, for example and image, the selection type is not "text".

I'm afraid I don't have a solution to this problem as yet. If you do then, once again, let me know.

Another Solution

Bärje Henriksson has suggested another solution that does detect clicks on the whole control – and so avoids the gotchas above.

Bärje's solution depends on finding the window handle for the browser control. We do this by searching for a window whose class is named "Internet Explorer_Server". This is ultimately a child of the form window. The diagram below shows the relationship.

Diagram of browser control window in relationship to form window
Image 2: Diagram showing how a broswer control relates to its host form

And so to the code. Re-using the code we developed for the previous solution, add a private field named IEServerWindow of type HWND to the form class and drop a TApplicationEvents component on the form. Now remove the WebBrowser1CommandStateChange event handler we created in listing 5.

If your version of Delphi doesn't have the TApplicationEvents component all is not lost. Alternatives are discussed below.

Modify the timer component's OnTimer event handler as shown in listing 6 below:

  1procedure TDemoForm.Timer1Timer(Sender: TObject);
  2var
  3  NextWin: HWND; // handle of various child windows
  4begin
  5  while IEServerWindow = 0 do
  6  begin
  7    NextWin := FindWindowEx(DemoForm.Handle, 0, 'Shell Embedding', nil);
  8    NextWin := FindWindowEx(NextWin, 0, 'Shell DocObject View', nil);
  9    IEServerWindow := FindWindowEx(
 10      NextWin, 0, 'Internet Explorer_Server', nil
 11    );
 12  end;
 13  if Assigned(ActiveControl) then
 14    StatusBar1.SimpleText := 'ActiveControl = ' + ActiveControl.Name
 15  else
 16    StatusBar1.SimpleText := 'ActiveControl = nil';
 17end;
Listing 6

The timer event is used to find the window since the handle may not be available straight away when the application is displayed.

We now need to handle mouse-down messages sent to the browser control window. Create an OnMessage event handler for the TApplicationEvents component:

  1procedure TDemoForm.ApplicationEvents1Message(var Msg: tagMSG;
  2  var Handled: Boolean);
  3begin
  4  if (Msg.hwnd = IEServerWindow) then
  5    if (Msg.message = WM_LBUTTONDOWN)
  6      or (Msg.message = WM_RBUTTONDOWN)
  7      or (Msg.message = WM_MBUTTONDOWN) then
  8    ActiveControl := WebBrowser1;
  9end;
Listing 7

No TApplicationEvents?

If you don't have TApplicationEvents with your version of Delphi simply create a private method with the same parameter list as ApplicationEvents1Message and implement it as in listing 7. Then create a OnCreate message handler for your form and assign your private method to the Application.OnMessage event within that message handler.

This code ensures the browser control is set as the form's active control whenever the control receives mouse-down events from the left, middle or right mouse buttons.

And that's it – clicking anywhere on the browser control activates it.

Gotcha #3

There has to be a problem doesn't there? This solution works fine providing there is only one web browser control on the form. If there is more than one then the solution fails. This is because the code that finds the browser control's window handle searches for a window class name – and the class name is the same for each browser control instance. And that means only one browser control is found.

A possible fix is to put each browser control in a separate TPanel and then find the required windows by using the appropriate panel's window handle in the FindWindowEx call that searches for the "Shell Embedding" window class (see listing 6). Of course, you'll need to track which window relates to which browser control.

Summary

In this article we noted that clicking in a TWebBrowser control does not make it the active control of the parent form. We developed some code to fix this problem by detecting a mouse click on the control's active document and setting the parent form's ActiveControl property to the browser control in response to the click. This was done by handling the browser object's OnCommandStateChange event and detecting a text selection. We observed that this solution is far from perfect because it doesn't work if:

  • there is no document loaded in the browser;
  • the user clicks in the border of an HTML document;
  • the user clicks on a image or other non-text area in the display.

An alternative method that does not have the above problems was then presented. This method depended on finding the browser control's window handle and detecting mouse messages directed to that window. It works when no document is loaded and detects clicks anywhere in the control, including the scroll bars. However, it was noted that the solution fails if there are two or more browser controls on the form.

Demonstration code

A demo program to accompany this article can be found in the delphidabbler/article-demos Git repository on GitHub.

You can view the code in the article-19 sub-directory. Alternatively download a zip file containing all the demos by going to the repository's landing page and clicking the Clone or download button and selecting Download ZIP.

See the demo's README.md file for details.

This source code is merely a proof of concept and is intended only to illustrate this article. It is not designed for use in its current form in finished applications. The code is provided on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.

The demo is open source. See the demo's LICENSE.md file for licensing details.

Feedback

I hope you found this article useful.

If you have any observations, comments, or have found any errors there are two places you can report them.

  1. For anything to do with the article content, but not the downloadable demo code, please use this website's Issues page on GitHub. Make sure you mention that the issue relates to "article #19".
  2. For bugs in the demo code see the article-demo project's README.md file for details of how to report them.