将C++类型属性暴露给QML

QML可以很容易地通过C++代码中定义的功能进行扩展。由于QML引擎与Qt元对象系统紧密集成,任何通过QObject-派生类或Q_GADGET类型适当暴露的功能都可以从QML代码中访问。这使得C++数据和功能可以直接从QML访问,通常无需进行修改。

QML引擎可以通QObject实例通过元对象系统进行反省。这意味着任何QML代码都可以访问QObject-派生类实例的以下成员

  • 属性
  • 方法(如果它们是公共槽或用Q_INVOKABLE标记的)
  • 信号

(此外,如果它们已经用Q_ENUM声明,则可用枚举。有关更多详细信息,请参阅QML和C++之间数据类型转换。)

通常,这些在QML中是可访问的,无论QObject-派生类是否已被注册到QML类型系统中。但是,如果某些类要以需要引擎获取更多类型信息的方式使用(例如,如果类本身要作为方法参数或属性使用,或者其枚举类型之一要以这种方式使用)则该类可能需要注册。除非注册,否则无法分析已注册的类型。

对于Q_GADGET类型,注册是必需的,因为它们不能从一个已知的公共基类中衍生,并且无法自动提供给它们。如果没有注册,它们的属性和方法是不可访问的。

您可以通过在qt_add_qml_module调用中添加依赖项并将DEPENDENCIES选项来使来自不同模块的C++类型在您的模块中可用。例如,您可能想要依赖QtQuick,以便您的QML公开的C++类型可以作为方法参数和返回值使用QColor。作为值类型color公开QColorQtQuick。此类依赖关系可以在运行时自动推断,但您不应依赖于这一点。

另外请注意,本文件中介绍的一些重要概念在使用C++编写QML扩展教程中得到了演示。

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

数据类型处理与所有者权

从C++传输到QML的任何数据,无论作为属性值、方法参数或返回值,还是信号参数值,都必须是QML引擎支持的数据类型。

默认情况下,引擎支持多种Qt C++类型,并可从QML中适当使用时自动进行转换。此外,与QML类型系统注册的C++类可以用作数据类型,如果适当注册,它们的枚举也可以用作数据类型。有关更多信息,请参阅QML和C++之间的数据类型转换

此外,在从C++到QML传输数据时,还需考虑数据所有者权规则。请参阅数据所有者权以获取更多详细信息。

公开属性

可以使用QObject派生类使用Q_PROPERTY() 宏指定一个属性。属性是带有相关读取函数和可选写入函数的类数据成员。

QObject派生或Q_GADGET类的所有属性都可通过QML访问。

例如,下面是一个具有author属性的Message类。由Q_PROPERTY宏调用指定,此属性可通过author()方法读取,并可通过setAuthor()方法写入。

注意:不要为Q_PROPERTY类型使用typedefusing,因为这会使moc混乱。这可能会导致某些类型比较失败。

不是

using FooEnum = Foo::Enum;

class Bar : public QObject
{
    Q_OBJECT
    Q_PROPERTY(FooEnum enum READ enum WRITE setEnum NOTIFY enumChanged)
};

直接引用类型

class Bar : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Foo::Enum enum READ enum WRITE setEnum NOTIFY enumChanged)
};

为了使Message可用,您需要在C++中使用QML_ELEMENT,在CMake中使用qt_add_qml_module

class Message : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged)
public:
    void setAuthor(const QString &a)
    {
        if (a != m_author) {
            m_author = a;
            emit authorChanged();
        }
    }

    QString author() const
    {
        return m_author;
    }

signals:
    void authorChanged();

private:
    QString m_author;
};

Message的一个实例可以传递作为名为MyItem.qml的文件的所需属性来使其可用。

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

    QQuickView view;
    Message msg;
    view.setInitialProperties({{"msg", &msg}});
    view.setSource(QUrl::fromLocalFile("MyItem.qml"));
    view.show();

    return app.exec();
}

然后,可以从MyItem.qml读取author属性。

// MyItem.qml
import QtQuick

Text {
    required property Message msg

    width: 100; height: 100
    text: msg.author    // invokes Message::author() to get this value

    Component.onCompleted: {
        msg.author = "Jonah"  // invokes Message::setAuthor()
    }
}

为了与QML的最大互操作性,任何可写属性都应该有一个相关的NOTIFY信号,该信号会在属性值发生更改时发出。这允许使用财产绑定,这是QML的一个基本功能,通过自动更新其依赖项更改值的属性强制关系。

在上面的例子中,与author属性相关的NOTIFY信号是authorChanged,正如在Q_PROPERTY()宏调用中所指定的那样。这意味着每当信号发出时——当在Message::setAuthor()中作者更改时——这就会通知QML引擎,任何涉及author属性的绑定都必须更新,然后引擎将再次通过调用Message::author()来更新text属性。

如果author属性是可写的,但没有与其相关的NOTIFY信号,则text值将使用由Message::author()返回的初始值初始化,但不会根据此属性的任何后续更改进行更新。此外,从QML绑定到属性的任何尝试都将从引擎产生运行时警告。

注意:建议将NOTIFY信号命名为<属性>Changed,其中<属性>是属性的名称。由QML引擎生成的相关属性变化信号处理器始终采用on<Property>Changed的形式,而不管相关的C++信号名称如何,因此建议信号名称遵循此约定以避免混淆。

NOTIFY信号使用注意事项

为了防止循环或过度评估,开发人员应确保只有在属性值实际发生变化时才发出属性变化信号。此外,如果一个属性或一组属性很少使用,可以使用相同的NOTIFY信号为多个属性服务。在确保性能不受影响的前提下,这样做是可以的。

NOTIFY信号的存在确实会产生一点开销。有些情况下,属性的值是在对象构造时设置的,之后不再改变。这种情况的最常见例子是当一个类型使用分组属性时,分组属性对象只分配一次,只在对象被删除时释放。在这些情况下,可以在属性声明中使用CONSTANT属性,而不是NOTIFY信号。

CONSTANT属性仅应用于值只在类构造函数中设置和完成的属性。所有其他希望在绑定中使用的属性都应该有NOTIFY信号。

具有对象类型的属性

只要对象类型已适当与QML类型系统注册,就可以从QML中访问对象类型属性。

例如,Message类型可能有一个类型为MessageBody*body属性

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(MessageBody* body READ body WRITE setBody NOTIFY bodyChanged)
public:
    MessageBody* body() const;
    void setBody(MessageBody* body);
};

class MessageBody : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString text READ text WRITE text NOTIFY textChanged)
// ...
}

假设Message类型已与QML类型系统注册,允许在QML代码中使用它作为对象类型

Message {
    // ...
}

如果MessageBody类型也与类型系统注册,那么就可以将MessageBody分配给Messagebody属性,所有这些都可以在QML代码中完成

Message {
    body: MessageBody {
        text: "Hello, world!"
    }
}

具有对象列表类型的属性

包含QObject派生类型列表的属性也可以暴露给QML。为了这个目的,应该使用QQmlListProperty而不是QList作为属性类型。这是因为QList不是QObject派生的类型,因此无法通过Qt元对象系统提供必要的QML属性特性,例如在列表修改时的信号通知。

例如,下面的MessageBoard类有一个类型为QQmlListPropertymessages属性,它存储了Message实例的列表

class MessageBoard : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty<Message> messages READ messages)
public:
    QQmlListProperty<Message> messages();

private:
    static void append_message(QQmlListProperty<Message> *list, Message *msg);

    QList<Message *> m_messages;
};

MessageBoard::messages()函数仅从其成员QListm_messages创建并返回一个QQmlListProperty,传递给QQmlListProperty构造函数所需适当的列表修改函数

QQmlListProperty<Message> MessageBoard::messages()
{
    return QQmlListProperty<Message>(this, 0, &MessageBoard::append_message);
}

void MessageBoard::append_message(QQmlListProperty<Message> *list, Message *msg)
{
    MessageBoard *msgBoard = qobject_cast<MessageBoard *>(list->object);
    if (msg)
        msgBoard->m_messages.append(msg);
}

注意,对于QQmlListProperty模板类类型,在此情况下为Message——必须与QML类型系统进行注册

分组属性

任何只读的对象类型属性都可以作为分组属性从QML代码中访问。这可以用于暴露一组相关属性,这些属性描述了一个类型的属性集。

例如,假设Message::author属性的类型为MessageAuthor而不是简单的字符串,并且有子属性nameemail

class MessageAuthor : public QObject
{
    Q_PROPERTY(QString name READ name WRITE setName)
    Q_PROPERTY(QString email READ email WRITE setEmail)
public:
    ...
};

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(MessageAuthor* author READ author)
public:
    Message(QObject *parent)
        : QObject(parent), m_author(new MessageAuthor(this))
    {
    }
    MessageAuthor *author() const {
        return m_author;
    }
private:
    MessageAuthor *m_author;
};

可以使用QML中的分组属性语法写入author属性,如下所示

Message {
    author.name: "Alexandra"
    author.email: "[email protected]"
}

公开为分组属性的类型与对象类型属性的不同之处在于,分组属性是只读的,并且在构造时由父对象初始化为一个有效值。虽然可以在QML中修改分组属性的子属性,但分组属性对象本身永远不会改变,而对象类型属性则可以在任何时间内从QML代码分配一个新的对象值。因此,分组属性对象的生存期完全受C++父实现的控制,而对象类型属性可以通过QML代码自由创建和销毁。

公开方法(包括Qt插槽)

如果满足以下条件,则QObject派生类型的任何方法都可以从QML代码中访问:

  • 使用Q_INVOKABLE()宏标记的公共方法
  • 公共Qt插槽的方法

以下MessageBoard类有一个标记了Q_INVOKABLE宏的postMessage()方法,以及一个公共插槽refresh()方法

class MessageBoard : public QObject
{
    Q_OBJECT
    QML_ELEMENT

public:
    Q_INVOKABLE bool postMessage(const QString &msg) {
        qDebug() << "Called the C++ method with" << msg;
        return true;
    }

public slots:
    void refresh() {
        qDebug() << "Called the C++ slot";
    }
};

如果将MessageBoard的实例设置为文件MyItem.qml所需属性的属性,那么MyItem.qml可以像下面示例所示调用这两个方法

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

    MessageBoard msgBoard;
    QQuickView view;
    view.setInitialProperties({{"msgBoard", &msgBoard}});
    view.setSource(QUrl::fromLocalFile("MyItem.qml"));
    view.show();

    return app.exec();
}
QML
// MyItem.qml
import QtQuick 2.0

Item {
    required property MessageBoard msgBoard

    width: 100; height: 100

    MouseArea {
        anchors.fill: parent
        onClicked: {
            var result = msgBoard.postMessage("Hello from QML")
            console.log("Result of postMessage():", result)
            msgBoard.refresh();
        }
    }
}

如果C++方法有一个参数类型为QObject*,则可以使用引用该对象的对象id或JavaScript var值从QML传递参数值。

QML支持调用重载的C++函数。如果有多个具有相同名称但参数不同的C++函数,则会根据提供的参数数量和类型调用正确的函数。

从QML中的JavaScript表达式访问C++方法返回的值时,将其转换为JavaScript值。

C++方法和'this'对象

您可能想从一个对象获取C++方法并在另一个对象上调用它。考虑以下在一个名为Example的QML模块中的示例

C++
class Invokable : public QObject
{
    Q_OBJECT
    QML_ELEMENT
public:
    Invokable(QObject *parent = nullptr) : QObject(parent) {}

    Q_INVOKABLE void invoke() { qDebug() << "invoked on " << objectName(); }
};
QML
import QtQml
import Example

Invokable {
    objectName: "parent"
    property Invokable child: Invokable {}
    Component.onCompleted: child.invoke.call(this)
}

如果您从合适的主.cpp加载QML代码,它应该打印“在父对象上调用”。然而,由于长期存在的问题,它不起作用。历史上,C++方法的基础'this'对象与方法不可分割地绑定在一起。更改现有代码的行为会导致细微的错误,因为'this'对象在许多地方都是隐式的。从Qt 6.5开始,您可以显式选择响应的行为,并允许C++方法接受一个'this'对象。为此,在您的QML文档中添加以下pragma

pragma NativeMethodBehavior: AcceptThisObject

添加此行后,上面的示例将按预期工作。

公开信号

QObject派生类型的任何公共信号都可以从QML代码中访问。

QML引擎会自动为从QML中使用的任何由QObject派生类型的信号创建信号处理器。信号处理器始终命名为on<Signal>,其中<Signal>是信号名称,首字母大写。信号传递的所有参数都可通过参数名称在信号处理器中使用。

例如,假设MessageBoard类有一个具有单个参数subject的newMessagePosted()信号。

class MessageBoard : public QObject
{
    Q_OBJECT
public:
   // ...
signals:
   void newMessagePosted(const QString &subject);
};

如果将MessageBoard类型注册到QML类型系统,则可以声明QML中的MessageBoard对象使用名为onNewMessagePosted的信号处理器接收newMessagePosted()信号,并检查subject参数值。

MessageBoard {
    onNewMessagePosted: (subject)=> console.log("New message received:", subject)
}

与属性值和方法参数类似,信号参数必须具有QML引擎支持的类型;请参阅重金属俺和C++之间的数据类型转换(使用未注册的类型不会生成错误,但无法从处理器访问参数值)。

类可以有多个同名的信号,但是只有最后的信号才能作为QML信号访问。注意,具有相同名称但参数不同的信号是无法区分的。

© 2024 Qt公司有限公司。本文件中的文档贡献为各自所有者的版权。本文件中的文档是根据自由软件基金会发布的GNU自由文档许可证第1.3版许可的。Qt及其相关标志是芬兰及其它国家/地区Qt公司拥有的商标。其他所有商标均为各自所有者的财产。