在 C++ 中, 怎么和 QML 对象交互 ?

以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「englyf」https://www.jianshu.com/p/66649d0e9bb6


请注意这里使用的环境是
IDE:Qt5.12
Lang:C++、QML
Compiler:vs2015x64

所有 QML 对象都是 QObject 的派生类型, 无论这个对象是由引擎内部实现或者是由第三方源定义而来。也就是说,QML 引擎可以利用 Qt 的元对象系统(Meta Object System
)去动态实例化任何的 QML 对象类型,以及检查被创建的对象。
所以说,在 C++ 代码中,无论是因为要显示一个可渲染的 QML 对象,或者需要集成非可视化的 QML 对象数据,创建 QML 对象都是非常容易实现的。当一个 QML 对象被创建之后,无论是为了读写这个对象的属性,或者调用这个对象的方法,还是接收这个对象的信号通知,都可以在 C++ 中对此进行检查。

怎么在 C++ 中加载 QML 对象呢?

加载对象前,我们需要先使用 Qt 提供的两个类来加载 QML 文档,分别是 QQmlComponent 和 QQuickView。
QQmlComponent 加载一个 QML 文档后,生成一个 C++ 对象,可以在 C++ 代码中对这个对象进行修改。
QQuickView 同样可以做到这些,但 QQuickView 是 QWindow 的一个派生类型,加载之后的对象也会被渲染出来。 QQuickView 通常被用来将可视化的 QML 对象集成到应用的用户界面中。

下面看个举个栗子,

// textItem.qml
import QtQuick 2.0

Item {
    width: 100; height: 100
}

既可以用 QQmlComponent 也可以用 QQuickView 将上面这个 QML 文件加载到 C++ 代码中。如果使用 QQmlComponent 则需要调用 QQmlComponent::create() 创建组件的实例并返回对象(实例)指针,而使用 QQuickView 就会自动创建组件的实例,然后调用 QQuickView::rootObject() 获取实例指针。看看下面的代码就很清楚了。

// Using QQmlComponent
QQmlEngine engine;
QQmlComponent component(&engine,
        QUrl::fromLocalFile("textItem.qml"));
QObject *object = component.create();
...
delete object;
// Using QQuickView
QQuickView view;
view.setSource(QUrl::fromLocalFile("textItem.qml"));
view.show();
QObject *object = view.rootObject();

既然拿到了 QML 对象的指针了,怎么去设置属性呢?
可以通过对象调用 QObject::setProperty() 或者借用 QQmlProperty::write() 来修改根对象 Item 的属性:

object->setProperty("width", 300);
QQmlProperty(object, "width").write(300);

但是,上面列举的这两种方式是有区别的,后者 QQmlProperty::write() 除了设置属性新值之外,还会移除原来的绑定,所以要特别注意一下。
比如,假设在 QML 文件中,已将 width 值绑定到 height:

width: height

如果设置属性新值的方式是调用 object->setProperty("width", 500),那么 width 的值只是临时被设置为 500,一旦 height 改变了,width也是会跟随改变的,因为绑定关系任然有效并没有被移除。但是,如果设置属性新值的方式是调用 QQmlProperty(object, "width").write(500) ,那么width 的值不会再跟随 height 的改变而改变,因为原来的绑定关系已被移除了。
此外呢,设置属性还有一种方法就是,先将对象强制转换为实际类型,然后使用编译时安全性调用方法来设置新属性值。在上面的文件 textItem.qml 中,根对象 Item 由类 QQuickItem 定义:

QQuickItem *item = qobject_cast(object);
item->setWidth(500);

你也可以通过 QObject::connect() 连接 QML 组件中定义的任何信号,通过 QMetaObject::invokeMethod() 调用 QML 组件中定义的方法。详细内容?别急,在下面呢!

怎么在 C++ 中按照对象名访问已加载的 QML 对象呢?

QML 组件实际上是一组具有子节点的对象树,子节点同样有兄弟对象和子对象。可以使用 QObject::findChild() 并传入属性值 QObject::objectName(也即是对象名) 来定位到 QML 组件的子对象。下面看看我这的栗子大不大:

// textItem.qml
import QtQuick 2.0

Item {
    width: 100; height: 100

    Rectangle {
        anchors.fill: parent
        objectName: "rect"
    }
}

可以看到根项是 Item,然后还有个子项 Rectangle 和属性 objectName。可以通过下面的方式定位子对象:

QObject *rect = object->findChild("rect");
if (rect)
    rect->setProperty("color", "red");

这里要注意一下,一个对象可以有多个相同 objectName(属性) 的子对象。比如,ListView 会创建其委托的多个实例,如果使用特定的 objectName 声明其委托,那么 ListView 将具有多个相同 objectName 的子节点。这种情况下,可以使用 QObject::findChildren() 来查找符合 objectName 的所有子节点。
特别注意:虽然可以在 C++ 中访问并且操作 QML 的内部对象,但是除非用于测试和原型设计,否则不应该使用这种方法!QML 和 C++ 集成的优势之一就是实现与 C++ 逻辑和数据集后端分离的 QML UI 界面,如果在 C++ 中直接操作 QML 将意味着放弃这个优势。如果想不影响 C++ 相关代码的同时改动 QML UI,这种方法会使得目标难于实现。

怎么在 C++ 中访问 QML 对象类型的成员呢?

访问属性

任何 QML 对象中声明的属性都自动可以在 C++ 代码中访问。下面再来个栗子:

// textItem.qml
import QtQuick 2.0

Item {
    property int a: 100
}

在上面 QML 文件中声明的属性 a 的值可以使用 QQmlProperty 来读写,或者用 QObject::setProperty() 来写属性值和用 QObject::property() 来读属性值:

QQmlEngine engine;
QQmlComponent component(&engine, "textItem.qml");
QObject *object = component.create();

qDebug() << "Property value:" << QQmlProperty::read(object, "someNumber").toInt();
QQmlProperty::write(object, "someNumber", 5000);

qDebug() << "Property value:" << object->property("someNumber").toInt();
object->setProperty("someNumber", 100);

为了确保 QML 引擎知道属性的改变,你应该始终采用 QObject::setProperty(), QQmlProperty 或者 QMetaProperty::write() 来设置 QML 对象的属性值。比如,假如你有个自定义类型 PushButton, 在内部有个属性 buttonText 并且和成员变量 m_buttonText 关联。 像下面这样子直接修改成员变量 m_buttonText 是不建议的:

// un-recommended
QQmlComponent component(engine, "textItem.qml");
PushButton *button = qobject_cast(component.create());
button->m_buttonText = "clicked !";

如果变量 m_buttonText 被直接修改,那么QML 引擎将不会知道属性改变了,因为这种操作完美地躲开了 Qt 的 meta-object system。后果就是,绑定的 buttonText 属性不会被更新,而且 QML 中的属性变更信号槽 onButtonTextChanged() 不会被调用。

访问 QML 方法

由于所有的 QML 方法都暴露给了元对象系统 Meta-object system, 所以在 C++ 代码中可以通过 QMetaObject::invokeMethod() 调用对应的 QML 方法,并且输入的参数和来自 QML 中的返回值在 C++ 中通常被转换成 QVariant 类型值。下面看看调用的例子:

// textItem.qml
import QtQuick 2.0

Item {
    function qmlFunction(msg) {
        console.log("Got msg:", msg)
        return "return value"
    }
}
// main.cpp
QQmlEngine engine;
QQmlComponent component(&engine, "textItem.qml");
QObject *object = component.create();

QVariant returnedValue;
QVariant msg = "hi from C++";
QMetaObject::invokeMethod(object, "qmlFunction",
        Q_RETURN_ARG(QVariant, returnedValue),
        Q_ARG(QVariant, msg));

qDebug() << "value returned from QML :" << returnedValue.toString();
delete object;

这里要注意一下,Q_RETURN_ARG() and Q_ARG() 参数必须指定为 QVariant 类型, 因为 QVariant 是用于 QML 方法输入参数和返回值的通用数据类型。

连接 QML 信号

所有 QML 信号都自动适用于 C++ 代码,就像任何普通 Qt C++ 信号一样用 QObject::connect() 来连接。反过来,任何 C++ 信号都可以被 QML 对象的信号处理程序接收到。
这里来个栗子,有个 QML 组件定义了一个带 string 类型参数的信号 qmlSignal. 这个信号通过 QObject::connect() 连接到 C++ 对象的信号槽 cppSlot(),所以每当 QML 的信号 qmlSignal 被发送时都会调用 C++ 里的 cppSlot()。

// textItem.qml
import QtQuick 2.0

Item {
    id: item
    width: 200; height: 200

    signal qmlSignal(string message)

    MouseArea {
        anchors.fill: parent
        onClicked: item.qmlSignal("QML say Hi ")
    }
}
class MyClass : public QObject
{
    Q_OBJECT
public slots:
    void cppSlot(const QString &message) {
        qDebug() << "C++ got message:" << message;
    }
};

int main(int argc, char *argv[]) {
    QGuiApplication app(argc, argv);

    QQuickView view(QUrl::fromLocalFile("textItem.qml"));
    QObject *item = view.rootObject();

    MyClass myClass;
    QObject::connect(item, SIGNAL(qmlSignal(QString)),
                     &myClass, SLOT(cppSlot(QString)));

    view.show();
    return app.exec();
}

当 QML 对象定义的信号带参数并且参数类型为 QML 对象类型时,参数类型应该声明为 var 并且 C++ 的对应接收类型应该使用 QVariant 类型。临走带斤板栗吧:

// textItem.qml
import QtQuick 2.0

Item {
    id: item
    width: 200; height: 200

    signal qmlSignal(var anObject)

    MouseArea {
        anchors.fill: parent
        onClicked: item.qmlSignal(item)
    }
}
class MyClass : public QObject
{
    Q_OBJECT
public slots:
    void cppSlot(const QVariant &v) {
       qDebug() << "C++ slot got value:" << v;

       QQuickItem *item =
           qobject_cast(v.value());
       qDebug() << "Item w & h:" << item->width()
                << item->height();
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QQuickView view(QUrl::fromLocalFile("textItem.qml"));
    QObject *item = view.rootObject();

    MyClass myClass;
    QObject::connect(item, SIGNAL(qmlSignal(QVariant)),
                     &myClass, SLOT(cppSlot(QVariant)));

    view.show();
    return app.exec();
}

参考英文资料[Qt]https://doc.qt.io/qt-5/qtqml-cppintegration-interactqmlfromcpp.html

你可能感兴趣的:(在 C++ 中, 怎么和 QML 对象交互 ?)