QML学习笔记(四十六)QML与C++交互:Q_PROPERTY宏映射
前言
在之前的学习中,我们可以通过对C++对象暴露其上下文属性,让qml端可以直接操作该对象,调用其设置了Q_INVOKABLE宏或槽的函数接口。但这种方式还不能让这个C++对象具备其他qml组件一样的功能,比如直接获取它的属性、或者拿这个对象的某个属性进行属性绑定。
这样说可能有点绕,举一个简单的例子。
我的C++类Counter 会对一个成员变量m_count 进行计数,这个时候,我希望qml端的一个Text文本和它是属性绑定的,当m_count改变的时候,文本上显示的数字也发生改变。
我们可以理解为Counter对象是一个组件,count就是他的一个属性,而qml端Text文本需要和这个属性进行属性绑定。
这个时候,我们就需要利用Q_PROPERTY宏映射,让这个C++类实现一个或多个属性,这些属性能够在qml端进行使用。
一、Q_PROPERTY
Q_PROPERTY 宏映射 = “把 C++ 成员变量变成 QML 属性”
一次声明,读、写、通知全打通,QML 侧就能像普通变量一样用。
语法结构如下:
Q_PROPERTY(type name // 属性类型和名字READ getter // 读函数[WRITE setter] // 可选:写函数[RESET resetFn] // 可选:恢复默认值[NOTIFY signal] // 可选:变化信号[CONSTANT] // 可选:永不变,无 NOTIFY[FINAL] // 可选:禁止 QML 重写
)
举一个最简单的例子:
class Counter : public QObject {Q_OBJECT// 属性名 类型 READ getter WRITE setter NOTIFY 变化信号Q_PROPERTY(int count READ count WRITE setCount NOTIFY countChanged)
public:int count() const { return m_count; }void setCount(int v) {if (m_count == v) return;m_count = v;emit countChanged(); // 必须发射,QML 才能刷新}
signals:void countChanged();
private:int m_count = 0;
};
暴露给qml侧:
engine.rootContext()->setContextProperty("counter", &counter);
qml侧当做属性变量一样使用:
Text {text: counter.count // 读
}
Button {onClicked: counter.count++ // 写(自动调 setCount)
}
Connections {target: counteronCountChanged: console.log("C++ 通知 QML 值变了") // 通知
}
注意!!!
Counter 中的实际成员变量是m_count,它和Q_PROPERTY中的count并不是同一个东西。
Q_PROPERTY(int count READ count WRITE setCount NOTIFY countChanged)
这句宏属性的定义,其实是凭空虚构一个叫count的属性,为其附上基本的读、写、通知(也就是信号)的方法。我们是在这些方法的具体实现中,间接链接上了m_count的。
void setCount(int v) {if (m_count == v) return;m_count = v;emit countChanged(); // 必须发射,QML 才能刷新
}
所以理论上,之后在C++端修改m_count的时候,应该是调用setCount来进行修改,这样才会触发count这个属性的值改变,进而影响到qml端的绑定设置。
我们做一个简单的测试例子,用一个定时器每秒递增m_count,你会发现qml端是没有反应的。原因就是根本没有触发到countChanged信号。
二、完整例子
我们做一个完整的例子,更深入理解一下Q_PROPERTY这个宏的使用。
创建一个C++类,叫做Movie,它有两个成员变量被设置成了Q_PROPERTY,分别是mainCharacter和title,代表这部电影的主演角色和标题。
贴上完整代码:
movie.h
#ifndef MOVIE_H
#define MOVIE_H#include <QObject>class Movie : public QObject
{Q_OBJECTQ_PROPERTY(QString mainCharacter READ mainCharacter WRITE setMainCharacter NOTIFY mainCharacterChanged)Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged)public:explicit Movie(QObject *parent = nullptr);QString mainCharacter() const;void setMainCharacter(const QString &newMainCharacter);QString title() const;void setTitle(const QString &newTitle);signals:void mainCharacterChanged();void titleChanged();private:QString m_mainCharacter;QString m_title;};#endif // MOVIE_H
movie.cpp
#include "movie.h"
#include <QDebug>
#include <QTimer>Movie::Movie(QObject *parent) : QObject(parent)
{
}QString Movie::mainCharacter() const
{return m_mainCharacter;
}void Movie::setMainCharacter(const QString &newMainCharacter)
{if(m_mainCharacter == newMainCharacter)return;m_mainCharacter = newMainCharacter;emit mainCharacterChanged(); // 非常重要,否则qml中绑定该属性的地方将会失效qDebug() << "setMainCharacter..." << newMainCharacter;
}QString Movie::title() const
{return m_title;
}void Movie::setTitle(const QString &newTitle)
{if(m_title == newTitle)return;m_title = newTitle;emit titleChanged();qDebug() << "setTitle..." << newTitle;
}
qml代码:
import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.12Window {visible: truewidth: 640height: 480title: qsTr("QPROPERTY Mappings")Connections{target: MovieonMainCharacterChanged:{console.log("onMainCharacterChanged"+Movie.mainCharacter);}onTitleChanged:{console.log("onTitleChanged"+Movie.title);}}Column{spacing: 20Text {id: titleIdtext: Movie === null ? "" : Movie.titlefont.pointSize: 20anchors.horizontalCenter: parent.horizontalCenter}Text {id: mainCharIdtext: Movie === null ? "" : Movie.mainCharacterfont.pointSize: 20anchors.horizontalCenter: parent.horizontalCenter}Row{anchors.horizontalCenter: parent.horizontalCenterTextField{id: titleTextFieldIdwidth: 300}Button{width: 200id: button1text : "Change title"onClicked: {Movie.title = titleTextFieldId.text}}}Row{anchors.horizontalCenter: parent.horizontalCenterTextField{id: mainCharTextFieldIdwidth: 300}Button{width: 200id: button2text : "Change main character"onClicked: {Movie.mainCharacter = mainCharTextFieldId.text}}}}
}
运行界面:
这个例子的功能就是可以在qml界面中分别修改标题和主演,然后更新到上方的两个text文本中。
细节说明:
1.movie头文件中的Q_PROPERTY宏
Q_PROPERTY(QString mainCharacter READ mainCharacter WRITE setMainCharacter NOTIFY mainCharacterChanged)
Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged)
mainCharacter 和title 并不是成员变量m_mainCharacter和m_title,它们只是通过相关联起来的概念。这里READ 、WRITE 和NOTIFY 对应的方法和信号名是默认扩展的,其实也可以自己自定义名字,只要类中有具体实现即可。
2.属性绑定
Text {id: titleIdtext: Movie === null ? "" : Movie.title
}
这里实际上是拿Movie.title来进行属性绑定了,那title具体是啥呢?就是我们定义Q_PROPERTY的名字title。这里它如何能够感知到title产生了变化,并作用到text中?就是靠NOTIFY titleChanged这个信号。所以,我们如果期望能实现没有bug的属性绑定,一定要注意当c++中的m_title发生变化的时候,要手动发送一遍titleChanged信号。
3.读取和修改
读取:
onTitleChanged:{console.log("onTitleChanged"+Movie.title);
}
修改:
Movie.title = titleTextFieldId.text
这里的读取和修改看似是对一个变量进行操作,实际上会调用到定义Q_PROPERTY时的READ和WRITE接口,也就是:
QString Movie::title() const
{return m_title;
}void Movie::setTitle(const QString &newTitle)
{if(m_title == newTitle)return;m_title = newTitle;emit titleChanged();qDebug() << "setTitle..." << newTitle;
}
因为我们修改title的时候会发送titleChanged,触发属性绑定,于是Text的文本发生改变,作用到qml界面中。我们也可以是实现Connections,示例代码中也有了。
三、总结
Q_PROPERTY宏映射的方法,本质上是将C++中的成员变量重新封装,让它具有在qml端当做属性一样来进行绑定或赋值。这种方式比较灵活,能极大帮助C++和QML之间的交互。
这里再补充一下Q_PROPERTY定义的格式:
Q_PROPERTY(type name // 属性类型和名字READ getter // 读函数[WRITE setter] // 可选:写函数[RESET resetFn] // 可选:恢复默认值[NOTIFY signal] // 可选:变化信号[CONSTANT] // 可选:永不变,无 NOTIFY[FINAL] // 可选:禁止 QML 重写
)