向 QML 公开 C++ 类型的属性

QML 引擎和 Qt 元对象系统 是紧密集成在一起的,任何 QObject 派生类或 Q_GADGET 类型的成员变量、函数在经过很少量的处理后(或无需处理)都能够在 QML 中直接访问——因此,我们可以非常简单地实现使用 C++ 代码来对 QML 进行扩展。

QML 引擎能够通过元对象系统实现对 QObject 实例的 introspect(introspect 和反射类似,都能够在运行时获取对象的信息,只是相较于反射少了修改对象的能力)。这意味着任何 QML 代码都能够获取到 QObject 派生类的下列成员:

  • 属性
  • 方法(需要是 public slot 方法或使用 Q_INVOKABLE 宏标记过的方法)
  • 信号

(此外,使用 Q_ENUM 宏声明的枚举类型也是可以的。详见 QML 和 C++ 之间的数据类型转换

通常来说,无论 QObject 的派生类是否在 QML 类型系统中注册 过,其实都是可以从 QML 访问到的。只有在用这个类的时候需要 QML 引擎获取到额外的类型信息时——例如类本身要被作为其他函数的参数或属性,又或者类的枚举类型要这么用时,这个类才需要进行注册。建议对 QML 中使用的所有类型进行注册,因为在编译时只能分析已注册的类型。

Q_GADGET 类型需要注册,因为它们不是从已知的共同基础派生出来的,不能自动提供。如果没有注册,他们的属性和方法就无法访问。

另请注意,本文档中介绍的许多重要概念在 使用 C++ 编写 QML 扩展 教程中进行了演示。

有关 C++ 和 QML 集成方法的更多信息,请参阅 概述 —— QML 与 C++ 混合编程

数据类型管理和所有权问题

从 C++ 传到 QML 的所有数据——无论是一个属性值、一个方法参数、一个返回值,还是一个信号参数值,都必须是 QML 引擎所支持的类型。默认情况下,QML 引擎支持将一些 Qt C++ 类型自动转换为对应的 QML 类型。此外,使用 QML 类型系统 注册过 的 C++ 类和枚举也能够作为数据类型使用。详见 QML 和 C++ 之间的数据类型转换

当数据从C++传输到QML时,会考虑数据所有权规则。详见 数据所有权

公开属性

可以使用 Q_PROPERTY 宏为任何 QObject 派生类指定 属性。属性是具有关联的读取函数和可选的写入函数的类数据成员。

所有 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)
 };
 class Message : public QObject
 {
     Q_OBJECT
     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;
 };

如果从 C++ 加载名为 MyItem.qml 的文件时将此类的实例 设置为上下文属性,则:

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

     QQuickView view;
     Message msg;
     view.engine()->rootContext()->setContextProperty("msg", &msg);
     view.setSource(QUrl::fromLocalFile("MyItem.qml"));
     view.show();

     return app.exec();
 }

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

 // MyItem.qml
 import QtQuick 2.0

 Text {
     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的基本功能,它通过在属性的任何依赖关系值发生变化时自动更新属性来强制属性之间的关系。

在上面的示例中,如 Q_PROPERTY 宏调用中所指定,author 属性的关联 NOTIFY 信号为 authorChanged。 这意味着每当发出信号时(如作者在 Message::setAuthor() 中进行更改时一样),这会通知 QML 引擎必须更新所有涉及 author 属性的绑定,并且引擎将更新 text 属性。通过再次调用 Message::author() 设置属性。

如果 author 属性是可写的,但没有关联的 NOTIFY 信号,则将使用 Message::author() 返回的初始值来初始化 text 值,但以后对该属性进行的任何更改均不会更新该文本值。另外,从 QML 绑定到该属性的任何尝试都将产生来自引擎的运行时警告。

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

使用通知信号的注意事项:

为了防止循环或过度计算,开发人员应该确保只有在属性值实际更改时才会发出属性更改信号。此外,如果一个属性或一组属性很少使用,则允许对多个属性使用相同的通知信号。这样做时应小心,以确保性能不会受到影响。

通知信号的存在确实会产生少量开销。在某些情况下,属性的值在对象构造时设置,并且随后不会更改。最常见的情况是,当类型使用 分组属性 时,分组属性对象只分配一次,并且只在删除对象时释放。在这些情况下,常量属性可以添加到属性声明,而不是通知信号。

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 类型也已在类型系统中注册,则可以从 QML 代码中将 MessageBody 分配给 Messagebody 属性:

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

对象列表类型的属性

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

例如,下面的 MessageBoard 类具有一个 QQmlListProperty 类型的 messages 属性,该属性存储 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() 函数仅从其 QList<T> m_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: "alexandra@mail.com"
 }

作为分组属性公开的类型不同于 对象类型属性 ,因为分组属性是只读的,并且在构造时由父对象初始化为有效值。可以从 QML 修改分组属性的子属性,但分组属性对象本身不会更改,而对象类型属性可以随时从 QML 分配新的对象值。因此,分组属性对象的生命周期受到 C++ 父实现的严格控制,而对象类型属性可以通过 QML 代码自由创建和销毁。

公开方法(包括 Qt 槽函数)

QObject 派生类型的任何方法都可以从 QML 代码访问,如果它是:

  • Q_INVOKABLE 宏标记的 public 方法
  • 作为 public Qt slot 的方法

例如,下面的 MessageBoard 类具有已用 Q_INVOKABLE 宏标记的 postMessage() 方法以及作为 public slot 的 refresh() 方法:

 class MessageBoard : public QObject
 {
     Q_OBJECT
 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.engine()->rootContext()->setContextProperty("msgBoard", &msgBoard);
     view.setSource(QUrl::fromLocalFile("MyItem.qml"));
     view.show();

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

 Item {
     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 值。

公开信号

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

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

例如,假设 MessageBoard 类具有带有单个参数 subjectnewMessagePosted() 信号:

 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 引擎支持的类型。 请参见 QML 和 C++ 之间的数据类型转换(使用未注册的类型不会产生错误,但是无法从处理程序中访问参数值)。

类可能包含多个具有相同名称的信号,但是只有最终信号才能作为 QML 信号进行访问。 请注意,名称相同但参数不同的信号无法相互区分。