将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公开QColor的QtQuick。此类依赖关系可以在运行时自动推断,但您不应依赖于这一点。
另外请注意,本文件中介绍的一些重要概念在使用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类型使用typedef或using,因为这会使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
分配给Message
的body
属性,所有这些都可以在QML代码中完成
Message { body: MessageBody { text: "Hello, world!" } }
具有对象列表类型的属性
包含QObject派生类型列表的属性也可以暴露给QML。为了这个目的,应该使用QQmlListProperty而不是QList
例如,下面的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()函数仅从其成员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
而不是简单的字符串,并且有子属性name
和email
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 |
如果C++方法有一个参数类型为QObject*
,则可以使用引用该对象的对象id
或JavaScript var值从QML传递参数值。
QML支持调用重载的C++函数。如果有多个具有相同名称但参数不同的C++函数,则会根据提供的参数数量和类型调用正确的函数。
从QML中的JavaScript表达式访问C++方法返回的值时,将其转换为JavaScript值。
C++方法和'this'对象
您可能想从一个对象获取C++方法并在另一个对象上调用它。考虑以下在一个名为Example
的QML模块中的示例
C++ | |
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公司拥有的商标。其他所有商标均为各自所有者的财产。