前言
在之前的学习中,我们可以通过对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 侧就能像普通变量一样用。
语法结构如下:
cpp
Q_PROPERTY(
type name // 属性类型和名字
READ getter // 读函数
[WRITE setter] // 可选:写函数
[RESET resetFn] // 可选:恢复默认值
[NOTIFY signal] // 可选:变化信号
[CONSTANT] // 可选:永不变,无 NOTIFY
[FINAL] // 可选:禁止 QML 重写
)
举一个最简单的例子:
cpp
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侧:
cpp
engine.rootContext()->setContextProperty("counter", &counter);
qml侧当做属性变量一样使用:
cpp
Text {
text: counter.count // 读
}
Button {
onClicked: counter.count++ // 写(自动调 setCount)
}
Connections {
target: counter
onCountChanged: console.log("C++ 通知 QML 值变了") // 通知
}
注意!!!
Counter 中的实际成员变量是m_count,它和Q_PROPERTY中的count并不是同一个东西。
cpp
Q_PROPERTY(int count READ count WRITE setCount NOTIFY countChanged)
这句宏属性的定义,其实是凭空虚构一个叫count的属性,为其附上基本的读、写、通知(也就是信号)的方法。我们是在这些方法的具体实现中,间接链接上了m_count的。
cpp
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
cpp
#ifndef MOVIE_H
#define MOVIE_H
#include <QObject>
class Movie : public QObject
{
Q_OBJECT
Q_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
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代码:
cpp
import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.12
Window {
visible: true
width: 640
height: 480
title: qsTr("QPROPERTY Mappings")
Connections{
target: Movie
onMainCharacterChanged:{
console.log("onMainCharacterChanged"+Movie.mainCharacter);
}
onTitleChanged:{
console.log("onTitleChanged"+Movie.title);
}
}
Column{
spacing: 20
Text {
id: titleId
text: Movie === null ? "" : Movie.title
font.pointSize: 20
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
id: mainCharId
text: Movie === null ? "" : Movie.mainCharacter
font.pointSize: 20
anchors.horizontalCenter: parent.horizontalCenter
}
Row{
anchors.horizontalCenter: parent.horizontalCenter
TextField{
id: titleTextFieldId
width: 300
}
Button{
width: 200
id: button1
text : "Change title"
onClicked: {
Movie.title = titleTextFieldId.text
}
}
}
Row{
anchors.horizontalCenter: parent.horizontalCenter
TextField{
id: mainCharTextFieldId
width: 300
}
Button{
width: 200
id: button2
text : "Change main character"
onClicked: {
Movie.mainCharacter = mainCharTextFieldId.text
}
}
}
}
}
运行界面:
这个例子的功能就是可以在qml界面中分别修改标题和主演,然后更新到上方的两个text文本中。
细节说明:
1.movie头文件中的Q_PROPERTY宏
cpp
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.属性绑定
cpp
Text {
id: titleId
text: Movie === null ? "" : Movie.title
}
这里实际上是拿Movie.title来进行属性绑定了,那title具体是啥呢?就是我们定义Q_PROPERTY的名字title。这里它如何能够感知到title产生了变化,并作用到text中?就是靠NOTIFY titleChanged这个信号。所以,我们如果期望能实现没有bug的属性绑定,一定要注意当c++中的m_title发生变化的时候,要手动发送一遍titleChanged信号。
3.读取和修改
读取:
cpp
onTitleChanged:{
console.log("onTitleChanged"+Movie.title);
}
修改:
cpp
Movie.title = titleTextFieldId.text
这里的读取和修改看似是对一个变量进行操作,实际上会调用到定义Q_PROPERTY时的READ和WRITE接口,也就是:
cpp
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定义的格式:
cpp
Q_PROPERTY(
type name // 属性类型和名字
READ getter // 读函数
[WRITE setter] // 可选:写函数
[RESET resetFn] // 可选:恢复默认值
[NOTIFY signal] // 可选:变化信号
[CONSTANT] // 可选:永不变,无 NOTIFY
[FINAL] // 可选:禁止 QML 重写
)