一、放上去的按钮"点不动"------如何让点击操作绑定对应逻辑
当我们在窗口中建立了一个Label和Button
我们想通过按钮控制Label上的文本,直接点击按钮没有任何反应,那么该如何实现呢------这里就需要引入一个Qt的机制------信号槽
二、信号槽是什么:发件人-收件人模型
2.1 一句话概括
信号(Signal)= 事件发生时发出的"通知"
槽(Slot)= 收到通知后执行的"处理函数"
连接(Connect)= 把通知和处理器绑定的动作
2.2 一个生活化的比喻
想象一下你点外卖:
| 现实场景 | Qt 对应 |
|---|---|
| 你(用户) | 信号的发送者(如按钮) |
| 「下单」这个动作 | clicked 信号 |
| 商家(收单) | 槽函数(如 onOrderPlaced) |
| 外卖平台订单系统 | QObject::connect() |
关键点 :你点完单不需要知道是哪个骑手送餐,商家也不需要提前知道是谁点的单。下单动作(信号)和接单处理(槽)通过订单系统(connect)解耦。
这就是 Qt 信号槽最核心的设计思想:让对象之间松耦合。
2.3 例子:按钮点击改变文字
cpp
QPushButton* btn = new QPushButton("点我", this);
QLabel* label = new QLabel("等待中", this);
connect(btn, &QPushButton::clicked, label, &QLabel::clear);
btn->show();
label->show();
上面这行 connect 的含义:
"当
btn发出clicked信号时,调用label的clear方法。"
- 发送方:
btn,不知道接收方是谁 - 接收方:
label,不知道谁会调用自己 - 中介:
QObject::connect()帮它俩搭上线
这种"谁都不认识谁,但能合作 "的能力,就是 Qt 信号槽的灵魂。
在添加了这行connect代码后我们再次执行程序并且点击按钮,此时Label中的文本就发生了改变

2.4 常见信号速查表
Qt 控件自带了一大堆信号,下面是最常用的 15 个:

| 控件 | 信号 | 触发时机 |
|---|---|---|
QPushButton |
clicked() |
鼠标按下并释放(完整点击) |
QPushButton |
pressed() |
鼠标按下 |
QPushButton |
released() |
鼠标释放 |
QCheckBox |
stateChanged(int) |
选中状态改变 |
QRadioButton |
toggled(bool) |
选中状态切换 |
QLineEdit |
textChanged(QString) |
文本每次变化(含代码设置) |
QLineEdit |
textEdited(QString) |
仅用户输入触发 |
QLineEdit |
returnPressed() |
按回车键 |
QComboBox |
currentIndexChanged(int) |
当前选项改变 |
QSlider / QSpinBox |
valueChanged(int) |
数值改变 |
QListWidget |
itemClicked(QListWidgetItem*) |
列表项被点击 |
QListWidget |
itemDoubleClicked(QListWidgetItem*) |
双击 |
QTimer |
timeout() |
定时器到时 |
QAction |
triggered(bool) |
菜单/工具栏动作触发 |
QTableWidget |
cellClicked(int row, int col) |
单元格点击 |
速记口诀:
- 大部分控件都有
xxxChanged类信号(状态变化触发) - 输入类有
textChanged/valueChanged(每次变化都触发) - 按钮有
clicked/pressed/released(点击生命周期三态) - 列表/表格有
itemClicked/itemDoubleClicked/itemSelectionChanged
最常用的三个信号(背下来):
cpp
btn->clicked() // 按钮被点
edit->textChanged(QString) // 文本框内容变化
timer->timeout() // 定时器到时
2.5 什么是自定义信号
Qt 自带的信号只覆盖了"控件内部事件"。但你的业务逻辑里需要通知别人某事发生了------这就需要自定义信号。
自定义信号 = 你自己定义一个事件。
cpp
class Downloader : public QObject
{
Q_OBJECT
signals:
void downloadProgress(int percent); // 下载进度
void downloadFinished(const QString& filePath); // 下载完成
void downloadFailed(const QString& error); // 下载失败
};
为什么要自定义? 看个例子:
cpp
// ❌ 不用信号:强耦合
void Downloader::finishDownload() {
m_ui->setProgress(100); // 必须 include UI 头文件
m_db->saveRecord(m_filePath); // 必须知道 DB 对象
m_logger->log("下载完成"); // 必须知道 logger
}
// 加一个新功能?改三处
// ✅ 用信号:松耦合
void Downloader::finishDownload() {
emit downloadFinished(m_filePath); // 只发通知
}
// 谁关心?谁就 connect。下载器不需要知道 UI/DB/logger 的存在
自定义信号的三条铁律:
- 必须在
signals:块里声明(不是普通成员函数) - 只需声明,不能写实现(moc 自动补)
- 返回值必须是
void
2.6 怎么发送信号(emit)
用 emit 关键字发送:
cpp
void Downloader::onDataReceived(int bytes, int total)
{
int percent = bytes * 100 / total;
emit downloadProgress(percent); // 发送进度信号
if (bytes >= total) {
emit downloadFinished(m_filePath); // 发送完成信号
} else if (m_errorOccurred) {
emit downloadFailed("网络中断");
}
}
emit 是什么?
cpp
// emit 在 qobjectdefs.h 里就这么定义:
#define emit
// 没错,展开后是空的!
emit 没有任何运行时作用,纯标记------告诉读者"这里在发信号"。
真正的工作是 moc 在背后做的:
cpp
// moc 生成的代码(简化)
void Downloader::downloadProgress(int percent) {
QMetaObject::activate(this, &staticMetaObject, 0, ...);
// ^ 0 是信号在元信息表里的索引
}
// emit downloadProgress(percent) 实际调用的是这个自动生成的函数
emit 的本质 :emit 就是个文档化标记,让你和读代码的人一眼看出"这里是发信号的地方"。
对比三种 emit 写法(功能完全一样):
cpp
emit downloadProgress(percent); // ✅ 推荐,可读性最好
downloadProgress(percent); // ✅ 能跑,但别人不知道这是信号
Q_EMIT downloadProgress(percent); // Q_EMIT 是为了在某些宏环境里也能用
三、信号槽的 5 种写法
connect() 一共 5 种常用写法,推荐度从上到下递减:

写法 1:函数指针(Qt 5+ 推荐)
cpp
connect(btn, &QPushButton::clicked, label, &QLabel::clear);
优点 :编译期检查,信号槽签名不匹配会直接编译报错 。重载函数要写完整类型,例如 static_cast<void (QLabel::*)(const QString&)>(&QLabel::setText)。
写法 2:Lambda(C++ 11,最常用)
cpp
connect(btn, &QPushButton::clicked, this, [=](){
label->setText("已点击");
});
注意 :第 3 个参数是 context(一般是 this),用来在 this 销毁时自动断开所有连接,强烈建议加。
写法 3:Qt 4 宏(老代码里常见)
cpp
connect(btn, SIGNAL(clicked()), label, SLOT(clear()));
缺点 :纯字符串匹配,编译期不检查。拼错单词运行时才崩:
cpp
connect(btn, SIGNAL(cilcked()), label, SLOT(clear())); // 拼写错误
// 编译能过!运行不报错!按钮点了没反应!
新项目不要再用这种写法。
写法 4:信号连信号
cpp
connect(btn1, &QPushButton::clicked, btn2, &QPushButton::click); // 点 A 触发 B
把一个信号的"转发"给另一个对象。
写法 5:成员函数对象(Qt 5.10+,最优雅)
cpp
connect(btn, &QPushButton::clicked, this, [this](){ onBtnClicked(); });
// 改进:直接传成员函数指针 + lambda 包装
四、信号槽的工作原理(深入源码)
4.1 它怎么知道该调用谁?
每次 connect 调用,本质上是在某个对象的私有列表里塞了一行记录:
cpp
// QObject 内部伪代码
struct Connection {
QObject* sender;
int signal_index;
QObject* receiver;
int method_index;
Qt::ConnectionType type;
};
class QObjectPrivate {
QList<Connection> connections; // 关键
};
4.2 信号发出时发生了什么?
当你执行 emit mySignal() 时,moc 生成的代码会做这件事:
cpp
// moc 自动生成的代码(简化版)
void MyClass::mySignal() {
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
// ^ signal_index
}
QMetaObject::activate 内部就是遍历 connections 列表,逐个调用槽函数。
完整流程图:

4.3 moc 是什么?为什么需要它?
Qt 的信号槽是用普通 C++ 实现的"魔法" ------但这个魔法不是免费的,需要一个叫 moc(Meta-Object Compiler,元对象编译器) 的预处理程序。
moc 干的事:
- 扫描所有
Q_OBJECT宏标记的类 - 生成一份
moc_xxx.cpp文件 - 这份文件里包含:
- 类的元信息(
staticMetaObject) - 信号函数的实现(你只写了声明,实现是 moc 塞进去的)
- 槽函数的调用索引(让运行时能根据索引找到槽)
- 类的元信息(

一个反直觉的事实 :你在头文件里写的
signals: void mySignal();根本没有函数体 。moc帮你补上了。所以你编译时报"undefined reference to vtable for MyClass",99% 是因为忘了加
Q_OBJECT宏。
五、自定义信号与槽:完整实战
光说不练假把式。来一个跨窗口通信的完整例子:主窗口点按钮,弹窗收到信号后变色。
5.1 项目结构
SignalDemo/
├── mainwindow.h
├── mainwindow.cpp
├── subwidget.h
├── subwidget.cpp
└── main.cpp
5.2 自定义信号(SubWidget)
cpp
// subwidget.h
#ifndef SUBWIDGET_H
#define SUBWIDGET_H
#include <QWidget>
class SubWidget : public QWidget
{
Q_OBJECT
public:
explicit SubWidget(QWidget* parent = nullptr);
signals:
// 信号只需声明,无需实现(moc 会自动补)
void colorChanged(const QString& color);
void closed();
private slots:
void onRedBtnClicked();
void onBlueBtnClicked();
private:
void setupUI();
};
#endif // SUBWIDGET_H
cpp
// subwidget.cpp
#include "subwidget.h"
#include <QPushButton>
#include <QVBoxLayout>
SubWidget::SubWidget(QWidget* parent) : QWidget(parent)
{
setupUI();
}
void SubWidget::setupUI()
{
setWindowTitle("颜色选择器");
setFixedSize(300, 200);
QVBoxLayout* layout = new QVBoxLayout(this);
QPushButton* redBtn = new QPushButton("红色", this);
QPushButton* blueBtn = new QPushButton("蓝色", this);
layout->addWidget(redBtn);
layout->addWidget(blueBtn);
connect(redBtn, &QPushButton::clicked, this, &SubWidget::onRedBtnClicked);
connect(blueBtn, &QPushButton::clicked, this, &SubWidget::onBlueBtnClicked);
}
void SubWidget::onRedBtnClicked()
{
emit colorChanged("red"); // 发出信号
}
void SubWidget::onBlueBtnClicked()
{
emit colorChanged("blue");
}
5.3 接收方(MainWindow)
cpp
// mainwindow.cpp
#include "mainwindow.h"
#include "subwidget.h"
#include <QPushButton>
#include <QVBoxLayout>
#include <QDebug>
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent)
{
setWindowTitle("主窗口");
setFixedSize(400, 300);
QWidget* central = new QWidget(this);
setCentralWidget(central);
QVBoxLayout* layout = new QVBoxLayout(central);
m_label = new QLabel("当前颜色: 白色", this);
m_label->setAlignment(Qt::AlignCenter);
m_label->setStyleSheet("background: white; color: black; font-size: 20px;");
layout->addWidget(m_label);
QPushButton* openBtn = new QPushButton("打开颜色选择器", this);
layout->addWidget(openBtn);
connect(openBtn, &QPushButton::clicked, this, [=](){
if (!m_sub) {
m_sub = new SubWidget(this);
// 关键:把 SubWidget 的信号接到 MainWindow 的槽
connect(m_sub, &SubWidget::colorChanged,
this, &MainWindow::onColorChanged);
}
m_sub->show();
});
}
void MainWindow::onColorChanged(const QString& color)
{
m_label->setText("当前颜色: " + color);
QString style = QString("background: %1; color: white; font-size: 20px;")
.arg(color);
m_label->setStyleSheet(style);
qDebug() << "收到颜色信号:" << color;
}
cpp
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QLabel>
#include "subwidget.h"
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = nullptr);
~MainWindow() = default;
private slots:
void onColorChanged(const QString& color);
private:
QLabel* m_label = nullptr; // 显示"当前颜色:xxx"的标签
SubWidget* m_sub = nullptr; // 颜色选择子窗口
};
#endif // MAINWINDOW_H
跑起来点按钮,主窗口就能收到子窗口的信号------这就是跨窗口通信。
六、信号槽的高级玩法
6.1 一个信号 connect 多个槽
cpp
connect(btn, &QPushButton::clicked, this, &MyWidget::onLog);
connect(btn, &QPushButton::clicked, this, &MyWidget::onSave);
connect(btn, &QPushButton::clicked, this, &MyWidget::onNotify);
// 按钮一点,三个槽都会按顺序执行
默认是同步的 :按 connect 顺序依次调用,都在信号发出者的线程里跑。
6.2 多个信号 connect 一个槽
cpp
connect(btn1, &QPushButton::clicked, this, &MyWidget::onAnyBtnClicked);
connect(btn2, &QPushButton::clicked, this, &MyWidget::onAnyBtnClicked);
connect(btn3, &QPushButton::clicked, this, &MyWidget::onAnyBtnClicked);
6.3 断开连接
cpp
disconnect(btn, &QPushButton::clicked, this, &MyWidget::onLog);
Lambda 默认无法精确断开(没有名字)。如需断开,用「成员函数对象」写法。
6.4 信号参数可以比槽多
cpp
// 信号带两个参数
void dataReady(int code, const QString& msg);
// 槽只接一个
void onDataReady(int code);
// 可以:槽的参数 <= 信号的参数,多余的被丢弃
这是 Qt 的硬规则:槽的参数必须能完全匹配信号的前 N 个参数。
6.5 第 5 个参数:连接类型
cpp
connect(sender, &Sender::sig, receiver, &Receiver::slot, Qt::QueuedConnection);
| 类型 | 含义 |
|---|---|
AutoConnection(默认) |
自动判断(同线程直连,跨线程队列) |
DirectConnection |
直接调用,在信号发出者线程同步执行 |
QueuedConnection |
投递事件队列,在接收者线程异步执行 |
BlockingQueuedConnection |
队列 + 阻塞等待槽执行完 |
UniqueConnection |
标志位,禁止重复 connect(` |
跨线程必用 QueuedConnection。
七、5 大常见坑

坑 1:拼写错误不报错(Qt 4 宏的坑)
cpp
connect(btn, SIGNAL(cilcked()), label, SLOT(clear())); // clicked 拼错
// 编译通过!运行没反应!debug 到天荒地老!
解决:用函数指针写法。
坑 2:Lambda 捕获了悬空指针
cpp
QWidget* w = new QWidget;
connect(btn, &QPushButton::clicked, [=](){
w->setText("..."); // w 可能在别处被 deleteLater 了
});
解决 :第 3 个参数加 this 作为 context,this 销毁时自动断开。或者 lambda 里用 QPointer 判空。
坑 3:重复 connect 导致多次触发
cpp
for (int i = 0; i < 3; ++i) {
connect(btn, &QPushButton::clicked, this, &MyWidget::onClick);
}
// 按钮一点,onClick 执行 3 次!
解决 :加 Qt::UniqueConnection 标志,或先 disconnect。
坑 4:跨线程直接 connect 导致崩溃
cpp
// 工作线程里
connect(networkMgr, &QNetworkAccessManager::finished,
this, &MyWidget::onReply); // 接收者在主线程
// 默认 AutoConnection 在跨线程时会自动用 QueuedConnection,没事
// 但如果强写 DirectConnection 就会崩
解决 :跨线程明确写 Qt::QueuedConnection。
坑 5:析构了还在用(lambda 里 this 没了)
cpp
auto* w = new MyWidget;
connect(timer, &QTimer::timeout, [=](){
w->update(); // w 被外部 deleteLater 后 lambda 还在队列里
});
// 崩溃!在已 delete 的对象上调用 update
解决 :用 QPointer<MyWidget> w 替代裸指针,lambda 里 if (w) w->update();。
八、总结:一张图记住信号槽

