Utilizing several code snippets I've attempted to hook up an ActiveX object with a Javascript event handler. I'm unable to identify why the event handler isn't being called.
Github Repository with project.
Update
By placing the javascript call to SayHello() in an 'onLoad' event, I was able to get the ActiveX event to fire. Now I'm looking toward the C# call, and how to hook it into the ActiveX object utilized by Javascript.
(This may also have relied on enable local scripts from the Advanced options of IE).
Message Continued
The event handler is done in the same form as described for this question.
<script for="MyObject" event="OnUpdateString(stuff)">
document.write("<p>" + stuff);
document.writeln("</p>");
</script>
Utilizing MSDN documentation I created a WinForms app that contains a WebBrowser control that acts as the ObjectForScripting (not related to the issue). This container makes a call out to the ActiveX event, but is unhandled by the Javascript. I'm including the C# Form code to be complete in the ActiveX interactions and to allow this to be a reference for future users of ActiveX and/or WebBrowser control.
This file is intended to be used with a new Windows Form project where a WebBrowser control was added to the main window.
C# Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Security.Permissions;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using ActiveXObjectSpace;
namespace TestActiveX
{
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
[System.Runtime.InteropServices.ComVisibleAttribute(true)]
public partial class Form1 : Form
{
MyObject myObject = new MyObject();
public Form1()
{
InitializeComponent();
Text = "ActiveX Test";
Load += new EventHandler(Form1_Load);
}
private void Form1_Load(object sender, EventArgs e)
{
webBrowser1.AllowWebBrowserDrop = false;
webBrowser1.ObjectForScripting = this;
webBrowser1.Url = new Uri(#"C:\path\to\TestPage.html");
// Call ActiveX
myObject.SayHello("C# Launch");
}
public string ControlObject()
{
return "<p>Control Object Called.</p>";
}
}
}
Combining from the help of two other code snippets I created a the ActiveX object. Which, as noted, needs to be registered after being built.
C# ObjectX.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
/// http://blogs.msdn.com/b/asiatech/archive/2011/12/05/how-to-develop-and-deploy-activex-control-in-c.aspx
/// https://stackoverflow.com/questions/11175145/create-com-activexobject-in-c-use-from-jscript-with-simple-event
///
/// Register with %NET64%\regasm /codebase <full path of dll file>
/// Unregister with %NET64%\regasm /u <full path of dll file>
namespace ActiveXObjectSpace
{
/// <summary>
/// Provides the ActiveX event listeners for Javascript.
/// </summary>
[Guid("4E250775-61A1-40B1-A57B-C7BBAA25F194"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IActiveXEvents
{
[DispId(1)]
void OnUpdateString(string data);
}
/// <summary>
/// Provides properties accessible from Javascript.
/// </summary>
[Guid("AAD0731A-E84A-48D7-B5F8-56FF1B7A61D3"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IActiveX
{
[DispId(10)]
string CustomProperty { get; set; }
}
[ProgId("MyObject")]
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
[Guid("7A5D58C7-1C27-4DFF-8C8F-F5876FF94C64")]
[ComSourceInterfaces(typeof(IActiveXEvents))]
public class MyObject : IActiveX
{
public delegate void OnContextChangeHandler(string data);
new public event OnContextChangeHandler OnUpdateString;
// Dummy Method to use when firing the event
private void MyActiveX_nMouseClick(string index)
{
}
public MyObject()
{
// Bind event
this.OnUpdateString = new OnContextChangeHandler(this.MyActiveX_nMouseClick);
}
[ComVisible(true)]
public string CustomProperty { get; set; }
[ComVisible(true)]
public void SayHello(string who)
{
OnUpdateString("Calling Callback: " + who);
}
}
}
Last is the html page which is to be loaded by the browser or container. It loads the ActiveX object successfully and contains an event handler for OnUpdateString. It checks that the ActiveX provided function, SayHello, can be called and makes the call.
I'd expect the Javascript and C# call to be written to the document, but no such entries are written.
TestPage.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>DemoCSharpActiveX webpage</title>
</head>
<body>
<script type="text/javascript">
window.objectLoadFailure = false;
</script>
<object id="MyObject" onerror="window.objectLoadFailure = true" classid="clsid:7A5D58C7-1C27-4DFF-8C8F-F5876FF94C64"></object>
<script for="MyObject" event="OnUpdateString(stuff)">
document.write("<p>" + stuff);
document.writeln("</p>");
</script>
<script type="text/javascript">
document.write("<p>Loaded ActiveX Object: " + !window.objectLoadFailure);
document.writeln("</p>");
if (typeof window.external.ControlObject !== "undefined") {
document.write(window.external.ControlObject());
}
var obj = document.MyObject;
if (typeof obj.SayHello !== "undefined") {
document.writeln("<p>Can Call say hello</p>")
}
obj.SayHello("Javascript Load");
</script>
</body>
</html>
The containing page shows this output
Output
Loaded ActiveX Object: true
Control Object Called.
Can Call say hello
Updated, as long as you can get the <object> instantiated from HTML (MyObject.object != null), the ultimate problem with your JavaScript event handler is simply that you kill the original HTML document with document.write before you call MyObject.SayHello("Javascript Load"), and replace it with <p>Loaded ActiveX Object: ...</p>. By then, all original JavaScript event handlers are gone.
Thus, the following works fine, the event gets fired and handled (with an alert):
<!DOCTYPE html>
<html>
<head>
<title>DemoCSharpActiveX webpage</title>
</head>
<body>
<script type="text/javascript">
window.objectLoadFailure = false;
</script>
<object id="MyObject" onerror="window.objectLoadFailure = true" classid="clsid:7A5D58C7-1C27-4DFF-8C8F-F5876FF94C64"></object>
<script type="text/javascript" for="MyObject" event="OnUpdateString">
alert("Hello from event handler");
</script>
<script type="text/javascript" for="window" event="onload">
alert("Hello from window.onload!");
alert(MyObject.object);
MyObject.SayHello("Javascript Load");
</script>
</body>
</html>
To make your original logic work, you can manipulate the DOM directly instead of using document.write. Or, at least call it after OnUpdateString has been fired and handled.
Now that I've seen the full source, I can tell quite a few things going wrong here.
You can hit a break point inside SayHello because you create MyObject from C# [MyObject myObject = new MyObject()] and call it from C# [myObject.SayHello("C# Launch")]. Remove that and you'll see it never gets invoked when you call it from JavaScript [obj.SayHello("Javascript Load")].
That leads to another issue: the <object> doesn't get create successfully, and even more so, none of your JavaScript scripts even run, because your test HTML file is served from the local file system (via file:// protocol). This is a security restriction. Try changing your script like below to see none of the alerts actually show up:
<script type="text/javascript" for="window" event="onload">
alert("Hello from window.onload!");
alert(MyObject.object) // null! object wasn't created...
document.write("<p>Loaded ActiveX Object: " + !window.objectLoadFailure);
document.writeln("</p>");
if (typeof window.external.ControlObject !== "undefined") {
document.write(window.external.ControlObject());
}
var obj = document.MyObject;
if (typeof obj.SayHello !== "undefined") {
document.writeln("<p>Can Call say hello</p>")
}
obj.SayHello("Javascript Load");
</script>
There're several ways of fixing it. The easiest one is probably to use "Mark of Web". The hardest one would be to provide a custom implementation of IInternetSecurityManager. I myself would use yet another method - Internet Feature Control - and disable FEATURE_LOCALMACHINE_LOCKDOWN, FEATURE_BLOCK_LMZ_SCRIPT, FEATURE_BLOCK_LMZ_OBJECT keys. You can use following code I adapted from my other related answer:
// static constructor, runs first
static Form1()
{
SetWebBrowserFeatures();
}
static void SetWebBrowserFeatures()
{
// don't change the registry if running in-proc inside Visual Studio
if (LicenseManager.UsageMode != LicenseUsageMode.Runtime)
return;
var appName = System.IO.Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
var featureControlRegKey = #"HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\";
Registry.SetValue(featureControlRegKey + "FEATURE_BROWSER_EMULATION",
appName, GetBrowserEmulationMode(), RegistryValueKind.DWord);
// enable the features which are "On" for the full Internet Explorer browser
Registry.SetValue(featureControlRegKey + "FEATURE_ENABLE_CLIPCHILDREN_OPTIMIZATION",
appName, 1, RegistryValueKind.DWord);
Registry.SetValue(featureControlRegKey + "FEATURE_AJAX_CONNECTIONEVENTS",
appName, 1, RegistryValueKind.DWord);
Registry.SetValue(featureControlRegKey + "FEATURE_GPU_RENDERING",
appName, 1, RegistryValueKind.DWord);
Registry.SetValue(featureControlRegKey + "FEATURE_WEBOC_DOCUMENT_ZOOM",
appName, 1, RegistryValueKind.DWord);
Registry.SetValue(featureControlRegKey + "FEATURE_NINPUT_LEGACYMODE",
appName, 0, RegistryValueKind.DWord);
Registry.SetValue(featureControlRegKey + "FEATURE_LOCALMACHINE_LOCKDOWN",
appName, 0, RegistryValueKind.DWord);
Registry.SetValue(featureControlRegKey + "FEATURE_BLOCK_LMZ_SCRIPT",
appName, 0, RegistryValueKind.DWord);
Registry.SetValue(featureControlRegKey + "FEATURE_BLOCK_LMZ_OBJECT",
appName, 0, RegistryValueKind.DWord);
}
static UInt32 GetBrowserEmulationMode()
{
int browserVersion = 0;
using (var ieKey = Registry.LocalMachine.OpenSubKey(#"SOFTWARE\Microsoft\Internet Explorer",
RegistryKeyPermissionCheck.ReadSubTree,
System.Security.AccessControl.RegistryRights.QueryValues))
{
var version = ieKey.GetValue("svcVersion");
if (null == version)
{
version = ieKey.GetValue("Version");
if (null == version)
throw new ApplicationException("Microsoft Internet Explorer is required!");
}
int.TryParse(version.ToString().Split('.')[0], out browserVersion);
}
if (browserVersion < 7)
{
throw new ApplicationException("Unsupported version of Microsoft Internet Explorer!");
}
UInt32 mode = 11000; // Internet Explorer 11. Webpages containing standards-based !DOCTYPE directives are displayed in IE11 Standards mode.
switch (browserVersion)
{
case 7:
mode = 7000; // Webpages containing standards-based !DOCTYPE directives are displayed in IE7 Standards mode.
break;
case 8:
mode = 8000; // Webpages containing standards-based !DOCTYPE directives are displayed in IE8 mode.
break;
case 9:
mode = 9000; // Internet Explorer 9. Webpages containing standards-based !DOCTYPE directives are displayed in IE9 mode.
break;
case 10:
mode = 10000; // Internet Explorer 10.
break;
}
return mode;
}
Now, your scripts do run, but the <object> still doesn't get created (alert(MyObject.object) shows null). Finally, you'd need to implement IObjectSafety interface on your ActiveX object and site-lock it to only your very own HTML pages. Without proper IObjectSafety the object won't be getting created under default IE security settings. Without site-locking it might become a huge security threat, as any malicious script possibly could create and use your object outside the context of your application.
Updated to address the comment:
I've updated the project with your provided example, note that I had
made a change such that there is a C# button and a Javascript button
to fire the event. The JS button works, but C# does not fire. I'm
looking for a "Hello from: C# button" alert.
In your code, the myObject instance gets created and accessed exclusively from C#:
MyObject myObject = new MyObject();
// ...
private void button1_Click(object sender, EventArgs e)
{
// Call ActiveX
myObject.SayHello("C# Button");
}
This instance has nothing to do with the <object id="MyObject" onerror="window.objectLoadFailure = true" classid="clsid:7A5D58C7-1C27-4DFF-8C8F-F5876FF94C64"></object> instance that you create from HTML. They're two separate, unrelated objects. Your event handlers only work for the latter <object> instance. You don't even subscribe to any events on the new MyObject() instance.
If I understand your goal correctly, you need this:
private void button1_Click(object sender, EventArgs e)
{
// Call ActiveX
//myObject.SayHello("C# Button");
this.webBrowser1.Document.InvokeScript("eval",
new[] { "MyObject.SayHello('C# Button')" });
}
Now, the JavaScript event handler would get invoked and you'd see the "C# Button" alert.
Related
I am currently working on a JavaFX based application, where users can interact with places that are marked on a world map. To do this, I am using an approach similiar to the one described in http://captaincasa.blogspot.de/2014/01/javafx-and-osm-openstreetmap.html ([1]).
However, I am facing a hard-to-debug problem related to the Javascript callback variable injected to the embedded HTML-page using the WebEngine's setMember() method (see also https://docs.oracle.com/javase/8/javafx/embedded-browser-tutorial/js-javafx.htm ([2]) for an official tutorial).
When running the program for a while, the callback variable is loosing its state unpredictably! To demonstrate this behaviour, I developed a minimal working/failing example. I am using jdk1.8.0_121 64-bit on a Windows 10 machine.
The JavaFx App looks as follows:
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javafx.application.Application;
import javafx.concurrent.Worker.State;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;
public class WebViewJsCallbackTest extends Application {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
public static void main(String[] args) {
launch(args);
}
public class JavaScriptBridge {
public void callback(String data) {
System.out.println("callback retrieved: " + data);
}
}
#Override
public void start(Stage primaryStage) throws Exception {
WebView webView = new WebView();
primaryStage.setScene(new Scene(new AnchorPane(webView)));
primaryStage.show();
final WebEngine webEngine = webView.getEngine();
webEngine.load(getClass().getClassLoader().getResource("page.html").toExternalForm());
webEngine.getLoadWorker().stateProperty().addListener((observableValue, oldValue, newValue) -> {
if (newValue == State.SUCCEEDED) {
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("javaApp", new JavaScriptBridge());
}
});
webEngine.setOnAlert(event -> {
System.out.println(DATE_FORMAT.format(new Date()) + " alerted: " + event.getData());
});
}
}
The HTML file "page.html" looks as follows:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<!-- use for in-browser debugging -->
<!-- <script type='text/javascript' src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script> -->
<script type="text/javascript">
var javaApp = null;
function javaCallback(data) {
try {
alert("javaApp=" + javaApp + "(type=" + typeof javaApp + "), data=" + data);
javaApp.callback(data);
} catch (e) {
alert("caugt exception: " + e);
}
}
</script>
</head>
<body>
<button onclick="javaCallback('Test')">Send data to Java</button>
<button onclick="setInterval(function(){ javaCallback('Test'); }, 1000)">Send data to Java in endless loop</button>
</body>
</html>
The state of the callback variable javaApp can be observed by clicking on the "Send data to Java in endless loop" button. It will continuously try to run the callback method via javaApp.callback, which produces some logging message in the Java app. Alerts are used as an additional communication channel to back things up (always seems to work and currently used as work-around, but that's not how things are ment to be...).
If everything is working as supposed, each time logging similiar to the following lines should be printed:
callback retrieved: Test
2017/01/27 21:26:11 alerted: javaApp=webviewtests.WebViewJsCallbackTest$JavaScriptBridge#51fac693(type=object), data=Test
However, after a while (anything from 2-7 minutes), no more callbacks are retrieved, but only loggings like the following line are printed:
2017/01/27 21:32:01 alerted: javaApp=undefined(type=object), data=Test
Printing the variable now gives 'undefined' instead of the Java instance path. A strange observation is that the state of javaApp is not truly "undefined". using typeof returnsobject, javaApp === undefined evaluates to false. This is in accordance with the fact that the callback-call does not throw an exception (otherwise, an alert starting with "caugt exception: " would be printed).
Using Java VisualVM showed that the time of failure happens to coincide with the time the Garbage Collector is activated. This can be seen by observing the Heap memory consumption, which drops from approx. 60MB to 16MB due to GC.
What's goining on there? Do you have any idea how I can further debug the issue? I could not find any related know bug...
Thanks a lot for your advice!
PS: the problem was reproduced much faster when including Javascript code to display a world map via Leaflet (cf [1]). Loading or shifting the map most of the time instantly caused the GC to do its job. While debugging this original issue, I traced the problem to the minimal example presented here.
I solved the problem by creating an instance variable bridge in Java that holds the JavaScriptBridge instance sent to Javascript via setMember(). This way, Gargbage Collection of the instance is prevented.
Relevant code snippet:
public class JavaScriptBridge {
public void callback(String data) {
System.out.println("callback retrieved: " + data);
}
}
private JavaScriptBridge bridge;
#Override
public void start(Stage primaryStage) throws Exception {
WebView webView = new WebView();
primaryStage.setScene(new Scene(new AnchorPane(webView)));
primaryStage.show();
final WebEngine webEngine = webView.getEngine();
webEngine.load(getClass().getClassLoader().getResource("page.html").toExternalForm());
bridge = new JavaScriptBridge();
webEngine.getLoadWorker().stateProperty().addListener((observableValue, oldValue, newValue) -> {
if (newValue == State.SUCCEEDED) {
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("javaApp", bridge);
}
});
webEngine.setOnAlert(event -> {
System.out.println(DATE_FORMAT.format(new Date()) + " alerted: " + event.getData());
});
}
Altough the code now works smoothly (also in conjunction with Leaflet), I am still irritated of this unexpected behaviour...
Edit: The explanation for this behaviour is documented since Java 9 (thanks #dsh for your clarifying comment! I was working with Java 8 at the time and unfortunately didn't have this information at hand...)
I have a JavaFX application that makes heavy use of a WebView. I am trying to insert an object into the DOM that the JavaScript code can use, and I need these objects to be available as new pages are loaded.
However, when I run the program, FirebugLite shows the object in the DOM, but the functions do not execute.
According to some Oracle documentation, this seems to be the appropriate way to provide upcalls from JavaScript to Java. I've also seen a few StackOverflow posts explaining the same thing.
What am I missing? I'm using Java 8, Update 51, 64-bit on Windows 7.
Java:
public class DemoApplication extends Application {
Debug debug;
#Override
public void start(final Stage stage) throws Exception {
debug = new Debug();
WebView browser = new WebView();
WebEngine webEngine = browser.getEngine();
webEngine.getLoadWorker().stateProperty().addListener(
new ChangeListener<Worker.State>() {
#Override
public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue) {
if (newValue == Worker.State.SUCCEEDED) {
JSObject windowObject = (JSObject) webEngine.executeScript("window");
windowObject.setMember("Debug", debug);
}
}
}
);
webEngine.load("http://localhost:8080/page1.html");
stage.setScene(new Scene(browser));
stage.show();
}
}
public class Debug {
public void print(final Object text) {
System.err.println(text);
}
}
HTML/JavaScript:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript" src="https://getfirebug.com/firebug-lite.js"></script>
<script>
Debug.print("Hello");
</script>
</head>
<body>
Page 1
Page 2
</body>
</html>
Firebug Screenshot:
I believe what is happening is that WebEngine loads the page, the ChangeListener is invoked at various points (SCHEDULED, RUNNING, SUCCEEDED, etc.). Once the Worker.State.SUCCEEDED event happens, the page has already finished loading all content and has finished executing that content as well. So basically my calls to Debug.print() in the JavaScript code were happening early and calling on an object that was undefined or null.
That's my best guess anyway, because if I add a JavaScript function that is executed by the Java portion after adding in the objects, everything works as expected.
This is how I modified the JavaScript side:
<script>
// callback that uses java objects
window.ready = function() {
Debug.print("Hello");
}
</script>
And this is how I modified the Java side:
webEngine.getLoadWorker().stateProperty().addListener(
new ChangeListener<Worker.State>() {
#Override
public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue) {
if (newValue == Worker.State.SUCCEEDED) {
JSObject windowObject = (JSObject) webEngine.executeScript("window");
windowObject.setMember("Debug", debug); // insert object
windowObject.call("ready"); // execute callback
}
}
}
);
The key changes here are the ready() function in the JavaScript, and invoking that function after injecting the objects on the Java side. This ensures those objects are available before being called.
I tried this on a few different pages and when going from page to page, when the ready() function was called Debug.print() executed properly, even when using WebEngine.reload(), or WebHistory.go().
I want to make a class of mine accessible in JavaScript via a C# WebView-Control.
Therefore I am using the WebView.AddWebAllowedObject method. However if I assign an attribute, it works fine, but if I assign the whole class to get all attributes in js, all of the attributes(and methods btw) are "undefined". I tried everything I found in the www. See the attached code:
//The class I want to make accessible
[AllowForWeb, ComVisible(true)]
[MarshalingBehavior(MarshalingType.Agile)]
public class DeviceInformation
{
public string IPAdress { get; private set; }
public DeviceInformation()
{
IPAdress = GetIPAdress();
}
public string GetDeviceUUID()
{
EasClientDeviceInformation deviceinfo = new EasClientDeviceInformation();
return deviceinfo.Id.ToString();
}
public string GetIPAdress()
{
List<string> ipAddresses = new List<string>();
var hostnames = NetworkInformation.GetHostNames();
foreach (var hn in hostnames)
{
if (hn?.IPInformation != null && (hn.IPInformation.NetworkAdapter.IanaInterfaceType == 71 ||
hn.IPInformation.NetworkAdapter.IanaInterfaceType == 6))
{
string ipadress = hn.DisplayName;
return ipadress;
}
}
return string.Empty;
}
}
Here the objects are initialized.
DeviceInformation devinf = new DeviceInformation();
private void View_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
{
if (args.Uri.Host == "")
{
//win_ipadress has an ipadress as value
view.AddWebAllowedObject("win_ipadress", devinf.IPAdress);
//deviceInformation is initialized as well but I have no access to its attributes
view.AddWebAllowedObject("deviceInformation", devinf);
}
}
That's the way i call it in js:
else if ($.os.ie) {
myIpAdr = window.win_ipadress;
//Throws an exception because GetIPAdress() is "undefined"
myIpAdr = window.deviceInformation.GetIPAdress();
}
I am using this in a Windows Universal App. The Javascript and in the WebView displayed HTML-Code is already in use for Android an iOS.
I believe you need to define the method name starting with a lower case character.
For example: change GetIPAddress to getIPAddress.
I tested it on my side and found if I use the upper case name 'GetIPAddress', it won't work. But if I use getIPAddress, it works.
And after I read kangax's explanation in this thread, I think it makes sense.
[Update]
Since it still doesn't work after you make the change on method name, I think the issue should be related to how you expose the windows runtime object. I guess you simply defined the DeviceInformation class and tried to use it in the same project.
First, we need to create a separate windows universal windows runtime component project.
The c# class DeviceInformation should be put into this project. Keep the same code.
Then, in your universal app project, add reference to the windows runtime component and keep rest code to consume the windows runtime object.
[Update 2]
Just noticed an interesting behavior in VS. No matter if the Method name we defined in C# is starting with uppercase or lowercase, the visual studio intellisense shows the lowercase, so the method name will be automatically converted when we try to use it in js.
I'm using the new WebEngine to play around and learn.
I've been trying to find some similar methods found using Qt WebKit: addToJavaScriptWindowObject()
I found that using Qt WebEngine, I have to use the QWebChannel to register functions to the JavaScript window object. If this is correct, it takes me to the following question.
I've installed Qt 5.4.0 on my computer. I noticed that qwebchannel.js is not found in the SDK installed on my computer. I found it on the Git source.
If I have a Qt native desktop application with a QWebEnginePage and QWebEngineView, what do I need to be able to register functions on the JavaScript window object?
My desktop application navigates automatically to a http page that I have created. So I have access to the content connected to the QWebEngineView.
What are the steps to take so I can make this work?
In Qt5.6, if you want to make C++ part and JavaScript to communicate, the only way to do it is using QWebChannel on a QWebEngineView, as you stated. You do it this way in the .cpp file:
m_pView = new QWebEngineView(this);
QWebChannel * channel = new QWebChannel(page);
m_pView->page()->setWebChannel(channel);
channel->registerObject(QString("TheNameOfTheObjectUsed"), this);
Here, you just say that you register an object named TheNameOfTheObjectUsed that will be available on the JS side. Now, this is the part of code to use in the JS side :
new QWebChannel(qt.webChannelTransport, function (channel) {
// now you retrieve your object
var JSobject = channel.objects.TheNameOfTheObjectUsed;
});
Now, if you want to retrieve some properties of the class in the JS side, you need to have a method on the C++ side which returns a string, an integer, a long... This is what it looks like on the C++ side, in your .h:
Q_INVOKABLE int getInt();
Q_PROPERTY(int myIntInCppSide READ getInt);
And now, you get the int like this on the JS side :
var myIntInJSside= JSobject.myIntInCppSide;
This is a very simple explanation, and I recommend you to watch this video which was very useful to me. Also, you might want to read more about the JavaScript API provided by QWebChannel, as well as the documentation about QWebChannel.
Hope that helps!
I will summarize your questions as following:
Do I need QWebChannel to register JavaScript functions in the WebEngine?
Where can I find QWebChannel.js
How to communicate JS to C++ and C++ to JS
First, let take a simple code to play with:
#include <QApplication>
#include <QDebug>
#include <QWebEngineView>
#include <QWebChannel>
// ... DEFINITIONS HERE
auto main( int argn, char* argv[] )-> int
{
QApplication app(argn, argv);
QWebEngineView browser;
browser.resize(QSize(800,600));
browser.show();
browser.load(QUrl("http://www.wikipedia.org"));
// .. SETUP HERE
QObject::connect(&browser, &QWebEngineView::loadFinished, [&browser](bool ok)
{
qDebug()<<"Load Finished " << ok;
// TEST CODE HERE
));
return app.exec();
}
Explanation: This code creates a Qt application, creates a QWebEngineView and set some minimal properties to make it visible.
A page from 'Wikipedia' is loaded inside and a signal/slot event is connected to print some log when the page is finally loaded.
How to call JS functions from C++ ?
You can simply call JS using QWebEnginePage::runJavaScript as following. Add this code to the TEST CODE HERE.
QString code = QStringLiteral(
R"DELIM(
var links = document.getElementsByTagName('a');
for ( var i=0; i<links.length; ++i)
{
links[i].style.backgroundColor = 'yellow';
};
)DELIM");
browser.page()->runJavaScript(code, 42);
Explanation: This code execute some JS into the browser, on a context ID 42, avoiding collision with the default context of the page ID 0. The script change the background-color of each link to yellow.
How to call C++ from JS?
In this case, we need the QWebChannel mechanism to register C++ objects into JavaScript.
First, let create the C++ interface, callable from JS (in DEFINITION):
class JsInterface: public QObject
{
Q_OBJECT
public:
/// Log, for debugging
Q_INVOKABLE void log(const QString& str) const
{
qDebug() << "LOG from JS: " << str;
}
};
#include "main.moc"
Explanation: This code declare and define a QObject class with a simple log function inside. It is important to declare the function Q_INVOKABLE otherwise JavaScript can not find it!. As the declaration is inside the same file as the rest of the code, we include the auto-moc file from QT after (it is main.moc because my file is main.cpp).
Create a function in DEFINITION which return the JavaScript QWebChannel.js content. The content of QWebChannel.js can be found in your QT library (./5.12.2/Src/qtwebchannel/examples/webchannel/shared/qwebchannel.js or ./Examples/Qt-5.12.2/webchannel/shared/qwebchannel.js). You are free to load this directly in your page.
In DECLARATION section, append:
QString qWebChannelJs()
{
return R"DELIMITER(
// COPY HERE ALL THE FILE
)DELIMITER";
}
And we inject it in our code (Append it to TEST CODE HERE section):
browser.page()->runJavaScript(qWebChannelJs(), 42);
We need to setup the QWebChannel in C++ side (SETUP section):
QWebChannel channel;
JsInterface jsInterface;
browser.page()->setWebChannel(&channel, 42);
channel.registerObject(QString("JsInterface"), &jsInterface);
Explanation: We create a channel, the JsInterface object and register them into the browser. We need to use the same context id 42 (but could be another other number between 0 and 255).
Finally, in our JS code, we access the channel and call the function of the interface (append to TEST CODE section):
QString code2 = QStringLiteral(
R"DELIM(
window.webChannel = new QWebChannel(qt.webChannelTransport, function( channel)
{
var cpp = channel.objects.JsInterface;
cpp.log("Hello from JavaScript");
});
)DELIM");
browser.page()->runJavaScript(code2, 42);
Considerations
It is worth to mention that any call from C++ to JavaScript or from JavaScript to C++ goes through an Inter-Process-Communication (IPC) which is asynchronous. This means that runJavaScript returns before the JavaScript is executed, and that JavaScript returns before the C++ logis executed.
Full code
#include <QApplication>
#include <QDebug>
#include <QWebEngineView>
#include <QWebChannel>
QString qWebChannelJs()
{
return R"DELIMITER(
// TODO INSERT JS code here
)DELIMITER";
}
class JsInterface: public QObject
{
Q_OBJECT
public:
/// Log, for debugging
Q_INVOKABLE void log(const QString& str) const
{
qDebug() << "LOG from JS: " << str;
}
};
#include "main.moc"
auto main( int argn, char* argv[] )-> int
{
QApplication app(argn, argv);
QWebEngineView browser;
browser.resize(QSize(800,600));
browser.show();
browser.load(QUrl("http://www.wikipedia.org"));
// .. SETUP HERE
QWebChannel channel;
JsInterface jsInterface;
browser.page()->setWebChannel(&channel, 42);
channel.registerObject(QString("JsInterface"), &jsInterface);
QObject::connect(&browser, &QWebEngineView::loadFinished, [&browser](bool ok)
{
qDebug()<<"Load Finished " << ok;
// TEST CODE HERE
QString code = QStringLiteral(
R"DELIM(
var links = document.getElementsByTagName('a');
for ( var i=0; i<links.length; ++i)
{
links[i].style.backgroundColor = 'yellow';
};
)DELIM");
browser.page()->runJavaScript(code, 42);
browser.page()->runJavaScript(qWebChannelJs(), 42);
QString code2 = QStringLiteral(
R"DELIM(
window.webChannel = new QWebChannel(qt.webChannelTransport, function( channel)
{
var cpp = channel.objects.JsInterface;
cpp.log("Hello from JavaScript");
});
)DELIM");
browser.page()->runJavaScript(code2, 42);
});
return app.exec();
}
Related topics:
How to setup QWebChannel JS API for use in a QWebEngineView?
External documentation:
https://doc.qt.io/qt-5/qwebengineview.html
https://doc.qt.io/qt-5/qwebchannel.html
https://doc.qt.io/qt-5/qtwebengine-webenginewidgets-contentmanipulation-example.html
Qt has documentation on this now:
Qt WebChannel Standalone Example
You have to add a QWebSocketServer to your cpp app that the QWebEngineView's HTML/Javascript will connect to using a WebSocket. Then use QWebChannel for two way communication.
An alternative and much simpler way of communication with the page is to use runJavaScript function:
view->page()->runJavaScript("alert('Hello from C++');");
It has its limitations: the call must be initiated from the C++ side and you can get only synchronous response from JS. But there is an upside too: no modification of the underlying webpage is necessary.
Currently opened webpage can be accessed using QWebEngineView::page() function, as in the example above. During the navigation, the browser doesn't change the page until the next one is received from the network, so this function returns valid page object at any time. But your JS may still interrupt new page loading in a way that you'll appear in the document.readyState == 'loading' where the DOM tree is not yet constructed and some scripts on the page might not have been run yet. In this case you should wait for the DOMContentLoaded event.
I have read WebBrowser Control from .Net — How to Inject Javascript, Is it possible to call Javascript method from C# winforms and many others. Those examples were returns function value or alert window (synchronous calls). I have to get result from event handler (async call):
<script type="text/javascript">
window.onload = function() {
var o = new M.Build(document.getElementById("ZID"));
M.Events.observe(o, o.Events.Success, function() {
// I have to get some value!!
});
M.Events.observe(o, o.Events.Fault, function() {
// I have to get some value!!
});
}
</script>
Calling C# from JavaScript
Simply put, you can expose a C# object
to the WebBrowser that the JavaScript
can call directly The WebBrowser
class exposes a property called
ObjectForScripting that can be set by
your application and becomes the
window.external object within
JavaScript. The object must have the
ComVisibleAttribute set true
C#:
[System.Runtime.InteropServices.ComVisibleAttribute(true)]
public class ScriptInterface
{
public void callMe()
{
… // Do something interesting
}
}
webBrowser1.ObjectForScripting = new ScriptInterface();
Javascript:
window.external.callMe();
Calling JavaScript in a WebBrowser control from C#
This is code I have. In the DocumentCompleted event ('cause I'm getting a page from online)
var wb = (WebBrowser)sender
//Lots of other stuff
object obj = wb.Document.InvokeScript("MyFunctionName");
Create a function that returns whatever value you need and invoke away.
You can also inject a script into the page
string js = "function MyFunctionName(){alert('Yea!');}";
HtmlElement el = wb.Document.CreateElement("script");
IHTMLScriptElement element2 = (IHTMLScriptElement)el.DomElement;
element2.text = js;
head.AppendChild(el);
which can then be invoked. That's what I've done.
If your webBrowser control is in a form, you can do the following:
[System.Runtime.InteropServices.ComVisibleAttribute(true)]
public class Form1
{
public Form1()
{
InitializeComponent();
webBrowser1.ObjectForScripting = this;
}
public void CallMe()
{
//.... this method can be called in javascript via window.external.CallMe();
}
}