Qt对象模型与容器类
Qt 对象模型与容器类
[TOC]
1 对象模型
普通 C++ 类一般只有:
1 | 成员变量 |
Qt 在 C++ 基础上又加了一些特性,比如:
1 | 信号和槽 |
QObject 是 Qt 对象模型的核心。QWidget 继承自 QObject,所以 QWidget 也拥有 QObject 的能力。
QObject:Qt 对象能力QWidget:Qt 对象能力 + 图形界面能力
1.1 objectName
每个 QObject 都可以有对象名:
1 | button->setObjectName("saveButton"); |
获取对象名:
1 | QString name = button->objectName(); |
常用函数:
1 | void setObjectName(const QString &name); |
这个对象名不是变量名。
比如:
1 | QPushButton *button = new QPushButton("保存", this); |
button:C++ 变量名saveButton:Qt 对象名
对象名常用于:
1 | 样式表选择器 |
例如样式表:
1 | button->setObjectName("saveButton"); |
1.2 findChild
因为 QObject 有对象树,所以可以从父对象里查找子对象。
例如:
1 | QPushButton *button = this->findChild<QPushButton *>("saveButton"); |
意思是:在 this 的子对象里,查找 objectName 是 saveButton 的 QPushButton。
常用:
1 | T findChild(const QString &name = QString()) const; |
例子:
1 | QList<QLineEdit *> edits = this->findChildren<QLineEdit *>(); |
意思是:找到 this 下面所有 QLineEdit 子对象。
不过实际项目里,不要过度依赖 findChild()。自己有成员变量时,直接用成员变量更清楚:
1 | private: |
1.3 Q_OBJECT 宏
1 | class MyWidget : public QWidget |
Q_OBJECT 是一个宏。它不是普通变量,也不是函数。
它的作用是:告诉 Qt,这个类需要使用 Qt 的元对象能力,比如:
1 | 自定义信号 |
尤其是自己写信号时,必须写:
1 | Q_OBJECT |
例如:
1 | class ChatWindow : public QWidget |
没有 Q_OBJECT,自定义信号槽、属性系统等元对象相关功能会出问题。
2 信号和槽
信号和槽用于两个对象之间的通信,是Qt框架的核心特征。
2.1 概述
1 | 信号:某件事发生了 |
例如:
1 | 按钮被点击了 |
代码:
1 | connect(button, &QPushButton::clicked, this, &MyWidget::onButtonClicked); |
含义:
1 | 当 button 发出 clicked 信号时, |
2.2 connect 基本公式
connect() 的返回值类型是 QMetaObject::Connection,可以保存下来用于之后断开连接。
旧式字符串写法可以简化理解为:
1 | QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection); |
现代 Qt 更推荐成员函数指针写法,可以简化理解为:
1 | QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection); |
最常见形式:
1 | connect(发送者, 信号, 接收者, 槽函数); |
例如:
1 | connect(button, &QPushButton::clicked, this, &MyWidget::save); |
拆开看:
1 | button |
发送者。谁发信号。
1 | &QPushButton::clicked |
信号。发什么信号。
1 | this |
接收者。谁来响应。
1 | &MyWidget::save |
槽函数。响应时调用哪个函数。
整体意思:button 被点击时,调用 this->save()。
- 例子
1 |
|
这里 save() 是普通成员函数,也可以作为槽函数使用。
2.3 信号和槽的参数
信号可以带参数。
比如 QLineEdit::textChanged 信号会带一个参数:
1 | void textChanged(const QString &text); |
所以可以这样连接:
1 | connect(edit, &QLineEdit::textChanged, this, &MyWidget::onTextChanged); |
槽函数:
1 | void MyWidget::onTextChanged(const QString &text) |
整体意思:输入框文本变化时,Qt 自动把新文本传给 onTextChanged。
信号有参数时,槽函数可以接收这些参数。
例如:
1 | connect(slider, &QSlider::valueChanged, this, &MyWidget::setVolume); |
如果 valueChanged 是:
1 | void valueChanged(int value); |
那么槽函数可以是:
1 | void MyWidget::setVolume(int value) |
参数类型要能对应上。
2.4 Lambda 表达式
信号槽经常配合 lambda 使用。
例如:
1 | connect(button, &QPushButton::clicked, this, [=]() { |
意思是:
1 | 按钮被点击时,直接执行大括号里的代码。 |
这适合处理很短的逻辑。
比如:
1 | connect(closeButton, &QPushButton::clicked, this, [=]() { |
但是逻辑复杂时,更推荐写成成员函数:
1 | connect(saveButton, &QPushButton::clicked, this, &MyWidget::saveSettings); |
2.5 自定义信号
可以在自己的类里定义信号。
例如聊天窗口发送消息:
1 | class ChatInput : public QWidget |
注意:
1 | signals: |
下面声明的是信号。
信号只需要声明,不需要你写函数体。
也就是说,写:
1 | signals: |
不要写成:
1 | void ChatInput::messageSubmitted(const QString &text) |
信号的实现由 Qt 的 moc 工具生成。
2.6 发射信号 emit
定义了信号之后,可以在类内部发射信号:
1 | void ChatInput::submit() |
这里:
1 | emit messageSubmitted(text); |
意思是:
1 | 告诉外部:用户提交了一条消息,内容是 text。 |
然后别的对象可以连接它:
1 | connect(chatInput, &ChatInput::messageSubmitted, this, &ChatWindow::sendMessage); |
整体流程:
1 | 用户点击发送按钮 |
- 例子
1 |
|
另一个窗口使用它:
1 | ChatInput *chatInput = new ChatInput(this); |
槽函数:
1 | void ChatWindow::sendMessage(const QString &text) |
2.7 互联
- 一个信号可以连接多个槽。
1 | connect(button, &QPushButton::clicked, this, &MyWidget::save); |
按钮点击后:
1 | save() |
都会被调用。
- 多个信号可以连接同一个槽
例如多个按钮都关闭窗口:
1 | connect(okButton, &QPushButton::clicked, this, &QWidget::close); |
也可以多个输入框共用一个槽:
1 | connect(nameEdit, &QLineEdit::textChanged, this, &SettingsWindow::markDirty); |
意思是:
1 | 任意一个输入框变化,都标记设置已修改。 |
- 一个信号也可以连接到另一个信号。
例如:
1 | connect(inputEdit, &QLineEdit::returnPressed, this, &ChatInput::submitRequested); |
其中:
1 | signals: |
意思是:
1 | inputEdit 发出 returnPressed() |
这相当于把内部控件的信号转发给外部。
2.8 信号和槽的连接
2.8.1 自动连接
1 | void on_objectName_signalName(); |
比如按钮对象名是:saveButton
信号是:clicked()
那么槽函数可以写成:
1 | void on_saveButton_clicked(); |
Qt 可以自动把它们连接起来。
只要名字严格匹配,Qt 就把它们连起来。
2.8.2 断开连接
可以用:
1 | disconnect(sender, signal, receiver, slot); |
例如:
1 | disconnect(button, &QPushButton::clicked, this, &MyWidget::save); |
意思是:取消 button.clicked 和 this.save 的连接。
也可以保存连接对象:
1 | QMetaObject::Connection connection = connect(button, &QPushButton::clicked, this, &MyWidget::save); |
这种写法适合以后单独断开某一个连接。
2.8.3 关联类型 ConnectionType
2.8.3.1 自动关联
1 | connect(sender, &Sender::signalName, receiver, &Receiver::slotName, Qt::AutoConnection); |
这是第五个参数的默认值。
1 | connect(button, &QPushButton::clicked, this, &MyWidget::save); |
等价于:
1 | connect(button, &QPushButton::clicked, this, &MyWidget::save, Qt::AutoConnection); |
它的规则是:
1 | 如果发送者和接收者在同一个线程: |
2.8.3.2 直接关联
1 | connect(sender, &Sender::signalName, receiver, &Receiver::slotName, Qt::DirectConnection); |
意思是:信号一发出,槽函数立刻执行。
例如:
1 | emit somethingHappened(); |
如果是 DirectConnection,大概相当于马上执行:
1 | receiver->slotName(); |
特点:
1 | 同步执行 |
同一个 GUI 线程里,默认 AutoConnection 通常就会表现得像 DirectConnection。
2.8.3.3 队列关联
1 | connect(sender, &Sender::signalName, receiver, &Receiver::slotName, Qt::QueuedConnection); |
意思是:
1 | 信号发出后,不立刻执行槽函数。 |
类比:
1 | DirectConnection: |
它常用于跨线程。
例如:
1 | 工作线程下载完成 |
注意:GUI 控件只能在主线程更新,所以跨线程通知 UI 时,队列连接非常重要。
2.8.3.4 阻塞队列关联
1 | connect(sender, &Sender::signalName, receiver, &Receiver::slotName, Qt::BlockingQueuedConnection); |
它和 QueuedConnection 类似,都是把槽函数投递到接收者线程执行。
但区别是:
1 | 发送信号的线程会阻塞等待, |
特别注意:
1 | 不要在同一个线程里使用 BlockingQueuedConnection,否则很容易死锁。 |
因为同一个线程里:
1 | 发送者等槽函数执行完 |
2.8.3.5 唯一关联
1 | connect(button, &QPushButton::clicked, this, &MyWidget::save, Qt::UniqueConnection); |
意思是:
1 | 如果同样的连接已经存在,就不要重复连接。 |
比如你不小心写了两次:
1 | connect(button, &QPushButton::clicked, this, &MyWidget::save); |
那么按钮点击一次,save() 可能会执行两次。
如果用:
1 | connect(button, &QPushButton::clicked, this, &MyWidget::save, Qt::UniqueConnection); |
Qt 会避免重复连接。
但是注意,UniqueConnection 不是说这个信号只能连接一个槽。它只是防止完全相同的一组连接重复出现。
2.8.3.6 单次关联
1 | Qt::SingleShotConnection |
意思是:
1 | 这个连接只触发一次。 |
例如:
1 | connect(button, &QPushButton::clicked, this, &MyWidget::save, Qt::SingleShotConnection); |
效果是:
1 | 第一次点击按钮:调用 save() |
它适合:
1 | 只允许初始化一次 |
2.8.3.7 组合关联
比如想要队列连接 + 防止重复连接
可以写:
1 | connect(sender, &Sender::signalName, receiver, &Receiver::slotName, |
如果想要直接连接 + 只触发一次
可以写:
1 | connect(sender, &Sender::signalName, receiver, &Receiver::slotName, |
这里又是 flags,用 | 组合。
3 对象树与所有权
Qt中使用对象树
object tree来组织和管理所有的QObject类及其子类的对象。当创建一个QObject对象时,如果使用了其它的对象作为父对象,那么这个对象就会被添加到父对象的children()列表中,这样当父对象被销毁时,这个对象也会被销毁。
例如:
QPushButton button = new QPushButton(“确定”, this);
QLineEdit edit = new QLineEdit(this);
QTimer *timer = new QTimer(this);
1 | 这些对象都挂在 `this` 下面。 |
>
这里 button 的父对象是
this。但是这不表示button 继承自 this。
它只是运行时的对象管理关系。
C++ 继承关系是:
1
2
3 QObject
└─ QWidget
└─ QPushButtonQt 父子对象关系可能是:
1
2
3
4 SettingsWindow
├─ saveButton
├─ cancelButton
└─ apiKeyEdit所以不要被命名困扰了,
1
2 继承关系:类和类之间的关系
父子对象关系:对象和对象之间的关系
3.1 对象树
假设有一个窗口:
1 | QWidget *window = new QWidget; |
它的对象树大概是:
1 | window |
当你执行:
1 | delete window; |
Qt 会自动删除它的子对象:
1 | delete window |
3.2 parent() 和 children()
可以获取一个 QObject 的父对象:
1 | QObject *p = button->parent(); |
也可以获取子对象列表:
1 | const QObjectList &list = window->children(); |
常用函数:
1 | QObject *parent() const; |
例如:
1 | button->setParent(this); |
意思是:
1 | 把 button 挂到 this 下面。 |
不过实际开发中,通常直接在构造时传 parent:
1 | QPushButton *button = new QPushButton("保存", this); |
比后面再 setParent() 更清楚。
3.3 栈对象与堆对象
- 例如:
1 | QTimer *timer = new QTimer; |
这个 timer 没有父对象。
如果忘记:
1 | delete timer; |
它就会内存泄漏。
所以 Qt 里更常写:
1 | QTimer *timer = new QTimer(this); |
这样它跟着当前对象一起释放。
- 栈对象不要乱传 parent
例如:
1 | void MyWidget::createButton() |
这里 button 是局部变量,存在栈上。函数结束后它会自动销毁。
这种写法通常没意义,因为函数结束按钮就没了。
更危险的是,如果栈对象和父对象销毁顺序处理不好,可能造成重复析构问题。
所以 GUI 控件一般写成:
1 | QPushButton *button = new QPushButton("保存", this); |
或者作为成员变量:
1 | private: |
然后构造函数里:
1 | button = new QPushButton("保存", this); |
3.4 布局和 parent 的关系
比如:
1 | QVBoxLayout *layout = new QVBoxLayout(this); |
这里有两个概念:
1 | parent:负责对象生命周期和 QWidget 显示父子关系 |
也就是说:
1 | parent 管“谁释放谁” |
layout->addWidget(button) 不是说 layout 拥有 button。它主要是把 button 加入布局管理,让布局负责排列它。
实际写法常见:
1 | QVBoxLayout *layout = new QVBoxLayout(this); |
这种情况下,Qt 通常会把 button 的父 QWidget 设置为布局所在的窗口。也就是说,按钮最终还是会归窗口管理。
3.5 对象与信号槽
- Qt 很贴心:当 QObject 被销毁时,和它相关的信号槽连接通常会自动断开。
例如:
1 | connect(timer, &QTimer::timeout, this, &MyWidget::update); |
如果 timer 被销毁,连接会失效。
如果 this 被销毁,连接也会失效。
所以一般不需要在析构函数里手动:
1 | disconnect(timer, &QTimer::timeout, this, &MyWidget::update); |
Qt 会处理。
- 有时候你不能马上
delete一个 QObject,尤其是在事件处理或信号槽调用过程中。
Qt 提供:
1 | void deleteLater(); |
例如:
1 | worker->deleteLater(); |
意思是:
1 | 不要现在立刻 delete; |
这个在多线程、网络请求、异步对象里很常见。
4 元对象系统
4.1 元对象系统概述
普通 C++ 对象本身有成员变量、成员函数。
但 Qt 还想知道更多信息,比如:
- 这个对象的类名是什么?
- 这个类有哪些信号?
- 这个类有哪些槽函数?
- 这个类有哪些属性?
- 这个对象能不能安全转换成某个类型?
- 能不能通过字符串调用某个函数?
这些信息普通 C++ 本身不方便直接提供,所以 Qt 做了一套额外机制,叫元对象系统Meta-Object System.
- 条件:
元对象系统主要依赖三样东西。
QObject、Q_OBJECT 宏、moc 元对象编译器。
关系大概是:
1 | 你写的 QObject 子类 |
4.2 Q_OBJECT 宏
1 | class MyWidget : public QWidget |
Q_OBJECT 是一个宏,它会往类里插入一些 Qt 元对象系统需要的声明。
相当于告诉 Qt:这个类要加入元对象系统。
只要类需要下面这些能力,就应该写 Q_OBJECT:
1 | 自定义信号 |
最典型的是自定义信号:
1 | class ChatInput : public QWidget |
如果没有 Q_OBJECT,自定义信号、Q_PROPERTY、qobject_cast 等元对象相关功能通常会出问题。
4.3 moc
moc即Meta-Object Compiler元对象编译器
它是 Qt 自带的一个代码生成工具。
仅靠普通 C++ 编译器不会生成这些机制所需的元信息:
1 | Q_OBJECT |
所以 Qt 会先用 moc 扫描你的类,然后生成额外的 .cpp 代码。
比如你的头文件里有:
1 | class ChatInput : public QWidget |
moc 会生成一些底层代码,让 Qt 知道:
1 | ChatInput 这个类有一个信号: |
所以你才能写:
1 | emit messageSubmitted(text); |
也才能写:
1 | connect(chatInput, &ChatInput::messageSubmitted, this, &ChatWindow::sendMessage); |
这也就解释了一个问题:signals 为什么只声明,不写函数体。
因为信号的实现不是手写的,而是 moc 生成的。
例如:
1 | signals: |
不要写:
1 | void ChatInput::messageSubmitted(const QString &text) |
因为信号不是普通业务函数。它的底层发射逻辑由 Qt 生成。
4.4 元对象系统的方法
- 获取类名:
metaObject()
每个带有元对象能力的 QObject 都可以获取自己的元对象信息:
1 | const QMetaObject *meta = object->metaObject(); |
然后可以获取类名:
1 | QString className = object->metaObject()->className(); |
例子:
1 | qDebug() << button->metaObject()->className(); |
这个功能调试时挺有用。
- 判断是不是某个类:
inherits()
1 | bool inherits(const char *className) const; |
例如:
1 | if (object->inherits("QPushButton")) { |
意思是:
这个对象是不是 QPushButton,或者是不是 QPushButton 的子类对象。
不过日常写代码更常用 qobject_cast。
qobject_cast
qobject_cast 是 Qt 提供的类型转换工具。
例如:
1 | QObject *obj = button; |
如果 obj 真的是 QPushButton 或它的子类,转换成功。
如果不是,返回nullptr.
所以常见写法:
1 | QObject *obj = sender(); |
Q: qobject_cast 和 static_cast 的区别
A:
1 QPushButton *button = static_cast<QPushButton *>(obj);static_cast 更相信程序员,它不会在运行时帮你认真检查对象到底是不是 QPushButton。
如果你转错了,可能出问题。
1 QPushButton *button = qobject_cast<QPushButton *>(obj);qobject_cast 会通过 Qt 元对象系统检查,失败就返回 nullptr。
所以:
static_cast:我相信你,错了后果自负。
qobject_cast:我帮你查一下,不对就返回 nullptr。
sender()
在槽函数里,有时可以知道是谁发出的信号:
1 | QObject *sender() const; |
例子:
1 | void MyWidget::onButtonClicked() |
如果多个按钮连接到同一个槽:
1 | connect(okButton, &QPushButton::clicked, this, &MyWidget::onButtonClicked); |
那么槽函数里可以用 sender() 判断是哪一个按钮。
不过注意:sender() 不要滥用。项目里更清楚的写法通常是用 lambda:
1 | connect(okButton, &QPushButton::clicked, this, [=]() { |
这样更直观。
- 动态属性
setProperty / property
QObject 可以通过字符串设置属性:
1 | object->setProperty("role", "primary"); |
读取:
1 | QVariant value = object->property("role"); |
例如:
1 | saveButton->setProperty("role", "primary"); |
之后可以读取:
1 | QString role = saveButton->property("role").toString(); |
- 动态调用:
QMetaObject::invokeMethod
Qt 可以通过元对象系统动态调用方法。
比如:
1 | QMetaObject::invokeMethod(this, "saveSettings"); |
意思是:通过名字调用 this 对象上的 saveSettings 方法。
- 常见用途:
跨线程调用某个对象的方法
延迟执行某个槽函数
框架层通过字符串调用方法
- 例如投递到事件队列执行:
1 | QMetaObject::invokeMethod(this, "saveSettings", Qt::QueuedConnection); |
tr()翻译函数
你可能看到:
1 | setWindowTitle(tr("Settings")); |
tr() 是 Qt 国际化翻译用的函数,它和元对象系统也有关。
简单理解:tr(“Settings”) 表示这个字符串将来可以被翻译。
如果暂时不做多语言,写:
1 | setWindowTitle("Settings"); |
也能运行。
但正式 Qt 项目里常见:
1 | setWindowTitle(tr("Settings")); |
以后可以配合 Qt 翻译工具生成多语言版本。
5 属性系统
5.1 初识属性
- 什么是属性
普通 C++ 里,你可能这样写:
1 | class PetWidget : public QWidget |
这里 opacity 是一个普通成员变量。
但 Qt 里经常说“属性”,比如:
1 | button->setEnabled(false); |
这些都可以理解成对象的状态:
1 | enabled 是否可用 |
Qt 的属性系统就是把这些状态统一管理起来。
- 属性不等于成员变量
Q: 属性是不是变量?
A:
属性 ≈ 对外暴露出来的状态
成员变量 = 类内部真正保存数据的变量比如:
1
2 button->setText("保存");
QString text = button->text();这里
text是按钮的一个属性。但按钮内部到底用什么成员变量保存文字,你不用管。
Qt 常见写法是:
1 | setXxx(...) |
比如:
1 | setText(...) |
这就是典型的属性读写风格。
Qt 里可以粗略分两类属性:
静态属性:类定义时就声明好的属性
动态属性:运行时临时添加的属性
5.2 动态属性
5.2.1 setProperty 和 property
所有 QObject 都可以使用:
1 | bool setProperty(const char *name, const QVariant &value); |
例如:
1 | saveButton->setProperty("role", "primary"); |
读取:
1 | QString role = saveButton->property("role").toString(); |
这就是动态属性。
意思是:
1 | 给 saveButton 临时加一个叫 role 的属性,值是 primary。 |
注意,这个 role 不一定是 QPushButton 原本就有的属性,是你运行时加上去的。
Q: 为什么返回值不是 QString?
A: 因为属性值可能是各种类型:
1
2
3
4
5
6
7
8 QString
int
bool
double
QColor
QSize
QPoint
...所以 Qt 用一个通用容器
QVariant来装。比如:
1
2
3 button->setProperty("level", 3);
button->setProperty("enabledByConfig", true);
button->setProperty("role", "primary");读取时再转回具体类型:
1
2
3 int level = button->property("level").toInt();
bool enabled = button->property("enabledByConfig").toBool();
QString role = button->property("role").toString();
5.2.2 动态属性常用场景
- 场景一:给控件打标签
比如设置页面有很多输入框,想标记哪些是敏感字段:
1 | apiKeyEdit->setProperty("secret", true); |
之后统一处理:
1 | QList<QLineEdit *> edits = this->findChildren<QLineEdit *>(); |
- 场景二:配合样式表
Qt 样式表可以根据属性选择控件。
例如:
1 | saveButton->setProperty("role", "primary"); |
样式表:
1 | QPushButton[role="primary"] { |
这样就可以根据 role 设置样式。
不过注意,如果运行时改变属性,有时需要重新刷新样式:
1 | saveButton->setProperty("role", "danger"); |
- 场景三:多个控件共用一个槽
比如多个按钮都连接同一个槽:
1 | connect(openButton, &QPushButton::clicked, this, &MyWidget::handleButton); |
给它们设置属性:
1 | openButton->setProperty("action", "open"); |
槽函数里判断:
1 | void MyWidget::handleButton() |
不过现代写法里,很多时候用 lambda 更清楚:
1 | connect(openButton, &QPushButton::clicked, this, [=]() { openFile(); }); |
所以动态属性不是必须用,而是某些批量处理场景很方便。
5.3 静态属性
5.3.1 Q_PROPERTY
静态属性通常通过 Q_PROPERTY 声明。
例如:
1 | class PetWidget : public QWidget |
这句是重点:
1 | Q_PROPERTY(int petScale READ petScale WRITE setPetScale NOTIFY petScaleChanged) |
拆成几部分:
1 | int 属性类型 |
所以需要写:
1 | int PetWidget::petScale() const |
和:
1 | void PetWidget::setPetScale(int scale) |
这就是标准写法。
Q: 为什么成员变量常写成 m_xxx?
比如:
1
2 private:
int m_petScale = 100;A: 这个
m_只是命名习惯,意思是 member。
1
2
3 petScale 属性名 / getter 函数名
setPetScale setter 函数名
m_petScale 内部成员变量它们的关系是:
1
2 外部读属性 → 调用 petScale() → 返回 m_petScale
外部写属性 → 调用 setPetScale(...) → 修改 m_petScale
5.3.2 静态属性常用场景
- 用处一:可以通过字符串访问
有了 Q_PROPERTY 后,可以这样访问:
1 | widget->setProperty("petScale", 120); |
读取:
1 | int scale = widget->property("petScale").toInt(); |
也就是说,外部不一定非要直接调用:
1 | widget->setPetScale(120); |
也可以通过属性名字符串访问。
这就是元对象系统的能力。
- 用处二:可以配合动画
Qt 的属性动画 QPropertyAnimation 需要操作 Qt 属性。
例如:
1 | QPropertyAnimation *animation = new QPropertyAnimation(widget, "windowOpacity", this); |
这里 "windowOpacity" 就是 QWidget 的一个属性。
意思是:
1 | 300 毫秒内,把窗口透明度从 0.0 变到 1.0。 |
如果你自定义了 petScale,理论上也可以动画它:
1 | QPropertyAnimation *animation = new QPropertyAnimation(petWidget, "petScale", this); |
前提是这个属性有 WRITE,也就是能被修改。
5.4 NOTIFY 信号
比如:
1 | Q_PROPERTY(int petScale READ petScale WRITE setPetScale NOTIFY petScaleChanged) |
这里的:
1 | NOTIFY petScaleChanged |
表示属性改变时会发信号。
标准 setter 通常这样写:
1 | void PetWidget::setPetScale(int scale) |
为什么先判断?
1 | if (m_petScale == scale) { |
因为如果值没变,就没必要发 petScaleChanged。
否则可能导致:
1 | 明明值没变,却反复通知外部 |
5.5 属性的限定
有些属性可以只读:
1 | Q_PROPERTY(QString version READ version CONSTANT) |
意思是:version 只能读取,不能修改。(根本原因:只有 READ,没有 WRITE。)
只读属性一般只有 READ,没有 WRITE。
常见选项:
1 | READ 指定 getter |
6 容器类
Qt 库提供了一组通用的、基于模板的容器类(container classes)。这些容器类可以用来存储指定类型的项目(items),例如:
如果需要一个大小可变的QString数组,那么可以使用QList< QString >。
Q: 为什么 Qt 有自己的容器类?
C++ 标准库本来就有:
1
2
3
4
5
6 std::vector
std::list
std::map
std::unordered_map
std::set
std::string但是 Qt 也有自己的:
1
2
3
4
5 QList
QMap
QHash
QSet
QStringA: 原因主要是:
1
2
3
4 1. Qt API 很多地方直接使用 Qt 容器。
2. Qt 容器和 QObject、QVariant、QString 等体系配合更自然。
3. Qt 容器支持隐式共享,复制成本通常比较低。
4. Qt 早期为了跨平台和统一接口,提供了自己的容器体系。
6.1 QList< T >
QList<T> 是最常见的顺序容器。
可以理解成一串按顺序排列的数据
例如保存多个动作名:
1 | QList<QString> actions; |
也可以这样初始化:
1 | QList<QString> actions = {"idle", "blink", "wave"}; |
访问:
1 | QString firstAction = actions[0]; |
常用函数:
1 | void append(const T &value); // 追加元素 |
例子:
1 | QList<int> nums = {1, 2, 3}; |
输出:
1 | 1 |
6.2 QStringList
QStringList 可以理解成字符串列表,本质上就是一组 QString。
例如:
1 | QStringList actions; |
常用场景:
1 | 动作名列表 |
常用函数:
1 | QStringList split(const QString &sep) const; // 按分隔符切分字符串,返回字符串列表 |
例子:
1 | QString text = "idle,blink,wave"; |
结果:
1 | idle | blink | wave |
6.3 QMap
QMap 是键值对容器。
可以理解成:通过 key 找 value
例如:
1 | QMap<QString, QString> config; |
读取:
1 | QString provider = config["provider"]; |
更安全一点:
1 | QString provider = config.value("provider", "default"); |
这里:
1 | config.value("provider", "default"); |
意思是:
1 | 如果存在 provider,就返回它; |
常用函数:
1 | iterator insert(const Key &key, const T &value); // 插入一组键值对;如果 key 已存在,通常会覆盖原来的 value;返回插入位置的迭代器 |
例子:
1 | QMap<QString, int> scores; |
QMap 的 key 是有序的。比如 key 是字符串时,它会按 key 的顺序排列。
6.4 QHash
QHash 也是键值对容器。
用法和 QMap 很像:
1 | QHash<QString, int> scores; |
区别是:
1 | QMap:按 key 排序,内部有序。 |
例如配置项如果要稳定顺序显示,可以用 QMap。
如果只是快速根据 ID 找对象,可以用 QHash。
6.5 QSet< T >
QSet 是集合。
特点是:元素不能重复。
例如保存已经启用的功能名:
1 | QSet<QString> enabledFeatures; |
虽然插入了两次 "search",但集合里只有一个。
判断是否存在:
1 | if (enabledFeatures.contains("search")) { |
常用函数:
1 | iterator insert(const T &value); // 插入一个元素;如果元素已经存在,不会重复插入;返回元素位置的迭代器 |
适合保存:
1 | 去重后的文件名 |
6.6 遍历容器
6.6.1 遍历 QList
1 | QList<QString> actions = {"idle", "blink", "wave"}; |
这里建议用:
1 | const QString &action |
因为这样不会复制字符串。
6.6.2 遍历 QMap / QHash
简单遍历 key:
1 | QMap<QString, QString> config; |
不过 keys() 会生成一个 key 列表。数据很少时无所谓;数据很多时,可以用迭代器。
迭代器写法:
1 | for (auto it = config.constBegin(); it != config.constEnd(); ++it) { |
这个读法是:
1 | 从 config 的开头开始; |
6.7 常用 STL 算法
Qt 容器也可以配合很多标准库算法使用。
比如排序:
1 |
|
结果:
1 | 1 2 3 4 |
自定义排序:
1 | QList<QString> names = {"cat", "elephant", "dog"}; |
按照字符串长度排序。
查找:
1 | auto it = std::find(names.begin(), names.end(), "dog"); |
统计:
1 | int count = std::count(nums.begin(), nums.end(), 2); |
条件判断:
1 | bool hasLongName = std::any_of(names.begin(), names.end(), [](const QString &name) { |
常用的:
1 | std::sort 排序 |
6.8 QString
QString 是 Qt 的字符串类,非常重要。
普通 C++ 有:std::string
Qt 里主要用:QString
Qt API 大量使用 QString,比如:
1 | button->setText("保存"); |
6.8.1 创建 QString
1 | QString name = "DesktopPet"; |
6.8.2 拼接
1 | QString text = QString("Hello, ") + name; |
更推荐复杂拼接用 arg():
1 | QString message = QString("当前模型:%1,温度:%2").arg(modelName).arg(temperature); |
6.8.3 常用函数
1 | bool isEmpty() const; // 是否为空 |
例子:
1 | QString input = " hello qt "; |
输出:
1 | hello qt |
6.8.4 字符串转数字
1 | QString text = "123"; |
这个 ok 很重要。
如果QString text = "abc";
那么text.toInt(&ok);会失败,ok == false。
6.8.5 数字转 QString
整数:
1 | int value = 123; |
浮点数:
1 | double x = 3.14159; |
结果:
1 | 3.14 |
6.8.6 QString 和 std::string 转换
- Qt 字符串转标准字符串:
1 | QString qstr = "hello"; |
- 标准字符串转 Qt 字符串:
1 | std::string str = "hello"; |
不过在 Qt 项目里,界面相关尽量用 QString,别来回转太多。
6.9 QByteArray 和 QVariant
6.9.1 QByteArray
QByteArray 表示一段字节数据。
它和 QString 的区别是:
1 | QString:文本字符串,适合人读的字符。 |
例如网络请求返回的数据通常是:
1 | QByteArray data = reply->readAll(); |
JSON 字符串转字节:
1 | QByteArray jsonData = jsonString.toUtf8(); |
字节转 QString:
1 | QString text = QString::fromUtf8(jsonData); |
常用函数:
1 | bool isEmpty() const; // 判断字节数组是否为空 |
例如:
1 | QString text = "你好"; |
6.9.2 QVariant
QVariant 是一个可以保存多种类型值的容器。
比如:
1 | QVariant v1 = 123; |
读取时转回具体类型:
1 | int n = v1.toInt(); |
它经常出现在:
1 | QObject 属性系统 |
例如:
1 | button->setProperty("role", "primary"); |
property() 返回的就是 QVariant。
6.9.3 QVariant 和类型安全
QVariant 很方便,但不要滥用。
比如:
1 | QVariant value = "123"; |
能转。
但如果:
1 | QVariant value = "abc"; |
转换失败时可能得到 0,容易混淆。
更稳可以:
1 | bool ok = false; |
6.10 隐式共享
Qt 很多类支持隐式共享,比如:
1
2
3
4
5
6 QString
QByteArray
QList
QMap
QHash
QImage隐式共享也叫写时复制
copy-on-write
- 例子:
假设:
1 | QString a = "hello"; |
你可能以为复制了一整份字符串。
但 Qt 通常不会立刻复制底层数据,而是让 a 和 b 暂时共享同一份数据:
1 | a ─┐ |
如果你只是读取:
1 | qDebug() << a; |
它们一直共享,效率很高。
如果修改 b:
1 | b += " qt"; |
Qt 才会给 b 复制一份新数据:
1 | a ─── "hello" |
这就叫写时复制。
Q: 为什么这样设计
A: 因为 Qt 代码里经常会传递对象:
1
2
3 void setTitle(QString title);
QString getName();
QList<QString> actions = otherActions;如果每次都深拷贝,成本很高。
隐式共享让复制看起来简单,但底层很省:
1
2 复制时先共享。
真正修改时才复制。所以
QString、QByteArray、QList这些按值返回、按值传递在 Qt 里比较常见。
