奇牛Qt04 属性系统与动态属性 —— 奇牛+Gemini
奇牛Qt04 属性系统与动态属性 —— 奇牛+Gemini
1. C++ 对属性的常规封装
C++ 原生语法中并没有统一的 property 关键字。传统的标准处理方法是通过访问修饰符与显式的成员函数来模拟封装:
- 私有数据成员 (private):防止外部直接破坏对象内部状态。
- 读方法 (getter):一个
public函数,通常带const属性,用于返回内部变量的值。 - 写方法 (setter):一个
public函数,用于接收新值并修改内部变量,同时可以进行合法性检查。
效果与局限:
通过特定接口函数名来实现对属性的读写,隐藏了内部实现。但这需要为每个属性手动编写大量的样板代码(Boilerplate Code),且无法在运行时仅凭“属性名字符串”去动态查找和操作它们。
2. Qt 属性系统 (Qt Property System) 概述
Qt 框架提供了一个独立于编译器的属性系统,它基于元对象系统 (Meta-Object System),底层由 moc (Meta-Object Compiler) 支持。
核心原理:元对象系统的本质是编译器在编译时自动生成了查表代码,从而使原本静态的 C++ 对象在运行时能够通过查表实现信号通信和自我识别。
2.1 Qt 属性机制的核心效果
基于 C 字符串的统一访问:调用者无需关注具体的 get 和 set 方法名,只需提供属性名(以 C 字符串形式传递),即可通过 QObject 提供的统一接口实现对属性的读写。
QObject::property("属性名")和QObject::setProperty("属性名", 值)
主要优势:为自动化工具(如 Qt Designer)、脚本绑定(如 QML)以及反射机制提供了底层支持。
2.2 静态属性与动态属性的概念区分
1. 静态属性(通过 Q_PROPERTY 宏定义的属性)
- 定义(元数据)是类级别的:静态属性是在编译时确定的。它的名字、类型、读写函数(READ/WRITE)以及信号(NOTIFY)等信息都存储在该类的元对象(
QMetaObject)中。因为同一个类的所有实例共享同一个元对象,所以从“定义”上来说,它是类级别的。 - 值是对象级别的(绝大多数情况下):虽然属性的定义属于类,但属性所绑定的底层 C++ 变量(通常是类的成员变量)是每个对象实例各自拥有的。例如,
QWidget的width属性是在类级别定义的,但每一个独立的QWidget实例都有自己的宽度值。
2. 动态属性(通过 setProperty() 运行时添加的属性)
- 完全是对象级别的:如果在调用
QObject::setProperty(name, value)时,name这个属性在类的元对象(QMetaObject)中不存在,Qt 就会将它作为“动态属性”添加到当前的这个对象实例中。 - 不影响类和其他实例:动态属性及其对应的值完全保存在这个特定的
QObject实例内部的动态属性列表中。同一个类的其他实例并不会因为你给某个对象添加了动态属性而获得这个属性。
| 属性类型 | 声明方式 | 确定时机 | 特性与存储位置 |
|---|---|---|---|
| 静态属性 (Static Properties) | 在类声明中通过 Q_PROPERTY() 宏显式声明 |
编译期 | 元数据保存在由 Q_OBJECT 宏生成的元对象数据中,需提前定义。 |
| 动态属性 (Dynamic Properties) | 在程序运行期间,通过调用 setProperty() 直接追加 |
运行期 | 直接附加在某个具体的对象实例上,不需要提前在类中声明。 |
3. 静态属性 (Q_PROPERTY) 的几种常见用法
3.1 添加静态属性
-
1. 常规可读写属性 (
READ+WRITE+NOTIFY)最标准的写法。属性改变时会触发信号,完美支持 UI 联动。
Q_PROPERTY(属性类型 属性名 READ 读方法函数名 WRITE 写方法函数名 NOTIFY 信号函数名) -
2. 只读属性 (省略
WRITE,仅保留READ,可能有[CONSTANT])Q_PROPERTY(属性类型 属性名 READ 读方法函数名 [CONSTANT]) // 如果类中对应的变量时const的,则需要加上CONSTANT -
3. 最简属性声明 (使用
MEMBER映射)直接将属性映射到 private 变量上,省去
READ/WRITE函数的编写。Q_PROPERTY(属性类型 属性名 MEMBER 绑定变量) // e.g. Q_PROPERTY(int oil MEMBER m_oil)注:直接将属性映射到 private 变量上,由 Qt 自动接管读写逻辑。虽然省去了手写 READ/WRITE 函数,但为了让 UI 能够刷新,强烈建议依然加上 NOTIFY 信号(变量改变时需手动 emit 该信号)。
Q_PROPERTY(属性类型 属性名 MEMBER 绑定变量 NOTIFY 信号) // 示例:Q_PROPERTY(int oil MEMBER m_oil NOTIFY oilChanged)
3.2 静态属性及其相关方法的使用
3.2.1方法一:通过标准的 C++ 成员函数访问(性能最高)
如果在声明属性时写了 READ 和 WRITE 函数,或者直接访问 MEMBER 绑定的变量,可以像调用普通 C++ 函数一样使用它们。
// 假设有一个包含 oil 属性的对象指针 car
int currentOil = car->getOil(); // 调用 READ 函数
car->setOil(50); // 调用 WRITE 函数
3.2.2 方法二:通过元对象系统动态访问(Qt反射机制)
这是属性机制的精髓。即使不知道这个对象的具体类型(只知道它是 QObject*),也可以通过属性的字符串名称来读写它。这在做自动化框架、序列化、动态配置时极度有用。
- 读取属性:
QObject::property("属性名")-> 返回QVariant - 写入属性:
QObject::setProperty("属性名", 值)-> 返回bool
QObject *object = myWidget; // 模糊类型,只知道是 QObject*
// 1. 动态读取属性
QVariant val = object->property("oil");
if (val.isValid())
{
int oilV = val.toInt(); // 转换为具体类型
}
// 2. 动态修改属性
// 返回值自己定义,不一定一直都是bool
bool success = object->setProperty("oil", 80);
if (success)
{
// 修改成功
}
| 访问方式 | 调用示例 | 性能 | 适用场景 |
|---|---|---|---|
| 方式一:标准 C++ 成员函数 | car->setOil(50); |
最高 | 明确已知对象类型及接口的常规 C++ 开发。 |
| 方式二:元对象系统动态访问 | object->setProperty("oil", 50); |
较低 | 类型模糊(仅知是 QObject*),用于自动化框架、序列化、动态配置。 |
QObject::setProperty()是一个普通的非虚成员函数。QObject::setProperty()的“多态”本质上是借助 Qt 元对象系统,将统一的字符串访问动态路由到 MOC 生成的底层虚函数qt_metacall()(处理静态属性)或对象内部的哈希字典(处理动态属性),从而实现了与具体 C++ 类型的彻底解耦。
3.3 静态属性 (Q_PROPERTY) 示例代码
1. 常用用法:常规可读写属性(READ+WRITE+NOTIFY)
以下是一个完整的可读写属性示例。通过在 setter 中主动触发 NOTIFY 信号,使得外界能够感知属性变化。
student.h:
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject
{
Q_OBJECT
Q_PROPERTY(QString hobby READ hobby WRITE setHobby NOTIFY hobbyChanged)
public:
explicit Student(QObject *parent = nullptr);
public:
QString hobby() const;
void setHobby(QString newHobby);
signals:
void hobbyChanged(QString newHobby);
private:
QString m_hobby;
};
#endif // STUDENT_H
student.cpp:
#include "student.h"
Student::Student(QObject *parent) : QObject(parent) {}
QString Student::hobby() const { return m_hobby; }
void Student::setHobby(QString newHobby)
{
m_hobby = newHobby;
emit hobbyChanged(newHobby);
}
mainwindow.cpp中构造函数:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QObject* stu = new Student(this);
// 使用 qobject_cast 安全地向下转型
connect(qobject_cast<Student*>(stu), &Student::hobbyChanged, this, [](QString newHobby)
{
qDebug() << newHobby;
});
stu->setProperty("hobby", "watching anime");
}
2. 只读属性(READ+[CONSTANT])
在 Q_PROPERTY 宏中省略 WRITE 声明,属性即变为只读。外部若尝试使用 setProperty 强行写操作将会失败(返回 false)。
car.h:
#ifndef CAR_H
#define CAR_H
#include <QObject>
class Car : public QObject
{
Q_OBJECT
Q_PROPERTY(QString brand READ brand CONSTANT)
public:
explicit Car(QString brand, QObject *parent = nullptr);
public:
QString brand() const;
private:
const QString m_brand;
};
#endif // CAR_H
car.cpp:
#include "car.h"
Car::Car(QString brand, QObject *parent)
: QObject(parent)
, m_brand(brand)
{}
QString Car::brand() const { return m_brand; }
mainwindow.cpp的构造函数:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QObject* car = new Car("Mercedes");
qDebug() << car->property("brand");
// 输出:QVariant(QString, "Mercedes")
}
3. 简化案例:最简属性声明 (MEMBER+NOTIFY)
MEMBER 使 Qt 属性系统直接接管私有成员变量。当外部调用 setProperty() 或 property() 时,底层自动生成读写逻辑并直接操作绑定的变量。
character.h:
#ifndef CHARACTER_H
#define CHARACTER_H
#include <QObject>
class Character : public QObject
{
Q_OBJECT
// 直接绑定私有变量 m_hp 和 m_mp,结合 NOTIFY 实现自动发射信号
Q_PROPERTY(int hp MEMBER m_hp NOTIFY hpChanged)
Q_PROPERTY(int mp MEMBER m_mp NOTIFY mpChanged)
public:
explicit Character(int hp = 100, int mp = 100, QObject *parent = nullptr);
public:
// 内部业务逻辑
void castSpell(const QString& spellName);
void takeDamage(int damage);
signals:
void hpChanged(int newHp);
void mpChanged(int newMp);
private:
int m_hp;
int m_mp;
};
#endif // CHARACTER_H
character.cpp:
#include "character.h"
#include <QDebug>
#include <QString>
Character::Character(int hp, int mp, QObject *parent)
: QObject(parent)
, m_hp(hp)
, m_mp(mp)
{}
void Character::castSpell(const QString& spellName)
{
if(m_mp < 20)
{
qDebug() << "MP 不足!";
return;
}
// 在类的内部业务函数中直接修改变量时,需要手动发射信号
m_mp-= 20; // 内部直接操作变量
emit mpChanged(m_mp); // 必须手动发射信号,通知外部 MP 已改变
}
void Character::takeDamage(int damage)
{
m_hp -= damage;
if(m_hp < 0) m_hp = 0;
// 内部直接操作变量,需手动发射信号
emit hpChanged(m_hp);
}
mainwindow.cpp构造函数:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
Character* reimu = new Character;
reimu->setObjectName("博丽灵梦");
// 注册信号槽
connect(reimu, &Character::hpChanged, this, [](int newHp)
{
qDebug() << "信号通知:HP 改变,当前值为:" << newHp;
});
connect(reimu, &Character::mpChanged, this, [](int newMp)
{
qDebug() << "信号通知:MP 改变,当前值为:" << newMp;
});
// === 场景一:外部利用标准属性接口【写】数据 ===
// 此时 Qt 底层直接修改了 m_hp 和 m_mp
// 并且因为声明了 NOTIFY, Qt 底层会自动触发 hpChanged 和 mpChanged 信号
reimu->setProperty("hp", 85);
reimu->setProperty("mp", 40);
/*
* 输出:
* 信号通知:HP 改变,当前值为: 85
* 信号通知:MP 改变,当前值为: 40
*/
// === 场景二:外部利用标准属性接口【读】数据 ===
// 底层直接读取变量值并包装成 QVariant 返回
int curHp = reimu->property("hp").toInt();
int curMp = reimu->property("mp").toInt();
qDebug() << QString("通过属性系统读取:Hp = %1, Mp = %2")
.arg(curHp)
.arg(curMp);
/*
* 输出:
* "通过属性系统读取:Hp = 85, Mp = 40"
*/
// === 场景三:调用业务函数检验修改结果 ===
// 调用内部函数,内部会直接去读取、修改变量并手动 emit 信号
reimu->castSpell("梦想封印"); // MP 原被改为 40,消耗 20,触发 mpChanged(20)
reimu->takeDamage(30); // HP 原被改为 85,承受 30 伤害,触发 hpChanged(55)
/*
* 输出:
* 信号通知:MP 改变,当前值为: 20
* 信号通知:HP 改变,当前值为: 55
*/
}
4. 运行时动态添加属性 (Dynamic Properties)
4.1 添加/写入动态属性
函数原型:bool QObject::setProperty(const char *name, const QVariant &value)
使用格式:
对象指针->setProperty("目标属性名", 属性值);
在 Qt 的官方设计中,只要操作的是动态属性(即没有通过
Q_PROPERTY宏在QMetaObject中静态注册的属性),setProperty永远只会返回false。
运行机制详解:
当执行该代码时,Qt 底层会进行如下逻辑判断:
- 优先在类中查找是否存使用
Q_PROPERTY声明过的同名静态属性。 - 若存在,则调用对应的
WRITE函数或直接修改MEMBER变量(此时作为静态属性的修改器)。 - 若不存在,则触发动态属性机制,Qt 会直接创建/修改动态属性。
QObject::setProperty("myProp", value)
│
▼
【1. 查找元对象 (QMetaObject) 系统】
检查是否存在同名的 Q_PROPERTY?
│
┌──────────────────┴──────────────────┐
▼ ▼
【存在】 【不存在】
(静态属性分支) (动态属性分支)
│ │
▼ ▼
是否有 WRITE 函数或 MEMBER? 【2. 检查 value 是否有效】
│ (value.isValid()?)
┌────────┴────────┐ │
▼ ▼ ┌───────┴───────┐
【有】 【无】 ▼ ▼
│ │ 【有效】 【无效】
▼ ▼ │ │
【修改静态属性】 【修改失败】 ▼ ▼
调用写函数或修 (该属性为只读) 【新增/修改】 【删除属性】
改成员变量值 存入/更新内部动 从动态属性表
│ │ 态属性字典中 中移除该属性
▼ ▼ │ │
返回 true (成功) 返回 false ▼ ▼
返回 false 返回 false
4.2 读取动态属性
函数原型:QVariant QObject::property(const char *name) const
使用格式:
QVariant 变量名 = 对象指针->property("目标属性名");
使用注意点(重要):
由于动态属性的类型在编译时具有不确定性,因此 property() 函数的返回值是万能包装类型 QVariant。获取返回值后,必须手动转换为真实的 C++ 数据类型(如 toString(), toInt() 等)方可正常使用。
4.3 动态属性示例代码
新建一个类SimpleCar,没有任何静态属性
simplecar.h:
#ifndef SIMPLECAR_H
#define SIMPLECAR_H
#include <QObject>
class SimpleCar : public QObject
{
Q_OBJECT
public:
explicit SimpleCar(QObject *parent = nullptr);
signals:
};
#endif // SIMPLECAR_H
mainwindow.cpp 构造函数:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
SimpleCar* sc = new SimpleCar();
// ========================================================
// 1. 动态属性的【创建/写入】
// ========================================================
// 简单汽车类未定义 "color" , "maxSpeed", "price"等, Qt 会自动在运行时为 myCar 独家创建动态属性
sc->setProperty("color", "black");
sc->setProperty("maxSpeed", 500);
sc->setProperty("price", 1000000.5);
// ========================================================
// 2. 动态属性的【读取与类型转换】
// ========================================================
// 注意:property() 返回 QVariant,需调用 .toString(), .toInt() 等转换为目标类型
QString carColor = sc->property("color").toString();
int carMaxSpeed = sc->property("maxSpeed").toInt();
double carPrice = sc->property("price").toDouble();
qDebug() << QString("颜色%1, 最大素的%2, 价格$%3")
.arg(carColor)
.arg(carMaxSpeed)
// 0总宽度不限,'f'强制使用标准浮点格式,1保留1位小数
.arg(carPrice, 0, 'f', 1);
// 输出:"颜色black, 最大素的500, 价格$1000000.5"
// ========================================================
// 3. 动态属性的【覆盖修改】
// ========================================================
// 若再次对同名属性写入,发现该动态属性已存在,将直接覆盖原有值
sc->setProperty("color", "white");
qDebug() << "动态属性修改成功,\"color\" 变为" << sc->property("color");
// 输出:动态属性修改成功,"color" 变为 QVariant(QString, "white")
}
5. 属性的反射与遍历 (QMetaObject 与 dynamicPropertyNames)
Qt 属性系统的核心优势在于支持反射 (Reflection)。
反射的核心概念:程序在运行期间,能够动态查询对象所具备的属性与方法。
常规编码中,调用依赖于已知对象的确定属性(如
obj->hp())。而在反射机制下,对于未知结构的对象,可动态请求其属性清单并进行读写操作。
针对静态属性和动态属性,Qt 提供了两种不同的查询方式:
- 静态属性的遍历:使用类唯一的元对象
QMetaObject。该对象在编译期生成并写入底层代码。 - 动态属性的遍历:使用对象自带的函数
dynamicPropertyNames()。仅获取运行期间临时注入的属性。
5.1 静态属性的遍历
1.获取QMetaObject对象 -> 2.获取QMetaProperty对象 -> 3.访问属性信息
5.1.1 静态属性遍历涉及的核心类
1. QMetaObject (元对象类)
QMetaObject 是 MOC(元对象编译器)在后台自动生成的类。脱离具体对象实例,一个类全局仅存在一份元对象,可视为该类的“元数据设计图纸”。
并非整个 Qt 程序仅有一个
QMetaObject实例,而是“每个使用了Q_OBJECT宏的具体类,均各自拥有一份全局唯一的QMetaObject”。
获取元对象:通过多态 对象指针->metaObject(),在运行时获取指向其实际派生类元数据的常量指针。
// metaObject()为虚函数,所以可以实现多态
const QMetaObject *meta = 对象指针->metaObject();
该虚函数的重写由 Qt 框架自动生成。构建(Build)时:
- MOC 扫描:若检测到
Q_OBJECT,则生成moc_自定义类名.cpp,包含metaObject()的重写实现。 - C++ 预处理:
Q_OBJECT宏展开,在头文件中加入metaObject()的重写声明。 - C++ 编译与链接:编译器将自定义源码与 MOC 生成的代码编译并链接。
QMetaObject 核心 API:
-
int QMetaObject::propertyCount() const;int count = 元对象指针->propertyCount();- 作用:获取记录的静态属性总数量。注意,该数量包含从父类(如
QObject)继承的所有属性。 - 使用场景:通常作为
for循环遍历属性时的上限边界。
- 作用:获取记录的静态属性总数量。注意,该数量包含从父类(如
-
int QMetaObject::propertyOffset() const;// i 不从 0 开始,从 offset 开始以跳过父类属性 for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { // ... }- 作用:获取当前派生类自身新增的第一个属性的索引偏移量。
- 使用场景:仅需遍历当前派生类特有属性,忽略父类属性(如
QObject的objectName)时,将循环起点设为此值。
-
QMetaProperty QMetaObject::property(int index) const;QMetaProperty prop = meta->property(i);- 作用:根据索引号
index,提取对应的单条属性档案。 - 使用场景:反射遍历中的核心桥梁,将索引转换为具体属性操作对象。
- 作用:根据索引号
-
int QMetaObject::indexOfProperty(const char *name) const;int index = meta->indexOfProperty("spellCardCount");- 作用:反向查询。已知属性名时,传入 C 字符串,返回属性在元对象中的索引号(未找到返回
-1)。
- 作用:反向查询。已知属性名时,传入 C 字符串,返回属性在元对象中的索引号(未找到返回
2. QMetaProperty (元属性类)
QMetaProperty 代表单条属性的“规则档案卡”。该类绝对不保存具体的属性数据值,仅记录 Q_PROPERTY 宏制定的各项规则(如数据类型、名称、读写函数地址等)。
获取
QMetaProperty实例相当于获得“操作说明书”。读取真实数据时,必须将具体的对象实例与该说明书结合,底层根据地址动态调用READ或WRITE函数。
获取元属性:通常配合 QMetaObject::property(int index) 方法按索引提取。
QMetaProperty 元属性对象 = meta->property(i);
注意区分:
QMetaObject::property()和QObject::property()是两个不同的函数。
QMetaProperty 核心 API:
分为查询属性信息(查规则)与操作具体数据(读写值)两大类。
【查询属性信息】(仅查看规则,无需对象实例):
-
const char* QMetaProperty::name() const;- 作用:获取属性名称。
-
const char* QMetaProperty::typeName() const;- 作用:获取数据类型名称。常用于动态处理前判断
QVariant需转换为的目标类型。
- 作用:获取数据类型名称。常用于动态处理前判断
-
bool QMetaProperty::isReadable() const;和bool QMetaProperty::isWritable() const;-
作用:查询是否配置了
READ(可读)或WRITE(可写)方法。 -
使用场景:动态修改前进行安全校验。
-
【操作具体数据】(必须传入具体对象指针):
QMetaObject实例本身是static的,导致QMetaProperty并不属于某个具体的“对象(Instance)”,而是属于“类(Class)”。所以在操作具体数据的时候必须传入对象。
QVariant QMetaProperty::read(const QObject *obj) const;- 作用:核心读值方法。自动调用传入对象的
READ函数,将返回值打包进QVariant返回。
- 作用:核心读值方法。自动调用传入对象的
bool QMetaProperty::write(QObject *obj, const QVariant &value) const;- 作用:核心写值方法。自动调用传入对象的
WRITE函数进行赋值(成功返回true)。
- 作用:核心写值方法。自动调用传入对象的
5.1.2 遍历静态属性的格式
静态属性由 Q_PROPERTY 宏在类定义中声明,依赖 QMetaObject 进行遍历。
// 1. 获取元数据指针(类的设计图纸)
const QMetaObject *metaObj = 对象指针->metaObject();
// 2. 按照索引遍历属性
int count = metaObj->propertyCount();
for (int i = 0; i < count; i++)
{
// 3. 按索引提取属性元数据
QMetaProperty mp = metaObj->property(i);
// 4. 获取属性名称
const char* propName = mp.name();
// 5. 获取对象实例中该属性的具体值
QVariant value = 对象指针->property(propName);
}
5.2 动态属性的遍历
5.2.1 动态属性遍历涉及的核心类
1.获取QList<QByteAttar>对象 -> 2.获取QByteArray对象 -> 3.获取c_str名称并访问属性信息
1. QList (列表容器类)
在 Qt 5 中,QList 是一种基于特定需求设计的“数组指针混合体”,平衡插入与随机访问性能。在 Qt 6 中,QList 与 QVector 已合并(QVector 为 QList 的别名)。
使用场景:QObject::dynamicPropertyNames() 返回 QList<QByteArray>,封装所有查询到的动态属性名。
核心 API:
int QList::size() const;(获取元素总个数)。const T & QList::at(int i) const;(获取索引i处的元素,带越界断言检查,安全性高于operator[])。
2. QByteArray (字节数组类)
用于存储原始字节序列或纯 ASCII 字符串,可视为 C 语言 char* 的高级封装版本。
动态属性名通常仅含英文字母、数字和下划线。对于纯 ASCII 标识符,Qt 选用轻量级的
QByteArray存储,以符合底层执行效率。
核心 API:
const char * QByteArray::constData() const;- 作用:返回指向存储数据的常量字符指针 (
const char *),便于与 C 风格 API 交互。
- 作用:返回指向存储数据的常量字符指针 (
5.2.2 动态属性的遍历格式
// 1. 获取对象实例的动态属性名称清单
QList<QByteArray> dynamicNames = 对象指针->dynamicPropertyNames();
// 2. 遍历名称清单
for (int i = 0; i < dynamicNames.size(); ++i)
{
// 3. 提取单个动态属性名称 (QByteArray 类型)
QByteArray nameByteArray = dynamicNames.at(i);
// 4. 转换为底层 C 字符串 (const char*)
const char *propName = nameByteArray.constData();
// 5. 通过名称直接读取动态属性的具体值
QVariant value = 对象指针->property(propName);
}
5.3 属性遍历示例代码(动态+静态)
使用之前的Character类
mainwindow.cpp中的构造函数:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 实例化对象 + 绑定信号槽
Character* reimu = new Character(100, 100);
reimu->setObjectName("博丽灵梦");
connect(reimu, &Character::hpChanged, this, [](int newHp)
{
qDebug() << "[信号触发] HP 发生改变, 当前 HP: " << newHp;
});
connect(reimu, &Character::mpChanged, this, [](int newMp)
{
qDebug() << " [信号槽触发] MP 发生了改变,当前 MP:" << newMp;
});
// 运行期添加动态属性
reimu->setProperty("address", "博丽神社");
reimu->setProperty("weapon", "御币");
// ========================================================
// 阶段一:静态属性的深度反射(边界过滤与安全读写)
// ========================================================
qDebug() << "\n=== 阶段一:静态属性深度遍历与操作 ===";
const QMetaObject* metaObj = reimu->metaObject();
for(int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); i++)
{
// 提取单条属性档案
QMetaProperty metaProp = metaObj->property(i);
// 读取规则
const char* propName = metaProp.name();
const char* propType = metaProp.typeName();
// 读取数据并输出
QVariant pValue = metaProp.read(reimu);
qDebug() << QString("静态属性[%1]: 名称-%2, 类型-%3, 当前值-%4")
.arg(i)
.arg(propName)
.arg(propType)
.arg(pValue.toString());
// 如果遇到hp, 且hp可以被修改-->则修改
if(qstrcmp(propName, "hp") && metaProp.isWritable())
{
qDebug() << " !!! 通过反射接口将 hp 修改为 999 !!!";
// 使用 write 修改绑定了 NOTIFY 的属性时,Qt 底层会自动触发 hpChanged 信号
metaProp.write(reimu, 999);
}
}
/*
* "静态属性[1]: 名称-hp, 类型-int, 当前值-100"
* "静态属性[2]: 名称-mp, 类型-int, 当前值-100"
* !!! 通过反射接口将 hp 修改为 999 !!!
* [信号槽触发] MP 发生了改变,当前 MP: 999
*/
// ========================================================
// 阶段二:静态属性的反向定位查找
// ========================================================
qDebug() << "\n=== 阶段二:反向定位指定静态属性 ===";
// 已知属性名直接查询图纸获取索引
int targetIndex = metaObj->indexOfProperty("mp");
if(targetIndex != -1)
{
QMetaProperty targetProp = metaObj->property(targetIndex);
qDebug() << QString("通过字符串 'mp' 成功反向查找到该属性!")
<< QString("索引位置:%1,当前真实值:%2")
.arg(targetIndex)
.arg(targetProp.read(reimu).toInt());
}
/*
* 输出:
* "通过字符串 'mp' 成功反向查找到该属性!" "索引位置:2,当前真实值:999"
*/
// ========================================================
// 阶段三:动态属性的安全遍历与底层类型转换
// ========================================================
qDebug() << "\n=== 阶段三:动态属性遍历 ===";
// 提取当前对象专属动态属性清单
QList<QByteArray> dynamicProps = reimu->dynamicPropertyNames();
for(int i = 0; i < dynamicProps.size(); i++)
{
// 边界安全提取
QByteArray nameByteArray = dynamicProps.at(i);
// 转换为 c_str
const char* dynamicName = nameByteArray.constData();
// 直接调用 QObject::property() 提取数据
QVariant dynamicValue = reimu->property(dynamicName);
qDebug() << QString("动态属性 | 名称: %1 | 当前值: %2")
.arg(dynamicName)
.arg(dynamicValue.toString());
/*
* 输出:
* === 阶段三:动态属性遍历 ===
* "动态属性 | 名称: address | 当前值: 博丽神社"
* "动态属性 | 名称: weapon | 当前值: 御币"
*/
}
6. 静态属性与动态属性的对比总结
| 对比维度 | 静态属性 (Static Properties) | 动态属性 (Dynamic Properties) |
|---|---|---|
| 定义时机 | 编译期确定。编码时通过 Q_PROPERTY 声明在头文件中。 |
运行期确定。程序运行期间随时追加。 |
| 定义方式 | 类内部声明:Q_PROPERTY(int hp MEMBER m_hp)。 |
运行时调用:setProperty("新名字", 值)。 |
| 遍历方式 | 需通过元对象查表:metaObject()->property(i)。 |
直接获取列表:dynamicPropertyNames()。 |
| 数据存储 | 结构信息存储于该类唯一的公共元对象存储区。 | 数据值存储于该对象实例单独维护的内部字典(Map)中。 |
| 通用特性 | 对外的读写接口完全一致:均支持传入属性名字符串,通过 property() 读取、setProperty() 写入。 |
同左。 |

浙公网安备 33010602011771号