I was attempting to create some kind of two way communication between Javascript and my TWebBrowser
. In my first iteration I was able to register property change events so that when a tag changes, Delphi picks up the change and then reads the value of the tag that triggered the change. This way we could have the Javascript set a hidden tag, Delphi would pick up the change, then Delphi would read the hidden tag to get out a value. This worked properly but felt a bit hacky.
In this second iteration I am attempting to trigger on custom events thrown in Javascript. I was able to get this working properly but I cannot find a way to get the argument that was passed to the custom event. Here is my code to create a IDispatch
:
constructor TWebBrowserEvent.Create(const OnEvent: TCallback);
begin
inherited Create;
FOnEvent := OnEvent;
end;
function TWebBrowserEvent.GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount, LocaleID: Integer; DispIDs: Pointer): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEvent.GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEvent.GetTypeInfoCount(out Count: Integer): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEvent.Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult;
var
Parameters : TDispParams;
begin
if (Dispid = DISPID_VALUE) then begin
if Assigned(FOnEvent) then begin
FOnEvent;
Result := S_OK;
end;
end else begin
Result := E_NOTIMPL;
end;
end;
Then to register my custom events I applied this code:
procedure TWebBrowserWrapper.RegisterCustomEvent(EventName : String; CallbackFunction : TCallback);
var
Target : IEventTarget;
begin
Target := WebBrowser.Document as IEventTarget;
Target.addEventListener(EventName, TWebBrowserEvent.Create(CallbackFunction) as IDispatch, true);
end;
Essentially I just get the document, cast it to a IEventTarget
and then call addEventListener
. I pass the name of the event for the first param and then create a IDispatch
using the callback function. I can successfully trigger this event but I can't figure out how to get the parameters passed into the event out. This is my event triggering in javascript:
I am attempting to get the details
section of this event out in Delphi. I thought maybe the invoke function could cast its parameters to a TDispParams
but the args in this class were empty when I tried to cast.
Any hints or answers would be much appreciated.
To get access to detail
of CustomEvent
in Delphi code you need to:
IDOMEvent
in your event listener,IDOMCustomEvent
from it,detail
property.You obviously failed at step #1. You were on a good way to implement event listener via IDispatch
and pass it as the second parameter of IEventTarget.addEventListener
. At this point you expected to receive some parameters as per the documentation when an event is dispatched to the listener:
listener
[in]Type:
IDispatch
The event handler function to associate with the event. Be aware that the event handler function itself requires two parameters - the first is the event target (that is, the object on which the event handler function is being invoked) and the second is the
IDOMEvent
object.
After registering the event listener and raising the event in HTML you discovered that you didn't receive any values in parameter Params
in method Invoke
:
You're not the first one facing this issue and searching for the root cause yields only few results:
Based upon that your listener needs to implement IDispatchEx, which the documentation doesn't mention. You just need to implement its InvokeEx
method and ignore the rest. Here's sample implementation (I dared to rename the class to TWebBrowserEventListener
to better express its purpose):
uses
System.SysUtils, Winapi.Windows, Winapi.ActiveX, MSHTML;
type
THandleEvent = procedure(const Target: IDispatch; const DOMEvent: IDOMEvent) of object;
TWebBrowserEventListener = class(TInterfacedObject, IDispatchEx)
private
FOnHandleEvent: THandleEvent;
{ IDispatch }
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
{ IDispatchEx }
function GetDispID(const bstrName: TBSTR; const grfdex: DWORD;
out id: TDispID): HResult; stdcall;
function InvokeEx(const id: TDispID; const lcid: LCID; const wflags:
WORD; const pdp: PDispParams; out varRes: OleVariant; out pei:
TExcepInfo; const pspCaller: PServiceProvider): HResult; stdcall;
function DeleteMemberByName(const bstr: TBSTR;
const grfdex: DWORD): HResult; stdcall;
function DeleteMemberByDispID(const id: TDispID): HResult; stdcall;
function GetMemberProperties(const id: TDispID; const grfdexFetch:
DWORD; out grfdex: DWORD): HResult; stdcall;
function GetMemberName(const id: TDispID; out bstrName: TBSTR):
HResult; stdcall;
function GetNextDispID(const grfdex: DWORD; const id: TDispID;
out nid: TDispID): HResult; stdcall;
function GetNameSpaceParent(out unk: IUnknown): HResult; stdcall;
protected
procedure HandleEvent(const Target: IDispatch; const DOMEvent: IDOMEvent); virtual;
public
constructor Create(AOnHandleEvent: THandleEvent);
end;
constructor TWebBrowserEventListener.Create(AOnHandleEvent: THandleEvent);
begin
inherited Create;
FOnHandleEvent := AOnHandleEvent;
end;
function TWebBrowserEventListener.DeleteMemberByDispID(const id: TDispID): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEventListener.DeleteMemberByName(const bstr: TBSTR;
const grfdex: DWORD): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEventListener.GetDispID(const bstrName: TBSTR; const grfdex: DWORD;
out id: TDispID): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEventListener.GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer; DispIDs: Pointer): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEventListener.GetMemberName(const id: TDispID;
out bstrName: TBSTR): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEventListener.GetMemberProperties(const id: TDispID;
const grfdexFetch: DWORD; out grfdex: DWORD): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEventListener.GetNameSpaceParent(out unk: IInterface): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEventListener.GetNextDispID(const grfdex: DWORD; const id: TDispID;
out nid: TDispID): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEventListener.GetTypeInfo(Index, LocaleID: Integer;
out TypeInfo): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEventListener.GetTypeInfoCount(out Count: Integer): HResult;
begin
Result := E_NOTIMPL;
end;
procedure TWebBrowserEventListener.HandleEvent(const Target: IDispatch;
const DOMEvent: IDOMEvent);
begin
if Assigned(FOnHandleEvent) then
FOnHandleEvent(Target, DOMEvent);
end;
function TWebBrowserEventListener.Invoke(DispID: Integer; const IID: TGUID;
LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo,
ArgErr: Pointer): HResult;
begin
Result := E_NOTIMPL;
end;
function TWebBrowserEventListener.InvokeEx(const id: TDispID; const lcid: LCID;
const wflags: WORD; const pdp: PDispParams; out varRes: OleVariant;
out pei: TExcepInfo; const pspCaller: PServiceProvider): HResult;
var
DOMEvent: IDOMEvent;
begin
if (id = DISPID_VALUE) and (pdp^.cArgs = 2) and (pdp^.rgvarg^[0].vt = varDispatch) and
(pdp^.rgvarg^[1].vt = varDispatch) and Supports(IDispatch(pdp^.rgvarg^[1].dispVal), IDOMEvent, DOMEvent) then
begin
HandleEvent(IDispatch(pdp^.rgvarg^[0].dispVal), DOMEvent);
Result := S_OK;
end
else
Result := E_NOTIMPL;
end;
To test that I loaded this HTML into the web browser control:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="x-ua-compatible" content="IE=edge">
<script>
/* Internet Explorer doesn't support CustomEvent() constructor. Polyfill for IE9+ from
https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent */
(function () {
if ( typeof window.CustomEvent === "function" ) return false;
function CustomEvent ( event, params ) {
params = params || { bubbles: false, cancelable: false, detail: null };
var evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
window.CustomEvent = CustomEvent;
})();
function triggerPmweTest() {
var event = new CustomEvent('pmweTest', { detail: { dataPackage: document.getElementById('input-text').value } });
document.dispatchEvent(event);
}
</script>
</head>
<body>
<input id="input-text" type="text" value="hello Matt" />
<input id="input-checkbox" type="checkbox" />
<button id="button" onclick="triggerPmweTest()">Click</button>
</body>
</html>
And this is how I registered the listener:
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
var
Target : IEventTarget;
Listener: IDispatchEx;
begin
Target := WebBrowser1.Document as IEventTarget;
Listener := TWebBrowserEventListener.Create(WebBrowserEvent);
Target.addEventListener('change', Listener, True);
Target.addEventListener('pmweTest', Listener, True);
end;
procedure TForm1.WebBrowserEvent(const Target: IDispatch;
const DOMEvent: IDOMEvent);
var
EventInfo: string;
DOMCustomEvent: IDOMCustomEvent;
begin
EventInfo := 'Type: ' + DOMEvent.type_ + #13#10'SrcElement: ';
if Assigned(DOMEvent.srcElement) then
begin
EventInfo := EventInfo + DOMEvent.srcElement.tagName;
if DOMEvent.srcElement.id <> '' then
EventInfo := EventInfo + '#' + DOMEvent.srcElement.id;
end
else
EventInfo := EventInfo + '#document';
if (DOMEvent.type_ = 'pmweTest') and Supports(DOMEvent, IDOMCustomEvent, DOMCustomEvent) then
EventInfo := EventInfo + #13#10'detail.dataPackage: ' + VarToStr(DOMCustomEvent.detail.dataPackage);
ShowMessage(EventInfo);
end;
The code above listens for change
events on <input>
elements as well as custom pmweTest
event that is triggered by clicking the button. The same listener is used for both types.
When you change the value of text field and focus out of the field then it displays:
Type: change
SrcElement: INPUT#input-text
When you click the checkbox:
Type: change
SrcElement: INPUT#input-checkbox
When you click the button:
Type: pmweTest
SrcElement: #document
detail.dataPackage: hello Matt