接前面Qt中的JavaScript引擎小记,继续了解QtQml模块中的QJSEngine。QJSEngine因QML而生,但是在QML中,javascript已开始被边缘化——Make JavaScript an optional feature of QML,同时对QtWidgets的支持,又不如原来QtScript模块中的QScriptEngine。
概念?
对于基本的一些概念,QJSEngine手册中说明还算比较详细了。此处只记录几个临时用到的。
引擎配置
要在JavaScript脚本中能访问Qt程序中的对象(即提供API供JavaScript使用),需要先对QJSEngine进行配置:
| QJSValue QJSEngine::globalObject() const
|
将要暴露的对象,设置为globalObject的属性。而后就可以在脚本中才能访问。
QObject
如要将QObject及其派生类的对象暴露出去,需先转成QJSValue类型。(只能这么用,因为QJSValue也没有对应QObject的构造函数)。
| QJSValue QJSEngine::newQObject(QObject *object)
|
而后将其设置为globalObject属性。在脚本中即可访问对象的信号、槽 和 属性。
注意,调用这个函数后,对应的 object 如果没有parent的话,将由对应的引擎负责销毁。即,其ownership是JavaScriptOwnership。
如果对象暴露给多个JSEngine,或者对象压根就不在堆上,就需要设置为CppOwnership
:
| void QJSEngine::setObjectOwnership(QObject *object, QJSEngine::ObjectOwnership ownership)
|
注意,这个函数在Qt5中,位于QQmlEngine中,Qt6中才放到这个该有的位置上。尽管有些古怪,在Qt5下使用QJSEngine时,是可以直接使用QQmlEngine的setObjectOwnership()成员的。
例子?
网络上这部分内容,太少了...
简单起见,每个例子都是完整的Qt程序,且只包含一个main.cpp。只需要配合一个qmake或cmake工程文件,即可编译运行。
例子1
最简单的场景,把Qt应用程序中的一些对象暴露出来,通过js脚本直接修改其内容和属性。
比如,修改QLabel的大小和文字:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | #include <QApplication>
#include <QLabel>
#include <QJSEngine>
const char *js = R"js(
label.text = "Hello <font color=\"red\">1+1=10</font> from js!";
label.size = "400x300";
)js";
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QLabel label("Hello 1+1=10");
QJSEngine::setObjectOwnership(&label, QJSEngine::CppOwnership);
QJSEngine engine;
engine.globalObject().setProperty("label", engine.newQObject(&label));
auto ret = engine.evaluate(js);
if (ret.isError())
qDebug() << ret.toString();
label.show();
return a.exec();
}
|
- label在栈上,所以手动设置ownership为 CppOwnership。不然会程序崩溃
- 设置label的大小时,使用了 width
x
height" 字符串。注:Qt.size(400, 300)
只能配合QQmlEngine使用。
例子2
在js中,可以使用Qt对象的信号。
比如,为两个独立的slider,建立联动关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | #include <QApplication>
#include <QSlider>
#include <QJSEngine>
const char *js = R"js(
slider1.valueChanged.connect(function(value) {
slider2.value = value;
});
)js";
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QSlider slider1(Qt::Horizontal);
QSlider slider2(Qt::Horizontal);
QJSEngine::setObjectOwnership(&slider1, QJSEngine::CppOwnership);
QJSEngine::setObjectOwnership(&slider2, QJSEngine::CppOwnership);
QJSEngine engine;
engine.globalObject().setProperty("slider1", engine.newQObject(&slider1));
engine.globalObject().setProperty("slider2", engine.newQObject(&slider2));
engine.evaluate(js);
slider1.show();
slider2.show();
return a.exec();
}
|
我们将Qt信号连接到一个js的函数,以便于函数中做更多操作。
如果不需要其他操作,对于这个例子来说,直接连接信号槽更简单:
| const char *js = "slider1.valueChanged.connect(slider2.setValue);"
|
如果双向联动,只需要:
| const char *js = R"js(
slider1.valueChanged.connect(slider2.setValue);
slider2.valueChanged.connect(slider1.setValue);
)js";
|
注意:要保持连接关系,QJSEngine 必须保持有效。
要验证,可以将上面的QJSEngine engine 放入局部块作用域:
| {
QJSEngine engine;
engine.globalObject().setProperty("slider1", engine.newQObject(&slider1));
engine.globalObject().setProperty("slider2", engine.newQObject(&slider2));
engine.evaluate(js);
}
|
例子3
如果我们在C++中定义了一个类,比如MySpinBox,然后想在js中直接创建该类型的对象并显式出来。为了有意义,再建立该对象和其他对象的联动关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | #include <QApplication>
#include <QSpinBox>
#include <QJSEngine>
const char *js = R"js(
var spinBox1 = new MySpinBox;
var spinBox2 = new MySpinBox;
spinBox1.valueChanged.connect(spinBox2.setValue);
spinBox2.valueChanged.connect(spinBox1.setValue);
spinBox1.show();
spinBox2.show();
)js";
class MySpinBox : public QSpinBox
{
Q_OBJECT
public:
Q_INVOKABLE MySpinBox(QWidget *parent = nullptr)
: QSpinBox(parent) {}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QJSEngine engine;
engine.globalObject().setProperty("MySpinBox", engine.newQMetaObject<MySpinBox>());
auto ret = engine.evaluate(js);
if (ret.isError())
qDebug() << ret.toString();
return a.exec();
}
#include "main.moc"
|
注意:
-
只支持包含Q_OBJECT
的QObject的派生类
-
要在js中创建对象,需要先使用newQMetaObject()
注册该类型的staticMetaObject:
| template <typename T> QJSValue QJSEngine::newQMetaObject()
QJSValue newQMetaObject(const QMetaObject *metaObject)
|
这两个写法都可以,所以例子中的注册语句也可以写成:
| engine.globalObject().setProperty("MySpinBox", engine.newQMetaObject(&MySpinBox::staticMetaObject));
|
- 同时,确保构造函数前面使用
Q_INVOKABLE
在元对象系统中进行了注册。 例子中使用MySpinBox而不是QSpinBox就是因为它不满足构造函数这个条件。
例子4
对于简单类型,QJSValue提供有构造函数,所以setProperty()
时直接传入就可以了,对于javascript中的对象和数组,必须使用newArray()
或 newObject
进行创建。
在下面程序中,js直接读取c++中创建的array,遍历并将其显示在QPlainTextEdit中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | #include <QApplication>
#include <QPlainTextEdit>
#include <QJSEngine>
const char *js = R"js(
edit.appendPlainText(header);
for (const i in jsArray)
edit.appendPlainText("jsArray[" + i + "] = " + jsArray[i]);
)js";
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPlainTextEdit edit;
QJSEngine::setObjectOwnership(&edit, QJSEngine::CppOwnership);
QJSEngine engine;
auto jsArray = engine.newArray(10);
for (int i = 0; i < 10; ++i)
jsArray.setProperty(i, i*10);
engine.globalObject().setProperty("jsArray", jsArray);
engine.globalObject().setProperty("header", "Array demo:");
engine.globalObject().setProperty("edit", engine.newQObject(&edit));
auto ret = engine.evaluate(js);
if (ret.isError())
qDebug() << ret.toString();
edit.show();
return a.exec();
}
|
使用newObject的操作基本一致,就不写C++代码了,js部分可以改成下面这样:
1
2
3
4
5
6
7
8
9
10
11
12 | const char *js = R"js(
let obj = {
a: 1,
b: 2,
c: 3,
d: 4,
e: 5
};
for (const p in obj)
edit.appendPlainText(p + ": " + obj[p]);
)js";
|
例子5
- 简单类型,QJSValue()可以直接构造
- QObject类型, 可以使用newQObject()
- js中的Array和Object,可以newObject()和newArray
那么,如果要把一个QSize传入js引擎,如何办?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | #include <QApplication>
#include <QPlainTextEdit>
#include <QJSEngine>
const char *js = R"js(
edit.size = editSize;
edit.appendPlainText("size: " + editSize.toString());
)js";
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPlainTextEdit edit;
QJSEngine::setObjectOwnership(&edit, QJSEngine::CppOwnership);
QJSEngine engine;
engine.globalObject().setProperty("edit", engine.newQObject(&edit));
engine.globalObject().setProperty("editSize", engine.toScriptValue(QSize(400, 300)));
auto ret = engine.evaluate(js);
if (ret.isError())
qDebug() << ret.toString();
edit.show();
return a.exec();
}
|
例子6
如果想定义一个函数,它接受一个QList,并返回另一个QList。那么该函数在js中如何调用?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57 | #include <QApplication>
#include <QPlainTextEdit>
#include <QJSEngine>
const char *js = R"js(
mytest.hello();
let l = mytest.transformList([1, 2, 3, 4, 5]);
edit.appendPlainText("------add from js --------");
for (let v of l)
edit.appendPlainText(v);
l; // return the value to c++
)js";
class Test: public QObject
{
Q_OBJECT
public:
Test(QObject *parent = nullptr)
: QObject(parent) {}
Q_INVOKABLE void hello() { qDebug() << "Hello 1+1=10!"; }
Q_INVOKABLE QList<double> transformList(const QList<double> &l)
{
QList<double> ret;
for (auto v : l)
ret << v * v;
return ret;
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPlainTextEdit edit;
QJSEngine::setObjectOwnership(&edit, QJSEngine::CppOwnership);
QJSEngine engine;
engine.globalObject().setProperty("edit", engine.newQObject(&edit));
engine.globalObject().setProperty("mytest", engine.newQObject(new Test));
auto ret = engine.evaluate(js);
if (ret.isError()) {
qDebug() << ret.toString();
} else {
edit.appendPlainText("------add from c++ --------");
auto l = ret.toVariant().toList();
for (auto v : l)
edit.appendPlainText(v.toString());
}
edit.show();
return a.exec();
}
#include "main.moc"
|
由于QJSEngine只接受QObject成员函数,所以需要先定义一个类,而后注册。
运行结果直接显示在QPlainTextEdit中。
例子7
在js中定义了多个function,在C++中如何使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 | #include <QApplication>
#include <QPlainTextEdit>
#include <QFile>
#include <QJSEngine>
const char *js = R"js(
export function hello() {
edit.appendPlainText("---- Hello from js! ----");
}
export function transformList(l) {
let ret = [];
for (let v of l)
ret.push(v * v);
return ret;
}
)js";
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
{
QFile f("test.mjs");
f.open(QFile::WriteOnly);
f.write(js);
}
QPlainTextEdit edit;
QJSEngine::setObjectOwnership(&edit, QJSEngine::CppOwnership);
QJSEngine engine;
engine.globalObject().setProperty("edit", engine.newQObject(&edit));
auto m = engine.importModule("test.mjs");
auto hello = m.property("hello");
hello.call();
auto trans = m.property("transformList");
auto data = engine.newArray(5);
for (int i = 0; i < 5; ++i)
data.setProperty(i, i + 1);
auto ret = trans.call(QJSValueList() << data);
edit.appendPlainText("---- Hello from c++ ----");
if (ret.isError())
edit.appendPlainText(ret.toString());
auto l = ret.toVariant().toList();
for (auto v : l)
edit.appendPlainText(v.toString());
edit.show();
return a.exec();
}
|
将js文件作为模块导入,而后调用模块中的函数。结果显示在QPlainTextEdit中。
例子8
在浏览器环境中,有个叫window的对象。
与此类似,在Qt下,我们将主窗口(甚至将Applicaiton)作为API暴露出去,应该也是合理的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59 | #include <QApplication>
#include <QMainWindow>
#include <QSplitter>
#include <QPlainTextEdit>
#include <QJSEngine>
const char *js = R"js(
// we can change the properties of main window
window.windowTitle = "Hello 1+1=10";
window.geometry = "100,100,600x400";
window.findMyChild("leftEdit").appendPlainText(JSON.stringify(window, undefined, 4));
window.findMyChild("rightEdit").appendPlainText("---- Hello from js ----");
)js";
class MainWindow: public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr)
: QMainWindow(parent) {
QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership);
auto spliter = new QSplitter(this);
spliter->setObjectName("spliter");
setCentralWidget(spliter);
auto leftEdit = new QPlainTextEdit;
leftEdit->setObjectName("leftEdit");
auto rightEdit = new QPlainTextEdit;
rightEdit->setObjectName("rightEdit");
spliter->addWidget(leftEdit);
spliter->addWidget(rightEdit);
}
Q_INVOKABLE QObject *findMyChild(const QString &name) {
return findChild<QWidget *>(name, Qt::FindChildrenRecursively);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow mw;
QJSEngine engine;
engine.installExtensions(QJSEngine::ConsoleExtension);
engine.globalObject().setProperty("window", engine.newQObject(&mw));
if (auto ret = engine.evaluate(js); ret.isError()) {
qDebug() << "Uncaught exception at line"
<< ret.property("lineNumber").toInt()
<< ":" << ret.toString();
}
mw.show();
return a.exec();
}
#include "main.moc"
|
- findChild 没有对应的js绑定,需要手动封装一下,以便于访问界面上的其他控件。
效果如下:
参考
- https://doc.qt.io/qt-6/qjsengine.html
- https://doc.qt.io/qt-6/qtjavascript.html
- https://doc.qt.io/qt-6/qtqml-javascript-functionlist.html
- https://doc.qt.io/qt-6/qtqml-cppintegration-data.html
- https://doc.qt.io/qt-6/qml-qtqml-qt.html
- https://doc.qt.io/qt-5/qtscript-index.html