Contents
- Introduction
- Overview
- Implementing the external object
- Registering the external object with TWebBrowser
- Calling into Delphi from JavaScript
- Case study
- Summary
- References
- Feedback
Introduction
When writing programs that use the TWebBrowser control as part of the user interface I've sometimes needed to respond to user interaction with the control. The official way to do this is to extend the web browser's external object, and this is the technique we will use in this article.
The external object is part of TWebBrowser's document object model. The object enables interaction with the environment surrounding the browser control – in this case our program.
We can extend the external object by adding methods to it that are implemented in Delphi rather than in JavaScript or VBScript. We do this by creating a COM automation object that exposes the required methods and then notifying the TWebBrowser control that the COM object extends the external object. The new external object methods can then be called from JavaScript or VBScript running in the TWebBrowser. When these methods are called our Delphi code executes.
The rest of this article examines how to use Delphi to create and manipulate the external object.
Overview
The solution divides neatly into three main activities:
- Extend the external object in Delphi by creating a COM automation object that exposes the required methods.
- Register the extended external object with the browser control so that the object's methods can be called from within the control. We do this by implementing the IDocHostUIHandler interface and enabling the TWebBrowser control to use our implementation.
- Call the methods of the extended external object from JavaScript running in the browser control.
The next sections discuss each of the above activities in turn. Finally a case study will be presented that puts the techniques we have learned into practise.
We make a start in the next section by discussing how to extend the external object.
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 four things:
- Create the type library (
.tlb
) file. - Include the type library in the program's resources by inserting a suitable
$R
compiler directive in the project's.dpr
file. - 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.
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 using a parameter that has the [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] |
The *_TLB.pas
file created by Delphi would contain the following interface definition shown in Listing 1.
type
IMyIntf = interface(IDispatch)
['{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}']
function Foo(Param1: Integer): WideString; safecall;
procedure Bar(const Param1: WideString); safecall;
end;
We would therefore include the following methods in our class declaration:
type
TMyClass = class(TAutoIntfObject,
IMyIntf, IDispatch
)
private
...
protected
{ IMyIntf methods }
function Foo(Param1: Integer): WideString; safecall;
procedure Bar(const Param1: WideString); safecall;
...
end;
These methods would then be implemented as required. Remember that we don't declare or implement any methods 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.
{$R *.tlb}
directive mentioned earlier ensures that the
.tlb
file generated by the type library editor is included in our program's resources.
We do this by declaring a parameter-less 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. The object is then passed to the inherited constructor. Assuming our derived class is named TMyExternal, Listing 3 shows the constructor's implementation.
constructor TMyExternal.Create;
var
TypeLib: ITypeLib; // type library information
ExeName: WideString; // name of our program's exe file
begin
// Get name of application
ExeName := ParamStr(0);
// Load type library from application's resources
OleCheck(LoadTypeLib(PWideChar(ExeName), TypeLib));
// Call inherited constructor
inherited Create(TypeLib, IMyExternal);
// ...
// Do any other initialisation here
// ...
end;
We have now seen how to implement the external object. In the next section we will examine how to register the object with the web browser control.
Registering the external object with TWebBrowser
Having implemented the COM object that extends the external object how do we tell the web browser about it?
The answer is by creating a container object that hosts the web browser control and implements the IDocHostUIHandler interface. Any web browser control container object must also implement IOleClientSite. IDocHostUIHandler has a GetExternal method. In our implementation of this method we will pass a reference to our custom external object to the browser.
There are numerous other methods of IDocHostUIHandler that we have to implement, but we can get away with stubbing them out. In a previous article (see the "Further reading" box) I discussed IDocHostUIHandler in detail and presented a do nothing implementation – TNulWBContainer. I won't repeat that presentation here, so please check out the earlier article if you need to review how this is done.
Doing all the hard work in TNulWBContainer means that our implementation of IDocHostUIHandler and IOleClientSite, which we will call TExternalContainer, can be quite simple if we descend it from TNulWBContainer. Listing 4 has the declaration of the class, while Listing 5 shows its implementation.
type
TExternalContainer = class(TNulWBContainer,
IDocHostUIHandler, IOleClientSite)
private
fExternalObj: IDispatch; // external object implementation
protected
{ Re-implemented IDocHostUIHandler method }
function GetExternal(out ppDispatch: IDispatch): HResult; stdcall;
public
constructor Create(const HostedBrowser: TWebBrowser);
end;
constructor TExternalContainer.Create(
const HostedBrowser: TWebBrowser);
begin
inherited Create(HostedBrowser);
fExternalObj := TMyExternal.Create;
end;
function TExternalContainer.GetExternal(
out ppDispatch: IDispatch): HResult;
begin
ppDispatch := fExternalObj;
Result := S_OK; // indicates we've provided script
end;
Notice that we create an instance of our external object extension in the constructor and store it in a field of type IDispatch. We then implement GetExternal to pass back a reference to the external object in ppDispatch and return S_OK to indicate we have provided the object.
We pass a reference to the web browser control we are hosting to the constructor. This reference is simply passed on to the inherited constructor where it is recorded and notified that our object is its container. See the implementation of TNulWBContainer.Create for details of how this is done.
We have now completed the code necessary to register the external object with the web browser. In the next section we look at how to call into the external object from JavaScript.
Calling into Delphi from JavaScript
Having implemented our external object extension and registered it with the browser control, it is now time to look at how we call the object's methods from JavaScript. A similar approach is taken if you wish to use VBScript rather than JavaScript, but the precise details are, as they say, left as an exercise!
All we have to do to call into our Delphi code from JavaScript is to reference the relevant methods of the external object. For example to access external method Foo from JavaScript we would use the code presented in Listing 6.
external.Foo();
external object methods can be called anywhere that a JavaScript method call is valid – either from event handlers of HTML elements or from blocks of JavaScript code. For example, say we have implemented the interface shown in Listing 7 in Delphi, and have registered it as an extension of the external object:
type
ITest = interface(IDispatch)
['{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}']
procedure SetLabel(const Msg: WideString);
safecall;
function GetEMail(const Name: WideString): WideString;
safecall;
end;
Assume that SetLabel sets the caption of a TLabel to the given text and GetEMail looks up the name of the given person in a database and returns their email address.
We can ensure the label's caption is set when a user clicks a link by using HTML such as that presented in Listing 8.
<body>
...
<a href="#"
onclick="external.SetLabel('Hello Delphi');">Click me</a>
...
</body>
We can also write someone's email address into the HTML document using a block of JavaScript similar to that given in Listing 9.
<body>
...
<p>Fred's email address is:
<script type="text/JavaScript">
<--
document.write( external.GetEMail("fred") );
//-->
</script>
</p>
...
</body>
As can be seen, once the hard work of setting up and registering the external object has been done, it is very easy to call the required methods.
That concludes our discussion of the techniques involved in calling Delphi code from JavaScript. In the next section we will work through a case study that draws all the strands together.
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 (precis) of the program in a box that follows list of programs. When the mouse is moved over a program name the URL of the program's web page will be displayed in the status bar. The status bar will clear when there is no program name under the mouse. The precis of each program is stored in a data file that is read by our application.
Here is a screenshot of the completed program:
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 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.
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 precis 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. Now add three methods using the information in Table 2.
Name | Params | Types | Modifier |
---|---|---|---|
GetPrecis | ProgID Result |
BSTR BSTR * |
[in] [out,retval] |
ShowURL | ProgID | BSTR | [in] |
HideURL | - | - | - |
Press Ctrl+S to save the type library and name it Article22.tlb
. Delphi will now create a type library file of that name 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).
type
...
IMyExternal = interface(IDispatch)
['{4F995D09-CF9E-4042-993E-C71A8AED661E}']
function GetPrecis(const ProgID: WideString): WideString;
safecall;
procedure ShowURL(const ProgID: WideString); safecall;
procedure HideURL; safecall;
end;
...
Implementing the external object
Now that we have created the IMyExternal interface, we implement it in a class named TMyExternal. Listing 11 has the class declaration.
type
TMyExternal = class(TAutoIntfObject, IMyExternal, IDispatch)
private
fData: TStringList; // info from data file
procedure ShowSBMsg(const Msg: string); // helper method
protected
{ IMyExternal methods }
function GetPrecis(const ProgID: WideString): WideString;
safecall;
procedure ShowURL(const ProgID: WideString); safecall;
procedure HideURL; safecall;
public
constructor Create;
destructor Destroy; override;
end;
As expected, the methods of IMyExternal are specified in the class's 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 precis 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.
constructor TMyExternal.Create;
var
TypeLib: ITypeLib; // type library information
ExeName: WideString; // name of our program's exe file
begin
// Get name of application
ExeName := ParamStr(0);
// Load type library from application's resources
OleCheck(LoadTypeLib(PWideChar(ExeName), TypeLib));
// Call inherited constructor
inherited Create(TypeLib, IMyExternal);
// Create and load string list from file
fData := TStringList.Create;
fData.LoadFromFile(ChangeFileExt(ExeName, '.dat'));
end;
destructor TMyExternal.Destroy;
begin
fData.Free;
inherited;
end;
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 precis 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.
function TMyExternal.GetPrecis(
const ProgID: WideString): WideString;
begin
Result := fData.Values[ProgId];
end;
procedure TMyExternal.HideURL;
begin
ShowSBMsg('');
end;procedure TMyExternal.ShowURL(const ProgID: WideString);
begin
ShowSBMsg(
'http://www.delphidabbler.com/software?id=' + ProgID
);
end;
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.
procedure TMyExternal.ShowSBMsg(const Msg: string);
var
HintAct: THintAction;
begin
HintAct := THintAction.Create(nil);
try
HintAct.Hint := Msg;
HintAct.Execute;
finally
HintAct.Free;
end;
end;
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.
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.
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.
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.
procedure TForm1.FormShow(Sender: TObject);
begin
fContainer := TExternalContainer.Create(WebBrowser1);
WebBrowser1.Navigate(
ExtractFilePath(ParamStr(0)) + 'Article22.html'
);
end;
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.
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.
procedure TForm1.FormHide(Sender: TObject);
begin
fContainer.Free;
end;
Creating the HTML file
All that remains to be done is 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.
<?xml version="1.0"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xml:lang="en" lang="en">
<head>
<title>DelphiDabbler Articles</title>
<style type="text/css">
<!--
body {font-family: Tahoma; font-size: 10pt;}
h1 {font-size: 12pt;}
#precis {
border: 1px solid silver;
padding: 4px;
}
-->
</style>
<script type="text/javascript">
<!--
function ShowPrecis(progID) {
precisObj = document.getElementById("precis");
progObj = document.getElementById(progID);
precisObj.innerHTML = progObj.innerHTML.bold()
+ '\<br />'
+ external.GetPrecis(progID);
}
//-->
</script>
</head>
<body>
<h1>DelphiDabbler Program Descriptions</h1>
<ul>
<li><a href="javascript:void(0);" id="codesnip"
onclick="ShowPrecis('codesnip');"
onmouseover="external.ShowURL('codesnip');"
onmouseout="external.HideURL()";
>CodeSnip Database Viewer</a>
</li>
<li><a href="javascript:void(0);" id="htmlres"
onclick="ShowPrecis('htmlres');"
onmouseover="external.ShowURL('htmlres');"
onmouseout="external.HideURL()";
>HTML Resource Compiler</a>
</li>
<li><a href="javascript:void(0);" id="chi"
onclick="ShowPrecis('chi');"
onmouseover="external.ShowURL('chi');"
onmouseout="external.HideURL()";
>Component Help Installer</a>
</li>
<li><a href="javascript:void(0);" id="vis"
onclick="ShowPrecis('vis');"
onmouseover="external.ShowURL('vis');"
onmouseout="external.HideURL()";
>Version Information Spy</a>
</li>
</ul>
<div id="precis">
Click a program name to see its description here.
</div>
</body>
</html>
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. Every link has a unique id attribute that identifies the program to which it refers.
The a-links' onclick events 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 precis of the program. The precis is returned by the external.GetPrecis method, which executes TMyExternal.GetPrecis in the Delphi code.
Returning to the 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 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.
This description of the HTML file completes the discussion of the case study.
Source code
The case study is available for download. Delphi 7 was used to create the program and it was tested on Windows XP Pro SP2 using Internet Explorer 6.
A ReadMe file that describes how to use the source is included in the download.
That completes the substantive part of the article. The final section summarises what we've achieved and provides links to some reference material.