QML获得C++类中的属性
QML可以轻松地用C++中定义的功能进行扩展。由于QML引擎和Qt元对象系统的紧密结合,QML可以获取任意QObject派生类中的功能,这使得QML可以通过一些小的改动直接获得C++中的数据和方法。
QML引擎可以通过元对象系统对QObject实例对象进行内省,这意味着QML可以获取一个QObject派生类中的属性、方法(为公有槽或者标记为Q_INVOKABLE)以及信号,此外,申明为Q_ENMUMS的枚举类型也可以被获取(参考《QML和C++之间的数据类型转换》)。
总的来说,不管这个QObject派生类是否用QML类型系统注册过,以上的属性都可以由QML获得。但是,如果QML想获取其他的信息来实现某些功能,比如将类本身或者类中的枚举类型作为函数的参数或者一个属性,这时候就需要需对这个类进行注册。
注意,本文中提到的一些重要的概念将在《用C++写QML扩展》中进行证明。
数据类型处理和归属
从C++传入到QML中的任何数据,不管是属性值、函数参数、返回值亦或是一个信号参数,这些都必须属于一个被QML引擎所支持的类。
默认情况下,QML引擎支持很多Qt中的C++类型,也可以在使用时对他们进行适当的转换。此外,利用QML类型系统进行注册的C++类可以被用作数据类型,类中的枚举类型如果进行合适的注册的话,也可以达到同样的效果(参考《QML和C++之间的数据类型转换》)
此外,当数据从C++中转换到QML中时,数据的归属规则也是需要考虑的。
属性传递
任意的QObject派生类可以通过Q_PROPERTY()的宏来指定一个属性进行传递。这个属性有一个相关的读取函数和一个可选的写值函数。
所有QObject派生类的属性都可以由QML获取到。
举个例子,下面是一个含有author属性的Message类。由于被Q_PROPERTY的宏指定,这个属性可以通过author()方法进行读取,同时也可以通过setAuthor()方法进行写入。
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();
}
然后,这个author属性就可以在”MyItem.qml”中获取到:
//MyItem.qml
import QtQuick 2.0
Text{
width: 100;height: 100
text: msg.author //调用Message::author() 获取该值
Component.onCompleted: {
msg.author = "Jonah" //调用Message::setAuthor()
}
}
为了获取与QML的最大互用性,任何可写的属性都应该有一个相关的NOTIFY信号,当属性值改变的时候触发。这使得属性可以利用QML的属性绑定机制,一旦属性相关的值发生变化,QML就会更新这个属性。
在上面的例子中,和author属性相关的NOTIFY信号为Q_PROPERTY中指定的authorChanged, 这意味着当这个信号被发出,即通知QML引擎任何包含author属性的绑定都必须更新,反过来,QML引擎会通过再次调用Message::author()来更新text属性。
如果author属性是可写的但是没有NOTIFY信号,text属性会通过Message::author()返回的值来进行初始化,但之后author属性发生任何变化,text属性都不会被更新。此外,任何在QML中绑定这个属性的尝试都会在运行期间产生报警信息。
注意:推荐将NOTIFY信号命名为<property>Changed的形式,其中<property>为属性名称。不管相关的C++信号名称是什么,QML引擎生成的属性改变信号处理器会一直使用<property>Changed的形式,所以推荐使用这种惯例写法以免产生混淆。
提醒信号的注意点
为了防止循环和过度的分析,开发者应该确保信号只有在属性确实被改变的时候才发出来。同时,如果一个或一组属性并不是经常使用,允许不同的属性使用相同的提醒信号。这一点在操作时需要注意确保性能不会遭受损失。
NOTIFY信号的存在确实会引发一小部分开销。有时候,某些属性值在对象构造期间就设定好了并且后续不需要再做改动,最常见的情形是,当一个类使用属性组且这个属性组对象只分配一次内存,并且只有当对象被删除时空间才被释放。在这些情况中,CONSTANT属性可以加到属性申明中而NOTIFY信号则不被需要。
CONSTANT属性应该只用在属性值一旦设定好就不变的情况下(在类构造函数)。其他想使用绑定机制的属性则需要加上NOTIFY信号。
带有对象类型的属性
如果对象类型被正确注册(参考《在C++中定义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中将它作为一个QML对象使用:
Message{
//...
}
如果MessageBody类同样已经用QML类型系统注册过了,那么就可以将一个MessageBody直接赋给Message中的body属性:
Message{
body: MessageBody{
text: "Hello World!"
}
}
带有对象列表类型的属性
那些包含QObject派生类型的列表属性同样也可以传给QML。为了达到这个目的,需要注意的是,需要使用QQmlListProperty而不是QList<T>作为属性类型。这是因为QList并不是一个通过QObject派生的类型,从而就不能通过元对象系统来实现必要的QML属性特点,比如当一个列表发生改变时的提醒信号。
QQmlListProperty是一个很容易由QList对象进行构造的模板类。
举个例子,下面的MessageBoard类有一个类型为QQmlListProperty的messages的属性,它存储了一个Message实例对象的列表:
class MessageBoard : public QObject
{
Q_OBJECT
Q_PROPERTY(QQmlListProperty<Message> messages RAED 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获取,这可以用来传递一组描述类型性质相关的属性。
举个例子,假设MessageAuthor中的Message::author属性具有子属性:姓名和邮箱,而不是一个简单的字符串:
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@email.com"
}
属性组合对象类型属性不同之处在于属性组是只读的而且在构造的时候就被初始化为一个实际的值,属性组的子属性也许可以在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”就可以像下面这样调用那两个方法:
//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变量值传到QML中。
QML支持调用重载的C++函数。如果有多个重名的C++函数,但函数的参数不同,QML会根据参数的数量和类型来调用正确的函数。
当从JavaScript表达式中获取C++方法的返回值时,这些值会被转换为JavaScript值。
传递信号
任何公有的QObject派生类中的信号都可以通过QML code获取到。
QML引擎会为QObject派生类中的所有要在QML中使用的信号都创建一个信号处理器。信号处理器通常被命名为<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: console.log("new message received: ", subject)
}
正如属性值和函数参数一样,一个信号参数的类型必须被QML引擎所支持,使用一个未经注册的类型并不会保存,但参数值不能从处理器中获取到。(参看《QML和C++之间的数据类型转换》)
类也许会有多个同名的信号,但QML只能获取最后一个信号,注意同名但参数不同的信号不会被区别开来。