Skip over navigation

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 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:

  1uses
  2  MSHTML;
  3
  4procedure TForm1.CallFoo(S: string; I: Integer);
  5  { Calls JavaScript foo() function }
  6var
  7  Doc: IHTMLDocument2;      // current HTML document
  8  HTMLWindow: IHTMLWindow2; // parent window of current HTML document
  9  JSFn: string;             // stores JavaScipt function call
 10begin
 11  // Get reference to current document
 12  Doc := WebBrowser1.Document as IHTMLDocument2;
 13  if not Assigned(Doc) then
 14    Exit;
 15  // Get parent window of current document
 16  HTMLWindow := Doc.parentWindow;
 17  if not Assigned(HTMLWindow) then
 18    Exit;
 19  // Run JavaScript
 20  try
 21    JSFn := Format('foo("%s",%d)', [S, I]);  // build function call
 22    HTMLWindow.execScript(JSFn, 'JavaScript'); // execute function
 23  except
 24    // handle exception in case JavaScript fails to run
 25  end;
 26end;
Listing 1

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:

  1<script type="text/javascript">
  2  function SetFont(fontname) {
  3    document.body.style.fontFamily = fontname;
  4  }
  5</script>
Listing 2

This is the code that changes the font.

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:

  1procedure TForm1.FormShow(Sender: TObject);
  2  { Setup combo box and load document }
  3begin
  4  // Store screen fonts in combo box and disabled it
  5  ComboBox1.Items.Assign(Screen.Fonts);
  6  ComboBox1.Enabled := False;
  7  // Load the HTML page
  8  WebBrowser1.Navigate(ExtractFilePath(ParamStr(0)) + 'Test.html');
  9end;
Listing 3

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:

  1procedure TForm1.WebBrowser1DocumentComplete(Sender: TObject;
  2  const pDisp: IDispatch; var URL: OleVariant);
  3  { Document loaded: enable combo box }
  4begin
  5  ComboBox1.Enabled := True;
  6end;
Listing 4

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:

  1procedure TForm1.ComboBox1Change(Sender: TObject);
  2  { Make browser use selected font }
  3var
  4  Doc: IHTMLDocument2;      // current HTML document
  5  HTMLWindow: IHTMLWindow2; // parent window of current HTML document
  6  JSFn: string;             // stores JavaScipt function call
  7begin
  8  // Get reference to current document
  9  Doc := WebBrowser1.Document as IHTMLDocument2;
 10  if not Assigned(Doc) then
 11    Exit;
 12  // Get parent window of current document
 13  HTMLWindow := Doc.parentWindow;
 14  if not Assigned(HTMLWindow) then
 15    Exit;
 16  // Run JavaScript
 17  try
 18    JSFn := 'SetFont(''' + ComboBox1.Text + ''')';
 19    HTMLWindow.execScript(JSFn, 'JavaScript');
 20  except
 21    // handle exception in case JavaScript fails to run
 22    ShowMessage('Error running JavaScript');
 23  end;
 24end;
Listing 5

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:

  1uses
  2  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.
pvarRet
[out, retval] Address of a VARIANT of type VT_EMPTY. This method always returns VT_EMPTY.
Return Value
Returns S_OK if successful, or an error value otherwise.

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.

Huh?

What's the point of a non-void function result that's always the same value?

Search me!

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.:

  1<input type='hidden' id='result' value='' />
Listing 7

Assume that foo() was originally coded like this:

  1function foo(str, int) {
  2  var result;
  3  /* do something with str and int and store return value in result */
  4  return result;
  5}
Listing 8

We modify it to write its result into the hidden input field's value attribute like so:

  1function foo(str, int) {
  2  var result;
  3  /* do something with str and int and store return value in result */
  4  document.getElementById('result').value = result;
  5  return result;
  6}
Listing 9

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 general purpose function written by Christian that can get a named attribute from an HTML tag with a specified id.

  1function GetElementIdValue(WebBrowser: TWebBrowser;
  2  TagName, TagId, TagAttrib: string):string;
  3var
  4  Document: IHTMLDocument2;
  5  Body: IHTMLElement2;
  6  Tags: IHTMLElementCollection;
  7  Tag: IHTMLElement;
  8  I: Integer;
  9begin
 10  Result:='';
 11  if not Supports(WebBrowser.Document, IHTMLDocument2, Document) then
 12    raise Exception.Create('Invalid HTML document');
 13  if not Supports(Document.body, IHTMLElement2, Body) then
 14    raise Exception.Create('Can''t find <body> element');
 15  Tags := Body.getElementsByTagName(UpperCase(TagName));
 16  for I := 0 to Pred(Tags.length) do begin
 17    Tag:=Tags.item(I, EmptyParam) as IHTMLElement;
 18    if Tag.id=TagId then Result := Tag.getAttribute(TagAttrib, 0);
 19  end;
 20end;
Listing 10

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.

  1uses
  2  MSHTML;
  3  
  4function TForm1.CallFoo(S: string; I: Integer): string;
  5  { Calls JavaScript foo() function and returns its return value }
  6var
  7  Doc: IHTMLDocument2;      // current HTML document
  8  HTMLWindow: IHTMLWindow2; // parent window of current HTML document
  9  JSFn: string;             // stores JavaScipt function call
 10begin
 11  Result := '';
 12  // Get reference to current document
 13  Doc := WebBrowser1.Document as IHTMLDocument2;
 14  if not Assigned(Doc) then
 15    Exit;
 16  // Get parent window of current document
 17  HTMLWindow := Doc.parentWindow;
 18  if not Assigned(HTMLWindow) then
 19    Exit;
 20  // Run JavaScript
 21  try
 22    JSFn := Format('foo("%s",%d)', [S, I]);    // build function call
 23    HTMLWindow.execScript(JSFn, 'JavaScript'); // execute function
 24    // get result
 25    Result := GetElementIdValue(WebBrowser1, 'input', 'result', 'value')
 26  except
 27    // handle exception in case JavaScript fails to run
 28  end;
 29end;
Listing 11

There are only three changes to the code:

  1. CallFoo is changed from a procedure to a function that returns a string value.
  2. A default return value of the empty string is set in case of error.
  3. The return value of the function is set by calling the GetElementIdValue function from Listing 10.

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-21 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 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.

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