跳到主要内容

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 作为槽的注意事项

  1. 生命周期管理:Lambda 捕获的变量必须在其被调用时仍然有效
  2. 上下文对象:提供第三个参数(上下文对象)可以确保 Lambda 在对象销毁时自动断开
  3. 值捕获 vs 引用捕获:对于局部变量使用引用捕获时要格外小心
  4. 性能考虑:频繁触发的信号(如鼠标移动)中使用复杂 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 的析构顺序如下:

  1. 父对象的析构函数开始执行
  2. 遍历子对象列表(按添加顺序的逆序)
  3. 对每个子对象调用 delete
  4. 子对象的析构函数执行(递归处理孙对象)
  5. 父对象的析构函数完成

内存管理规则

// 规则 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 的核心概念相互关联,形成完整的开发框架:

  1. 元对象系统 - 提供反射、信号槽、属性等动态特性
  2. 信号槽 - 对象间松耦合的通信机制
  3. 对象树 - 自动内存管理
  4. 事件系统 - 响应用户交互和系统事件
  5. 属性系统 - 动态访问对象状态

理解这些概念后,你就能更好地利用 Qt 开发高效、稳定的应用程序。