How to customise the TWebBrowser user interface
Introduction
Among the most popular questions posed in the TWebBrowser newsgroups and discussed on many Delphi tips sites are:
- How do you get a TWebBrowser control to display the popup menu assigned to its PopupMenu property instead of the standard IE popup menu?
- How do you stop TWebBrowser displaying 3D borders when a document is loaded.
- 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:
- 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.
- Users can normally select text in a browser control – something you don't want in a dialogue box. So how do you stop that?
- 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
2WB.OleObject.document.body.style.overflowX := 'hidden';
3WB.OleObject.document.body.style.overflowY := 'hidden';
4
5
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.
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
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
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
8 ...
9
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
4
5begin
6
7
8 container := nil;
9 Result := E_NOINTERFACE;
10end;
11
12function TNulWBContainer.GetMoniker(dwAssign, dwWhichMoniker: Integer;
13 out mk: IMoniker): HResult;
14
15begin
16
17
18 mk := nil;
19 Result := E_NOTIMPL;
20end;
21
22function TNulWBContainer.OnShowWindow(fShow: BOOL): HResult;
23
24
25begin
26
27 Result := S_OK;
28end;
29
30function TNulWBContainer.RequestNewObjectLayout: HResult;
31
32
33begin
34
35 Result := E_NOTIMPL;
36end;
37
38function TNulWBContainer.SaveObject: HResult;
39
40begin
41
42 Result := S_OK;
43end;
44
45function TNulWBContainer.ShowObject: HResult;
46
47
48begin
49
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
5 function ShowContextMenu(
6 const dwID: DWORD;
7 const ppt: PPOINT;
8 const pcmdtReserved: IUnknown;
9 const pdispReserved: IDispatch): HResult; stdcall;
10
11
12 function GetHostInfo(
13 var pInfo: TDocHostUIInfo): HResult; stdcall;
14
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
22
23 function HideUI: HResult; stdcall;
24
25
26 function UpdateUI: HResult; stdcall;
27
28 function EnableModeless(
29 const fEnable: BOOL): HResult; stdcall;
30
31
32 function OnDocWindowActivate(
33 const fActivate: BOOL): HResult; stdcall;
34
35
36 function OnFrameWindowActivate(
37 const fActivate: BOOL): HResult; stdcall;
38
39
40 function ResizeBorder(
41 const prcBorder: PRECT;
42 const pUIWindow: IOleInPlaceUIWindow;
43 const fFrameWindow: BOOL): HResult; stdcall;
44
45 function TranslateAccelerator(
46 const lpMsg: PMSG;
47 const pguidCmdGroup: PGUID;
48 const nCmdID: DWORD): HResult; stdcall;
49
50
51 function GetOptionKeyPath(
52 var pchKey: POLESTR;
53 const dw: DWORD ): HResult; stdcall;
54
55
56 function GetDropTarget(
57 const pDropTarget: IDropTarget;
58 out ppDropTarget: IDropTarget): HResult; stdcall;
59
60
61 function GetExternal(
62 out ppDispatch: IDispatch): HResult; stdcall;
63
64 function TranslateUrl(
65 const dwTranslate: DWORD;
66 const pchURLIn: POLESTR;
67 var ppchURLOut: POLESTR): HResult; stdcall;
68
69
70
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
5 Result := S_OK;
6end;
7
8function TNulWBContainer.FilterDataObject(
9 const pDO: IDataObject;
10 out ppDORet: IDataObject): HResult;
11begin
12
13
14 ppDORet := nil;
15 Result := S_FALSE;
16end;
17
18function TNulWBContainer.GetDropTarget(
19 const pDropTarget: IDropTarget;
20 out ppDropTarget: IDropTarget): HResult;
21begin
22
23
24 ppDropTarget := nil;
25 Result := E_FAIL;
26end;
27
28function TNulWBContainer.GetExternal(
29 out ppDispatch: IDispatch): HResult;
30begin
31
32
33 ppDispatch := nil;
34 Result := E_FAIL;
35end;
36
37function TNulWBContainer.GetHostInfo(
38 var pInfo: TDocHostUIInfo): HResult;
39begin
40
41 Result := S_OK;
42end;
43
44function TNulWBContainer.GetOptionKeyPath(
45 var pchKey: POLESTR;
46 const dw: DWORD): HResult;
47begin
48
49
50 Result := E_FAIL;
51end;
52
53function TNulWBContainer.HideUI: HResult;
54begin
55
56 Result := S_OK;
57end;
58
59function TNulWBContainer.OnDocWindowActivate(
60 const fActivate: BOOL): HResult;
61begin
62
63 Result := S_OK;
64end;
65
66function TNulWBContainer.OnFrameWindowActivate(
67 const fActivate: BOOL): HResult;
68begin
69
70 Result := S_OK;
71end;
72
73function TNulWBContainer.ResizeBorder(
74 const prcBorder: PRECT;
75 const pUIWindow: IOleInPlaceUIWindow;
76 const fFrameWindow: BOOL): HResult;
77begin
78
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
89
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
101 Result := S_OK;
102end;
103
104function TNulWBContainer.TranslateAccelerator(
105 const lpMsg: PMSG;
106 const pguidCmdGroup: PGUID;
107 const nCmdID: DWORD): HResult;
108begin
109
110 Result := S_FALSE;
111end;
112
113function TNulWBContainer.TranslateUrl(
114 const dwTranslate: DWORD;
115 const pchURLIn: POLESTR;
116 var ppchURLOut: POLESTR): HResult;
117begin
118
119 Result := E_FAIL;
120end;
121
122function TNulWBContainer.UpdateUI: HResult;
123begin
124
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:
- Display either the browser's built in pop-up menu or the menu assigned to the TWebBrowser.PopupMenu property.
- Display or hide 3D borders.
- Display or hide scroll bars.
- Customize document appearance at run time, via a custom cascading style sheet.
- Allow or inhibit users from selecting text in the browser control.
- 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:
- ShowContextMenu to control the display of the popup menu;
- 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
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.
- ShowScrollBars – TWebBrowser 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
10 Result := S_OK;
11 if Assigned(HostedBrowser.PopupMenu) then
12
13 HostedBrowser.PopupMenu.Popup(ppt.X, ppt.Y);
14 end
15 else
16
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;
4 dwFlags: DWORD;
5 dwDoubleClick: DWORD;
6
7 pchHostCss: PWChar;
8 pchHostNS: PWChar;
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
12 ZeroMemory(@pInfo, SizeOf(TDocHostUIInfo));
13 pInfo.cbSize := SizeOf(TDocHostUIInfo);
14
15
16 if not fShowScrollBars then
17 pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_SCROLL_NO;
18
19
20 if not fShow3DBorder then
21 pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_NO3DBORDER;
22
23
24 if not fAllowTextSelection then
25 pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_DIALOG;
26
27
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
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
41 Result := S_OK;
42 except
43
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;
4begin
5
6 StrLen := Length(S) + 1;
7
8 Result := CoTaskMemAlloc(StrLen * SizeOf(WideChar));
9 if Assigned(Result) then
10
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.
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;
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
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 "How to
18 customise the TWebBrowser user interface ".
19 <input
20 type="button"
21 name="button"
22 id="button"
23 value="View article..."
24 onclick="ViewArticle();"
25 />
26 </p>
27 <p>
28 © 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:
Image 1: TWebBrowser showing normal UI
Doesn't look much like a dialogue box does it? There are numerous problems:
- The browser control is displaying its default border and scroll bar.
- The HTML is being displayed in the default style.
- The form's 'Close' button is themed while the 'View article' button displayed by the browser control is not.
- Despite setting the PopupMenu property, the standard browser's context menu is still displayed.
- 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
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;
27begin
28
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
36 fWBContainer := TWBContainer.Create(WebBrowser1);
37 fWBContainer.UseCustomCtxMenu := True;
38 fWBContainer.Show3DBorder := False;
39 fWBContainer.ShowScrollBars := False;
40 fWBContainer.AllowTextSelection := False;
41 fWBContainer.CSS := FmtCSS;
42
43 fWBContainer.HostedBrowser.Navigate(
44 ExtractFilePath(ParamStr(0)) + 'DlgContent.html'
45 );
46end;
Listing 21
Key points to note in this method are:
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;
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);
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:
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:
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.
- 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".
- For bugs in the demo code see the
article-demo
project's README.md
file for details of how to report them.