How to call JavaScript functions in a TWebBrowser from Delphi

Contents

Introduction

On some occasions when using a TWebBrowser I've needed to use
Delphi to call JavaScript functions contained in the current document.

This is quite easy to do. We'll first examine the techniques then we'll look
at a case study that changes the font in an HTML document.

Finally, thanks to contributions from Christian Sciberras, we will look at
how to get the return value from a JavaScript function called from Delphi.

Overview of Solution

The window object associated with an HTML document exposes a method –
execScript – that enables JavaScript to be called. The
first parameter of this method takes a string containing the required
function call (complete with actual parameters). The method's second
parameter specifies the script language being used – in our case
'JavaScript'.

So, how do we get hold of the window object that exposes the required
method? We simply use the parentWindow property of the web
browser control's document object. We need to ensure that a document object
is available by first loading the required document into the web browser and
waiting for it to finish loading.

Implementing the Solution

Let us assume we have a TWebBrowser control on a form into which
an HTML document has been loaded. Assume also that the HTML document defines
a JavaScript function named foo() that takes a string and an
integer parameter. Assuming the document is fully loaded, we can call
foo() with a method similar to this:

Let's look at what's going on here. We first check that a document is
available and store a reference to in Doc. We then attempt to get
a reference to the document's parent window object and store it in
HTMLWindow. Once we have a valid window object we call its
execScript method, passing the call to foo(). The
parameters to foo() must be literal values – we can't pass
variables. String literals must be enclosed by double or single quotes.

Escaping quotes in string literal parameters

When passing string literal parameters to JavaScript functions you need
to be careful to escape any quote characters contained in the
string, otherwise the quote will terminate the string prematurely. Since
JavaScript can use either single or double quotes to delimit literal
strings you may need to escape either of these types of quote by
preceeding it with a backslash.

As an example, suppose we need to pass the string He didn't say
"hello" to a JavaScript function. If we delimit the
string with double quotes we pass the it as "He didn't say
\"hello\"".

Alternatively we may delimit the string with single quotes and pass it
as 'He didn\'t say "hello"'.

If the JavaScript function contains errors or doesn't exist an exception
will be raised. We may wish to handle such exceptions before returning.

Case Study

In this case study we will develop a small application that displays an HTML
document in a browser window. The document will contain a Javascript
function – SetFont() – that can change document's
default font. The application will display a combo box containing a list of
all installed screen fonts. When the user selects a font from the combo box
the font used in the web browser will be changed accordingly. We do this by
calling SetFont() from Delphi.

Firstly, create an HTML document with any content you choose and include the
following JavaScript function in the document's <head>
section:

Now create a new delphi application and drop a TComboBox and a
TWebBrowser on the main form. We will load the HTML document when
the form is first shown. We will also load the screen fonts into the combo
box at the same time. To do this create an OnShow event handler
for the form with the following code:

Note that we disabled the combo box to prevent a font from being selected.
We do this because to set a font we need to access the browser's
Document object – and this object is only available when
the HTML document is fully loaded. TWebBrowser triggers an
OnDocumentComplete event when the document has finished loading.
Therefore we handle that event and enable the combo box:

We now come to the code that actually calls the JavaScript function. When
the user selects a new font the combo box triggers its OnChange
event. We handle this event by calling the JavaScript SetFont()
function with the name of the selected font, as follows:

This method is very similar to that described in the previous section.
SetFont() is called with the name of the font selected in the
combo box. If an exception is raised when the JavaScript is called we
display a message.

We have now developed all the code. All that remains is to add the following
uses clause to the implementation section of the unit to enable the program
to compile:

uses
SysUtils, MSHTML, Dialogs;

Listing 6

Getting the Return Value

All this is very well, but how can we grab the return value of a JavaScript
function called from Delphi? The short answer is that we can't, because the
execScript() function doesn't ever return a useful value. Here's
what the Microsoft SDK Help says about it (my emphasis):

Syntax

HRESULT execScript(
BSTR code,
BSTR language,
VARIANT *pvarRet
);

Parameters

code

[in] BSTR that specifies the code to be executed.

language

[in] BSTR that specifies the language in which the code is
executed. The language defaults to Microsoft JScript.

Because Delphi uses the safecall convention
for the definition of execScript in the MSHTML
unit, the return value from execScript is that of the
pvarRet parameter (flagged [retval]). (Delphi uses the documented
return value to generate an exception if it is not S_OK). Notice
that the help entry for pvarRet says it always returns
VT_EMPTY. This means that execScript on Delphi always
returns and empty variant, not the JavaScript function return value that we
may have expected.

However, Christian Sciberras has come up with a clever work-around,
providing you can modify the document's HTML and the JavaScript source code.

Taking our original JavaScript foo() function, let us assume it
returns a value we want to capture.

First of all modify your HTML document's body so that it contains a hidden
input field with an id attribute of 'result', i.e.:

Notice we've retained the return statement so that any other code that calls
foo() still works. It remains to grab the value of the hidden
input field from Delphi. Listing 10 shows a
generic function written by Christian that can get a named attribute from an
HTML tag with a specified id.

The function first gets a reference to the <body> tag in
the loaded HTML document. It then gets hold of a collection of all the
contained tags named TagName and searches the collection looking
for the tag with the required id (TagId). When the correct
tag is found the function returns the value of its TagAtrrib
attribute. If the tag or attribute is not found the empty string is
returned.

Finally we just need to adapt the code from Listing
1 that calls foo() to gets its return value.
Listing 11 shows the revised code.

CallFoo is changed from a procedure to a function that
returns a string value.

A default return value of the empty string is set in case of error.

The return value of the function is set by calling the
GetElementIdValue function from
Listing 10.

Demo code

Demo code to accompany this article is available for download (see below).
The demo includes the following:

An implementation of the case study.

A demo by Christian Sciberras that illustrates getting the return value
a JavaScript function call. This demo lets you enter code that will be
evaluated and displayed in a "console" window.

The source code was tested using Delphi 7 Professional.

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 license.txt included in the
download for license details.