I have started to dig in to C++ and Qt again, and have been mucking around with the WebKit Javascript/Qt bindings. I've got all the moving parts working, with the exception of my QObject subclass being "undefined" on the Javascript side. Here's the simple test app I'm having trouble with:
My main window implementation:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
// instantiate a webview
QWebView *webview = new QWebView(this);
webview->setGeometry(0, 0, 400, 300);
webview->setUrl(QUrl("file://localhost/Users/kyle/Sites/tests/qt/index.html"));
// instantiate and attach our QObject
hello *h = new hello();
QWebFrame *frame = webview->page()->mainFrame();
frame->addToJavaScriptWindowObject("Hello", h);
// show the window
webview->show();
}
Hello.cpp
...snip...
QString hello::say()
{
return QString("Kyle");
}
Hello.h
...snip includes...
class hello : public QObject
{
Q_OBJECT
public:
hello();
Q_INVOKABLE QString say();
};
The above-mentioned index.html file does a simple alert(Hello.say()) call, but doing typeof Hello, I get undefined.
I'm a bit rusty with C++, and pretty new to Qt, so I'm sure this is a noob mistake, but I'm stumped.
Objects can't be inserted in the page at any time. You should put that line:
frame->addToJavaScriptWindowObject("Hello", h);
in a slot connected to the javaScriptWindowObjectCleared() signal of the QWebFrame and move some code around, so you can access the frame from that slot.
See also the Form Extractor example included with Qt.
The core of this is really implemented in two methods, which are
shown below:
void MyApi::setWebView( QWebView *view )
{
QWebPage *page = view->page();
frame = page->mainFrame();
attachObject();
connect(frame, &QWebFrame::javaScriptWindowObjectCleared, this, &MyApi::attachObject);
// old approach
//connect( frame, SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(attachObject()) );
}
void MyApi::attachObject()
{
frame->addToJavaScriptWindowObject( QString("MyApi"), this );
}
This code is all that you need in order to make all of the public slots of the
MyApi object visible to javascript. The MyApi class provides two public slots:
public slots:
void doSomething( const QString ¶m );
int doSums( int a, int b );
The first slot simply logs a message to the debug output, the second returns
the sum of its two arguments (yes, slots can return things!). They're called
from javascript like this:
MyApi.doSomething( 'Hello from JS page 2!!!!' );
sum = MyApi.doSums( 2, 3 );
alert( 'C++ says the sum is ' + sum );
The code above was tested in QT5.5, and please note all methods should be put in "public slots" section.
Related
I've started learning QML recently (after trying it out a long time ago) and I'm stuck at the way Qt C++ code interacts with QML and vice versa.
I have a Counter which has the following header:
#include <QObject>
#include <QTimer>
class Counter : public QObject
{
Q_OBJECT
Q_PROPERTY(int count
READ getCount
WRITE setCount
NOTIFY signalCountChanged)
public:
Counter(QObject *parent = Q_NULLPTR);
int getCount();
void setCount(int count);
signals:
void signalCountChanged(int);
public slots:
void slotStart();
private slots:
void slotTimeout();
private:
int count;
QTimer *timer;
};
My main.cpp is as follows:
#include <QtGui/QGuiApplication>
#include <QtQml/QQmlContext>
#include <QtGui/QGuiApplication>
#include <QtQuick/QQuickItem>
#include <QtQuick/QQuickView>
#include "counter.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<Counter>("org.qmlplayground.counter", 0, 1, "Counter");
QQuickView view;
view.setSource(QUrl(QStringLiteral("qrc:/main.qml")));
QObject *viewO = dynamic_cast<QObject*>(view.rootObject());
Counter c;
// Register Counter instance as "counter" property of top level context so that it can be accessed from within the QML code (for example: set the value count)
view.rootContext()->setContextProperty("counter", &c);
QObject::connect(viewO, SIGNAL(signalStartCounter()),
&c, SLOT(slotStart()));
QObject::connect(viewO, SIGNAL(signalQuit()), &app, SLOT(quit()));
view.show();
return app.exec();
}
and finally part of my QML (the main.qml which is loaded in the QQuickView while the rest being an UI form):
import QtQuick 2.7
import QtQuick.Window 2.2
// Importing some JavaScript files
import "qrc:/loggingFunctions.js" as LOG
import "qrc:/parseFunctions.js" as PARSE
// Importing a Qt C++ custom module
import org.qmlplayground.counter 0.1
MainForm {
property int countState: counter.count // ERROR HERE
signal signalStartCounter()
signal signalQuit()
anchors.fill: parent
textInputMouseArea.onClicked: {
LOG.logger("Clicked! Selecting all text in text input field", "N")
textInput.selectAll()
}
textInput.onAccepted: {
if(textInput.text === "quit") signalQuit()//Qt.quit();
if(textInput.text === "help") textInput.text = LOG.logger("Displaying help", "H");
var res = PARSE.parseInput(textInput.text);
if(res && (typeof res === 'object') && res.constructor === Array) {
switch(res[0]) {
case "fact":
labelOutput.text = res[1];
break;
case "count":
counter.count = res[1];
signalStartCounter();
break;
}
}
}
onCountStateChanged:
console.log("Hello")
textInput.onTextChanged:
console.log("Text changed");
}
As you can see I've already tested two signals sent from my QML to my C++ code one being connected to my QGuiApplication's slot quit() and the other being connected to my counter's slot slotStart(). It works fine. It seems that the line
counter.count = res[1];
doesn't cause any issues (perhaps because it's JS and not QML?). Now I want to read the count value of my Counter instance and update my UI accordingly. If I'm not mistaken every QML property automatically gets a couple of things one of these being the onChanged method (event handler or whatever it's called).
When I run my code however I get
`qrc:/main.qml:21: ReferenceError: counter is not defined
I thought that doing view->rootContext()->setContextProperty("counter", &c); would be enough but it seems that I'm missing something. So the more general question would be how do I properly make a C++ object visible in QML context.
This took me perhaps 2 hours to figure out (I posted my question when I was on the verge of commiting a suicide :D) but the answer was really obvious: how can I call for a property which hasn't been initialized yet? The solution to my problem is basically to move the setContextProperty() BEFORE I load the QML file:
// ...
QQuickView view;
Counter c;
view.rootContext()->setContextProperty("counter", &c);
view.setSource(QUrl(QStringLiteral("qrc:/main.qml")));
// ...
By doing so the property is first added to the root context of the view and after that the additional QML stuff is loaded but the counter property is still present). With the previous version of my code I was basically trying to access counter inside my QML file BEFORE I have added it as a property.
There is an existing solution for CefGlue: Call .Net from javascript in CefSharp 1 - wpf
I want exactly this, but for CefGlue: I want to communicate with the App using JavaScript. So when I click a button in my HTML site, I want the application to handle this (for example: start a tcp server).
I tried to register an own CefV8Handler but without success, the Execute function on the handler is never called. Here is what I do right now
protected override void OnWebKitInitialized()
{
Console.WriteLine("Registering testy extension");
Xilium.CefGlue.CefRuntime.RegisterExtension("testy", "var testy;if (!testy)testy = {};(function() {testy.hello = function() {};})();", new V8Handler());
base.OnWebKitInitialized();
}
My V8Handler code looks as follows:
public class V8Handler : Xilium.CefGlue.CefV8Handler
{
protected override bool Execute(string name, CefV8Value obj, CefV8Value[] arguments, out CefV8Value returnValue, out string exception)
{
if (name == "testy")
Console.WriteLine("CALLED TESTY");
else
Console.WriteLine("CALLED SOMETHING WEIRED ({0})", name);
returnValue = CefV8Value.CreateNull();
exception = null;
return true;
}
}
I'm in multiprocess mode, no console window shows "CALLED TESTY" nor "CALLED SOMETHING WEIRED".
Found a solution for that. The trick is to create a CefV8Value (CreateFunction) and assign it to a V8Handler. Then assign this value to the global context. This is what it looks like:
internal class RenderProcessHandler : CefRenderProcessHandler
{
protected override void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context)
{
CefV8Value global = context.GetGlobal();
CefV8Value func = CefV8Value.CreateFunction("magic", new V8Handler());
global.SetValue("magic", func, CefV8PropertyAttribute.None);
base.OnContextCreated(browser, frame, context);
}
}
Another problem came up: it was called in the renderer process, but I required the callback in the browser process. In the CefV8Handlers execute function i did this:
var browser = CefV8Context.GetCurrentContext().GetBrowser();
browser.SendProcessMessage(CefProcessId.Browser, CefProcessMessage.Create("ipc-js." + name));
This way I can retrive the message in the OnProcessMessageReceived function in the CefClient implementation.
I'm trying to implement the possibility to script an existing QT application.
It works fine, but some function of my class return a TypeError.
Myclass.h (really simplified) :
class Myclass: public QObject
{
Q_OBJECT
public slots:
int firstfunction() const;
int secondfunction() const;
private:
int m_firstResult;
int m_secondResult;
}
Myclass.cpp :
int Myclass::firstfunction() const
{
return m_firstResult;
}
int Myclass::secondfunction() const
{
return m_secondResult;
}
The main :
Myclass qtObjectClass();
QScriptEngine scriptEngine;
QScriptValue qValue= scriptEngine.newQObject(&qtObjectClass);
Q_ASSERT (qtObjectClass.isQObject());
scriptEngine.globalObject().setProperty("QTscriptEngine", qValue);
QFile file("testScript.js");
file.open(QIODevice::ReadOnly);
QScriptValue result = scriptEngine.evaluate(file.readAll());
if(result.toString() != "undefined")
std::cout << result.toString().toStdString() << std::endl;
file.close();
if (scriptEngine.hasUncaughtException())
{
int lineNo = scriptEngine.uncaughtExceptionLineNumber();
printf("lineNo : %i", lineNo);
}
The script :
print(QTscriptEngine.firstfunction());
print(QTscriptEngine.secondfunction());
And the (strange) result :
5
TypeError: Result of expression 'QTscriptEngine.secondfunction' [1] is not a function.
Where 5 is the result of firstfunction() and [1] the result of secondfunction().
Of course, the result of my function is not a function, it's pretty normal, no ?
I don't understand why one is working when the other one is not (but evaluated because [1] is clearly the good returned value of secondfunction())
(I have really simplify all the code, and maybe the problem come from an other place, but it's clearly strange)
Any idea ?
Thanks.
The answer was stupid.
I had properties defined like that :
Q_PROPERTY(int m_firstResult READ firstfunction)
The function who are in a Q_PROPERTY return a TypeError.
And I havn't even put my properties in my question, so no one was able to answer my question, sorry for that.
I followed this article to use hostpage to pass an array to client:
https://developers.google.com/web-toolkit/articles/dynamic_host_page
Currently,I can see follow content in firebug
<html style="overflow: hidden;">
<head>
......
<script type="text/javascript">
var rcmdFriends=[{"Name":"Friend-0","Image":"url"}];
</script>
</head>
......
</html>
Then I tried to use these code to get js variable(a json array actually) from hostpage and print it to user:
//get array from host page
private native JsArrayExt<People> getRecommendedFriends()/*-{
return $wnd.rcmdFriends;
}-*/;
#Override
public void onModuleLoad()
{
final FlowPanel fPanel = new FlowPanel();
JsArrayExt<People> channels = getRecommendedFriends();
for (int i = 0, len = channels.length(); i < len; i++)
{
//"print" name to user
fPanel.add(new Label(channels.get(i).getName()));
}
RootPanel.get().add(fPanel);
}
//model definition
#SingleJsoImpl(PeopleImpl.class)
public interface People extends HasName
{
String getImage();
void setImage(String Image);
}
But got this eror:
java.lang.ClassCastException: com.google.gwt.core.client.JavaScriptObject$ cannot be cast to com.pkg.People
Strangely,I can already see the length of "channels" is 1,and why do I get this casting error?How to solove this problem?
You cannot cast to an ordinary Java pojo. You must implement an overlay type
public class PersonJSON extends JavaScriptObject {
protected PersonJSON() {
}
public final native String getName() /*-{
return this.Name;
}-*/;
public final native String getImage() /*-{
return this.Image;
}-*/;
}
Then you can call
JsArray<PersonJSON> channels = getRecommendedFriends();
and read out the values from the PersonJSON elements;
Assuming JsArrayExt is the interface from Why can't I define interface for overlay type lightweight collections?, I suppose that the fact you do not use an explicit JSO subclass confuses the DevMode.
Because you directly call a JSNI method, I don't understand why you don't use a JsArrayExtImpl<PersonImpl> which I believe would Just Workâ˘; there's no point in using the interfaces here.
If you really can't make it work, I'd suggest using AutoBeans instead (it unfortunately requires a small serialize/parse dance in DevMode: AutoBeanCodex.decode(factory, Person.class, new JSONObject(rawJso).toString()), whereas in prod mode you can simply use AutoBeanCodex.decode(factory, Person.class, (JsoSplittable) rawJso)). In your case, it'd require another dance because you're using an array as the root object; see GWT Autobean - how to handle lists?
I am trying to use void QWebFrame::addToJavaScriptWindowObject(const QString & name, QObject * object). My problem is when I try and call the function in the JavaScript
TypeError: Result of expression 'screen.valueChanged' [undefined] is not a function.
TimeSliceScreen::TimeSliceScreen(QWidget* parent)
:
QWidget( parent )
{
QVBoxLayout* layout = new QVBoxLayout( this );
_timeSlice = new QWebView( this );
_timeSlice->setMinimumSize( 200,200);
QSizePolicy policy = _timeSlice->sizePolicy();
policy.setVerticalStretch(3);
_timeSlice->setSizePolicy(policy);
_timeSlice->settings()->setAttribute( QWebSettings::JavascriptEnabled, true );
_timeSlice->settings()->setAttribute( QWebSettings::DeveloperExtrasEnabled, true );
layout->addWidget( _timeSlice );
layout->addStretch();
layout->addSpacing( 20 );
_timeSlice->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
interface = new WebPageInterface();
connect( _timeSlice->page()->mainFrame(), SIGNAL(javaScriptWindowObjectCleared()),
this, SLOT(populateJavaScriptWindowObject()) );
}
void TimeSliceScreen::populateJavaScriptWindowObject(){
_timeSlice->page()->mainFrame()->addToJavaScriptWindowObject(QString("screen"),
interface);
}
WebPageInterface is a very simple class that extends QObject and has one slot called valueChanged that is the function I am trying to call.
My JavaScript is:
function call() {
screen.valueChanged();
}
which gets called from
<input type="hidden" id="maxhid" name="maxhid" value="{maxSlider}" onchange="call()"/>
Everything I have read says that this is the way to do it, but it's not working for me.
I think screen is a reserved name in the js. Try changing the name to something else. Otherwise looks like it should work.