Qt 核心概念
Qt 框架有一些独特的设计理念和核心机制,理解这些概念是掌握 Qt 开发的关键。
元对象系统(Meta-Object System)
Qt 的元对象系统是其最核心的特性之一,它为 C++ 添加了反射、信号槽、属性系统等动态特性。
元对象编译器(moc)
Qt 使用 moc(Meta-Object Compiler)在编译时处理特殊的 C++ 代码:
class MyClass : public QObject
{
Q_OBJECT // 这个宏会触发 moc 处理
public:
MyClass(QObject *parent = nullptr);
signals:
void mySignal(); // 信号声明
public slots:
void mySlot(); // 槽声明
};
编译流程:
头文件 (.h)
│
▼
moc 编译器 ──► moc_xxx.cpp(生成的元对象代码)
│
▼
C++ 编译器 ──► 目标文件
│
▼
链接器 ──► 可执行文件
Q_OBJECT 宏的作用
Q_OBJECT 宏展开后包含:
static const QMetaObject staticMetaObject- 类的元对象virtual const QMetaObject *metaObject() const- 获取元对象virtual void *qt_metacast(const char *)- 动态类型转换virtual int qt_metacall(QMetaObject::Call, int, void **)- 调用信号槽
这些功能使得 Qt 能够在运行时获取类信息、调用方法、实现信号槽。
信号与槽(Signals & Slots)
信号槽是 Qt 最核心的特性之一,是一种类型安全、松耦合的对象间通信机制。理解信号槽的工作原理对于掌握 Qt 开发至关重要。
为什么需要信号槽?
在 GUI 编程中,组件之间经常需要通信。例如,当用户点击关闭按钮时,需要调用窗口的关闭函数。传统的解决方案是使用回调函数(Callback):将一个函数指针传递给另一个函数,当特定事件发生时调用这个函数指针。
回调函数虽然可行,但存在以下问题:
- 类型不安全:编译器无法检查回调函数的参数类型是否正确
- 不够直观:函数指针的语法复杂,容易出错
- 耦合度高:调用者需要知道被调用函数的具体信息
Qt 的信号槽机制正是为了解决这些问题而设计的。
信号槽的核心优势
类型安全:信号的参数类型必须与槽的参数类型兼容,编译器会在编译时检查类型匹配。如果使用函数指针语法,类型不匹配会在编译时报错;如果使用字符串语法,则会在运行时报错。
松耦合:发送信号的对象不需要知道哪些槽会接收这个信号,槽也不需要知道信号来自哪里。这种设计使得组件可以独立开发、测试和复用。
灵活性:一个信号可以连接多个槽,一个槽也可以接收多个信号。信号甚至可以直接连接到另一个信号,实现信号转发。
基本概念
- 信号(Signal):当对象状态发生变化时发出的通知。信号由
moc(元对象编译器)自动生成实现代码,开发者只需声明,无需实现 - 槽(Slot):响应信号的函数。槽就是普通的 C++ 成员函数,可以被直接调用,也可以通过信号触发
- 连接(Connect):建立信号与槽之间的关联,当信号被发射时,连接的槽会被调用
- 发射(Emit):触发信号的行为,通常使用
emit关键字(虽然emit实际上是可选的)
信号槽的工作原理
当信号被发射时,Qt 的元对象系统会查找所有连接到该信号的槽,并依次调用它们。这个过程是类型安全的,因为信号的参数会被正确地传递给槽函数。
基本用法
#include <QObject>
#include <QDebug>
// 发送者类:声明信号
class Sender : public QObject
{
Q_OBJECT // 必须包含此宏才能使用信号槽
public:
explicit Sender(QObject *parent = nullptr) : QObject(parent) {}
signals:
// 信号声明:只需声明,无需实现
// 信号是 public 的,可以从任何地方发射
void valueChanged(int newValue);
void stringSignal(const QString &text);
// 信号可以有参数,参数会被传递给槽
void dataReady(const QByteArray &data, int size);
};
// 接收者类:声明槽函数
class Receiver : public QObject
{
Q_OBJECT
public:
explicit Receiver(QObject *parent = nullptr) : QObject(parent) {}
public slots:
// 槽函数:普通的成员函数,可以被信号触发
// public slots: 可以被任何信号连接
// protected slots: 只能被本类及其子类的信号连接
// private slots: 只能被本类的信号连接
void onValueChanged(int value)
{
qDebug() << "收到数值:" << value;
// 槽函数可以进行任何操作
}
void onStringReceived(const QString &text)
{
qDebug() << "收到字符串:" << text;
}
// 槽的参数可以少于信号的参数(忽略多余参数)
void onDataReady(const QByteArray &data)
{
// 忽略了 size 参数
qDebug() << "收到数据:" << data.size() << "字节";
}
};
// 使用示例
void example()
{
Sender sender;
Receiver receiver;
// Qt 5+ 函数指针语法(推荐)
// 编译时类型检查,类型不匹配会编译错误
QObject::connect(&sender, &Sender::valueChanged,
&receiver, &Receiver::onValueChanged);
// 发射信号
emit sender.valueChanged(42); // receiver.onValueChanged(42) 会被调用
// 信号可以连接多个槽
QObject::connect(&sender, &Sender::valueChanged,
[](int v) { qDebug() << "Lambda槽:" << v; });
// 发射信号时,所有连接的槽都会被调用
emit sender.valueChanged(100);
// 输出:
// 收到数值: 100
// Lambda槽: 100
}
连接类型详解
Qt 提供多种连接方式,控制槽函数的执行时机和方式。正确理解连接类型对于处理跨线程通信至关重要。
| 连接类型 | 说明 | 槽执行线程 | 使用场景 |
|---|---|---|---|
Qt::AutoConnection | 自动选择(默认) | 同线程则直接,跨线程则队列 | 大多数情况,特别是信号接收者可能在不同线程时 |
Qt::DirectConnection | 立即直接调用 | 发送者线程 | 性能敏感场景,确保同线程 |
Qt::QueuedConnection | 放入事件队列异步执行 | 接收者线程 | 跨线程 UI 更新,避免阻塞发送者 |
Qt::BlockingQueuedConnection | 阻塞等待槽执行完成 | 接收者线程 | 跨线程需要同步结果 |
Qt::UniqueConnection | 确保连接唯一 | - | 防止重复连接导致槽被多次调用 |
// AutoConnection 的工作原理
// 发送信号时,Qt 检查发送者和接收者是否在同一线程:
// - 同线程:使用 DirectConnection
// - 不同线程:使用 QueuedConnection
// DirectConnection - 槽函数在发送者线程立即执行
// 类似于直接函数调用,但通过信号槽机制
connect(sender, &Sender::signal, receiver, &Receiver::slot,
Qt::DirectConnection);
// QueuedConnection - 槽函数在接收者线程的事件循环中执行
// 发射信号后立即返回,不等待槽执行
// 适用于跨线程更新 UI
connect(worker, &Worker::progressChanged, progressBar, &QProgressBar::setValue,
Qt::QueuedConnection);
// BlockingQueuedConnection - 发送者线程阻塞,等待槽执行完成
// 注意:如果接收者线程的事件循环未运行,会死锁!
connect(uiThread, &UIThread::getValue, worker, &Worker::getValue,
Qt::BlockingQueuedConnection);
int result = worker->getLastValue(); // 此时槽已执行完毕
// UniqueConnection - 如果连接已存在,不会重复连接
// 适合可能多次调用 connect 的场景
connect(button, &QPushButton::clicked, this, &MyClass::onClick,
Qt::UniqueConnection);
// 再次连接同样的信号和槽,不会生效
connect(button, &QPushButton::clicked, this, &MyClass::onClick,
Qt::UniqueConnection);
跨线程通信注意事项:
class Worker : public QObject
{
Q_OBJECT
signals:
void resultReady(const QString &result); // 信号可以跨线程传递
};
class Controller : public QObject
{
Q_OBJECT
public:
Controller()
{
worker = new Worker;
workerThread = new QThread(this);
worker->moveToThread(workerThread);
// 跨线程连接 - AutoConnection 自动变为 QueuedConnection
connect(worker, &Worker::resultReady, this, &Controller::handleResult);
workerThread->start();
}
private slots:
void handleResult(const QString &result)
{
// 这个槽在 Controller 的线程(通常是主线程)执行
// 可以安全地更新 UI
label->setText(result);
}
};
Lambda 表达式作为槽
Qt 5 起支持使用 Lambda 表达式作为槽函数,这提供了极大的灵活性,特别适合处理简单的响应逻辑。
#include <QPushButton>
#include <QLabel>
// Lambda 作为槽的基本用法
QPushButton *button = new QPushButton("点击我");
connect(button, &QPushButton::clicked, [](bool checked) {
// clicked 信号带一个 bool 参数(按钮是否被按下)
qDebug() << "按钮被点击,状态:" << checked;
});
// 捕获外部变量
QLabel *label = new QLabel("计数: 0");
int count = 0;
connect(button, &QPushButton::clicked, [label, &count]() {
// 捕获 label(值捕获)和 count(引用捕获)
count++;
label->setText(QString("计数: %1").arg(count));
});
// 使用上下文对象管理生命周期(推荐)
connect(button, &QPushButton::clicked, this, [this]() {
// 当 this 被销毁时,连接会自动断开
// 避免 Lambda 捕获的成员变成悬垂指针
updateStatus("按钮被点击");
});
// 不推荐的做法(可能导致崩溃)
QWidget *widget = new QWidget;
QPushButton *btn = new QPushButton("危险", widget);
connect(btn, &QPushButton::clicked, [widget]() {
// 如果 widget 在 Lambda 执行前被删除,程序会崩溃
widget->setWindowTitle("已点击"); // 危险!
});
// 推荐的做法
connect(btn, &QPushButton::clicked, btn, [btn]() {
// 提供上下文对象(btn),当 btn 被删除时自动断开连接
btn->parentWidget()->setWindowTitle("已点击"); // 安全
});
Lambda 作为槽的注意事项:
- 生命周期管理:Lambda 捕获的变量必须在其被调用时仍然有效
- 上下文对象:提供第三个参数(上下文对象)可以确保 Lambda 在对象销毁时自动断开
- 值捕获 vs 引用捕获:对于局部变量使用引用捕获时要格外小心
- 性能考虑:频繁触发的信号(如鼠标移动)中使用复杂 Lambda 可能影响性能
// 实际应用示例:带条件判断的槽
QLineEdit *input = new QLineEdit();
QLabel *status = new QLabel();
connect(input, &QLineEdit::textChanged, this, [this, status](const QString &text) {
// 根据输入内容更新状态
if (text.isEmpty()) {
status->setText("请输入内容");
status->setStyleSheet("color: gray;");
} else if (text.length() < 3) {
status->setText("内容太短");
status->setStyleSheet("color: orange;");
} else {
status->setText("输入有效");
status->setStyleSheet("color: green;");
}
});
信号连接信号
信号可以直接连接到另一个信号,实现信号转发。这在封装组件或简化接口时非常有用。
// 场景:封装一个自定义按钮,转发内部按钮的信号
class MyButton : public QWidget
{
Q_OBJECT
public:
MyButton(QWidget *parent = nullptr) : QWidget(parent)
{
m_button = new QPushButton("按钮", this);
// 信号转发:内部按钮的 clicked 信号转发为 MyButton 的 clicked
connect(m_button, &QPushButton::clicked, this, &MyButton::clicked);
// 也可以转发带参数的信号
connect(m_button, &QPushButton::toggled, this, &MyButton::toggled);
}
signals:
void clicked();
void toggled(bool checked);
private:
QPushButton *m_button;
};
// 使用时,外部只需连接 MyButton 的信号
MyButton *btn = new MyButton();
connect(btn, &MyButton::clicked, []() {
qDebug() << "MyButton 被点击";
});
// 内部按钮的点击会自动触发 MyButton::clicked
// 另一个应用:简化复杂的信号链
class NetworkManager : public QObject
{
Q_OBJECT
public:
NetworkManager(QObject *parent = nullptr) : QObject(parent)
{
m_reply = nullptr;
}
void fetchData(const QUrl &url)
{
QNetworkReply *reply = m_manager.get(QNetworkRequest(url));
// 将 QNetworkReply 的 finished 信号转发为自定义信号
// 同时附加响应数据
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
QByteArray data = reply->readAll();
emit dataReceived(data); // 发射自定义信号
reply->deleteLater();
});
}
signals:
void dataReceived(const QByteArray &data);
private:
QNetworkAccessManager m_manager;
QNetworkReply *m_reply;
};
断开连接
当对象被销毁时,Qt 会自动断开所有相关的连接。但在某些情况下,你可能需要手动断开连接。
Sender *sender = new Sender;
Receiver *receiver = new Receiver;
// 建立连接
QMetaObject::Connection conn = connect(sender, &Sender::valueChanged,
receiver, &Receiver::onValueChanged);
// 方式 1:断开特定连接
disconnect(conn); // 使用返回的连接句柄
// 方式 2:断开特定的信号-槽对
disconnect(sender, &Sender::valueChanged, receiver, &Receiver::onValueChanged);
// 方式 3:断开接收者的所有槽
disconnect(sender, nullptr, receiver, nullptr);
// sender 发出的所有信号都不会再触发 receiver 的任何槽
// 方式 4:断开发送者的所有信号
disconnect(sender, nullptr, nullptr, nullptr);
// sender 发出的所有信号都不会触发任何槽
// 方式 5:断开所有连接到特定槽的连接
disconnect(nullptr, nullptr, receiver, &Receiver::onValueChanged);
// 任何信号都不会再触发 receiver 的 onValueChanged
断开连接的常见场景:
class TempConnection : public QObject
{
Q_OBJECT
public:
void setupTemporaryConnection(QObject *sender)
{
// 保存连接句柄,以便稍后断开
m_connection = connect(sender, &QObject::destroyed,
this, &TempConnection::onSenderDestroyed);
}
void cleanup()
{
// 手动断开连接
if (m_connection) {
disconnect(m_connection);
m_connection = QMetaObject::Connection();
}
}
private slots:
void onSenderDestroyed() { /* ... */ }
private:
QMetaObject::Connection m_connection;
};
连接的自动管理
Qt 提供了强大的连接生命周期管理机制,可以避免悬垂指针和内存问题。
// 当发送者或接收者被删除时,连接自动断开
{
Sender *sender = new Sender;
Receiver *receiver = new Receiver;
connect(sender, &Sender::signal, receiver, &Receiver::slot);
delete receiver; // 连接自动断开,不会导致崩溃
emit sender->signal(); // 安全,不会有任何调用
delete sender;
}
// Lambda 连接的生命周期管理
{
QPushButton *button = new QPushButton;
// 不提供上下文对象 - button 删除后连接仍存在,但 Lambda 捕获的 button 是悬垂指针
connect(button, &QPushButton::clicked, [button]() {
button->setText("已点击"); // 危险!如果 button 已被删除
});
// 提供上下文对象 - 当 button 被删除时,连接自动断开
connect(button, &QPushButton::clicked, button, [button]() {
button->setText("已点击"); // 安全,因为 Lambda 只在 button 存活时被调用
});
delete button; // 连接自动断开
}
// 使用 QPointer 进行安全追踪
#include <QPointer>
void setupConnection(QObject *sender, QObject *receiver)
{
QPointer<QObject> safeReceiver = receiver;
connect(sender, &QObject::destroyed, [safeReceiver]() {
if (safeReceiver) {
// 安全地访问 receiver
safeReceiver->deleteLater();
}
});
}
处理重载的信号和槽
当一个类中有多个同名但参数不同的信号或槽时,需要使用 qOverload 或函数指针转换来明确指定。
class MyWidget : public QWidget
{
Q_OBJECT
signals:
void dataChanged(); // 无参数版本
void dataChanged(int value); // int 参数版本
void dataChanged(const QString &text); // QString 参数版本
public slots:
void update(int value);
void update(const QString &text);
};
// 错误:编译器不知道要连接哪个版本
// connect(sender, &Sender::dataChanged, receiver, &Receiver::update);
// 方式 1:使用 qOverload(Qt 5.7+,推荐)
connect(sender, qOverload<int>(&Sender::dataChanged),
receiver, qOverload<int>(&Receiver::update));
connect(sender, qOverload<const QString&>(&Sender::dataChanged),
receiver, qOverload<const QString&>(&Receiver::update));
// 无参数版本使用 qOverload<>()
connect(sender, qOverload<>(&Sender::dataChanged),
receiver, &Receiver::refresh);
// 方式 2:使用 static_cast(兼容旧代码)
connect(sender, static_cast<void(Sender::*)(int)>(&Sender::dataChanged),
receiver, static_cast<void(Receiver::*)(int)>(&Receiver::update));
// 方式 3:使用 Lambda 表达式(最灵活)
connect(sender, &Sender::dataChanged, this, [this](int value) {
// Lambda 会自动匹配参数类型
update(value);
});
// 实际应用:QComboBox 的 currentIndexChanged 有两个重载
QComboBox *combo = new QComboBox();
// currentIndexChanged(int) - 带索引
connect(combo, qOverload<int>(&QComboBox::currentIndexChanged),
this, &MyClass::onIndexChanged);
// currentTextChanged(const QString&) - 带文本
connect(combo, &QComboBox::currentTextChanged,
this, &MyClass::onTextChanged);
信号槽的性能考虑
信号槽机制虽然强大,但也有一定的性能开销:
- 每次发射信号,Qt 需要查找所有连接
- 参数需要被安全地传递(可能涉及拷贝)
- 跨线程连接需要通过事件队列传递
// 性能优化建议
// 1. 避免在紧密循环中发射信号
// 不好的做法
for (int i = 0; i < 1000000; i++) {
emit progressChanged(i); // 每次发射都有开销
}
// 好的做法
for (int i = 0; i < 1000000; i++) {
if (i % 1000 == 0) { // 每 1000 次发射一次
emit progressChanged(i);
}
}
// 2. 对于高频信号,考虑使用 DirectConnection(如果安全)
connect(sender, &Sender::highFrequencySignal,
receiver, &Receiver::handleSignal,
Qt::DirectConnection); // 避免事件队列开销
// 3. 使用 const 引用传递大对象
signals:
void dataReady(const QByteArray &data); // 好:避免拷贝
// void dataReady(QByteArray data); // 差:会拷贝数据
对象树与内存管理
Qt 使用对象树(Object Tree)机制实现半自动的内存管理,这是 Qt 区别于其他 C++ 框架的重要特性。理解对象树机制对于避免内存泄漏和悬垂指针至关重要。
父子对象关系的原理
当创建 QObject 时,可以指定一个父对象。父对象会维护一个子对象列表,当父对象被销毁时,会自动销毁所有子对象。
// 创建父对象
QWidget *window = new QWidget;
// 创建子对象,指定父对象
QPushButton *button1 = new QPushButton("Button 1", window);
QPushButton *button2 = new QPushButton("Button 2", window);
QLabel *label = new QLabel("Label", window);
// 对象树结构:
// window(父对象)
// ├── button1(子对象)
// ├── button2(子对象)
// └── label(子对象)
// 删除父对象会自动删除所有子对象
delete window; // button1, button2, label 也会被删除
// 不需要手动 delete button1, button2, label
对象树的工作机制
当调用 delete parent 时,Qt 的析构顺序如下:
- 父对象的析构函数开始执行
- 遍历子对象列表(按添加顺序的逆序)
- 对每个子对象调用
delete - 子对象的析构函数执行(递归处理孙对象)
- 父对象的析构函数完成
内存管理规则
// 规则 1:有父对象时,不要手动 delete
void setupUI(QWidget *parent)
{
// 好的做法:创建时指定父对象
QVBoxLayout *layout = new QVBoxLayout(parent);
QPushButton *btn1 = new QPushButton("OK", parent);
QPushButton *btn2 = new QPushButton("Cancel", parent);
layout->addWidget(btn1);
layout->addWidget(btn2);
// 无需手动 delete,parent 销毁时会自动清理
}
// 规则 2:布局添加控件时,控件自动获得父对象
void setupLayout()
{
QWidget *widget = new QWidget;
QVBoxLayout *layout = new QVBoxLayout(widget); // layout 的父对象是 widget
// addWidget 会自动将控件的父对象设为 layout 所在的 widget
QPushButton *button = new QPushButton("Button"); // 无父对象
layout->addWidget(button); // button 的父对象变为 widget
// 同样,setLayout 也会设置父对象关系
QWidget *widget2 = new QWidget;
QHBoxLayout *hLayout = new QHBoxLayout; // 无父对象
widget2->setLayout(hLayout); // hLayout 的父对象变为 widget2
}
// 规则 3:无父对象时需要手动管理
void standaloneObject()
{
QObject *obj = new QObject; // 无父对象
// ... 使用 obj
delete obj; // 必须手动删除
}
// 规则 4:可以随时改变父对象关系
void reparenting()
{
QWidget *parent1 = new QWidget;
QWidget *parent2 = new QWidget;
QPushButton *button = new QPushButton("Button", parent1);
// button 的父对象是 parent1
button->setParent(parent2);
// 现在 button 的父对象是 parent2
// 当 parent2 被删除时,button 也会被删除
}
// 规则 5:栈对象 vs 堆对象
void stackObject()
{
// 栈上创建的对象(推荐用于局部、生命期确定的对象)
QWidget window; // 栈对象
QPushButton button("OK", &window); // 子对象也在栈上
window.show();
// 函数结束时,window 先析构(自动删除 button)
// 然后 button 析构(但已经被父对象删除,会有问题!)
}
void heapObject()
{
// 堆上创建的对象(推荐用于需要超出函数作用域的对象)
QWidget *window = new QWidget;
QPushButton *button = new QPushButton("OK", window);
window->show();
// 需要 delete window(或在某处设置父对象)
}
常见陷阱与最佳实践
// 陷阱 1:在栈上创建有父对象的 QObject
void badPractice()
{
QWidget parent;
QPushButton *button = new QPushButton("Button", &parent);
// 问题:parent 在函数结束时析构,删除 button
// 但 button 是堆对象,用 delete 删除是正确的
// 这实际上是可以的!
// 但如果是这样就有问题:
QWidget parent2;
QPushButton button2("Button", &parent2);
// parent2 析构时删除 button2
// 然后 button2 的析构函数也被调用(栈对象自动析构)
// 双重删除!程序崩溃!
}
// 正确做法:子对象要么都在栈上,要么都在堆上
void goodPractice()
{
// 方式 1:都在栈上
{
QWidget parent;
QPushButton button("Button", &parent); // 不推荐,容易出错
// ...
} // parent 析构 -> button 被删除 -> button 析构(但已被删除)-> 问题
// 方式 2:都在堆上(推荐)
{
QWidget *parent = new QWidget;
QPushButton *button = new QPushButton("Button", parent);
// ...
delete parent; // button 也被删除
}
// 方式 3:使用对象树 + 智能指针(C++11)
{
auto parent = std::make_unique<QWidget>();
auto button = new QPushButton("Button", parent.get());
// ...
} // parent 析构,button 也被删除
}
// 陷阱 2:删除父对象时子对象已提前删除
void doubleDelete()
{
QWidget *parent = new QWidget;
QPushButton *button = new QPushButton("Button", parent);
delete button; // 手动删除子对象
// 此时 parent 的子对象列表中仍有 button 的指针(悬垂指针)
delete parent; // 会尝试再次删除 button -> 崩溃!
}
// 正确做法:删除子对象前先移除父对象关系,或直接删除父对象
void correctDelete()
{
QWidget *parent = new QWidget;
QPushButton *button = new QPushButton("Button", parent);
// 方式 1:先移除父对象关系
button->setParent(nullptr);
delete button;
delete parent;
// 方式 2:只删除父对象(推荐)
// delete parent; // button 会自动删除
}
// 陷阱 3:在错误的线程操作 QObject
// QObject 的父子关系必须在同一线程
void wrongThread()
{
QThread *thread = new QThread;
QWidget *widget = new QWidget;
// 错误:不能将 widget 移动到其他线程(QWidget 必须在 GUI 线程)
// widget->moveToThread(thread);
}
// 特殊情况:延迟删除
void delayedDelete()
{
QDialog *dialog = new QDialog(this);
// 设置关闭时自动删除
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
// 用户关闭对话框时,对话框会被自动删除
// 或使用 deleteLater
QObject *obj = new QObject;
obj->deleteLater(); // 在当前事件循环结束后安全删除
}
检查对象树状态
void inspectObjectTree()
{
QObject *parent = new QObject;
QObject *child1 = new QObject(parent);
QObject *child2 = new QObject(parent);
// 检查父对象
qDebug() << "child1 的父对象:" << child1->parent(); // 指向 parent
// 检查子对象列表
QList<QObject*> children = parent->children();
qDebug() << "parent 有" << children.size() << "个子对象";
// 检查是否是某对象的子对象
qDebug() << "child1 是 parent 的子对象:" << parent->isWidgetType();
qDebug() << "child1 的父对象是 parent:" << (child1->parent() == parent);
// 查找子对象(按名称)
child1->setObjectName("firstChild");
QObject *found = parent->findChild<QObject*>("firstChild");
qDebug() << "找到的子对象:" << found; // 指向 child1
// 查找所有某类型的子对象
QList<QPushButton*> buttons = parent->findChildren<QPushButton*>();
}
事件系统
Qt 使用事件驱动编程模型,所有用户交互都通过事件处理。
事件循环
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow window;
window.show();
// 启动事件循环
// 不断从系统获取事件并分发处理
return app.exec();
}
常见事件类型
| 事件类 | 触发时机 | 处理方法 |
|---|---|---|
QMouseEvent | 鼠标操作 | mousePressEvent, mouseMoveEvent |
QKeyEvent | 键盘操作 | keyPressEvent, keyReleaseEvent |
QPaintEvent | 需要重绘 | paintEvent |
QResizeEvent | 窗口大小改变 | resizeEvent |
QCloseEvent | 窗口关闭 | closeEvent |
QTimerEvent | 定时器触发 | timerEvent |
事件处理示例
class MyWidget : public QWidget
{
public:
MyWidget(QWidget *parent = nullptr) : QWidget(parent) {}
protected:
// 重写鼠标按下事件
void mousePressEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton) {
qDebug() << "左键点击:" << event->pos();
}
// 调用基类实现(保持默认行为)
QWidget::mousePressEvent(event);
}
// 重写键盘事件
void keyPressEvent(QQKeyEvent *event) override
{
switch (event->key()) {
case Qt::Key_Escape:
close();
break;
case Qt::Key_F1:
showHelp();
break;
default:
QWidget::keyPressEvent(event);
}
}
// 重绘事件
void paintEvent(QPaintEvent *event) override
{
QPainter painter(this);
painter.setBrush(Qt::blue);
painter.drawRect(10, 10, 100, 80);
}
};
事件过滤器
事件过滤器允许一个对象监视另一个对象的事件:
class EventFilter : public QObject
{
protected:
bool eventFilter(QObject *watched, QEvent *event) override
{
if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
qDebug() << "拦截到鼠标事件:" << mouseEvent->pos();
return true; // 消费事件,不再传递
}
return QObject::eventFilter(watched, event); // 继续传递
}
};
// 使用
EventFilter *filter = new EventFilter(this);
ui->button->installEventFilter(filter); // 为按钮安装过滤器
发送自定义事件
// 定义自定义事件类型
const QEvent::Type MyCustomEventType =
static_cast<QEvent::Type>(QEvent::User + 1);
class MyCustomEvent : public QEvent
{
public:
MyCustomEvent(const QString &data)
: QEvent(MyCustomEventType), m_data(data) {}
QString data() const { return m_data; }
private:
QString m_data;
};
// 发送事件
QCoreApplication::postEvent(receiver, new MyCustomEvent("Hello"));
// 处理自定义事件
void MyObject::customEvent(QEvent *event) override
{
if (event->type() == MyCustomEventType) {
MyCustomEvent *myEvent = static_cast<MyCustomEvent*>(event);
qDebug() << "收到自定义事件:" << myEvent->data();
}
}
属性系统
Qt 的属性系统提供了动态访问对象属性的能力。
定义属性
class Person : public QObject
{
Q_OBJECT
// 声明属性
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)
public:
explicit Person(QObject *parent = nullptr) : QObject(parent) {}
// Getter
QString name() const { return m_name; }
int age() const { return m_age; }
// Setter
void setName(const QString &name) {
if (m_name != name) {
m_name = name;
emit nameChanged(name);
}
}
void setAge(int age) {
if (m_age != age) {
m_age = age;
emit ageChanged(age);
}
}
signals:
void nameChanged(const QString &name);
void ageChanged(int age);
private:
QString m_name;
int m_age = 0;
};
使用属性
Person person;
// 通过 setter/getter
person.setName("张三");
person.setAge(25);
qDebug() << person.name(); // "张三"
// 通过元对象系统动态访问
person.setProperty("name", "李四");
QVariant name = person.property("name");
qDebug() << name.toString(); // "李四"
// 获取所有属性
const QMetaObject *meta = person.metaObject();
for (int i = 0; i < meta->propertyCount(); i++) {
QMetaProperty prop = meta->property(i);
qDebug() << "属性:" << prop.name();
}
字符串与容器
Qt 提供了自己的字符串和容器类,与 C++ STL 类似但更适合 Qt 开发。
QString
Qt 的字符串类,支持 Unicode:
// 创建字符串
QString str1 = "Hello";
QString str2 = QStringLiteral("World"); // 编译期优化
// 字符串操作
QString result = str1 + " " + str2; // "Hello World"
result.append("!");
result.prepend("Say: ");
// 格式化
QString formatted = QString("Name: %1, Age: %2").arg("张三").arg(25);
// 与数字转换
int num = QString("42").toInt();
QString str = QString::number(3.14159, 'f', 2); // "3.14"
// 中文字符串
QString chinese = QStringLiteral("你好,世界");
int length = chinese.length(); // 6(字符数,不是字节数)
Qt 容器类
| Qt 容器 | STL 等价 | 特点 |
|---|---|---|
QList<T> | std::vector<T> | 通用列表,推荐首选 |
QVector<T> | std::vector<T> | 连续内存存储 |
QLinkedList<T> | std::list<T> | 双向链表 |
QMap<Key, T> | std::map<Key, T> | 有序字典 |
QHash<Key, T> | std::unordered_map<Key, T> | 哈希表,查找更快 |
QSet<T> | std::set<T> | 集合 |
QStringList | - | QString 的列表 |
// QList 示例
QList<int> numbers;
numbers << 1 << 2 << 3; // 流式插入
numbers.append(4);
numbers.prepend(0);
for (int num : numbers) {
qDebug() << num;
}
// 使用 Java 风格迭代器
QListIterator<int> iter(numbers);
while (iter.hasNext()) {
qDebug() << iter.next();
}
// QMap 示例
QMap<QString, int> scores;
scores["张三"] = 90;
scores["李四"] = 85;
scores.insert("王五", 95);
// 遍历
for (auto it = scores.begin(); it != scores.end(); ++it) {
qDebug() << it.key() << ":" << it.value();
}
// C++11 范围 for
for (const auto &[name, score] : scores.toStdMap()) {
qDebug() << name << score;
}
总结
Qt 的核心概念相互关联,形成完整的开发框架:
- 元对象系统 - 提供反射、信号槽、属性等动态特性
- 信号槽 - 对象间松耦合的通信机制
- 对象树 - 自动内存管理
- 事件系统 - 响应用户交互和系统事件
- 属性系统 - 动态访问对象状态
理解这些概念后,你就能更好地利用 Qt 开发高效、稳定的应用程序。