javascriptdelphicef4delphi

How to Invoke the Delphi function from JavaScript using Cef4Delphi


I am beginner in Delphi. Presently using Delphi Berlin version.

I am trying to invoke Delphi function/method from JavaScript. For example I want to open a new Delphi form on clicking on html button with additional data attribute.

HTML CODE


<input type="button" name="btn" value="Button" id="edit" data-prop="24"></button>
<input type="button" name="btnAnother" value="Button2" id="edit2" data-prop="1"></button>

When button will be clicked a new Delphi [second form] will be opened that will display data-prop of button on a TLabel.

[UPDATE - 12-10-2020]

I have tried to create application with the help of JSExtension demo. I have tried to add javascript click event but click event on html is not firing and second form is not loading. Here is some of the code

HTML [jsExtensionClickEvent.html]

<!DOCTYPE html>
<html>
<body>

<form method="POST">
  <input type="button" name="btnEx" value="Button" id="edit" data-prop="1"></button>
  <input type="button" name="anotherBtn" value="Another Button" id="edit2" data-prop="24"></button>
</form>
</body>
</html>

DELPHI

Extension handler class [uExtensionHandler.pas]

    unit uExtensionHandler;

{$I cef.inc}

interface

uses
{$IFDEF DELPHI16_UP}
  Winapi.Windows,
{$ELSE}
  Windows,
{$ENDIF}
  uCEFRenderProcessHandler, uCEFBrowserProcessHandler, uCEFInterfaces,
  uCEFProcessMessage,
  uCEFv8Context, uCEFTypes, uCEFv8Handler;

const
  MOUSECLICK_MESSAGE_NAME = 'mouseclick';

type
  TExtensionHelper = class(TCefv8HandlerOwn)
  protected
    function Execute(const name: ustring; const object_: ICefv8Value;
      const arguments: TCefv8ValueArray; var retval: ICefv8Value;
      var exception: ustring): Boolean; override;
  end;

implementation

{ TExtensionHelper }

function TExtensionHelper.Execute(const name: ustring;
  const object_: ICefv8Value; const arguments: TCefv8ValueArray;
  var retval: ICefv8Value; var exception: ustring): Boolean;
var
  TempMessage: ICefProcessMessage;
  TempFrame: ICefFrame;
begin
  Result := False;

  try
    if (name = 'mouseclick') then
    begin
      if (length(arguments) > 1) and arguments[0].IsString and arguments[1].IsString
      then
      begin
        TempMessage := TCefProcessMessageRef.New(arguments[1].GetStringValue);
        TempMessage.ArgumentList.SetString(0, arguments[0].GetStringValue);

        TempFrame := TCefv8ContextRef.Current.Browser.MainFrame;

        if (TempFrame <> nil) and TempFrame.IsValid then
          TempFrame.SendProcessMessage(PID_BROWSER, TempMessage);
      end;

      Result := True;
    end;

  finally
    TempMessage := nil;
  end;

end;

end.

Main Form [uMainForm.pas]

unit uMainForm;

interface

uses
{$IFDEF DELPHI16_UP}
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
  Vcl.ComCtrls, System.IOUtils,
{$ELSE}
  Windows, Messages, SysUtils, Variants, Classes, Graphics,
  Controls, Forms, Dialogs, StdCtrls, ExtCtrls, ComCtrls, IOUtils,
{$ENDIF}
  uCEFChromium, uCEFWindowParent, uCEFInterfaces, uCEFApplication, uCEFTypes,
  uCEFConstants,
  uCEFWinControl, uCEFSentinel, uCEFChromiumCore;

const
  MINIBROWSER_SHOWSECONDFORM = WM_APP + $100;

type
  TForm1 = class(TForm)
    CEFWindowParent1: TCEFWindowParent;
    Chromium1: TChromium;
    Timer1: TTimer;
    procedure FormShow(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure Timer1Timer(Sender: TObject);
    procedure Chromium1ProcessMessageReceived(Sender: TObject;
      const browser: ICefBrowser; const frame: ICefFrame;
      sourceProcess: TCefProcessId; const message: ICefProcessMessage;
      out Result: Boolean);
    procedure Chromium1AfterCreated(Sender: TObject;
      const browser: ICefBrowser);
    procedure Chromium1BeforePopup(Sender: TObject; const browser: ICefBrowser;
      const frame: ICefFrame; const targetUrl, targetFrameName: ustring;
      targetDisposition: TCefWindowOpenDisposition; userGesture: Boolean;
      const popupFeatures: TCefPopupFeatures; var windowInfo: TCefWindowInfo;
      var client: ICefClient; var settings: TCefBrowserSettings;
      var extra_info: ICefDictionaryValue;
      var noJavascriptAccess, Result: Boolean);
    procedure Chromium1Close(Sender: TObject; const browser: ICefBrowser;
      var aAction: TCefCloseBrowserAction);
    procedure Chromium1BeforeClose(Sender: TObject; const browser: ICefBrowser);
    procedure Chromium1LoadEnd(Sender: TObject; const browser: ICefBrowser;
      const frame: ICefFrame; httpStatusCode: Integer);
  private
    { Private declarations }
  public
    { Public declarations }
  protected
    Fid: string;
    // Variables to control when can we destroy the form safely
    FCanClose: Boolean; // Set to True in TChromium.OnBeforeClose
    FClosing: Boolean; // Set to True in the CloseQuery event.

    procedure BrowserCreatedMsg(var aMessage: TMessage);
      message CEF_AFTERCREATED;
    procedure BrowserDestroyMsg(var aMessage: TMessage); message CEF_DESTROY;
    procedure ShowSecondForm(var aMessage: TMessage);
      message MINIBROWSER_SHOWSECONDFORM;
    procedure WMMove(var aMessage: TWMMove); message WM_MOVE;
    procedure WMMoving(var aMessage: TMessage); message WM_MOVING;
    procedure WMEnterMenuLoop(var aMessage: TMessage); message WM_ENTERMENULOOP;
    procedure WMExitMenuLoop(var aMessage: TMessage); message WM_EXITMENULOOP;
  end;

var
  Form1: TForm1;

procedure CreateGlobalCEFApp;

implementation

uses
  uSecondForm, uCEFMiscFunctions, uCEFDictionaryValue, uExtensionHandler;

procedure GlobalCEFApp_OnWebKitInitialized;
var
  TempExtensionCode: string;
  TempHandler: ICefv8Handler;
begin
  try
    TempExtensionCode := 'var myextension;' + 'if (!myextension)' +
      '  myextension = {};' + '(function() {' +
      '  myextension.mouseclick = function(b,c) {' +
      '    native function mouseclick();' + '    mouseclick(b,c);' + '  };'
      + '})();';

    TempHandler := TExtensionHelper.Create;

    if CefRegisterExtension('myextension', TempExtensionCode, TempHandler) then
{$IFDEF DEBUG}CefDebugLog('JavaScript extension registered successfully!'){$ENDIF}
    else
{$IFDEF DEBUG}CefDebugLog('There was an error registering the JavaScript extension!'){$ENDIF};
  finally
    TempHandler := nil;
  end;
end;

procedure CreateGlobalCEFApp;
begin
  GlobalCEFApp := TCefApplication.Create;
  GlobalCEFApp.OnWebKitInitialized := GlobalCEFApp_OnWebKitInitialized;
{$IFDEF DEBUG}
  GlobalCEFApp.LogFile := 'debug.log';
  GlobalCEFApp.LogSeverity := LOGSEVERITY_INFO;
{$ENDIF}
end;

{$R *.dfm}
{ TForm1 }

procedure TForm1.BrowserCreatedMsg(var aMessage: TMessage);
begin
  CEFWindowParent1.UpdateSize;
end;

procedure TForm1.BrowserDestroyMsg(var aMessage: TMessage);
begin
  CEFWindowParent1.Free;
end;

procedure TForm1.Chromium1AfterCreated(Sender: TObject;
  const browser: ICefBrowser);
begin
  PostMessage(Handle, CEF_AFTERCREATED, 0, 0);
end;

procedure TForm1.Chromium1BeforeClose(Sender: TObject;
  const browser: ICefBrowser);
begin
  FCanClose := True;
  PostMessage(Handle, WM_CLOSE, 0, 0);
end;

procedure TForm1.Chromium1BeforePopup(Sender: TObject;
  const browser: ICefBrowser; const frame: ICefFrame;
  const targetUrl, targetFrameName: ustring;
  targetDisposition: TCefWindowOpenDisposition; userGesture: Boolean;
  const popupFeatures: TCefPopupFeatures; var windowInfo: TCefWindowInfo;
  var client: ICefClient; var settings: TCefBrowserSettings;
  var extra_info: ICefDictionaryValue; var noJavascriptAccess, Result: Boolean);
begin
  Result := (targetDisposition in [WOD_NEW_FOREGROUND_TAB,
    WOD_NEW_BACKGROUND_TAB, WOD_NEW_POPUP, WOD_NEW_WINDOW]);
end;

procedure TForm1.Chromium1Close(Sender: TObject; const browser: ICefBrowser;
  var aAction: TCefCloseBrowserAction);
begin
  PostMessage(Handle, CEF_DESTROY, 0, 0);
  aAction := cbaDelay;
end;

procedure TForm1.Chromium1LoadEnd(Sender: TObject; const browser: ICefBrowser;
  const frame: ICefFrame; httpStatusCode: Integer);
var
  TempJSCode: string;
begin
  Chromium1.LoadURL('file:///jsExtensionClickEvent.html');
  TempJSCode := 'document.body.addEventListener("click", function (evt) { ' +
    ' function getpath(n) {' +
    ' var result = document.getElementById(n.id).getAttribute("data-prop"); ' +
    ' return result; ' + ' } '
  +' myextension.mouseclick(getpath(evt.target), ' +
    quotedstr(MOUSECLICK_MESSAGE_NAME) + ');});';
  frame.ExecuteJavaScript(TempJSCode, 'about:blank', 0);
end;

procedure TForm1.Chromium1ProcessMessageReceived(Sender: TObject;
  const browser: ICefBrowser; const frame: ICefFrame;
  sourceProcess: TCefProcessId; const message: ICefProcessMessage;
  out Result: Boolean);
begin
  Result := False;

  if (message = nil) or (message.ArgumentList = nil) then
    exit;

  // This function receives the messages with the JavaScript results

  if (message.Name = MOUSECLICK_MESSAGE_NAME) then
  begin
    Fid := message.ArgumentList.GetString(0);
    PostMessage(Handle, MINIBROWSER_SHOWSECONDFORM, 0, 0);
    // this doesn't create/destroy components
    Result := True;
  end;
end;

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  CanClose := FCanClose;

  if not(FClosing) then
  begin
    FClosing := True;
    Visible := False;
    Chromium1.CloseBrowser(True);
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FCanClose := False;
  FClosing := False;
end;

procedure TForm1.FormShow(Sender: TObject);
begin
  // GlobalCEFApp.GlobalContextInitialized has to be TRUE before creating any browser
  // If it's not initialized yet, we use a simple timer to create the browser later.
  if not(Chromium1.CreateBrowser(CEFWindowParent1, '')) then
    Timer1.Enabled := True;
end;

procedure TForm1.ShowSecondForm(var aMessage: TMessage);
begin
  Form2.Label1.Caption := Fid;
  Form2.ShowModal;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Timer1.Enabled := False;
  if not(Chromium1.CreateBrowser(CEFWindowParent1, '')) and
    not(Chromium1.Initialized) then
    Timer1.Enabled := True;
end;

procedure TForm1.WMEnterMenuLoop(var aMessage: TMessage);
begin
  inherited;

  if (aMessage.wParam = 0) and (GlobalCEFApp <> nil) then
    GlobalCEFApp.OsmodalLoop := True;

end;

procedure TForm1.WMExitMenuLoop(var aMessage: TMessage);
begin
  inherited;

  if (aMessage.wParam = 0) and (GlobalCEFApp <> nil) then
    GlobalCEFApp.OsmodalLoop := False;

end;

procedure TForm1.WMMove(var aMessage: TWMMove);
begin
  inherited;

  if (Chromium1 <> nil) then
    Chromium1.NotifyMoveOrResizeStarted;
end;

procedure TForm1.WMMoving(var aMessage: TMessage);
begin
  inherited;

  if (Chromium1 <> nil) then
    Chromium1.NotifyMoveOrResizeStarted;
end;

end.

Second Form [uSecondForm.pas]

unit uSecondForm;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm2 = class(TForm)
    Label1: TLabel;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form2: TForm2;

implementation

{$R *.dfm}

end.

Content of the log file

[1012/202103.335:ERROR:CEF4Delphi(1)] PID: 6708, TID: 2872, PT: Renderer - JavaScript extension registered successfully!
[1012/203832.621:ERROR:CEF4Delphi(1)] PID: 6660, TID: 6748, PT: Renderer - JavaScript extension registered successfully!
[1012/203832.688:ERROR:CEF4Delphi(1)] PID: 5436, TID: 6016, PT: Renderer - JavaScript extension registered successfully!

When clicking on button getting below Debug Event Log. Cef4DelphiJsExtension.exe is application name.

Thread Start: Thread ID: 1732. Process Cef4DelphiJsExtension.exe (7156)
Thread Exit: Thread ID: 1732. Process Cef4DelphiJsExtension.exe (7156)
Thread Start: Thread ID: 1180. Process Cef4DelphiJsExtension.exe (7156)
Thread Exit: Thread ID: 2076. Process Cef4DelphiJsExtension.exe (7156)
Thread Exit: Thread ID: 6592. Process Cef4DelphiJsExtension.exe (7156)
Thread Start: Thread ID: 7200. Process Cef4DelphiJsExtension.exe (7156)
Thread Start: Thread ID: 7220. Process Cef4DelphiJsExtension.exe (7156)
Thread Start: Thread ID: 7276. Process Cef4DelphiJsExtension.exe (7156)
Thread Exit: Thread ID: 7276. Process Cef4DelphiJsExtension.exe (7156)
Thread Start: Thread ID: 7376. Process Cef4DelphiJsExtension.exe (7156)
Thread Exit: Thread ID: 7376. Process Cef4DelphiJsExtension.exe (7156)
Thread Exit: Thread ID: 7220. Process Cef4DelphiJsExtension.exe (7156)

Thank you.


Solution

  • You can do that in two ways:

    1. Add a JavaScript function in the "onclick" event of that button. That function would only have to call "console.log()" with "OpenMyNewFormInDelphi" or whatever you want as the text parameter. Then use the TChromium.OnConsoleMessage event and check the "aMessage" parameter. In case aMessage has "OpenMyNewFormInDelphi" then send a Windows message to the main form to show your new form in the main application thread. This solution is the easiest, it's not very elegant but it gets the work done. See the DOMVisitor demo for more details.
    2. You can also register a "JavaScript extension" with CEF4Delphi to execute Delphi code from JavaScript. This is by far the most complicated solution because it involves the creation and registration of a custom class inherited from TCefv8HandlerOwn. That class will receive the calls from your JS code and you can send an IPC message to the main browser process if your application needs to do something in response to that JS call. See the JSExtension and JSRTTIExtension demos for more information.

    The full explanation for the JavaScript extension is a bit long but you can read it here : https://github.com/salvadordf/CEF4Delphi/blob/d44db3bf2a3ead0654ca90178161b09bfbe33602/demos/Delphi_VCL/JavaScript/JSExtension/uJSExtension.pas#L122

    Please remember that all TChromium and GlobalCEFApp events are executed in a CEF thread that it's different than the main application thread. The VCL is not thread safe and you may have problems if you create, destroy or modify Windows controls inside those events. The CEF4Delphi demos are oversimplified and you should always move the VCL code outside the those events. The first solution sends a Windows message to the main form for this reason.