暴露C++类型的属性给QML
官方文档:Exposing Attributes of C++ Types to QML
QML 可以轻松地通过在 C++ 代码中定义的功能进行扩展。由于 QML 引擎与 Qt 元对象系统(Qt meta-object system)的紧密集成,任何由 QObject
派生的类或 Q_GADGET 类适当公开的功能都可以从 QML 代码中访问。这使得 C++ 的数据和函数可以直接从 QML 中访问,通常几乎不需要修改。
QML 引擎可以通过元对象系统自省 QObject 实例。这意味着任何 QML 代码都可以访问 QObject 派生类的实例的以下成员:
- 属性
- 方法(只要它们是公共槽或带有 Q_INVOKABLE 标志)
- 信号
(此外,如果已使用 Q_ENUM
声明了枚举类型,则可用。有关更多详细信息,请参阅 Data Type Conversion Between QML and C++)
通常情况下,无论 QObject 派生类是否已在 QML 类型系统中注册(registered with the QML type system),这些成员都可以从 QML 中访问。但是,如果要以需要引擎访问其他类型信息的方式使用类 — 例如,如果类本身要用作方法参数或属性,或者如果其中一个枚举类型要以这种方式使用 — 那么可能需要注册该类。建议为在 QML 中使用的所有类型进行注册,因为只有注册的类型可以在编译时进行分析。
对于 Q_GADGET 类型,需要进行注册,因为它们不是从已知的公共基类派生的,无法自动提供。如果没有注册,它们的属性和方法将无法访问。
还请注意,本文档中涵盖的一些重要概念在Writting QML Extensions with C++教程中有所示范。
有关 C++ 和不同的 QML 集成方法的更多信息,请参阅 C++ and QML integration overview。
数据类型处理和所有权
从C++传输到QML的任何数据,无论是作为属性值、方法参数或返回值,还是信号参数值,都必须是QML引擎支持的类型。
默认情况下,引擎支持一些Qt C++类型,并且在从QML中使用时可以自动进行适当的转换。此外,已在QML类型系统中注册的C++类可以用作数据类型,如果已适当注册,它们的枚举类型也可以使用。有关更多信息,请参阅Data Type Conversion Between QML and C++。
此外,当从C++传输数据到QML时,还将考虑数据所有权规则。有关更多详细信息,请参阅Data Ownership。
暴露属性
可以使用Q_PROPERTY()
宏为任何QObject派生类指定属性。属性是一个类数据成员,具有关联的读取函数和可选的写入函数。
QObject派生类或Q_GADGET类的所有属性都可以从QML中访问。
例如,下面是一个具有author属性的Message类。根据Q_PROPERTY宏调用的规定,该属性可以通过author()方法进行读取,并通过setAuthor()方法进行写入:
(注意:不要对Q_PROPERTY类型使用typedef或using,因为这会让moc混淆。这可能导致某些类型比较失败。)
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的文件时,将此类的实例设置为上下文属性set as a context property:
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 的一个基本特性,它通过自动更新属性来强制属性之间的关系,无论其依赖项的值如何变化。
在上面的例子中,author属性的相关 NOTIFY 信号是 authorChanged,如 Q_PROPERTY() 宏调用中所指定的。这意味着每当信号被发出——就像在 Message::setAuthor() 中author更改时一样——这就通知 QML 引擎必须更新任何涉及author属性的绑定,并且引擎将通过再次调用 Message::author() 来更新文本属性。
如果author属性是可写的但没有相关的 NOTIFY 信号,则文本值将使用 Message::author() 返回的初始值进行初始化,但不会根据此属性的任何后续更改进行更新。此外,任何尝试从 QML 绑定到该属性的操作都将从引擎产生运行时警告。
注意:建议将 NOTIFY 信号命名为 <property>Changed,其中 <property> 是属性的名称。由 QML 引擎生成的相关属性更改信号处理程序将始终采用 on<Property>Changed 的形式,而与相关 C++ 信号的名称无关,因此建议信号名称遵循此约定,以避免任何混淆。
使用通知信号的注意事项
为了防止循环或过度评估,开发人员应确保当属性值实际更改时才发出属性更改信号。此外,如果某个属性或一组属性很少使用,则允许为几个属性使用相同的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类型也在类型系统中注册,就可以从QML代码中将MessageBody分配给Message的body属性:
Message {
body: MessageBody {
text: "Hello, world!"
}
}
➋具有对象列表类型的属性
包含 QObject 派生类型列表的属性也可以暴露给 QML。然而,为此应使用 QQmlListProperty
而不是 QList <T> 作为属性类型。这是因为 QList 不是 QObject 派生类型,因此无法通过 Qt 元对象系统提供必要的 QML 属性特性,例如在修改列表时发出信号通知。
例如,下面的 MessageBoard 类具有一个 messages 属性,其类型为 QQmlListProperty,用于存储 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类型而不是简单的字符串,具有名称和电子邮件的子属性:
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()
宏标记的公共方法 - 是公共 Qt 槽的方法
例如,下面的 MessageBoard 类具有已使用 Q_INVOKABLE 宏标记的 postMessage() 方法,以及作为公共槽的 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可以像下面的示例那样调用这两个方法:
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();
}
// 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++函数具有相同的名称但不同的参数,则根据提供的参数数量和类型调用正确的函数。
从C++方法返回的值在从QML中的JavaScript表达式访问时转换为JavaScript值。
C++方法和'this'对象
您可能希望从一个对象中检索C++方法,并在不同的对象上调用它。考虑以下示例,在名为Example的QML模块中:
class Invokable : public QObject {
Q_OBJECT
QML_ELEMENT
public:
Invokable(QObject *parent = nullptr) : QObject(parent) {}
Q_INVOKABLE void invoke() {
qDebug() << "invoked on " << objectName(); }
};
import QtQml
import Example
Invokable {
objectName: "parent"
property Invokable child: Invokable {}
Component.onCompleted: child.invoke.call(this)
}
如果你从一个合适的 main.cpp 文件中加载 QML 代码,它应该会打印出 "invoked on parent"。然而,由于一个长期存在的 bug,它并没有。历史上,基于 C++ 的方法的 'this' 对象与方法密不可分。改变现有代码的这种行为将会导致微妙的错误,因为 'this' 对象在许多地方是隐含的。自 Qt 6.5 开始,你可以明确地选择正确的行为并允许 C++ 方法接受一个 'this' 对象。为此,请在你的 QML 文档中添加以下编译指示:
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 引擎支持的类型;请参阅 QML和C++之间的数据类型转换。(使用未注册的类型不会生成错误,但是无法从处理程序中访问参数值。)
类可以具有多个具有相同名称的信号,但是只有最终信号可以作为 QML 信号访问。请注意,具有相同名称但具有不同参数的信号无法彼此区分。