Implementing the external object
As already noted we extend the external object by creating a COM automation object – i.e. one that implements an interface that derives from IDispatch.
An easy way to do this is to derive the new class from TAutoIntfObject. This class implements the methods of IDispatch and so saves us from having to do this ourselves. However, TAutoIntfObject needs a type library to work with. Consequently we will use Delphi's Type Library editor to create a suitable type library that defines our new interface.
Once we have defined the required interface in the Type Library Editor we create the type library by pressing Ctrl+S. This will do three things:
- Depending on your version of Delphi it will either create the type library (
.tlb
) file directly or create an intermediate .ridl
file that will be compiled to the required .tlb
file when you next build the program.
- Create a Pascal unit containing, amongst other things, the interface definition. The unit will have the same name as the
.tlb
file but will end in _TLB.pas
.
- Add a reference to the
_TLB.pas
unit to the project file.
Some versions of the type library editor will also include the type library in the program's resources by inserting a suitable $R compiler directive in the project's .dpr
file. Other versions don't do this for you. If this is the case you need to add the line {$R *.tlb} to your .dpr
file.
When creating the interface in the Type Library Editor we must abide by the following rules:
- Ensure the new interface is derived from IDispatch.
- Ensure all methods have a return value of HRESULT.
- Use only automation compatible parameter types.
- Ensure all [out] parameters are pointer types, i.e. they end with * (for example, BSTR *).
- Return any values from methods via a parameter that has an [out,retval] modifier.
Once we have our new type library and interface we create a new class that descends from TAutoIntfObject. Then we implement the methods of our interface in the class. This can be done by copying the method prototypes from the interface declaration in the *_TLB.pas
file and pasting them into the class's declaration.
Note that Delphi creates the method prototypes using the safecall calling convention which means that any [out,retval] parameters become function results. For example, suppose we use the Type Library Editor to create an interface called IMyIntf that has two methods, Foo and Bar. Assume the method parameters are defined as in Table 1.
Method |
Parameters |
Type |
Modifiers |
Foo |
Param1 |
long |
[in] |
Result |
BSTR * |
[out,retval] |
Bar |
Param1 |
BSTR |
[in] |
Table 1
The *_TLB.pas
file created by Delphi would contain the following interface definition shown in Listing 1, except that the interface's GUID will be different.
1type
2 IMyIntf = interface(IDispatch)
3 ['{E1CDA762-584F-4E9E-9808-2312C689FA17}']
4 function Foo(Param1: Integer): WideString; safecall;
5 procedure Bar(const Param1: WideString); safecall;
6 end;
Listing 1
We would therefore include the following methods in our class declaration:
1type
2 TMyClass = class(TAutoIntfObject,
3 IMyIntf, IDispatch
4 )
5 private
6 ...
7 protected
8
9 function Foo(Param1: Integer): WideString; safecall;
10 procedure Bar(const Param1: WideString); safecall;
11 ...
12 end;
Listing 2
These methods would then be implemented as required.
Remember that we don't need to declare or implement anymethods of IDispatch since they are already implemented by TAutoIntfObject.
Now TAutoIntfObject's implementation of the IDispatch methods depends on having access to the type library that describes the methods of the interfaces implemented by descendent classes. This is achieved by passing an object, that implements ITypeLib as a parameter, to TAutoIntfObject's constructor. It is our job to create such an ITypeLib object that "knows about" our type library.
We do this by declaring a parameterless constructor for the derived class. In the constructor we call the LoadTypeLib API function, passing the name of our application as a parameter. LoadTypeLib accesses the type library information that is embedded in the application's resources and creates the required ITypeLib object based on this information. We then pass this ITypeLib object to the inherited constructor. Assuming our derived class is named TMyExternal, Listing 3 shows the constructor's implementation.
1constructor TMyExternal.Create;
2var
3 TypeLib: ITypeLib;
4 ExeName: WideString;
5begin
6
7 ExeName := ParamStr(0);
8
9 OleCheck(LoadTypeLib(PWideChar(ExeName), TypeLib));
10
11 inherited Create(TypeLib, IMyExternal);
12
13
14
15end;
Listing 3
Case study
Overview
Our case study is a simple application that illustrates the techniques discussed in this article.
The program we will develop lists some of the programs available on the DelphiDabbler website. Clicking one of the program names will display a brief description (précis) of the program, in a box underneath the list of programs. When the mouse is moved over a program name, the URL of the its web page will be displayed in the status bar. The status bar will clear when there is no program name under the mouse. The précis of each program is stored in a data file that is read by our application.
Here is a screenshot of the completed program, compiled with Delphi 11.0 Alexandria as a 32 bit application and running on Windows 11:
We will develop the program in the following order:
- Design the main form.
- Define the required external object and create its type library / interface.
- Implement the external object.
- Implement the IDocHostUIHandler interface's GetExternal method.
- Register the IDocHostUIHandler implementation with the web browser control.
- Create the HTML file, containing the required JavaScript, that will be displayed in the browser control.
Designing the main form
To begin the project, start a new Delphi VCL GUI application and set up the main form as follows:
- Drop a TStatusBar and set its SimplePanel and AutoHint properties to True.
- Drop a TWebBrowser control and set its Align property to alClient.
That is all there is to the main form. SSave it with the default name, probably Unit1.pas
.
Defining the external object
Let us consider the methods we need to add to the browser's external object. From the specification above we see that we need methods to perform the following actions:
- Get the précis of a specified program when a program name is clicked (GetPrecis method). We will use an id string to uniquely identify each program.
- Display the URL of a program's web page in the status bar when the mouse cursor is over a program name (ShowURL method). Again we will pass the program's id as a parameter.
- Clear the status bar when no program's name is under the cursor (HideURL method).
We can now create an interface that contains each of the required methods. Start Delphi's Type Library Editor and use it to create a new interface named IMyExternal. Make sure the new interface has IDispatch as it parent interface. Now add three methods using the information in Table 2, ensuring that each method has a return type of HRESULT.
Method |
Parameters |
Type |
Modifiers |
GetPrecis |
ProgID Result |
BSTR BSTR * |
[in] [out,retval] |
ShowURL |
ProgID |
BSTR |
[in] |
HideURL |
- |
- |
- |
Table 2
Press Ctrl+S to save the type library and name it either Article22.ridl
or Article22.tlb
, depending on the version of Delhi in use – just accept the suggested file type in any Save dialogue box. Either way, Delphi will now create a type library file named Article22.tlb
and a Pascal unit named Article22_TLB.pas
. Opening Article22_TLB.pas
will reveal the IMyExternal interface declaration shown in Listing 10. (Note that the GUID will be different):
1type
2 ...
3 IMyExternal = interface(IDispatch)
4 ['{4F995D09-CF9E-4042-993E-C71A8AED661E}']
5 function GetPrecis(const ProgID: WideString): WideString;
6 safecall;
7 procedure ShowURL(const ProgID: WideString); safecall;
8 procedure HideURL; safecall;
9 end;
10 ...
Listing 10
Article22_TLB.pas
will also contain a dispinterface named IMyExternalDisp. You can ignore this since we won't be using it.
Implementing the external object
Now that we have created the IMyExternal interface, we implement it in a class named TMyExternal. We will add a new unit, UMyExternal.pas
to store the class. Listing 11 has the class declaration.
1type
2 TMyExternal = class(TAutoIntfObject, IMyExternal, IDispatch)
3 private
4 fData: TStringList;
5 procedure ShowSBMsg(const Msg: string);
6 protected
7
8 function GetPrecis(const ProgID: WideString): WideString;
9 safecall;
10 procedure ShowURL(const ProgID: WideString); safecall;
11 procedure HideURL; safecall;
12 public
13 constructor Create;
14 destructor Destroy; override;
15 end;
Listing 11
As expected, the methods of IMyExternal are specified in the class' protected section. We also have a private helper method, ShowSBMsg, that displays a given message in the status bar. This method is used by both ShowURL and HideURL as we will see in a moment. The fData string list is used to store the précis of the different programs. This field is accessed by GetPrecis.
Let us look at the implementation of the class. We will start with the constructor and destructor shown in Listing 12:
1constructor TMyExternal.Create;
2var
3 TypeLib: ITypeLib;
4 ExeName: WideString;
5begin
6
7 ExeName := ParamStr(0);
8
9 OleCheck(LoadTypeLib(PWideChar(ExeName), TypeLib));
10
11 inherited Create(TypeLib, IMyExternal);
12
13 fData := TStringList.Create;
14 fData.LoadFromFile(ChangeFileExt(ExeName, '.dat'));
15end;
16
17destructor TMyExternal.Destroy;
18begin
19 fData.Free;
20 inherited;
21end;
Listing 12
The first three executable lines in the constructor are boilerplate code that has been explained earlier in the article. Following the call to the inherited constructor we create the TStringList object that is to store the précis data. We then read the string list's contents from a data file named Article22.dat
that is expected to be found in the same directory as the application. The format of the data file is described later. The destructor simply frees the string list.
Now move on to examine the implementation of the three IMyExternal methods shown in Listing 13:
1function TMyExternal.GetPrecis(
2 const ProgID: WideString): WideString;
3begin
4 Result := fData.Values[ProgId];
5end;
6
7procedure TMyExternal.HideURL;
8begin
9 ShowSBMsg('');
10end;
11
12procedure TMyExternal.ShowURL(const ProgID: WideString);
13begin
14 ShowSBMsg(
15 'https://delphidabbler.com/software/' + ProgID
16 );
17end;
Listing 13
GetPrecis looks up the given ProgID in the Values property of fData and returns the value found there. The data file that was loaded into fData in the constructor contains a line for each program. Each line has the format ProgID=Precis
. The TStringList.Values[] property is designed to work with strings in this format.
HideURL uses ShowSBMsg to display an empty string in the status bar, which has the effect of clearing any previous message.
ShowURL simply appends its ProgID parameter to a literal string to produce the URL of the program's web page. It then calls ShowSBMsg to display the URL in the status bar.
All that remains is to look at Listing 14, which shows the implementation of ShowSBMsg.
1procedure TMyExternal.ShowSBMsg(const Msg: string);
2var
3 HintAct: THintAction;
4begin
5 HintAct := THintAction.Create(nil);
6 try
7 HintAct.Hint := Msg;
8 HintAct.Execute;
9 finally
10 HintAct.Free;
11 end;
12end;
Listing 14
Hmm – no mention of the status bar! What's happening here is that we're creating an instance of the VCL's THintAction action class, storing the message we want to display in its Hint property then executing the action. A magical feature of THintAction is that it automatically displays its hint in any TStatusBar that has its AutoHint property set to True. This let's us decouple our external object implementation quite nicely from the program's form.
Finally, we need to add a uses clause, containing the following units in order for the code to compile: Classes, ComObj, SysUtils, ActiveX, StdActns and Article22_TLB.
A note about unit names
All unit names in this example are unqualified. Depending on which version of Delphi you are using you may need to qualify the unit names with the required unit scope name. Unit scopes can vary between versions of Delphi, so it is left to the reader to determine the required name qualificatons.
If you need help doing this you could try the DelphiDabbler Unit2NS program.
Implementing IDocHostUIHandler
As already noted, we are re-using code from an earlier article for our declaration of IDocHostUIHandler and for the do-nothing implementation of the interface, TNulWBContainer So, to begin with, we must add the IntfDocHostUIHandler.pas
and UNulContainer.pas
units, developed in that article, to our project.
Unit source code
Units containing IDocHostUIHandler & TNulWBContainer are included with this article's source code.
We now create our custom container class, TExternalContainer, by descending from TNulWBContainer and overriding the GetExternal method to get the functionality we need. We will use exactly the same code as we developed in listings 4 & 5.
We will create a new unit, UExternalContainer.pas
for TExternalContainer and add a uses clause that references the following units: ActiveX, SHDocVw, UNulContainer, IntfDocHostUIHandler and UMyExternal.
Registering the external object
Our final piece of Delphi code registers our TExternalContainer object as a client site (container) for the browser control. This is done in the main form simply by instantiating a TExternalContainer object and passing a reference to the browser control to its constructor. Recall that TExternalContainer's inherited constructor automatically registers the object as a client site of the contained web browser control.
We will use the form's OnShow event handler to create TExternalContainer. We will also use this event handler to load the required HTML file, as Listing 15 shows:
1procedure TForm1.FormShow(Sender: TObject);
2begin
3 fContainer := TExternalContainer.Create(WebBrowser1);
4 WebBrowser1.Navigate(
5 ExtractFilePath(ParamStr(0)) + 'Article22.html'
6 );
7end;
Listing 15
Notice that we have stored a reference to the container object in a field named fContainer. Add such a field, with type TExternalContainer to the form's declaration.
Use FormShow and not FormCreate
We have to create TExternalContainer in the form's OnShow event handler instead of OnCreate (which is where we usually create objects) because the contained TWebBrowser will raise an exception if we create the container object in OnCreate. Delaying construction until the OnShow event handler gets called solves this problem.
Why? I don't really know, but I'm guessing that TWebBrowser requires the host form window to be created before it can interact with any container object.
Having created the container object we must also ensure it gets freed. This is done in the form's OnHide event handler as Listing 16 illustrates:
1procedure TForm1.FormHide(Sender: TObject);
2begin
3 fContainer.Free;
4end;
Listing 16
The last step is to add UExternalContainer to the main form's uses clause.
Creating the HTML file
We now need to create the HTML file that we loaded in Listing 15. This file is named Article22.html
and listing 17 shows the file in full:
1<?xml version="1.0"?>
2
3<!DOCTYPE html
4 PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
5 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
6
7<html xmlns="http://www.w3.org/1999/xhtml"
8 xml:lang="en" lang="en">
9 <head>
10 <title>DelphiDabbler Articles</title>
11 <style type="text/css">
12 body {font-family: Tahoma; font-size: 10pt;}
13 h1 {font-size: 12pt;}
14 #precis {border: 1px solid silver; padding: 4px;}
15 </style>
16 <script type="text/javascript">
17 function ShowPrecis(progID) {
18 precisObj = document.getElementById("precis");
19 progObj = document.getElementById(progID);
20 precisObj.innerHTML = progObj.innerHTML.bold()
21 + '\<br />'
22 + external.GetPrecis(progID);
23 }
24 </script>
25 </head>
26 <body>
27 <h1>DelphiDabbler Program Descriptions</h1>
28 <ul>
29 <li>
30 <a
31 href="javascript:void(0);"
32 id="codesnip"
33 onclick="ShowPrecis('codesnip');"
34 onmouseover="external.ShowURL('codesnip');"
35 onmouseout="external.HideURL()";
36 >CodeSnip Database Viewer</a>
37 </li>
38 <li>
39 <a
40 href="javascript:void(0);"
41 id="htmlres"
42 onclick="ShowPrecis('htmlres');"
43 onmouseover="external.ShowURL('htmlres');"
44 onmouseout="external.HideURL()";
45 >HTML Resource Compiler</a>
46 </li>
47 <li>
48 <a
49 href="javascript:void(0);"
50 id="unit2ns"
51 onclick="ShowPrecis('unit2ns');"
52 onmouseover="external.ShowURL('unit2ns');"
53 onmouseout="external.HideURL()";
54 >Unit2NS</a>
55 </li>
56 <li>
57 <a
58 href="javascript:void(0);"
59 id="bdiff"
60 onclick="ShowPrecis('bdiff');"
61 onmouseover="external.ShowURL('bdiff');"
62 onmouseout="external.HideURL()";
63 >BDiff / BPatch Utilities</a>
64 </li>
65 </ul>
66 <div id="precis">
67 Click a program name to see its description here.
68 </div>
69 </body>
70</html>
Listing 17
Looking at the body section of the file we see that it contains a list of four program names, each defined as links (<a>
tags) that reference no URL.
Do-nothing links
To ensure that a link does nothing when clicked we set the href attribute to call the JavaScript void() function, like this: href="javascript:void(0);". It's also possible to simply use href="#".
Every link has a unique id attribute that identifies the program to which it refers.
The a-links' onclick event handlers call the ShowPrecis JavaScript routine, passing in the id of the relevant program as a parameter. ShowPrecis is defined in the HTML head section. The function first finds the <div>
tag with the id of "precis" and then finds the a-link element associated with the program id. The HTML enclosed by the "precis" <div>
tag is then replaced by HTML comprising of the program name in bold, a line break and the actual précis of the program. The précis is returned by the external.GetPrecis method, which executes TMyExternal.GetPrecis in the Delphi code.
Returning to the a-link tags, note that the onmouseover events directly call external.ShowURL with the id of the required program while the onmouseout events call external.HideURL. These JavaScript methods execute methods of the same name in TMyExternal, which in turn show and hide the program's URL in the main window status bar.
The only other item of note in the HTML file is that the <head>
section contains an embedded style sheet that styles the <body>
, <h1>
and <div id="precis">
elements.
Correct location of HTML file
It is important that Article22.html
can be found by the compiled program when it is run. By default this is in the same directory as the executable file.
If you want to store the HTML file in a separate location you can either place a copy with the executable file or modify the source code to change the expected location of the file (see Listing 15 line 5).
Data file
The final step is to create the data file, named Article22.dat
, that we load in TMyExternal.Create. The file needs to be in the form Key=Value
, where Key
is the id of one of the listed items of software and Value
is the text of a description of the software.
You can use any content you want, providing all the software ids are used as keys.
Avoid HTML reserved characters
When I said "use any content you want", I didn't quite mean it! You need to make sure that the Value
items contain only characters that are safe to use in HTML. This is because the case study code doesn't do any checking on the text. Production code should do this for security reasons.
Unsafe characters include >
(use the >
character entity instead), <
(use <
), &
(use &
) and "
(use "
).
Listing 18 shows the content of the file provided in the case study's source code:
1codesnip=Offline viewer for routines from CodeSnip database. Displays source of each unit and can test compile with any supported installed version of Delphi and Free Pascal.
2htmlres=Compiles HTML and associated files into RT_HTML resources in 32 bit resource files suitable for use with Internet Explorer's res:// protocol or with TWebBrowser.
3unit2ns=Maintains one or more 'mappings' of Delphi Pascal units to the namespaces to which they may belong. Copies the fully qualified namespace / unit name to the clipboard for pasting into Pascal code.
4bdiff=BDiff computes differences between two binary files and outputs either a human readable file of a binary patch file. BPatch uses the binary patch files produced by BDiff to patch files.
Listing 18
Correct location of data file
It is important that Article22.dat
can be found by the compiled program when it is run. By default this is in the same directory as the executable file.
If you want to store the data file in a separate location you can either place a copy with the executable file or modify the source code to change the expected location of the file (see Listing 12, line 14).
Source 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-22
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.
Delph 2007 problems
I've had a report that the demo code does not compile with Delphi 2007 unless you rename the unit Article22_TLB.pas
to Article22TLB.pas
and change references to it accordingly.