How to call JavaScript functions in a TWebBrowser from Delphi
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
6var
7 Doc: IHTMLDocument2;
8 HTMLWindow: IHTMLWindow2;
9 JSFn: string;
10begin
11
12 Doc := WebBrowser1.Document as IHTMLDocument2;
13 if not Assigned(Doc) then
14 Exit;
15
16 HTMLWindow := Doc.parentWindow;
17 if not Assigned(HTMLWindow) then
18 Exit;
19
20 try
21 JSFn := Format('foo("%s",%d)', [S, I]);
22 HTMLWindow.execScript(JSFn, 'JavaScript');
23 except
24
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
3begin
4
5 ComboBox1.Items.Assign(Screen.Fonts);
6 ComboBox1.Enabled := False;
7
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
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
3var
4 Doc: IHTMLDocument2;
5 HTMLWindow: IHTMLWindow2;
6 JSFn: string;
7begin
8
9 Doc := WebBrowser1.Document as IHTMLDocument2;
10 if not Assigned(Doc) then
11 Exit;
12
13 HTMLWindow := Doc.parentWindow;
14 if not Assigned(HTMLWindow) then
15 Exit;
16
17 try
18 JSFn := 'SetFont(''' + ComboBox1.Text + ''')';
19 HTMLWindow.execScript(JSFn, 'JavaScript');
20 except
21
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
3
4
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
3
4 document.getElementById('result').value = result;
5
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
6var
7 Doc: IHTMLDocument2;
8 HTMLWindow: IHTMLWindow2;
9 JSFn: string;
10begin
11 Result := '';
12
13 Doc := WebBrowser1.Document as IHTMLDocument2;
14 if not Assigned(Doc) then
15 Exit;
16
17 HTMLWindow := Doc.parentWindow;
18 if not Assigned(HTMLWindow) then
19 Exit;
20
21 try
22 JSFn := Format('foo("%s",%d)', [S, I]);
23 HTMLWindow.execScript(JSFn, 'JavaScript');
24
25 Result := GetElementIdValue(WebBrowser1, 'input', 'result', 'value')
26 except
27
28 end;
29end;
Listing 11
There are only three changes to the 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
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.
- 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".
- For bugs in the demo code see the
article-demo
project's README.md
file for details of how to report them.