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 重写
)
相关推荐
_F_y2 小时前
C++11拓展语法
c++
_OP_CHEN2 小时前
从零开始的Qt开发指南:(三)信号与槽的概念与使用
开发语言·c++·qt·前端开发·qt creator·信号与槽·gui开发
乄夜2 小时前
嵌入式面试高频!!!C语言(十四) STL(嵌入式八股文)
c语言·c++·stm32·单片机·mcu·面试·51单片机
草莓熊Lotso3 小时前
《算法闯关指南:优选算法--位运算》--38.消失的两个数字
服务器·c++·算法·1024程序员节
Pluchon4 小时前
硅基计划6.0 伍 JavaEE 网络原理
网络·网络协议·学习·tcp/ip·udp·java-ee·信息与通信
paopao_wu4 小时前
DeepSeek-OCR实战(03):本地部署+简单UI测试
ui·ocr
时光不去7 小时前
java接口自动化之allure本地生成报告
运维·笔记·自动化
杨浦老苏8 小时前
简单直观的笔记管理器Poznote
笔记·docker·群晖
Molesidy10 小时前
【VSCode】【Clangd】Win下的基于LLVM/Clangd+Clangd插件+MINGW+CMake的VSCode配置C/C++开发环境的详细教程
c++·ide·vscode·clangd·llvm
椰壳也可10 小时前
06_作业基于CubeMx实现按键控制LED灯(裸机)(立芯嵌入式笔记)
笔记·stm32·学习