Well, Allen pretty much stole the thunder on this one so I'll post this article I wrote a few days ago and try to get the code cleaned up and posted ASAP. Though I suppose now I should look to incorporate his code rather than duplicating that effort. Anyhoo...
In a previous post, I talked about using Google maps from within a Windows Client application (in this case a Delphi VCL application). I demonstrated how it's possible to call into the Javascript loaded in a hosted WebBrowser control using IHTMLWindow2::execScript method. Through this technique it's possible to manipulate a things like Google maps rendered in the browser control.
Closing the Loop
In this post, I'll discuss a technique that allows Javascript, on a page rendered in the WebBrowser control, to execute VCL application code using IDocHostUIHandler::GetExternal. If you happened to read this post from Hallvard Vassbotn, a little over a year ago, then you're already aware of the unit which is the secret behind getting this technique to work. The unit is ObjComAuto.pas. It introduces TObjectDispatch which implements IDispatch and provides the plumbing necessary to script VCL objects compiled with the directive {$METHODINFO ON} which is used to generate extended RTTI for public and published methods.
Up's and Down's
Now, if you're familiar with the VCL you know it's not compiled with METHODINFO ON since the additional RTTI (Runtime Type Information) would dramatically increase the size of the resulting executables. As a result, we need a mechanism to map an instance of a VCL object that does not have this additional RTTI to one that does, using wrapper classes and a class map. The up-side is that we're able to control the amount of additional RTTI that's generated by providing implementations for only those things we really want. The downside of course, is that everything we want to script has to be exposed manually though as you'll see it's not that bad. Another issue is that the code for any classes added to the class map will be pulled into our application regardless of whether or there instantiated.
Base Wrapper Class
The first step is to create a base class with METHODINFO ON that's will be bound to a VCL object and implements a few key methods to get the ball rolling:
{ TObjectWrapper }
{$METHODINFO ON}
TObjectWrapper = class(TObject)
private
function GetClassName: string;
function GetParentClass: string;
protected
FObject: TObject;
public
constructor Connect(AObject: TObject); virtual;
function InheritsFrom(const ClassName: string): Boolean;
published
property ParentClass: string read GetParentClass;
{$WARNINGS OFF}
property ClassName: string read GetClassName;
{$WARNINGS ON}
end;
From this we'll construct additional classes which mirror VCL classes like TComponentWrapper, TControlWrapper and TWinControlWrapper that expose various key properties and methods we need to build a VCL scripting framework.
A Mapper Class
Next, we construct a list that will allow us to map VCL classes to these wrapper classes which we can do with a TObjectList:
TObjectWrapperClass = class of TObjectWrapper;
TClassMap = class(TObjectList)
public
procedure AddClass(AClass: TClass; AObjWrapper: TObjectWrapperClass);
procedure RemoveClass(AClass: TClass; AObjWrapper: TObjectWrapperClass);
function FindObjectWrapper(AClass: TClass): TObjectWrapperClass;
end;
Using the Class Map
Then we implement a descendant of TObjectDispatch and override GetObjectDispatch, GetMethodInfo and GetPropInfo to create instances of our wrapper classes from the class map for use with automation:
TAutoObjectDispatch = class(TObjectDispatch)
protected
function GetObjectDispatch(Obj: TObject): TObjectDispatch; override;
function GetMethodInfo(const AName: ShortString;
var AInstance: TObject): PMethodInfoHeader; override;
function GetPropInfo(const AName: string; var AInstance: TObject;
var CompIndex: Integer): PPropInfo; override;
end;
Connecting with Javascript
Then we implement IDocHostUIHandler::GetExternal and use TAutoObjectDispatch class to create objects that can be used from Javascript:
function TWebBrowser.GetExternal(out ppDispatch: IDispatch): HResult;
var
W: TAppObjectWrapper;
begin
W := TAppObjectWrapper.Connect(Forms.Application);
ppDispatch := TAutoObjectDispatch.Create(W) as IDispatch;
Result := S_OK;
end;
Putting it all together
Once we have these pieces in place it's possible to build VCL applications that host the WebBrowser control and allow for rich interactions between the web page and the application itself. Of course, this same technique can be utilized in other ways as well though like Hallvard I'll leave that as an exercise for the reader.
Related posts