将 C++ 类型的属性暴露给 QML
QML 可以轻松通过 C++ 代码中定义的功能进行扩展。由于 QML 引擎与Qt 元对象系统的紧密集成,任何由QObject 派生类或Q_GADGET 类型正确暴露的功能,均可从 QML 代码中访问。 这使得 C++ 数据和函数能够直接从 QML 中访问,通常只需很少的修改甚至无需修改。
QML引擎能够通过元对象系统对QObject 实例进行内省。这意味着任何QML代码都可以访问QObject 派生类的实例的以下成员:
- 属性
- 方法(前提是它们是公共插槽或标记为Q_INVOKABLE )
- 信号
(此外,如果枚举类型已使用Q_ENUM 声明,则也可使用。更多详情请参阅《QML与C++之间的数据类型转换》。)
通常情况下,无论是否已将QObject 派生的类注册到QML类型系统中,均可从QML访问这些内容。 但是,如果某类的使用方式需要引擎访问额外的类型信息——例如,该类本身将作为方法参数或属性使用,或者其某个枚举类型将以这种方式使用——则可能需要注册该类。 建议将您在 QML 中使用的所有类型进行注册,因为只有已注册的类型才能在编译时进行分析。
Q_GADGET 类型必须进行注册,因为它们并非派生自已知的公共基类,因此无法自动提供。如果不进行注册,则无法访问其属性和方法。
您可以通过在调用qt_add_qml_module时使用DEPENDENCIES选项添加依赖项,使来自其他模块的 C++ 类型在您自己的模块中可用。 例如,您可能希望依赖 `QtQuick `,以便您通过 QML 暴露的 C++ 类型能够将 `QColor ` 用作方法参数和返回值。`QtQuick ` 将 `QColor ` 作为值类型 `color` 进行暴露。此类依赖关系可能在运行时被自动推断出来,但您不应依赖此机制。
另请注意,本文档中介绍的许多重要概念都在《使用 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 的实例作为 required 属性传递给名为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 信号命名为<property>Changed,其中 <property> 是该属性的名称。QML 引擎生成的相关属性变化信号处理程序始终采用on<Property>Changed 的形式,无论相关的 C++ 信号名称为何,因此建议信号名称遵循此约定以避免混淆。
关于使用通知信号的注意事项
为防止循环或过度评估,开发者应确保仅在属性值实际发生变化时才发出属性变化信号。此外,如果某个属性或一组属性使用频率较低,允许为多个属性使用同一个 NOTIFY 信号。操作时应谨慎,以确保性能不受影响。
NOTIFY 信号的存在确实会带来微小的开销。在某些情况下,属性的值是在对象构造时设置的,此后不再发生变化。 最常见的情况是:一个包含子对象的只读属性,通常通过分组属性语法访问,其中子对象仅分配一次,且仅在所有者被删除时才被释放。在这些情况下,可以在属性声明中添加 CONSTANT 属性,以代替 NOTIFY 信号。
CONSTANT 属性仅应用于那些在类构造函数中设置且最终确定的属性的值。所有其他希望在绑定中使用的属性都应使用 NOTIFY 信号。
具有对象类型的属性
只要该对象类型已在 QML 类型系统中正确注册,即可从 QML 访问对象类型的属性。
例如,Message 类型可能具有一个名为body 的属性,其类型为MessageBody* :
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 类有一个类型为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 类型系统中注册。
分组属性
任何类型本身具有子属性的属性,都可以使用分组属性语法进行操作,无论该类型是font 这样的值类型,还是对象类型。分组属性有助于公开一组相关的属性,以描述某种类型的属性集合。
例如,假设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;
};author 属性可以使用 QML 中的分组属性语法进行写入,如下所示:
Message {
author.name: "Alexandra"
author.email: "alexandra@mail.com"
}由于 `author ` 是一个对象类型的属性,因此分组赋值不会创建 `MessageAuthor ` 对象。它们会被写入 `Message::author() ` 返回的任意对象中。这会带来以下几个后果:
- 子属性的名称是根据属性的声明类型(此处为
MessageAuthor)来解析的,而不是根据运行时恰好存储在其中的对象的类型。Message的不同实例化可能会产生不同类型的author对象。在创建 QML 组件时,我们唯一能依赖的就是声明的类型。 - 在应用分组赋值时,该对象必须已经存在。否则,引擎会抛出类似
Cannot set properties on author as it is null的错误。如上所述,在所有者的构造函数中创建对象是保证这一点的最简单方法,但一个在首次访问时创建对象的获取器同样有效。 - 对象的生命周期由 C++ 实现决定。分组属性语法既不会创建对象,也不会销毁对象。
如上例所示,将 `author ` 声明为只读,还能防止 QML 代码替换 `MessageAuthor ` 对象。如果 `author ` 具有 `WRITE ` 访问器,你同样可以将其赋值为一个新对象:
Message {
author: MessageAuthor {
name: "Alexandra"
email: "alexandra@mail.com"
}
}在同一个对象定义中,不能对同一属性同时使用这两种形式。如果在同一个Message 中既将一个对象赋值给author ,又写入author.name ,将会引发错误。 你可以通过将分组属性置于不同的作用域中来规避这一限制。但是,这样做会产生令人困惑的结果,因为此时存在两个MessageAuthor 对象:一个是由构造函数创建的,另一个是显式赋值的。只有绑定求值顺序才能决定分组属性应应用于哪个对象。
这意味着最佳实践是:
- 将用作分组属性的对象设为只读。
- 如果你还需要操作组本身的基属性,请避免使用分组属性语法。
对于font等值类型属性,分组赋值要求该属性可写,因为引擎会读取该值、对其进行修改,然后写回。不过,潜在的混淆问题依然存在。 如果你就地重新赋值整个 `font `,然后在其他地方操作 `font.bold ` 属性,则绑定求值顺序将决定哪一项先执行,以及最终 `bold ` 的值会是什么。
方法的公开(包括 Qt 槽)
QObject 派生类型的任何方法,只要满足以下条件,均可从 QML 代码中访问:
- 使用Q_INVOKABLE() 宏标记为公共的方法
- 作为公共 Qt槽的方法
例如,下面的MessageBoard 类有一个标记了Q_INVOKABLE 宏的postMessage() 方法,以及一个作为公共插槽的refresh() 方法:
classMessageBoard :publicQObject
{
Q_OBJECT
QML_ELEMENT
public:
Q_INVOKABLEboolpostMessage(constQString&msg) {
qDebug() << "Called the C++ method with" << msg;
return true;
}
public slots:
voidrefresh() {
qDebug() << "Called the C++ slot";
}
};如果将MessageBoard 的实例设置为文件MyItem.qml 的必填属性,则MyItem.qml 可以像以下示例所示那样调用这两个方法:
| C++ | |
| QML |
如果某个 C++ 方法的参数类型为QObject* ,则可以通过 QML 中的id 对象,或引用该对象的 JavaScriptvar 值,将参数值传递给该方法。
QML 支持调用重载的 C++ 函数。如果有多个同名但参数不同的 C++ 函数,系统将根据提供的参数个数和类型调用正确的函数。
当从 QML 中的 JavaScript 表达式访问 C++ 方法返回的值时,这些值会被转换为 JavaScript 值。
C++ 方法与 'this' 对象
您可能希望从一个对象中获取一个 C++ 方法,然后在另一个对象上调用它。请看以下示例,该示例位于名为Example 的 QML 模块中:
| C++ | |
| QML | |
如果您从相应的 main.cpp 文件中加载该 QML 代码,它本应输出“invoked on parent”。然而,由于一个长期存在的错误,实际并不会输出该内容。从历史上看,基于 C++ 的方法中的 'this' 对象与该方法是不可分割地绑定在一起的。 对于现有代码,更改此行为可能会导致难以察觉的错误,因为“this”对象在许多地方都是隐含的。从 Qt 6.5 开始,您可以显式地选择正确的行为,并允许 C++ 方法接受“this”对象。要做到这一点,请在您的 Qt Qml 文档中添加以下 pragma 指令:
pragma NativeMethodBehavior: AcceptThisObject添加此行后,上面的示例将按预期运行。
重写 toString()
如果你提供了一个名为toString(无参数)的Q_INVOKABLE 方法,该方法将被用于将对象转换为字符串,从而取代 JavaScript 原生的toString实现。
暴露信号
QObject 派生类型的任何公共信号均可从 QML 代码中访问。
QML 引擎会自动为从QObject 派生的类型中、在 QML 中被使用的任何信号生成一个信号处理程序。信号处理程序的名称始终为on<Signal>,其中<Signal> 是信号的名称,首字母大写。信号传递的所有参数均可通过参数名称在信号处理程序中获取。
例如,假设MessageBoard 类有一个名为newMessagePosted() 的信号,该信号有一个参数subject :
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 信号使用。请注意,名称相同但参数不同的信号无法相互区分。
另请参阅《 QML Type Registration Macros 》 和《从 C++ 定义 QML 类型》。
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.