接前面Qt中的JavaScript引擎小记,QJSEngine在QtWidgets下使用小记,简单看看QJSEngine和QUiLoader能碰撞出什么火花。
这是很老的方案,在QtScript时期,应该不少人折腾过这个。只不过qml时代,似乎意义不大了。
不过,随便折腾一下也无妨...
我们知道:
- 只要用Qt Designer设计一个ui界面 (x.ui),不需要编译,就可以交由现有的Qt程序来显示(QUiLoader)
- 只要写一个javascript文件(x.js),不需要编译,就可以交由现有的Qt程序来执行某些逻辑(QJSEngine)
既然界面显示和逻辑代码都可以随时修改而无需编译,那么二者配合作为Qt程序扩展机制,会怎么样?
- 在客户现场,用随便一个文件编辑器,修改 x.ui 和 x.js 文件。即可影响界面效果
- 在客户现场,用随便一个文件编辑器,添加 x.ui 和 x.js 文件,即可增加新的功能
- 在Qt程序编写上,只需要通过QJSEngine暴露几个API接口,再加简单的模块搜索功能
准备1
先创建一个main.cpp文件:
- 主窗口:MainWindow,包含一个QPlainTextEdit作为log窗口,包含一个QTabWidget作为带扩展的容器
- 该窗口会作为window对象注册进QJSEngine中,以便在js中被访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | class MainWindow: public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr)
: QMainWindow(parent) {
setWindowTitle("1+1=10's Demo for QJSEngine and QUiLoader");
auto tabWidget = new QTabWidget;
tabWidget->setObjectName("tabWidget");
setCentralWidget(tabWidget);
auto logEdit = new QPlainTextEdit;
logEdit->setObjectName("logEdit");
auto dock = new QDockWidget("Log");
dock->setWidget(logEdit);
addDockWidget(Qt::BottomDockWidgetArea, dock);
}
};
|
准备2
准备 widget.ui 和 widget.js 文件,将其放置到特定文件夹下,比如工作目录下的js-ui-1
子目录
用Qt Designer设计一个界面
界面上放置两个控件 slider1 和 spinBox1,记住名字,我们在js脚本中要用。
保存为widget.ui
:
编写js脚本
- 输出log信息到主窗口界面
- 建立ui组件之间的联动关系
完整脚本 widget.js
:
| logEdit = window.findMyChild("logEdit");
logEdit.appendPlainText("---- widget.js loaded ----\n");
slider1 = window.findMyChild("slider1");
spinBox1 = window.findMyChild("spinBox1");
slider1.valueChanged.connect(spinBox1.setValue);
|
整合一下
其实没什么好整合的,整个程序就一个main.cpp源文件:
| src/main.cpp
src/CMakeLists.txt
<app-working-dir>/js-ui-1/widget.ui
<app-working-dir>/js-ui-1/widget.js
|
完善一下main.cpp文件,使其启动时加载js-ui-1目录下的ui和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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88 | #include <QApplication>
#include <QMainWindow>
#include <QDockWidget>
#include <QTabWidget>
#include <QPlainTextEdit>
#include <QFile>
#include <QUiLoader>
#include <QJSEngine>
class MainWindow: public QMainWindow
{
Q_OBJECT
QTabWidget *tabWidget;
QPlainTextEdit *logEdit;
public:
MainWindow(QWidget *parent = nullptr)
: QMainWindow(parent) {
setWindowTitle("1+1=10's Demo for QJSEngine and QUiLoader");
QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership);
tabWidget = new QTabWidget;
tabWidget->setObjectName("tabWidget");
setCentralWidget(tabWidget);
logEdit = new QPlainTextEdit;
logEdit->setObjectName("logEdit");
auto dock = new QDockWidget("Log");
dock->setWidget(logEdit);
addDockWidget(Qt::BottomDockWidgetArea, dock);
}
Q_INVOKABLE QObject *findMyChild(const QString &name, QObject *my=nullptr) {
if (!my)
my = this;
return my->findChild<QWidget *>(name, Qt::FindChildrenRecursively);
}
void load(const QString &dir, QJSEngine &engine) {
doLoadUi(QString("%1/widget.ui").arg(dir));
doLoadJS(QString("%1/widget.js").arg(dir), engine);
}
void doLoadUi(const QString &fileName) {
QUiLoader loader;
QFile f( fileName );
if ( !f.open(QIODevice::ReadOnly) )
return;
auto w = loader.load(&f);
tabWidget->addTab(w, w->windowTitle());
}
void doLoadJS(const QString &fileName, QJSEngine &engine) {
QFile f( fileName );
if ( !f.open(QIODevice::ReadOnly) )
return;
auto js = f.readAll();
if (auto ret = engine.evaluate(js, fileName); ret.isError()) {
qDebug() << "Uncaught exception at line"
<< ret.property("lineNumber").toInt()
<< ":" << ret.toString();
}
}
};
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));
QString js("window.size=\"800x600\"");
if (auto ret = engine.evaluate(js); ret.isError()) {
qDebug() << "Uncaught exception at line"
<< ret.property("lineNumber").toInt()
<< ":" << ret.toString();
}
mw.load("js-ui-1", engine);
mw.show();
return a.exec();
}
#include "main.moc"
|
直接编译运行即可看到效果:
然后,就可以在程序发布后:随时修改ui文件和js文件,以便界面上显示各种不同的组件及调整它们之间的联动关系。
完善??
简单折腾可以,用于内部的各种工具程序应该问题也不大。不过真将这种方案用于生产,还有很多很多问题存在。
比如,多个模组之间,怎么约定各自的地盘(作用域),如果重名怎么办,如何规避。javascript脚本如何调试,是不是要有一个简单的环境。
接下来,并不考虑或尝试去完善这些东西,只关注自然而然的扩展方向...
自动搜索和加载
main函数中的load语句,可以改成自动搜索,以便于加载所有符合规则的模组:
| QDir dir(".");
for( auto module : dir.entryList(QStringList()<<"js-ui-*")) {
qDebug() << "Loading module" << module;
mw.load(module, engine);
}
|
模组的入口
为了简单,例子中将x.ui和x.js都作为模块的入口分别处理。但从设计上,应该把权力都下发给js文件,让它负责触发ui文件的加载和处理(如果需要ui的话)。
这样的话,Qt程序只需要找到并加载 js文件就行了。
比如,脚本module.js
写成下面这样(需要加载什么ui文件,加载几个,都自己控制):
1
2
3
4
5
6
7
8
9
10
11
12 | logEdit = window.findMyChild("logEdit");
logEdit.appendPlainText("---- module.js loaded ----\n");
// add logic for the widget
let widget = window.loadUi("js-ui-1/widget.ui");
console.log(widget);
slider1 = window.findMyChild("slider1", widget);
spinBox1 = window.findMyChild("spinBox1", widget);
slider1.valueChanged.connect(spinBox1.setValue);
// add widget to main window
window.addWidgetToTab(widget, widget.windowTitle);
|
而后在Qt程序中,通过window提供几个API接口就供js使用就行了。
对应的main.cpp
的内容如下:
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93 | #include <QApplication>
#include <QMainWindow>
#include <QDockWidget>
#include <QTabWidget>
#include <QPlainTextEdit>
#include <QFile>
#include <QDir>
#include <QUiLoader>
#include <QJSEngine>
class MainWindow: public QMainWindow
{
Q_OBJECT
QTabWidget *tabWidget;
QPlainTextEdit *logEdit;
public:
MainWindow(QWidget *parent = nullptr)
: QMainWindow(parent) {
setWindowTitle("1+1=10's Demo for QJSEngine and QUiLoader");
QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership);
tabWidget = new QTabWidget;
tabWidget->setObjectName("tabWidget");
setCentralWidget(tabWidget);
logEdit = new QPlainTextEdit;
logEdit->setObjectName("logEdit");
auto dock = new QDockWidget("Log");
dock->setWidget(logEdit);
addDockWidget(Qt::BottomDockWidgetArea, dock);
}
// helper function to find child named name of the given object
Q_INVOKABLE QObject *findMyChild(const QString &name, QObject *my=nullptr) {
if (!my)
my = this;
return my->findChild<QWidget *>(name, Qt::FindChildrenRecursively);
}
// helper function to load the .ui file
Q_INVOKABLE QWidget *loadUi(const QString &fileName) {
QUiLoader loader;
QFile f( fileName );
if ( !f.open(QIODevice::ReadOnly) )
return nullptr;
return loader.load(&f);
}
Q_INVOKABLE void addWidgetToTab(QWidget *widget, const QString &title) {
tabWidget->addTab(widget, title);
}
void loadJSModule(const QString &fileName, QJSEngine &engine) {
QFile f( fileName );
if ( !f.open(QIODevice::ReadOnly) )
return;
auto js = f.readAll();
if (auto ret = engine.evaluate(js, fileName); ret.isError()) {
qDebug() << "Uncaught exception at line"
<< ret.property("lineNumber").toInt()
<< ":" << ret.toString();
}
}
};
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));
QString js("window.size=\"800x600\"");
if (auto ret = engine.evaluate(js); ret.isError()) {
qDebug() << "Uncaught exception at line"
<< ret.property("lineNumber").toInt()
<< ":" << ret.toString();
}
QDir dir(".");
for( auto module : dir.entryList(QStringList()<<"js-ui-*")) {
qDebug() << "Loading module" << module;
mw.loadJSModule(module+"/module.js", engine);
}
mw.show();
return a.exec();
}
#include "main.moc"
|
运行结果和上面一样。只不过灵活性更强。
其他
为保持完整,附上widget.ui文件的内容:
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 | <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>widget1</class>
<widget class="QWidget" name="widget1">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>420</width>
<height>320</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QSlider" name="slider1">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox1"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
|
参考
- https://doc.qt.io/qt-6/qjsengine.html
- https://doc.qt.io/qt-6/quiloader.html