QML 引擎和 Qt 元对象系统(Qt meta-object system)是紧密集成在一起的,任何 QObject 派生类的成员变量、函数在经过很少量的处理后(或无需处理)都能够再 QML 中直接访问——因此,我们可以非常简单地实现使用 C++ 代码来对 QML 进行扩展。
QML 引擎能够通过元对象系统实现对 QObject 实例的 introspect 。这意味着任何 QML 代码都能够获取到 QObject 派生类的下列成员:
- 属性( Property )
- 方法( 需要是 public slot 方法或使用 Q_INVOKABLE 宏标记过的方法)
- 信号
没有想到如何翻译 introspect 。introspect 和反射类似,都能够在运行时获取对象的信息,只是相较于反射少了修改对象的能力。
此外,使用 Q_ENUMS
宏声明的枚举类型也是可以的。更多信息请读 Data Type Conversion Between QML and C++
通常来说,无论 QObject 的派生类是否使用 QML 类型系统注册(registered with the QML type system)过,其实都是可以从 QML 访问到的。只有在用这个类的时候需要 QML 引擎获取到额外的类型信息时——例如类本身要被做作为其他函数的参数或属性,又或者类的枚举类型要这么用时,这个类才需要进行注册。
本文中涉及到的一些重要概念都在 使用 C++ 编写 QML 扩展 教程中讲到过,有需要可以看看
数据类型管理和所有权问题
从 C++ 传到 QML 的所有数据——无论是一个属性值、一个方法参数、一个返回值,还是一个信号参数值,都必须是 QML 引擎所支持的类型。
默认情况下,QML 引擎支持将一些 Qt C++ 类型自动转换为对应的 QML 类型。此外,使用 QML 类型系统注册过的 C++ 类和枚举也能够作为数据类型使用。相关信息详见 Data Type Conversion Between QML and C++ 。
在数据从 C++ 传输到 QML 时,数据所有权也是需要考虑的一个问题。当数据从 C++ 传到 QML 时,数据的所有权总是会留在 C++ 部分的。但当 C++ 显式函数调用的返回值是 QObject 时是个例外:这种情况下,除非 C++ 代码中显式地通过指定 QQmlEngine::CppOwnership
调用 QQmlEngine::setObjectOwnership()
,否则其他情况下 QML 引擎将拥有被返回对象的所有权。
暴露属性
属性( Property )是一个类中提供了其自身的读(必有)写(可选)函数的数据成员。我们可以使用 Q_PROPERTY()
宏来将任意的 QObject 派生类指定为属性( Property )。
听起来有些拗口,其实【属性】指的就是一个成员,这个成员在类中存在为其准备的读写函数。看看下面的代码例子也许会容易理解一些
所有 QObject 派生类的属性都是可以在 QML 中访问的。
下面是一个带有 author
属性的 Message
类。author
成员变量可以通过成员函数 author()
读取,可以通过 setAuthor()
函数写入新值。author
变量及其读、写函数通过 Q_PROPERTY
宏调用相关联:
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;
}
singals:
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();
}
那么, author
属性就能够在 MyItem.qml
中读取到:
// MyItem.qml
import QtQuick 2.0
Text {
width: 100; height: 100
text: msg.author // 调用 Message::author() 获取到 author 值
component.onCompleted: {
msg.author = "Jonah" // 调用 Message::setAuthor()
}
}
若想最大程度加强 C++ 和 QML 的互操作性,则可以为所有可写属性都匹配一个在属性值发生变化时触发的 NOTIFY 信号。关联上这样的信号后,该属性就能和属性绑定一起使用了——属性绑定是 QML 的一个基本特性,该特性让属性的值随着其依赖的其他属性值自动变化,以强制保持属性和属性之间的关系。
在上面的例子中,Q_PROPERTY()
宏调用指定了 authorChanged
是与 author
属性关联的 NOTIFY
信号。这意味着每当 Message::setAuthor()
的调用触发信号时,该信号都会通知 QML 引擎:所有与 author 属性相关的其他绑定值都必须被更新一次,反过来,QML 引擎也会再使用 Message::author()
来再更新一次 text
属性的值。
若 author
属性是可写的,却没有设置与之关联的 NOTIFY 信号,那么 text
值仍能够被 Message::author()
在初始化时返回的初始值改变,但之后 author
值变化时就无法再更新 text
属性的值了。此外,任何试图从 QML 绑定该值的操作都会引起 QML 引擎的运行时警告。
注意:推荐将 NOTIFY 信号命名为 <property>Changed
。由 QML 引擎生成的属性改变信号命名全都是 on<property>Changed
这种形式的,所以将自定义的信号按 <property>Changed
规则命名能够避免造成不必要的困惑。
使用通知信号的注意事项
为了避免循环求值和过度求值,开发者需要确保属性改变信号只会在属性值真的改变时才会被触发。同样的,如果一个属性或一组属性不会被频繁使用,那么实际上也可以让这些属性共享同一个 NOTIFY 信号。在确定这么做之前,需要小心确认不会引起严重的性能问题。
需要注意的是,使用 NOTIFY 信号的确会带来一些很小的性能开销。在一些情况下,属性值在对象创建时确定后就不会再改变了,例如:当一个类型使用组属性时,成组的属性对象可能只被创建一次,然后保持不变,直到对象被删除时其本身才会被释放。声明这样的属性时,可以考虑使用 CONSTANT 来代替 NOTIFY 信号。
CONSTANT 属性( attribute )应该只用于那些在构造函数中确定值的属性( properties ),所有其他需要绑定的属性还是应该设置一个 NOTIFY 信号。
组属性:
在一些情况下,一个属性可能会包含多个子属性,这些子属性可以用【点符号(dot)】或【组符号(group)】进行赋值。以下面这个例子来说明:一个 Text 类型有一个 font 组属性,其中的第一个 Text 对象使用点符号来初始化 font,第二个用组符号来初始化:
Text { // 点符号 font.pixelSize: 12 font.b: true } Text { // 组符号 font { pixelSize: 12; b: true } }
成组属性类型是【包含了多个子属性】的基本类型。某些这样的基本类型由 QML 语言提供,其他的可能由被 import 的 Qt Quick 模块提供。
有对象类型的属性
在 QML 类型系统中注册过的对象类型的属性时可以在 QML 中直接访问的。
如下面这个例子:Message
类型包含一个子属性 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
作为属性的类型(只能用 QQmlListProperty
,不能是 QList<T>
):这是因为 QList
并不是一个 QObject
的派生类型,所以无法通过 Qt 元对象系统提供足够 QML 使用的属性信息,以让其完成应该具备的功能(比如, list 不是 QObject 派生来的,当然也就加不上需要有的通知信号)。
QQmlListProperty
是一个能够使用 QList
值进行构造的模板类。
例如:下面的 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<Message *> m_messages
成员创建一个 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 类型系统中注册过。
成组的属性( Grouped Properties )
任何只读的对象类型属性都可以作为一个【成组属性(Grouped property)】从 QML 中进行访问。这可以被用于暴露一个类型( type )的一组相互关联属性。
例如:假设 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"
}
作为组属性暴露的类型,和【对象类型属性】的不同之处在于:
- 前者是只读的,而且只能在构造阶段由父对象进行其值的初始化;
- 组属性的子属性也可以在 QML 中进行修改,但组属性对象本身是不会随着这样的修改而改变的,与之相对的,对象类型属性却可以在 QML 中随时进行修改;
- 组属性对象由其的 C++ 父对象( parent )实现严格控制,而对象类型属性却可以在 QML 中随时创建和销毁。
暴露方法(包括 Qt 槽)
任何满足下列条件的 QObject 派生类型函数,都能够被从 QML 代码中访问:
- 使用
Q_INVOKABLE()
宏作为标志声明的 public 函数; - 公共 Qt 槽函数
例如:下面的 MessageBoard
类有一个 postMessage()
函数,该函数带有一个 Q_INVOKABLE
宏,还带有一个公共槽函数 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*
类型的参数,那么参数值也是可以在 QML 中用对象 id 或引用那个对象的 JavaScript var 变量来作为参数传给函数的。
QML 同样支持调用重载的 C++ 函数。QML 能够根据参数的数量和类型,调用正确的 C++ 函数。
C++ 函数执行完后的返回值会被转化为 JavaScript 类型的值,这样就可以在 QML 中直接取值了。
暴露信号
任何 QObject 派生类型的公共信号都是可以从 QML 中访问的。
QML 引擎会为符合要求的信号自动创建一个信号处理器( signal handler )。信号处理器的命名规则是 on<Signal>
,其中的 <Signal>
是首字母大写的信号名称。所有传给信号的参数都可以在信号处理器中直接通过参数名使用。
例如,假设 MessageBoard
类有一个 newMessagePosted()
信号,该信号带一个 subject
参数,定义如下:
class MessageBoard: public QObject
{
Q_OBJECT
public:
// ...
signals:
void newMessagePosted(const QString &subject);
};
在 MessageBoard
已经于 QML 类型系统中注册过的情况下,MessageBoard
在 QML 中声明的一个对象就可以通过 onNewMessagePosted
信号处理器接收 newMessagePosted()
信号并从参数中取值了,用法如下:
MessageBoard {
onNewMessagePosted: console.log("New message received:", subject)
}
和属性值及函数参数一样,信号的参数也必须是 QML 引擎支持的类型,详见QML 和 C++ 之间的数据类型转换 。直接用未注册过的类型不会报错,但无法取到值。
对于一个类中有多个同名信号的情况需要特别注意:QML 是无法区分同名不同参数的多个信号的,这些信号中只有最后一个能够被 QML 获取到。