C++与QML对象交互

所有QML对象类型都是QObject派生类型,无论它们是引擎内部实现还是由第三方资源定义的。这意味着QML引擎可以使用Qt元对象系统动态实例化任何QML对象类型并检查创建的对象。

这对于从C++代码创建QML对象非常有用,无论是要显示一个可视觉化的QML对象,还是将非可视QML对象数据集成到C++应用程序中。一旦创建了QML对象,就可以从C++进行检测,以便读取和写入属性,调用方法和接收信号通知。

有关C++和不同的QML集成方法的信息,请参阅C++和QML集成概述页面。

从C++加载QML对象

可以使用QQmlComponent或QQuickView加载QML文档。QQmlComponent将QML文档加载为一个C++对象,然后可以从C++代码中对其进行修改。QQuickView也这样做,但是因为QQuickView是一个QWindow派生类,所以加载的对象也将渲染到可视显示中;QQuickView通常用于将可显示的QML对象集成到应用程序的用户界面中。

例如,假设有一个名为MyItem.qml的文件,其外观如下所示

import QtQuick

Item {
    width: 100; height: 100
}

可以使用以下C++代码使用QQmlComponent或QQuickView加载此QML文档。使用QQmlComponent需要调用QQmlComponent::create()来创建组件的新实例,而QQuickView自动创建组件实例,可以通过QQuickView::rootObject()访问。

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

这个object是该MyItem.qml组件的实例。您现在可以使用QObject::setProperty()或QQmlProperty::write()修改项目的属性。

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

QObject::setProperty()和QQmlProperty::write()之间的区别是,后者除了设置属性值外,还会删除绑定。例如,假设上面的width分配已被绑定到height。

width: height

如果调用 object->setProperty("width", 500) 之后 heightItem 高度发生变化,则会再次更新 width,因为绑定仍然处于活动状态。但是,如果在调用 QQmlProperty(object, "width").write(500) 之后 height 发生变化,由于绑定已不复存在,所以 width 不会被更改。

或者,您可以将其转换为其实际类型并通过编译时安全的方式调用方法。在这种情况下,MyItem.qml 的基对象是一个由 Item 定义的,而 QQuickItem 类定义了 Item

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

您还可以使用 QMetaObject::invokeMethod() 和 QObject::connect() 连接到组件中定义的任何信号或调用方法。有关更多详细信息,请参阅以下内容:调用 QML 方法连接到 QML 信号

通过定义良好的 C++ 接口访问 QML 对象

以 C++ 与 QML 交互的最佳方式是在 C++ 中定义用于此目的的接口,并在 QML 中访问它。使用其他方法,重构您的 QML 代码可能导致您的 QML/C++ 交互失效。它还有助于推断 QML 和 C++ 代码的交互方式,因为通过 QML 驱动它可以通过 qmllint 等工具更容易地进行推理。从 C++ 访问 QML 将导致无法在不手动验证没有任何外部 C++ 代码修改特定 QML 组件的情况下理解 QML 代码,即使这样,访问范围也可能会随时间变化,使得持续使用此策略成为一项维护负担。

要让 QML 驱动交互,首先需要定义一个 C++ 接口

class CppInterface : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    // ...
};

使用由 QML 驱动的框架,此接口可以通过两种方式进行交互

单例

一个选项是通过将 QML_SINGLETON 宏添加到接口中,将其注册为单例,使其对所有组件可用。此后,接口可以通过简单的导入语句来访问。

import my.company.module

Item {
    Component.onCompleted: {
        CppInterface.foo();
    }
}

如果您的接口需要用于比根组件更多的地方,请使用这种方法,因为仅仅通过传输对象就需要显式通过属性传递给其他组件,或者使用不推荐使用且速度较慢的未限定的访问方法。

初始属性

另一个选项是将接口标记为不可创建的,通过 QML_UNCREATABLE 并使用 QQmlComponent::createWithInitialProperties() 以及 QML 端的 必填属性 将其传递给根 QML 组件。

您的根组件可能看起来像这样

import QtQuick

Item {
    required property CppInterface interface
    Component.onCompleted: {
        interface.foo();
    }
}

在此将属性标记为必填可以保护组件免于在没有设置接口属性的情况下创建。

然后,您可以通过与 从 C++ 加载 QML 对象 中概述的方式初始化您的组件,除了使用 createWithInitialProperties()

component.createWithInitialProperties(QVariantMap{{u"interface"_s, QVariant::fromValue<CppInterface *>(new CppInterface)}});

如果您的接口只需要对根组件可用,则首选此方法。它还允许在 C++ 端更轻松地连接到接口的信号和槽。

如果上述两种方法都不适合您的需求,您可能需要调查使用 C++ 模型 的用途。

通过对象名称访问已加载的 QML 对象

QML组件本质上是由具有兄弟姐妹及其自身子代的对象树组成的。可以使用QObject::objectName属性和QObject::findChild()方法找到QML组件的子对象。例如,如果MyItem.qml的根项有一个Rectangle项子代

import QtQuick

Item {
    width: 100; height: 100

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

可以这样找到子代

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

请注意,一个对象可能有多个具有相同objectName的子代。例如,ListView创建了其委托的多个实例,因此如果其委托具有特定的objectName,那么ListView将具有多个相同objectName的孩子。在这种情况下,可以使用QObject::findChildren()来找到所有具有匹配objectName的孩子。

警告:虽然从C++访问QML对象并操作它们是可能的,但这不是推荐的方法,除非是为了测试和原型设计。QML和C++集成的优势之一是在与C++逻辑和数据集后端分开的情况下实现UI,而如果在C++端直接操作QML则会违反这一点。这样的方法也使得在不影响C++对应内容的情况下更改QML UI变得困难。

从C++中访问QML对象类型的成员

属性

在QML对象中声明的任何属性都可以从C++自动访问。给定以下这样的QML项

// MyItem.qml
import QtQuick

Item {
    property int someNumber: 100
}

可以使用QQmlPropertyQObject::setProperty()和QObject::property()来设置和读取someNumber属性的值。

QQmlEngine engine;
QQmlComponent component(&engine, "MyItem.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);

您应该始终使用QObject::setProperty()、QQmlPropertyQMetaProperty::write()来更改QML属性值,以确保QML引擎意识到属性已更改。例如,假设您有一个自定义类型PushButton,该类型具有一个buttonText属性,该属性反映了内部m_buttonText成员变量的值。直接修改成员变量,如下所示,不是一个好方法

//bad code
QQmlComponent component(engine, "MyButton.qml");
PushButton *button = qobject_cast<PushButton*>(component.create());
button->m_buttonText = "Click me";

由于值是直接修改的,这绕过了Qt的meta-object系统,QML引擎不会意识到属性更改。这意味着到buttonText的属性绑定就不会更新,任何onButtonTextChanged处理程序也不会被调用。

调用QML方法

所有QML方法都对meta-object系统可见,可以使用QMetaObject::invokeMethod()从C++调用。您可以在冒号后指定参数和返回值类型,如下面的代码片段所示。这在您想将具有特定签名的信号与通过QML定义的方法连接时很有用。如果您省略了类型,则C++签名将使用QVariant

这是一个使用QMetaObject::invokeMethod()调用QML方法的C++应用程序

QML
// MyItem.qml
import QtQuick

Item {
    function myQmlFunction(msg: string) : string {
        console.log("Got message:", msg)
        return "some return value"
    }
}
C++
// main.cpp
QQmlEngine engine;
QQmlComponent component(&engine, "MyItem.qml");
QObject *object = component.create();

QString returnedValue;
QString msg = "Hello from C++";
QMetaObject::invokeMethod(object, "myQmlFunction",
        Q_RETURN_ARG(QString, returnedValue),
        Q_ARG(QString, msg));

qDebug() << "QML function returned:" << returnedValue;
delete object;

注意冒号后面的参数和返回类型。您可以使用作为类型名的值类型对象类型

如果在QML中省略类型或指定为var,则必须使用QVariant作为类型与Q_RETURN_ARG()和Q_ARG()一起调用QMetaObject::invokeMethod()。

连接到QML信号

所有QML信号都自动可用于C++,可以使用QObject::connect()像任何普通Qt C++信号一样连接。

以下是一个包含名为qmlSignal的信号的QML组件,该信号使用字符串类型参数发射。此信号使用QObject::connect()连接到C++对象的槽,因此每当QMetaObject::invokeMethod()被调用时,就会调用cppSlot()方法。

// MyItem.qml
import QtQuick

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

    signal qmlSignal(msg: string)

    MouseArea {
        anchors.fill: parent
        onClicked: item.qmlSignal("Hello from QML")
    }
}
class MyClass : public QObject
{
    Q_OBJECT
public slots:
    void cppSlot(const QString &msg) {
        qDebug() << "Called the C++ slot with message:" << msg;
    }
};

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

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

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

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

信号参数中的QML对象类型将转换为C++中的类指针

// MyItem.qml
import QtQuick 2.0

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

    signal qmlSignal(anObject: Item)

    MouseArea {
        anchors.fill: parent
        onClicked: item.qmlSignal(item)
    }
}
class MyClass : public QObject
{
    Q_OBJECT
public slots:
    void cppSlot(QQuickItem *item) {
       qDebug() << "Called the C++ slot with item:" << item;

       qDebug() << "Item dimensions:" << item->width()
                << item->height();
    }
};

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

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

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

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

© 2024 Qt公司。此处包含的文档贡献的版权属于其所有者。本指南中的文档根据由自由软件基金会发布的GNU自由文档许可协议版本1.3的条款获得许可。Qt及其标志是芬兰的Qt公司及其在全球的子公司和分公司的商标。所有其他商标均为其所有者的财产。