Skip over navigation

How to customise the TWebBrowser user interface

Contents

Introduction

Among the most popular questions posed in the TWebBrowser newsgroups and discussed on many Delphi tips sites are:

  1. How do you get a TWebBrowser control to display the popup menu assigned to its PopupMenu property instead of the standard IE popup menu?
  2. How do you stop TWebBrowser displaying 3D borders when a document is loaded.
  3. How do you prevent TWebBrowser from displaying scrollbars?

We're going to answer these in this article, along with three other related questions that arose when I was developing the CodeSnip Database Viewer application:

  1. How can you make the browser control take on the look and feel of the hosting form, given that each user may have a different display configuration that is not known at design time? I needed to do this to add some HTML content to a couple of dialogue boxes and make the HTML appear to be part of the dialogue box.
  2. Users can normally select text in a browser control – something you don't want in a dialogue box. So how do you stop that?
  3. How do you ensure that the browser control uses themes when rendering widgets such as buttons when, and only when, your application is using themes?

The answer to the question 1 is to create an object that implements the IDocHostUIHandler interface and use that object to control the behaviour of the popup menu.

A common solution to the questions 2 & 3 is to set one of the DOM object properties to some suitable value. For example, if WB has type TWebBrowser, we must wait until a document has loaded and then do:

  1// Switch off scrollbars
  2WB.OleObject.document.body.style.overflowX := 'hidden';
  3WB.OleObject.document.body.style.overflowY := 'hidden';
  4
  5// Switch off borders
  6WB.OleObject.document.body.style.borderstyle := 'none';
Listing 1

Now, what is less well known is that we can also set the border style and the scroll bar visibility by implementing IDocHostUIHandler.

IDocHostUIHandler also lets us provide a default Cascading Style Sheet (CSS) to the browser object. This provides an answer to question 4: we can dynamically create a style sheet that knows about a form's colour and fonts and tell the browser to use it.

And as for the last two questions? You guessed it: IDocHostUIHandler can help here too!

Unsurprisingly then, this article is all about how to create an object that implements IDocHostUIHandler and how to use it to customize the browser control. Along the way we will also have to find out how to get TWebBrowser to interact with the object.

Overview of a solution

Let us assess what are trying to do. We will work towards creating an object that can customize the TWebBrowser control's appearance and take control of its context menu. In the interests of ease of use and reusability we will expose properties to enable users to easily configure these attributes of the browser control.

So how do we go about customizing the TWebBrowser? According to the Microsoft® Developer Network Library documentation (my emphasis):

"The mechanism for WebBrowser Control customization is designed to be automated when a container provides support for ActiveX controls. Whenever the WebBrowser Control is instantiated, it attempts to find IDocHostUIHandler, IDocHostUIHandler2 and IDocHostShowUI implementations from the host, if they are available. The WebBrowser Control does this by a QueryInterface call on the host's IOleClientSite interface.

"This architecture works automatically for an application that implements an IOleClientSite interface and that passes an IOleClientSite pointer to the WebBrowser Control through the browser's IOleObject::SetClientSite method."

This tells us the following:

  • We need to create a "container" object to host our TWebBrowser control.
  • This container must implement the IOleClientSite interface to enable the browser control to query it when looking for our IDocHostUIHandler interface.
  • We need to notify the TWebBrowser that we are hosting it by passing a reference to our container's IOleClientSite interface to the browser control via its IOleObject.SetClientSite method.
  • The container object must also implement IDocHostUIHandler. This implementation will be used to provide the required customization of the browser control. (We will not concern ourselves with IDocHostUIHandler2 or IDocHostShowUI here).

A few design decisions

We have establised that we need to develop a container control that supports IOleClientSite, IDocHostUIHandler and, by implication, IUnknown. The object will also expose properties that can be used calling code to control various aspects of the browser's customization.

Client code will instantiate the container object and manipulate its properties in the usual way. The web browser control will also access the container, but will do so automatically via its supported interfaces. This means we will be mixing two different methods of access – by interface and by object reference. Whenever this is done on an object that supports reference counted interfaces there is a danger of the object being prematurely freed when an interface reference goes out of scope. Consequently we will manage the object's lifetime ourselves, i.e. it will not be reference counted.

In the interests of reusability we will develop two classes:

  • TNulWBContainer [sic] – a do-nothing container object that hosts the web browser control and implements all the required interfaces in way that has no visible effect on the web browser control. This provides a base class for classes that seek to customize TWebBrowser in different ways.
  • TWBContainer – a descendant of TNulWBContainer that adds all the customization we noted in the introduction.

Reusing the code

See "How to call Delphi code from scripts running in a TWebBrowser" for an example of how we reuse TNulWBContainer.

In the following two sections we will develop the code for these classes.

Developing a reusable base class

Recall that this class, TNulWBContainer, will have all the features of a suitable container that hosts TWebBrowser. It will:

  • Implement IOleClientSite.
  • Implement IDocHostUIHandler in a neutral way.
  • Implement IUnknown without reference counting.
  • Register the container with the web browser control.
  • Expose the hosted web browser control as a property.

Getting Started

We will begin with the basics – getting hold of the hosted browser control and registering the container as its host. Declare a new class as per Listing 2:

  1type
  2  TNulWBContainer = class(TObject,
  3    IUnknown, IOleClientSite, IDocHostUIHandler
  4  )
  5  private
  6    fHostedBrowser: TWebBrowser;
  7    // Registration method
  8    procedure SetBrowserOleClientSite(const Site: IOleClientSite);
  9  public
 10    constructor Create(const HostedBrowser: TWebBrowser);
 11    destructor Destroy; override;
 12    property HostedBrowser: TWebBrowser read fHostedBrowser;
 13  end;
Listing 2

This is quite straightforward. We define the new type and list the interfaces we are to support. The private SetBrowserOleClientSite method is used to register / un-register the container with the hosted browser control. We then declare a constructor that takes a reference to the browser control and make it available via a property. A destructor is also declared.

Listing 3 shows how the constructor is implemented.

  1constructor TNulWBContainer.Create(const HostedBrowser: TWebBrowser);
  2begin
  3  Assert(Assigned(HostedBrowser));
  4  inherited Create;
  5  fHostedBrowser := HostedBrowser;
  6  SetBrowserOleClientSite(Self);
  7end;
Listing 3

Here we simply check that the browser control passed to the constructor is not nil and record a reference to it in a field. Our container object then registers itself as the web browser's host by calling SetBrowserOleClientSite and passing a reference to its own IOleClientSite interface.

The destructor simply un-registers the container from the browser by passing nil to SetBrowserOleClientSite, as Listing 4 shows.

  1destructor TNulWBContainer.Destroy;
  2begin
  3  SetBrowserOleClientSite(nil);
  4  inherited;
  5end;
Listing 4

Having discussed the purpose of SetBrowserOleClientSite we can now look at its implementation. See Listing 5:

  1procedure TNulWBContainer.SetBrowserOleClientSite(
  2  const Site: IOleClientSite);
  3var
  4  OleObj: IOleObject;
  5begin
  6  Assert((Site = Self as IOleClientSite) or (Site = nil));
  7  if not Supports(
  8    fHostedBrowser.DefaultInterface, IOleObject, OleObj
  9  ) then
 10    raise Exception.Create(
 11      'Browser''s Default interface does not support IOleObject'
 12    );
 13  OleObj.SetClientSite(Site);
 14end;
Listing 5

In this method we retrieve the browser's IOleObject interface and then call its SetClientSite method, passing the Site parameter. This parameter must be either a reference to our object's IOleClientSite interface, which registers the container with the TWebBrowser control, or it must be nil, to un-register the container.

IOleObject

To learn about IOleObject see the Windows SDK help file that ships with Delphi.

Implementing a non reference counted object

We will now move on to implement the required interfaces, starting with IUnknown. As already noted, the interface will be implemented so that the object is not reference counted.

IUnknown is defined in the System unit. We begin by adding its methods to a protected section of TNulWBContainer's declaration. Listing 6 shows the changes needed.

  1type
  2  TNulWBContainer = class(TObject,
  3    IUnknown, IOleClientSite, IDocHostUIHandler)
  4  private
  5    ...
  6  protected
  7    { IUnknown }
  8    function QueryInterface(const IID: TGUID; out Obj): HResult;
  9      stdcall;
 10    function _AddRef: Integer; stdcall;
 11    function _Release: Integer; stdcall;
 12  public
 13    ...
 14  end;
Listing 6

We implement these methods as shown in Listing 7.

  1function TNulWBContainer.QueryInterface(
  2  const IID: TGUID; out Obj): HResult;
  3begin
  4  if GetInterface(IID, Obj) then
  5    Result := S_OK
  6  else
  7    Result := E_NOINTERFACE;
  8end;
  9
 10function TNulWBContainer._AddRef: Integer;
 11begin
 12  Result := -1;
 13end;
 14
 15function TNulWBContainer._Release: Integer;
 16begin
 17  Result := -1;
 18end;
Listing 7

Listing 7 is quite straightforward. We copy the QueryInterface code from Delphi's implementation of TInterfacedObject except that we substitute S_OK for the 0 returned by TInterfacedObject's code.

To ensure the object is not reference counted, _AddRef and _Release each return -1. Furthermore, unlike in TInterfacedObject, our version of _Release never frees the object. This leaves us to manage the object's lifetime ourselves.

Implementing IOleClientSite

Next we move on to look at how our object will implement IOleClientSite.

IOleClientSite

IOleClientSite is declared in the ActiveX unit. The Delphi Windows SDK help provides a full description of the interface.

Remember that, in our constructor, we register our object's IOleClientSite interface with the browser control. We know that browser control calls our QueryInterface method via the registered IOleClientSite interface to try to gain access to our IDocHostUIHandler implementation. Because the browser object doesn't require any other functionality of IOleClientSite we can get away with providing a minimal, "do nothing" implementation. Therefore we will just stub out the methods.

Once again we begin by adding the interface's methods to the protected section of our class declaration. Listing 8 shows the required additions:

  1type
  2  TNulWBContainer = class(TObject,
  3    IUnknown, IOleClientSite, IDocHostUIHandler)
  4  private
  5    ...
  6  protected
  7    { IUnknown }
  8    ...
  9    { IOleClientSite }
 10    function SaveObject: HResult; stdcall;
 11    function GetMoniker(dwAssign: Longint;
 12      dwWhichMoniker: Longint;
 13      out mk: IMoniker): HResult; stdcall;
 14    function GetContainer(
 15      out container: IOleContainer): HResult; stdcall;
 16    function ShowObject: HResult; stdcall;
 17    function OnShowWindow(fShow: BOOL): HResult; stdcall;
 18    function RequestNewObjectLayout: HResult; stdcall;
 19    ...
 20  public
 21    ...
 22  end;
Listing 8

Our minimal implementation is shown in Listing 9. The comments in the code should outline the purpose of each method and explain what is being done. We won't discuss this further here.

  1function TNulWBContainer.GetContainer(
  2  out container: IOleContainer): HResult;
  3  {Returns a pointer to the container's IOleContainer
  4  interface}
  5begin
  6  { We do not support IOleContainer.
  7    However we *must* set container to nil }
  8  container := nil;
  9  Result := E_NOINTERFACE;
 10end;
 11
 12function TNulWBContainer.GetMoniker(dwAssign, dwWhichMoniker: Integer;
 13  out mk: IMoniker): HResult;
 14  {Returns a moniker to an object's client site}
 15begin
 16  { We don't support monikers.
 17    However we *must* set mk to nil }
 18  mk := nil;
 19  Result := E_NOTIMPL;
 20end;
 21
 22function TNulWBContainer.OnShowWindow(fShow: BOOL): HResult;
 23  {Notifies a container when an embedded object's window
 24  is about to become visible or invisible}
 25begin
 26  { Return S_OK to pretend we've responded to this }
 27  Result := S_OK;
 28end;
 29
 30function TNulWBContainer.RequestNewObjectLayout: HResult;
 31  {Asks container to allocate more or less space for
 32  displaying an embedded object}
 33begin
 34  { We don't support requests for a new layout }
 35  Result := E_NOTIMPL;
 36end;
 37
 38function TNulWBContainer.SaveObject: HResult;
 39  {Saves the object associated with the client site}
 40begin
 41  { Return S_OK to pretend we've done this }
 42  Result := S_OK;
 43end;
 44
 45function TNulWBContainer.ShowObject: HResult;
 46  {Tells the container to position the object so it is
 47  visible to the user}
 48begin
 49  { Return S_OK to pretend we've done this }
 50  Result := S_OK;
 51end;
Listing 9

Our class can now act as a client site, or host, for the web browser control. It remains to implement the document host handler, IDocHostUIHandler.

Implementing IDocHostUIHandler

At the time this code was being developed (with Delphi 7), no declaration of IDocHostUIHandler was available in the RTL. Therefore the interface is declared in Listing 10. The comments in the code give brief explanations of each method.

  1type
  2  IDocHostUIHandler = interface(IUnknown)
  3    ['{bd3f23c0-d43e-11cf-893b-00aa00bdce1a}']
  4    { Display a shortcut menu }
  5    function ShowContextMenu(
  6      const dwID: DWORD;
  7      const ppt: PPOINT;
  8      const pcmdtReserved: IUnknown;
  9      const pdispReserved: IDispatch): HResult; stdcall;
 10    { Retrieves the user interface capabilities and requirements
 11      of the application that is hosting the web browser }
 12    function GetHostInfo(
 13      var pInfo: TDocHostUIInfo): HResult; stdcall;
 14    { Enables us to replace browser menus and toolbars etc }
 15    function ShowUI(
 16      const dwID: DWORD;
 17      const pActiveObject: IOleInPlaceActiveObject;
 18      const pCommandTarget: IOleCommandTarget;
 19      const pFrame: IOleInPlaceFrame;
 20      const pDoc: IOleInPlaceUIWindow): HResult; stdcall;
 21    { Called when the browser removes its menus and toolbars.
 22      We remove menus and toolbars we displayed in ShowUI }
 23    function HideUI: HResult; stdcall;
 24    { Notifies that the command state has changed so the host
 25      can update toolbar buttons etc. }
 26    function UpdateUI: HResult; stdcall;
 27    { Called when a modal UI is displayed }
 28    function EnableModeless(
 29      const fEnable: BOOL): HResult; stdcall;
 30    { Called when the document window is activated or
 31      deactivated }
 32    function OnDocWindowActivate(
 33      const fActivate: BOOL): HResult; stdcall;
 34    { Called when the top-level frame window is activated or
 35      deactivated }
 36    function OnFrameWindowActivate(
 37      const fActivate: BOOL): HResult; stdcall;
 38    { Called when a frame or document's window's border is
 39      about to be changed }
 40    function ResizeBorder(
 41      const prcBorder: PRECT;
 42      const pUIWindow: IOleInPlaceUIWindow;
 43      const fFrameWindow: BOOL): HResult; stdcall;
 44    { Called when accelerator keys such as TAB are used }
 45    function TranslateAccelerator(
 46      const lpMsg: PMSG;
 47      const pguidCmdGroup: PGUID;
 48      const nCmdID: DWORD): HResult; stdcall;
 49    { Called by the web browser control to retrieve a registry
 50      subkey path that overrides the default IE registry settings }
 51    function GetOptionKeyPath(
 52      var pchKey: POLESTR;
 53      const dw: DWORD ): HResult; stdcall;
 54    { Called when the browser is used as a drop target and enables
 55      the host to supply an alternative IDropTarget interface }
 56    function GetDropTarget(
 57      const pDropTarget: IDropTarget;
 58      out ppDropTarget: IDropTarget): HResult; stdcall;
 59    { Called to obtain our IDispatch interface. Used to enable the
 60      browser to call methods in the host (e.g. from JavaScript) }
 61    function GetExternal(
 62      out ppDispatch: IDispatch): HResult; stdcall;
 63    { Gives the host an opportunity to modify the URL to be loaded }
 64    function TranslateUrl(
 65      const dwTranslate: DWORD;
 66      const pchURLIn: POLESTR;
 67      var ppchURLOut: POLESTR): HResult; stdcall;
 68    { Allows us to replace the web browser data object. It enables
 69      us to block certain clipboard formats or support additional
 70      clipboard formats }
 71    function FilterDataObject(
 72      const pDO: IDataObject;
 73      out ppDORet: IDataObject): HResult; stdcall;
 74  end;
Listing 10

The various interfaces and structures referenced in this definition are defined in the ActiveX and Windows units. The exception is (or was?) TDocHostUIInfo which is defined later in the article.

Now we have defined the interface we can create the "do nothing" implementation in our class. To begin with, add all the methods of IDocHostUIHandler to the protected section of TNulWBContainer's declaration. I have not shown the new declaration here since it will simply repeat the methods shown in Listing 10.

Listing 11 shows how we stub out the methods in such a way as to leave the browser object unchanged. An exploration of the MSDN documentation for the interface, along with some experimentation, showed me the way. The comments in the listing should explain.

  1function TNulWBContainer.EnableModeless(
  2  const fEnable: BOOL): HResult;
  3begin
  4  { Return S_OK to indicate we handled (ignored) OK }
  5  Result := S_OK;
  6end;
  7
  8function TNulWBContainer.FilterDataObject(
  9  const pDO: IDataObject;
 10  out ppDORet: IDataObject): HResult;
 11begin
 12  { Return S_FALSE to show no data object supplied.
 13    We *must* also set ppDORet to nil }
 14  ppDORet := nil;
 15  Result := S_FALSE;
 16end;
 17
 18function TNulWBContainer.GetDropTarget(
 19  const pDropTarget: IDropTarget;
 20  out ppDropTarget: IDropTarget): HResult;
 21begin
 22  { Return E_FAIL since no alternative drop target supplied.
 23    We *must* also set ppDropTarget to nil }
 24  ppDropTarget := nil;
 25  Result := E_FAIL;
 26end;
 27
 28function TNulWBContainer.GetExternal(
 29  out ppDispatch: IDispatch): HResult;
 30begin
 31  { Return E_FAIL to indicate we failed to supply external object.
 32    We *must* also set ppDispatch to nil }
 33  ppDispatch := nil;
 34  Result := E_FAIL;
 35end;
 36
 37function TNulWBContainer.GetHostInfo(
 38  var pInfo: TDocHostUIInfo): HResult;
 39begin
 40  { Return S_OK to indicate UI is OK without changes }
 41  Result := S_OK;
 42end;
 43
 44function TNulWBContainer.GetOptionKeyPath(
 45  var pchKey: POLESTR;
 46  const dw: DWORD): HResult;
 47begin
 48  { Return E_FAIL to indicate we failed to override
 49    default registry settings }
 50  Result := E_FAIL;
 51end;
 52
 53function TNulWBContainer.HideUI: HResult;
 54begin
 55  { Return S_OK to indicate we handled (ignored) OK }
 56  Result := S_OK;
 57end;
 58
 59function TNulWBContainer.OnDocWindowActivate(
 60  const fActivate: BOOL): HResult;
 61begin
 62  { Return S_OK to indicate we handled (ignored) OK }
 63  Result := S_OK;
 64end;
 65
 66function TNulWBContainer.OnFrameWindowActivate(
 67  const fActivate: BOOL): HResult;
 68begin
 69  { Return S_OK to indicate we handled (ignored) OK }
 70  Result := S_OK;
 71end;
 72
 73function TNulWBContainer.ResizeBorder(
 74  const prcBorder: PRECT;
 75  const pUIWindow: IOleInPlaceUIWindow;
 76  const fFrameWindow: BOOL): HResult;
 77begin
 78  { Return S_FALSE to indicate we did nothing in response }
 79  Result := S_FALSE;
 80end;
 81
 82function TNulWBContainer.ShowContextMenu(
 83  const dwID: DWORD;
 84  const ppt: PPOINT;
 85  const pcmdtReserved: IInterface;
 86  const pdispReserved: IDispatch): HResult;
 87begin
 88  { Return S_FALSE to notify we didn't display a menu and to
 89    let browser display its own menu }
 90  Result := S_FALSE
 91end;
 92
 93function TNulWBContainer.ShowUI(
 94  const dwID: DWORD;
 95  const pActiveObject: IOleInPlaceActiveObject;
 96  const pCommandTarget: IOleCommandTarget;
 97  const pFrame: IOleInPlaceFrame;
 98  const pDoc: IOleInPlaceUIWindow): HResult;
 99begin
100  { Return S_OK to say we displayed own UI }
101  Result := S_OK;
102end;
103
104function TNulWBContainer.TranslateAccelerator(
105  const lpMsg: PMSG;
106  const pguidCmdGroup: PGUID;
107  const nCmdID: DWORD): HResult;
108begin
109  { Return S_FALSE to indicate no accelerators are translated }
110  Result := S_FALSE;
111end;
112
113function TNulWBContainer.TranslateUrl(
114  const dwTranslate: DWORD;
115  const pchURLIn: POLESTR;
116  var ppchURLOut: POLESTR): HResult;
117begin
118  { Return E_FAIL to indicate that no translations took place }
119  Result := E_FAIL;
120end;
121
122function TNulWBContainer.UpdateUI: HResult;
123begin
124  { Return S_OK to indicate we handled (ignored) OK }
125  Result := S_OK;
126end;
Listing 11

That completes the implementation of the "do nothing" base class. If we were to create an instance of this object it would successfully host a TWebBrowser control, but would have no visible effect upon it.

Developing the customization class

As we decided above, TWBContainer will derive from TNulWBContainer and add all the required browser customization. Let us review what we want this customization to achieve:

  1. Display either the browser's built in pop-up menu or the menu assigned to the TWebBrowser.PopupMenu property.
  2. Display or hide 3D borders.
  3. Display or hide scroll bars.
  4. Customize document appearance at run time, via a custom cascading style sheet.
  5. Allow or inhibit users from selecting text in the browser control.
  6. The browser control will display themed controls (such as buttons) only when the host application is using themes.

We will define properties to configure the customization. We will then need to re-implement just two methods of IDocHostUIHandler to achieve the desired results:

  1. ShowContextMenu to control the display of the popup menu;
  2. GetHostInfo to enable the browser control's appearance to be customized.

Listing 12 shows the declaration of TWBContainer.

  1type
  2  TWBContainer = class(TNulWBContainer,
  3    IDocHostUIHandler, IOleClientSite)
  4  private
  5    fUseCustomCtxMenu: Boolean;
  6    fShowScrollBars: Boolean;
  7    fShow3DBorder: Boolean;
  8    fAllowTextSelection: Boolean;
  9    fCSS: string;
 10  protected
 11    { Re-implemented IDocHostUIHandler methods }
 12    function ShowContextMenu(const dwID: DWORD;
 13      const ppt: PPOINT; const pcmdtReserved: IUnknown;
 14      const pdispReserved: IDispatch): HResult; stdcall;
 15    function GetHostInfo(var pInfo: TDocHostUIInfo): HResult; stdcall;
 16  public
 17    constructor Create(const HostedBrowser: TWebBrowser);
 18    property UseCustomCtxMenu: Boolean
 19      read fUseCustomCtxMenu write fUseCustomCtxMenu default False;
 20    property Show3DBorder: Boolean
 21      read fShow3DBorder write fShow3DBorder default True;
 22    property ShowScrollBars: Boolean
 23      read fShowScrollBars write fShowScrollBars default True;
 24    property AllowTextSelection: Boolean
 25      read fAllowTextSelection write fAllowTextSelection default True;
 26    property CSS: string
 27      read fCSS write fCSS;
 28  end;
Listing 12

Notice that we expose a property for each of the aspects of the browser control that we will customize, with the exception of theme support which we handle automatically. The properties are:

  • UseCustomCtxMenu – the browser control displays its default context menu when the property is false and uses the menu assigned to TWebBrowser.PopupMenu when true. If UseContextMenu is true but PopupMenu is not assigned then no popup menu is displayed.
  • Show3DBorder – the web browser displays a 3D border when the property is true and no border when false.
  • ShowScrollBarsTWebBrowser displays scroll bars only if the property is true.
  • AllowTextSelection – permits text selection in the browser control when true and inhibits it when false.
  • CSS – provides the default cascading style sheet. This string property must contain valid CSS or be set to the empty string. We use this property to customize the document appearance.

The class constructor simply sets the default property values. These are chosen to leave the browser control in its default state. Listing 13 shows the constructor.

  1constructor TWBContainer.Create(const HostedBrowser: TWebBrowser);
  2begin
  3  inherited;
  4  fUseCustomCtxMenu := False;
  5  fShowScrollBars := True;
  6  fShow3DBorder := True;
  7  fAllowTextSelection := True;
  8  fCSS := '';
  9end;
Listing 13

Now we come on to the meat of the code. First let's look at how we re-implement ShowContextMenu. This is easier to write than to describe. See Listing 14 below.

  1function TWBContainer.ShowContextMenu(
  2  const dwID: DWORD;
  3  const ppt: PPOINT;
  4  const pcmdtReserved: IInterface;
  5  const pdispReserved: IDispatch): HResult;
  6begin
  7  if fUseCustomCtxMenu then
  8  begin
  9    // tell IE we're handling the context menu
 10    Result := S_OK;
 11    if Assigned(HostedBrowser.PopupMenu) then
 12      // browser has a pop up menu so activate it
 13      HostedBrowser.PopupMenu.Popup(ppt.X, ppt.Y);
 14  end
 15  else
 16    // tell IE to use default action: display own menu
 17    Result := S_FALSE;
 18end;
Listing 14

We first check the fUseCustomCtxMenu field to see what to do. If it is false we simply return S_FALSE to tell the web browser we have not handled the context menu. This causes the browser to display its default pop-up menu.

When fUseCustomCtxMenu is true we return S_OK to show we are handling the context menu ourselves. This prevents the default pop-up menu from being displayed. If the browser control's PopupMenu property is set we display the menu by calling its Popup method. The ppt parameter supplies the co-ordinates where the mouse was right clicked. We use this to position the top left corner of the pop-up menu.

GetHostInfo is more complex because it determines the display of the border, scroll bars, text selection, theme support and the default style sheet. We instruct the browser control about how to handle these items by filling in a TDocHostUIInfo structure, a pointer to which is passed as a parameter to the method. Listing 15 defines this structure and comments describe its fields. Listing 16 presents GetHostInfo itself.

  1type
  2  TDocHostUIInfo = record
  3    cbSize: ULONG;          // size of structure in bytes
  4    dwFlags: DWORD;         // flags that specify UI capabilities
  5    dwDoubleClick: DWORD;   // specified response to double click
  6                            // ** some browser versions ignore this field
  7    pchHostCss: PWChar;     // pointer to CSS rules
  8    pchHostNS: PWChar;      // pointer to namespace list for custom tags
  9  end;
Listing 15
  1function TWBContainer.GetHostInfo(
  2  var pInfo: TDocHostUIInfo): HResult;
  3const
  4  DOCHOSTUIFLAG_SCROLL_NO = $00000008;
  5  DOCHOSTUIFLAG_NO3DBORDER = $00000004;
  6  DOCHOSTUIFLAG_DIALOG = $00000001;
  7  DOCHOSTUIFLAG_THEME = $00040000;
  8  DOCHOSTUIFLAG_NOTHEME = $00080000;
  9begin
 10  try
 11    // Clear structure and set size
 12    ZeroMemory(@pInfo, SizeOf(TDocHostUIInfo));
 13    pInfo.cbSize := SizeOf(TDocHostUIInfo);
 14    
 15    // Set scroll bar visibility
 16    if not fShowScrollBars then
 17      pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_SCROLL_NO;
 18    
 19    // Set border visibility
 20    if not fShow3DBorder then
 21      pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_NO3DBORDER;
 22    
 23    // Determine if text can be selected
 24    if not fAllowTextSelection then
 25      pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_DIALOG;
 26        
 27    // Ensure browser uses themes if application is doing so
 28    if ThemeServices.ThemesEnabled then
 29      pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_THEME
 30    else if ThemeServices.ThemesAvailable then
 31      pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_NOTHEME;
 32        
 33    // Record default CSS as Unicode
 34    pInfo.pchHostCss := TaskAllocWideString(fCSS);
 35    if not Assigned(pInfo.pchHostCss) then
 36      raise Exception.Create(
 37        'Task allocator can''t allocate CSS string'
 38      );
 39    
 40    // Return S_OK to indicate we've made changes
 41    Result := S_OK;
 42  except
 43    // Return E_FAIL on error
 44    Result := E_FAIL;
 45  end;  
 46end;
Listing 16

For our purposes we only need to use the cbSize, dwFlags and pchHostCss fields of TDocHostUIInfo. We set the remaining fields to zero. cbSize is set to the size of the structure – a common Windows idiom.

Next we decide which flags to store in dwFlags to customize the control's user interface capabilities. Which flags we specify depends on the state of the ShowScrollBars, Show3DBorder and AllowTextSelection properties and whether themes are enabled. The flags we use are:

  • DOCHOSTUIFLAG_SCROLL_NO prevents the vertical scroll bar from being displayed.
  • DOCHOSTUIFLAG_NO3DBORDER inhibits the display of 3D borders.
  • Slightly less obviously, DOCHOSTUIFLAG_DIALOG prevents text selection – the name comes from the main use of this flag to prevent text selection in dialogue boxes.
  • More complex is the way we ensure that the application's themes are echoed by the web browser control. We use the Themes unit's ThemeServices object to check if themes are enabled. If so we use the DOCHOSTUIFLAG_THEME flag to request that the browser also uses themes. If themes are not enabled, but available, we switch off the browser's theme support by using DOCHOSTUIFLAG_NOTHEME. If themes are not available at all (e.g. in Windows 2000) we do nothing.

Other flags

There are numerous other DOCHOSTUIFLAG_ flags that can be assigned to dwFlags. The full set is defined in this article's demo code.

Lastly, we store the default style sheet (per the CSS property) as Unicode text in the pchHostCss field. We have to allocate storage for this Unicode string – and we must use the task allocator to do this. Note that we are not responsible for freeing this memory – TWebBrowser does this.

We use the TaskAllocWideString helper function (taken from the DelphiDabbler Code Snippets Database) to allocate the storage and ensure the string is in Unicode format. The routine is shown in Listing 17. Note that this code works on both Unicode and non-Unicode Delphis.

  1function TaskAllocWideString(const S: string): PWChar;
  2var
  3  StrLen: Integer;  // length of string in bytes
  4begin
  5  // Store length of string in characters, allowing for terminal #0
  6  StrLen := Length(S) + 1;
  7  // Allocate buffer for wide string using task allocator
  8  Result := CoTaskMemAlloc(StrLen * SizeOf(WideChar));
  9  if Assigned(Result) then
 10    // Convert string to wide string and store in buffer
 11    StringToWideChar(S, Result, StrLen);
 12end;
Listing 17

And that completes the browser customization code. If you need to specify different customizations define a new class that descends from TNulWBContainer and provide the required functionality by re-implementing the appropriate IDocHostUIHandler methods.

Other examples

Article #22: "How to call Delphi code from scripts running in a TWebBrowser" demonstrates another class that descends from TNulWBContainer, this time re-implementing the GetExternal method.

To use our customization class create an instance of TWBContainer, passing a reference to the browser control that is to be customized, set the required properties of TWBContainer then load the required document.

Note for legacy OSs

You should always set the properties of TWBContainer before loading any document into the browser control. The reason for this is that on operating systems earlier than Windows XP SP2 the control only reads the default CSS when the first document is loaded. Changing the CSS property after loading the first document will have no effect.

Exercising the code – a sample application

One of the problems that motivated this exploration was my need to use a TWebBrowser to display some HTML in a dialogue box. The HTML needed to look and behave as if it was part of the dialogue box, which could be using themes. The dialogue box would also need to display a custom pop-up menu.

This sample application will emulate such a dialogue box. It will use our custom TWBContainer to take control over the appearance of a TWebBrowser control. This will not be production code – for example we load the HTML from a file and we wouldn't do that in released code – but it should suffice to demonstrate that the customization class works.

The application will be developed in two stages. Stage one will be the bare application with no TWebBrowser customization code. We will run the program to check what it looks like in its natural state before we intervene. In stage two we'll apply the classes we've developed here and see the difference.

Stage 1

Open a new Delphi project and select the main form, then:

  • Give the form a BorderStyle of bsDialog.
  • Place a TButton centred at the bottom of the form, set its Caption to 'Close' and create an OnClick event handler for it containing just the code shown in Listing 18:

      1procedure TForm1.Button1Click(Sender: TObject);
      2begin
      3  Close;  // close the application
      4end;
    Listing 18
  • Add a TPopupMenu and give it a single menu item with Caption set to 'Show the CSS'.
  • Drop a TWebBrowser control, set its Align property to alTop and size it to take up most of the form. Leave room for the 'Close' button below it. Set the TWebBrowser's' PopupMenu property to PopupMenu1.
  • Add the XPMan unit (Delphi 7 and later) to the form's uses clause to ensure the application displays themes if available.

    Note: This step may not be necessary with later Delphis that can enable themes by default in the project resource file.

Now, double-click the form to open a new OnCreate event handler and enter the code shown in Listing 19. This loads the HTML file into the browser:

  1procedure TForm1.FormCreate(Sender: TObject);
  2begin;
  3  // load content
  4  WebBrowser1.Navigate(
  5    ExtractFilePath(ParamStr(0)) + 'DlgContent.html'
  6  );
  7end;
Listing 19

Finally create the HTML file, DlgContent.html, that is loaded in FormCreate above. This file will provide the content of our dialogue box. Listing 20 shows the HTML.

  1<html>
  2  <head>
  3    <title>Demo Dialogue Content</title>
  4    <script type="text/javascript">
  5      function ViewArticle() {
  6        wdw = window.open();
  7        wdw.document.location =
  8          "https://delphidabbler.com/articles/article-18";
  9      }
 10    </script>
 11  </head>
 12  <body>
 13    <h1>
 14      About this demo
 15    </h1>
 16    <p>
 17      This demo relates to the DelphiDabbler.com article &quot;How to
 18      customise the TWebBrowser user interface &quot;.
 19      <input
 20        type="button"
 21        name="button"
 22        id="button"
 23        value="View article..."
 24        onclick="ViewArticle();"
 25      />
 26    </p>
 27    <p>
 28      &copy; Copyright P D Johnson
 29      (<a href="https://delphidabbler.com/"
 30      target="_blank">delphidabbler.com</a>), 2004-2022.
 31    </p>
 32    <p class="ruled">
 33      Right click above the line to see the custom pop-up menu.
 34    </p>
 35  </body>
 36</html>
Listing 20

This HTML defines a document which has:

  • Some plain text including a paragraph styled using a CSS class named .ruled that is not defined in the document.
  • A button that, when clicked, runs some JavaScript that opens a new window and displays this article in it.
  • Some clickable text that opens a new browser window and navigates to the DelphiDabbler.com home page.

If you compile and run this application then right click in the browser control you should see something like this:

TWebBrowser showing normal UI and context menu on Windows XP
Image 1: TWebBrowser showing normal UI

Doesn't look much like a dialogue box does it? There are numerous problems:

  1. The browser control is displaying its default border and scroll bar.
  2. The HTML is being displayed in the default style.
  3. The form's 'Close' button is themed while the 'View article' button displayed by the browser control is not.
  4. Despite setting the PopupMenu property, the standard browser's context menu is still displayed.
  5. Although it can't be seen here, you could select the text in the HTML document.

Let us now adapt the program to correct all these problems.

Stage 2

We will now utilize the classes we have developed in this article. To begin with add the units containing IDocHOstUIHandler, TNulWBContainer and TWBContainer to the project and refer to them in the main form's uses clause. Also add the ActiveX and SysUtils units to the uses clause.

It's now time to set up and use the container object. Switch to the form unit we developed in Stage 1 and add a field named fWBContainer of type TWBContainer to the form's class declaration. Now rewrite the form's OnCreate event handler as shown in Listing 21.

  1procedure TForm1.FormCreate(Sender: TObject);
  2const
  3  // Template for default CSS style
  4  cCSSTplt = 'body {'#13#10
  5    + '    background-color: %0:s;'#13#10
  6    + '    color: %1:s;'#13#10
  7    + '    font-family: "%2:s";'#13#10
  8    + '    font-size: %3:dpt;'#13#10
  9    + '    margin: 4px;'#13#10
 10    + '}'#13#10
 11    + 'h1 {'#13#10
 12    + '    font-size: %3:dpt;'#13#10
 13    + '    font-weight: bold;'#13#10
 14    + '    text-align: center;'#13#10
 15    + '}'#13#10
 16    + 'input#button {'#13#10
 17    + '    color: %1:s;'#13#10
 18    + '    font-family: "%2:s";'#13#10
 19    + '    font-size: %3:dpt;'#13#10
 20    + '}'#13#10
 21    + '.ruled {'#13#10
 22    + '    border-bottom: %4:s solid 2px;'#13#10
 23    + '    padding-bottom: 6px;'#13#10
 24    + '}';
 25var
 26  FmtCSS: string;  // Stores default CSS
 27begin
 28  // Create the CSS from system colours
 29  FmtCSS := Format(
 30    cCSSTplt,
 31    [ColorToHTML(Self.Color), ColorToHTML(Self.Font.Color),
 32    Self.Font.Name, Self.Font.Size,
 33    ColorToHTML(clInactiveCaption)]
 34  );
 35  // Create web browser container and set required properties
 36  fWBContainer := TWBContainer.Create(WebBrowser1);
 37  fWBContainer.UseCustomCtxMenu := True;    // use our popup menu
 38  fWBContainer.Show3DBorder := False;       // no border
 39  fWBContainer.ShowScrollBars := False;     // no scroll bars
 40  fWBContainer.AllowTextSelection := False; // no text selection
 41  fWBContainer.CSS := FmtCSS;               // CSS to be used
 42  // load content
 43  fWBContainer.HostedBrowser.Navigate(
 44    ExtractFilePath(ParamStr(0)) + 'DlgContent.html'
 45  );
 46end;
Listing 21

Key points to note in this method are:

  • First we provide a template for a cascading style sheet that we will use to make the HTML take on the appearance of the dialogue box. This template includes some format strings as placeholders for colour and font information that will be filled in at run time.
  • Next we fill in the required values in the CSS template and store the resulting code in FmtCSS. We get the colours and font from various properties of the form and from suitable system colours. Note that we use the ColorToHTML helper function (taken from the Code Snippets Database) to format colour information correctly:

      1function ColorToHTML(const Color: TColor): string;
      2var
      3  ColorRGB: Integer;
      4begin
      5  ColorRGB := ColorToRGB(Color);
      6  Result := Format(
      7    '#%0.2X%0.2X%0.2X',
      8    [GetRValue(ColorRGB), GetGValue(ColorRGB), GetBValue(ColorRGB)]
      9  );
     10end;
    Listing 22
  • Next we create the container object, passing the hosted TWebBrowser as a parameter, then set the properties as required.
  • Finally we revert to the original code and load the document, except that we use the container object's HostedBrowser property to access the browser control rather than accessing it directly.

A couple more things remain to be done. The first is to handle the form's OnDestroy event to free the container object, as shown in Listing 23.

  1procedure TForm1.FormDestroy(Sender: TObject);
  2begin
  3  fWBContainer.Free;  // free the container pbject
  4end;
Listing 23

Finally we need to handle the OnClick event of our single context menu item. All we do here is display the default CSS in a message box, as Listing 24 illustrates.

  1procedure TForm1.ShowtheCSS1Click(Sender: TObject);
  2begin
  3  ShowMessage(fWBContainer.CSS);  // display the CSS code
  4end;
Listing 24

The moment of truth arrives. Let's run the revised application and see if it all works. Here's what we see on Windows XP with themes enabled:

User-styled, themed, TWebBrowser showing custom context menu on Windows XP
Image 2: User-styled, themed, TWebBrowser showing custom context menu

Notice that:

  • The borders and scroll bar have gone.
  • The HTML is displayed in the correct dialogue box font and colours.
  • The 'View article' button is correctly themed.
  • The paragraph with the .ruled CSS class now has a bottom border in the same colour as an inactive caption.
  • Right clicking displays the single menu item of the TPopupMenu we assigned to the browser control's PopupMenu property.
  • Take it on trust that you can't select text either!

Just to show that the browser control correctly adopts the current dialogue box style, here is the same application running unchanged in on Windows XP using its classic style with the Spruce colour scheme:

User-styled, un-themed, TWebBrowser showing custom context menu on Windows XP using the Classic style
Image 2: User-styled, un-themed, TWebBrowser showing custom context menu

All the code has now been completed. The source code can be downloaded in the next section .

Demo 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-18 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.

Summary

In this article we have investigated how to customize the appearance of a TWebBrowser control.

We noted that we can control the appearance and behaviour of TWebBrowser by developing a container object that implements the IDocHostUIHandler interface. In order for the browser control to find the IDocHostUIHandler implementation we also needed to implement IOleClientSite.

We first created a reusable, "do nothing" container class that provided a minimal implementation of the required interfaces. Then we derived a custom class from the "do nothing" container that provided the required customization. We also exposed properties to enable users to manipulate the browser's appearance and behaviour. Finally we developed a sample application to exercise the customizable container.

A downloadable demo program was made available that includes all the code discussed in the article.

References

Several references from MSDN were used in researching this article. They are:

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 #18".
  2. For bugs in the demo code see the article-demo project's README.md file for details of how to report them.