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 侧就能像普通变量一样用。

语法结构如下:

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 重写
)
相关推荐
JJJJ_iii5 小时前
【机器学习07】 激活函数精讲、Softmax多分类与优化器进阶
人工智能·笔记·python·算法·机器学习·分类·线性回归
再睡一夏就好5 小时前
【C++闯关笔记】深究继承
java·数据结构·c++·stl·学习笔记
新子y5 小时前
【小白笔记】最大化安全评分
笔记
新子y6 小时前
【小白笔记】关于 Python 类、初始化以及 PyTorch 数据处理的问题
pytorch·笔记·python
mjhcsp6 小时前
C++ char 类型深度解析:字符与字节的双重身份
开发语言·c++·char
光影少年6 小时前
网络安全生态及学习路线
学习·安全·web安全
江公望6 小时前
Qt enum ApplicationAttribute枚举值浅解
linux·qt
友友马6 小时前
『 QT 』信号-槽 补充: Qt信号槽断开连接与Lambda槽技巧
开发语言·数据库·qt
Source.Liu6 小时前
【CMakeLists.txt】 Qt 自动化构建配置详解
qt·自动化·librecad