I'm writing a serialization of objects to QML, and want to be able to get source code of functions defined in QML object. Suppose I have the following example in QML (test.qml):
import QtQml 2.2
QtObject {
function foo() {
return 42;
}
}
I created a QObject: obj from that.
Is there any way (can be hacky) to get the source code of obj's method foo without parsing the QML file obj was created from?
It's okay to use QQmlComponent obj was created from or any other Qt classes, as long as I don't have to parse it myself. Alternatively, how to get the function's source code from the test.qml file without writing my own parser? I don't want to assume anything special about test.qml (e.g. it can be different than the one above and it doesn't have to be simple enough to use a regexp or other not full-fledged QML parser).
Assuming this works like JavaScript, I tried something like:
QQmlExpression expr(engine.rootContext(), obj, "foo.toString()");
QVariant sourceCode = expr.evaluate();
However, it doesn't work.
Edit: According to http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.4.2 the toString method of a function object prototype is implementation-defined. In case of QML I get the result:
QVariant(QString, "function() { [code] }")
Since there doesn't seem to be a way to get the code by JS or C++, I'm not limiting myself to public Qt API anymore.
I think it is impossible to get a source code of a function from an already created QML object. There doesn't seem to be any C++ interface for it and JavaScript doesn't return it using toSource method either.
However, it can be retrieved using QML parser. The bad news are that QML parser is a part of Qt private API, so it might not work when using different Qt library builds.
The code to parse QML using Qt 5.3.0 private API is more or less:
.pro file:
QT += qml qml-private
cpp file:
using namespace QQmlJS::AST;
class LVisitor: public QQmlJS::AST::Visitor {
public:
LVisitor(QString code): _code(code) {}
virtual bool visit(FunctionBody *fb) {
qDebug() << "Visiting FunctionBody" <<
printable(fb->firstSourceLocation(), fb->lastSourceLocation());
return true;
}
private:
QStringRef printable(const SourceLocation &start, const SourceLocation &end) {
return QStringRef(&_code, start.offset, end.offset + end.length - start.offset);
}
private:
QString _code;
};
void testQmlParser() {
QFile file(":/test.qml");
file.open(QFile::ReadOnly);
QString code = file.readAll();
file.close();
QQmlJS::Engine engine;
QQmlJS::Lexer lexer(&engine);
lexer.setCode(code, 1, true);
QQmlJS::Parser parser(&engine);
if (!parser.parse() || !parser.diagnosticMessages().isEmpty()) {
foreach (const QQmlJS::DiagnosticMessage &m, parser.diagnosticMessages()) {
qDebug() << "Parse" << (m.isWarning() ? "warning" : "error") << m.message;
}
}
UiProgram *ast = parser.ast();
LVisitor visitor(code);
ast->accept(&visitor);
}
To get more precise information about the object where the function is defined or just get more information from the AST, implement more methods of QQmlJS::AST::Visitor.
I also didn't find any way to do so. but I found a way around to do it.
reffer this firts
Now, As you knwo we can access an QML object in C++. will do following to run the function in QML.
Item {
width: 100; height: 100
Rectangle {
property bool call:true
objectName: "rect"
onCallChanged()
{
myFunction();
}
function myFunction()
{
//your code
}
}
}
and in C++ you have to do following:
QObject *rect = object->findChild<QObject*>("rect");
if (rect)
rect->setProperty("call", !(rect->property("call").toBool()));
here we are using change event of property 'call' to call the myFunction()
Related
Edit: Here is a minimal project that illustrates my issue. You can see the error described by serving it to the browser: pub get and then either pub serve (dartium) or pub build --mode=debug (other browsers).
How can I access an arbitrary JavaScript property from Dart through a JsObjectImpl? I am using the ace.js library with an interop to Dart that I've adapted from a typescript interface, and the method I am calling returns a plain javascript object with key-value pairs.
Dart gives me a JsObjectImpl, which cannot be casted to a Map or a JsObject, both of which have [] accessors. It confusingly seems to inherit from the deprecated JSObject (note the 's' is capitalized in the latter) which does not have the [] accessor, so I can't get the data out.
Some error messages:
When attempting a cast from JsObjectImpl to JsObject:
ORIGINAL EXCEPTION: type 'JSObjectImpl' is not a subtype of type 'JsObject' of 'obj' where
JSObjectImpl is from dart:js
JsObject is from dart:js. I get a similar message when using Map as well.
Looking at the object in the debugger, I can frustratingly see the property in JS view but not in the Dart object: The 4: Object is the data I want.
Ok, this was a fun one, happy holidays :)
It looks like Map is not a supported auto-conversion for package:js. So a couple of things:
Filed https://github.com/dart-lang/sdk/issues/28194
Sent your a PR introducing a workaround
For interested parties, we can use the browser-native Object.keys:
#JS()
library example;
import 'package:js/js.dart';
/// A workaround to converting an object from JS to a Dart Map.
Map jsToMap(jsObject) {
return new Map.fromIterable(
_getKeysOfObject(jsObject),
value: (key) => getProperty(jsObject, key),
);
}
// Both of these interfaces exist to call `Object.keys` from Dart.
//
// But you don't use them directly. Just see `jsToMap`.
#JS('Object.keys')
external List<String> _getKeysOfObject(jsObject);
And call it once we have an arbitrary JavaScript object:
var properties = jsToMap(toy.getData());
print(properties);
I had to modify #matanlurey solution so it works on dart 2.12 and is recursive.
import 'dart:js';
/// A workaround to deep-converting an object from JS to a Dart Object.
Object jsToDart(jsObject) {
if (jsObject is JsArray || jsObject is Iterable) {
return jsObject.map(jsToDart).toList();
}
if (jsObject is JsObject) {
return Map.fromIterable(
getObjectKeys(jsObject),
value: (key) => jsToDart(jsObject[key]),
);
}
return jsObject;
}
List<String> getObjectKeys(JsObject object) => context['Object']
.callMethod('getOwnPropertyNames', [object])
.toList()
.cast<String>();
Migrating from QtWebKit to QtWebEngine, using QWebChannel.
I have an invokable function that sends a QVariant Object to the Javascript, which is seen as a JSON object. So a QString becomes a string, a QInt an int, etc.
Using QtWebKit without QWebChannel, a QByteArray was seen as a Uint8ClampedArray, but is now directly transformed to a string using UTF-8 (Which my QByteArray is not :( )
Did I do something wrong ? What should I do ?
Here is the relevant code part :
//Qt Window class signal to javascript
void MyWindow::uplink(Response msg)
{
emit _nativeToJs(msg->toJson());
}
//Response class toJson() method
QVariantMap Response::toJson() const
{
QVariantMap map;
map["id"] = m_id; //qulonglong
map["src"] = QString(m_src);
map["dst"] = QString(m_dst);
map["status"] = m_status; //qint16
map["result"] = m_result; //QVariant, can be a map of string, arrays, etc
return map;
}
//Javascript
var foo;
new QWebChannel(qt.webChannelTransport, function(channel) {
//we connect the signal
channel.objects.foo._nativeToJs.connect(function(msg){
//msg is now a JSON object
});
});
msg.result should contains a clamped array (msgpack data) that I later decode. Now I have an ugly string of not UTF-8 chars interpreted as UTF-8, which I can't do anything with.
Not an answer at all, but a beginning of research, as it is a very interesting question.
In Qt versions < Qt5.6, you can find how the conversion is done by looking into Qt sources. In particular, I found this function in the file C:\Qt\5.5\Src\qtwebkit\Source\WebCore\bridge\qt\qt_runtime.cpp:
JSValueRef convertQVariantToValue(JSContextRef context, PassRefPtr<RootObject> root, const QVariant& variant, JSValueRef *exception)
and this piece of code inside it :
if (type == QMetaType::QByteArray) {
QByteArray qtByteArray = variant.value<QByteArray>();
WTF::RefPtr<WTF::Uint8ClampedArray> wtfByteArray = WTF::Uint8ClampedArray::createUninitialized(qtByteArray.length());
memcpy(wtfByteArray->data(), qtByteArray.constData(), qtByteArray.length());
ExecState* exec = toJS(context);
APIEntryShim entryShim(exec);
return toRef(exec, toJS(exec, static_cast<JSDOMGlobalObject*>(exec->lexicalGlobalObject()), wtfByteArray.get()));
}
which seems to be the processing of a QByteArray on the JS side.
I also believe that by migrating from Qt WebKit to Qt WebEngine, Qt uses now V8, whereas before it was WebCore and JavaScript Core (source : this thread). So, things might have changed but I don't know to what extent.
At the moment, I can't search further in the Qt sources for Qt5.6, and thus I can't provide a real answer, but I hope this will motivate you or anyone else to look into it and to clarify this behavior :-).
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.
im thinking about replacing all my xml files and builders that i use for configuration with javascript / nashorn. lets say i have a java class that is builder style configuration object
class Configuration {
String name;
Configuration withName(String name) {
this.name = name;
return this;
}
int number;
Configuration withNumber(int number) {
this.number = number;
return this;
}
}
i would like to instantiate this class directly in javascript and have nashorn return to me an instance of it. i would like to code it in javascript like
{
name: 'qwerty',
number: 42
};
and then finally read the file, pass it into the script engine, and have it evaluate the object as an instance of Configuration.
is this possible with a json like syntax?
i wouldnt have a problem using the Packages.Configuration / Java.type("Configuration"); Java.extend(), but have yet to have any success with that.
or would i have to make / use a proper reader for the returned value?
No, Nashorn won't support this from what I've seen from available docs and JSR-223. You might need to use i.e. Jackson's ObjectMapper to deserialize JSON. And this should not be a problem, because Nashorn allows you to do almost anything in JavaScript: A JavaFX Script Application Examples.
You might also want to consider Groovy which has it's own map syntax which can be used inside constructors: Groovy Goodness: Using Lists and Maps As Constructors. Groovy is often used to define configuration.
You could cheat a little by attaching a converter function to your configuration type. This could be added easily on the JS side rather than trying to make it in Java.
var ConfigClass = Java.type('Configuration');
ConfigClass.fromJsObj = function(jsObj) {
var newConfig = new ConfigClass();
foreach(var prop in jsObj) {
newConfig[prop] = jsObj[prop];
}
return newConfig;
}
var myConfig = ConfigClass.fromJsObj( { name: "qwerty", number: 42 } );
I have an MFC application that uses CHtmlView. It displays some text in html format from some temp html file. Is it possible to handle mouse click on a paragraph to send some data to the program? I understand that javascript can be used to handle click, but how to pass the data from javascript function to the application??
Thanks.
It is possible to cleanly call the containing application from within the Javascript of the HTML page. At the Javascript level the MSHTML interface that is doing the actual work of the CHtmlView provides an "external" object that acts as a way back to the calling application.
Suppose we want to add a method "someCall()" that can be called from Javascript, and that the method takes a string as an argument. In JavaScript we would call it with something like
external.someCall("An example string");
In the MFC application, we need to write a CCmdTarget derived object to act as the implementation of the "external" object as a dispatch-based COM object, something like:
class TestExternal : public CCmdTarget
{
public:
TestExternal()
{
EnableAutomation();
}
void SomeCall(LPCWSTR str)
{
// This is where we get called when the Javascript runs...
}
private:
DECLARE_DISPATCH_MAP()
};
BEGIN_DISPATCH_MAP(TestExternal,CCmdTarget)
DISP_FUNCTION(TestExternal,"someCall",SomeCall,VT_EMPTY,VTS_WBSTR)
END_DISPATCH_MAP()
To tie this implementation of "external" with the HTML view, in a class derived from CHtmlView you need to over-ride OnGetExternal() and to point it to an instance of TestExternal that lives at least as long as the CHtmlView:
class TestHtmlView : public CHtmlView
{
// Usual implementation stuff goes here...
public:
HRESULT OnGetExternal(LPDISPATCH *lppDispatch)
{
*lppDispatch = m_external.GetIDispatch(TRUE);
return S_OK;
}
private:
TestExternal m_external;
};
Note that I haven't actually tested this, but it seems about right from memory ...