1+1=10

扬长避短 vs 取长补短

QJSEngine在QtWidgets下使用小记

接前面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的大小和文字:

#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的大小时,使用了 widthxheight" 字符串。注:Qt.size(400, 300)只能配合QQmlEngine使用。

例子2

在js中,可以使用Qt对象的信号。

比如,为两个独立的slider,建立联动关系:

#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中直接创建该类型的对象并显式出来。为了有意义,再建立该对象和其他对象的联动关系:

#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"

注意:

  1. 只支持包含Q_OBJECT的QObject的派生类

  2. 要在js中创建对象,需要先使用newQMetaObject()注册该类型的staticMetaObject:

template <typename T> QJSValue QJSEngine::newQMetaObject()
QJSValue newQMetaObject(const QMetaObject *metaObject)

这两个写法都可以,所以例子中的注册语句也可以写成:

    engine.globalObject().setProperty("MySpinBox", engine.newQMetaObject(&MySpinBox::staticMetaObject));
  1. 同时,确保构造函数前面使用 Q_INVOKABLE 在元对象系统中进行了注册。 例子中使用MySpinBox而不是QSpinBox就是因为它不满足构造函数这个条件。

例子4

对于简单类型,QJSValue提供有构造函数,所以setProperty()时直接传入就可以了,对于javascript中的对象和数组,必须使用newArray()newObject进行创建。

在下面程序中,js直接读取c++中创建的array,遍历并将其显示在QPlainTextEdit中:

#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部分可以改成下面这样:

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引擎,如何办?

#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中如何调用?

#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++中如何使用:

#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暴露出去,应该也是合理的:

#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绑定,需要手动封装一下,以便于访问界面上的其他控件。

效果如下:

qt-qjsengine-mainwindow-api

参考

  • 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

Comments