关于QT信号、槽、槽函数的讲解
也是好久没有发帖子了,最近博主主要还是在边学QT边完成任务,所以进度很慢,但确实在这几天对于QT自身槽和信号这类特殊的机制有了一定简单的理解,所以还是想记录下来,如果有初学者看到帖子对他有一定的帮助,那自然也会让我感到十分荣幸
一. 什么是信号与槽
首先按照专业术语的描述是,QT的信号与槽机制是一种对象间通信机制。直观的可以表现出一个对象发出信号,另一个对象通过槽函数响应这个信号,从而实现多模块之间的解耦通信。这个也是属于Qt框架最强大的特性之一,也是尤其适合GUI事件响应和模块间协作。
通过举一个简单的例子,家门口有一个门铃按钮(就是等同于我们的信号),我们按了一下,屋里的电铃声就响了(槽函数)。
按门铃 = 发出信号 电铃声响了 = 槽函数被触发 按下和响铃之间 = connect()建立连接。
二. 信号/槽基本概念
信号(Signal)是QObject的子类在特定事件发生时自动发出的“通知”,就是一个事件的广播。
而槽(Slot)可以与信号连接的普通成员函数,专门用于响应通知,也就是“接收广播后做事”的函数
connect(),Qt提供的函数,用于建立信号与槽之间的绑定关系,“把信号线插进槽口”
emit,是一个关键字,用于触发自定义信号,也就是程序员手动“发出广播”
QObject,QT所有支持信号、槽的类的基类,信号槽必须用它派生出来的类
三. 广播机制
在QT的信号与槽机制种,广播机制指的是。信号一旦发出,系统会自动通知所有已连接的槽函数执行,无需显示调用,且发送者不关心接收者的存在与行为。
这种机制体现了一种松耦合、发布-订阅式的通信模型,是Qt框架中组件之间高效协作的核心基础。
解耦(Decoupling),信号发送者和接收者之间并没有直接依赖关系。发送者只负责发出信号,而不需要知道是否有接收者,更不需知道接收者是谁或要做什么。这样可以使模块之间保持独立,便于维护或拓展。
一对多连接,一个信号可以同时连接多个槽函数。当信号被发出时,所有已经连接的槽将会被系统一次调用,按连接顺序一次执行。这样可以使一个事件可以触发多个处理逻辑,从而实现模块联动
多对一连接:多个 不同的信号可以连接到同一个槽函数。当任意一个信号被发出时,该槽函数都会被执行。这使得不同来源的事件可以集中处理
自动调用:当信号发出时,所有与之连接的槽函数会被自动执行,对于我们开发者来说并不需要手动调用。Qt框架内部通过事件系统和元对象机制完成槽函数的自动触发,我们只需要专注于逻辑实现
在Qt中信号是广播式的,信号本身只是简单的发出,并没有指定特定的接收者。接收者(槽函数)会通过connect()来“订阅”这个信号。
可以理解为信号式“广播”出去的,它不会知道谁会接收到它,接收者通过connect()函数明确指定自己要接受哪个信号,所以如果没有明确指定的接收者,那么这个函数就无法触发任何行为,因为没有对象响应找个信号
在代码中可以这么理解,nullptr表示没有指定接收者,那么即使按钮被点击,信号也不会触发任何函数
// 如果没有指定接收者,信号会发出,但没有接收者来处理它
QObject::connect(&button, &QPushButton::clicked, nullptr, nullptr);
刚开始我将Qt的广播形式和网络广播的形式弄混淆了,这里还是想做一个简单的澄清,是两个不同的概念
网络广播在网络中,广播通常指的是将信息发送到网络上的所有设备,而不仅仅是发送给某一个指定的接收者。常见的网络广播是无差别地将信息发送给网络上的所有设别,而接收这些广播的设备通常是主动应答的,
例如UDP广播,数据包被发送到一个特定的广播地址(如255.255.255.255),网络上的所有设备都有机会接收到这个广播。然后,每个接收到广播信息的设备可以选择是否回应(例如一般就是发送一个响应报文)
在Qt中,信号的“广播”并不是指将消息发送给所有对象,而是一个信号可以连接到多个槽函数,因此多个接收者可以对同一个信号做出响应。信号本身并不主动去寻找接收者,而是通过connect()函数明确指定了哪些接收者应该处理这个信号
信号发出:信号并不是想网络广播那样发送给所有设备,而是只从一个对象发出。可以想像是在说“我发生了某个事件,看下是否会有人响应我”
“接收者”:只有通过connect()函数明确指定了接收者(即槽函数)后,信号才会被“接收”,并且接收者会执行相应的操作。这个过程并不需要接收者主动应答,而是由QT框架自动处理的
四. 信号与与槽的连接方式及适用场景
我们将他分为一下五种
系统信号(Qt自带) + 系统槽(如QAPPlication::quit)这种组合方式可以快速调用系统功能。
信号部分是&QPushButton::clicked,这是Qt框架内置的标准信号,属于系统控件QPushButton,当按钮被点击时自动发出。所有这是系统信号(也就是由Qt控件自带)
槽部分 &QApplication::quit,这是Qt提供的一个系统级槽函数,作用是关闭应用程序主事件循环,相当于”退出应用“所以是系统槽(由Qt提供)
#include "mainwindow.h"#include <QApplication>
#include <QPushButton>int main(int argc, char *argv[])
{QApplication a(argc, argv);MainWindow w;QPushButton button("Quit",&w);button.move(100,100);QObject::connect(&button, &QPushButton::clicked,&a,&QApplication::quit);w.show();return a.exec();
}
系统信号 + 自定义槽函数 是目前较为常见的方式,如按键触发逻辑。那么同样的代码逻辑如何在系统信号 + 自定义槽函数中体现。
在下面这段代码中系统信号自然是,Qt自带的信号,如QPushButton::clicked(),表示按钮被点击。
而自定义的槽函数,是我们写在类中的处理函数,在这里是MainWindow::onButtonClicked()
所以简单来理解“系统信号 + 自定义槽函数”就是——,按钮这种系统组件发出信号,用户自己写的函数去接收并响应
QpushButton内部有个信号:void clicked(bool checked = false);当我们用connect()把这个信号和函数onButtonClicked()连接在一起,当我们点击按钮的时候,按钮会发出clickced信号(也就是前面提到的广播机制)。Qt自动调用我们自定义的槽函数onButtonClicked来处理事件
//在MainWindow.cpp中MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);QPushButton *button =new QPushButton("Quit",this);button->move(100,100);connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);}MainWindow::~MainWindow()
{delete ui;
}void MainWindow::onButtonClicked()
{qDebug() << "按钮被点击";
}//在MainWindow.h中#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private:Ui::MainWindow *ui;private slots:void onButtonClicked();
};
#endif // MAINWINDOW_H
自定义信号 + 系统槽 一般适用于自定义行为影响系统行为
我们通过程序执行的顺序来理解代码,首先第一步显示从连接按钮点击信号开始,对应mainwindows.cpp,connect(),当button被点击的时候,调用MainWindow的onButtonclicked()方法。button是发出信号的对象,&QPushButton::Clicked()是系统信号(点击按钮后发出)。
但是这里就要有疑惑了,明明是使用我们的自定义信号,为什么还是是系统信号呢?先不着急我们先分析完,之后再进行解释
this是当前窗口对象(MainWindow),&Mainwindow::onButtonClicked是我们自定义的槽函数,响应按钮点击
在按钮点击过后,在void MainWindow这个我们自己编写的槽函数中,在这个函数中我们有发出了另外一个信号quitApp(),这是我们在头文件中signals:定义的自定义信号
也就是说,刚开始按钮被点击后发出系统信号,触发了我们自己的函数onButtonClicked(自定义槽),然后onButtonClicked中又调用了emit quitApp(发出我们的自定义信号)
总结一下我们代码中完成的工作是,我们点击按钮触发系统信号,然后Qt自动调用我们定义的槽函数,我们的槽函数里发出quitApp(),里面是我们自定义的信号
//在mainwindow.h#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private:Ui::MainWindow *ui;private slots:void onButtonClicked();//自定义信号
signals:void quitApp();};
#endif // MAINWINDOW_H//在mainwindow.cpp中#include "mainwindow.h"
#include "ui_mainwindow.h"#include "QPushButton"
#include "QDebug"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);QPushButton *button =new QPushButton("Quit",this);button->move(100,100);connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);connect(this,&MainWindow::quitApp,qApp,&QApplication::quit);
}MainWindow::~MainWindow()
{delete ui;
}void MainWindow::onButtonClicked()
{emit quitApp();
}
现在我们来解释一下,为什么整个程序执行的流程始终是系统信号。那为什么又还要发出自定义信号,博主后面了解到,Qt设计的很巧妙的地方就在于,信号链的“解耦”和“转发”能力。
例如我们需要在点击按钮的时候退出程序,还想要加入记录日志、播放音效、保存设置等,如果全部写在onButtonclicked()里,那么这个槽函数就会越来越大,越来月耦合。
那么我们在遇到这样的情况下就可以采取,在onButtonClicked()里发出一个自定义信号quitApp(),然后让其他模块自由决定是否接收这个信号,或者怎么处理信号
例如如下情况,把“按钮点击的事件”,拆成了“点击后发出quitApp”信号,然后让其他的函数也来订阅这个信号
connect(this, &MainWindow::quitApp, qApp, &QApplication::quit); // 退出程序
connect(this, &MainWindow::quitApp, logger, &Logger::writeQuitLog); // 写日志
connect(this, &MainWindow::quitApp, soundPlayer, &Player::playExitSound); // 播放音效
我们再举一个实际的例子,如果我们把多个行为都写再onButtonClicked中
void MainWindow::onButtonClicked() {qApp->quit(); // 退出程序settingsManager->save(); // 保存设置logger->log("用户点击退出"); // 写日志soundPlayer->play("bye.wav"); // 播放音效
}
我们将所有的行为都写在了一个函数中,主窗口知道太多别的类(Logger、Player、SettingManager),如果将来我们选哟将其中的功能进行改变,要修改的地方就有很多,而且假如其他的地方也需要退出行为,还要复制代码到其他地方
// MainWindow.h
signals:void quitApp(); // ⛳️ 只发信号,不处理具体细节
void MainWindow::onButtonClicked() {emit quitApp(); // ⛳️ 你们谁爱处理谁处理,我不管
}
在程序初始化的时候就可以通过信号连接不同的行为,所以总结就是,如果在一个槽函数中做了太多和“自己职责无关的事”,就可以考虑广播事件,让其他人来听
connect(mainWindow, &MainWindow::quitApp, qApp, &QApplication::quit);
connect(mainWindow, &MainWindow::quitApp, logger, &Logger::logQuit);
connect(mainWindow, &MainWindow::quitApp, settings, &Settings::save);
connect(mainWindow, &MainWindow::quitApp, soundPlayer, &Player::playExitSound);
自定义信号 自定义槽函数 一般常用于模块解耦、自主通信,如果针对于自定义槽函数和信号,肯定会有更高的自由度,我们还是结合代码来分析
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private:Ui::MainWindow *ui;private slots:void onButtonClicked(); //自定义槽(用于发送信号)void handClose(); //自定义槽(响应信号关闭窗口)//自定义信号
signals:void closeApp(); //自定义信号};
#endif // MAINWINDOW_H//在mainwindow.cpp中#include "mainwindow.h"
#include "ui_mainwindow.h"#include "QPushButton"
#include "QDebug"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);QPushButton *button =new QPushButton("Quit",this);button->move(100,100);connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);connect(this,&MainWindow::closeApp,this,&MainWindow::handClose);
}MainWindow::~MainWindow
()
{delete ui;
}void MainWindow::onButtonClicked()
{emit closeApp();
}void MainWindow::handClose(){qDebug() << "收到信号,关闭窗口";this->close();}
首先还是在头文件总声明一个自定义信号,以及private slots中全是自定义的槽函数,也是在头文件中声明。
然后在mainwindow中的构造函数中进行连接
,connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
//这行是系统信号(QPushButton::clicked)
//->自定义槽(onButtonClicked)作用是:当按钮被点击,就会自动调用onButtonClicked()这个槽函数
connect(this, &MainWindow::closeApp, this, &MainWindow::handleClose);这行是自定义信号(closeApp)->自定义槽(handleClose)作用就是当你发出closeApp信号时,Qt就会自动去调用handleClose()这个函数。
然后在槽函数中发出信号,这个函数并不会直接关闭窗口。它只是发出一个“广播”,说明我准备关闭了
void MainWindow::onButtonClicked()
{emit closeApp();
}
最后在响应的槽中执行操作
void MainWindow::handleClose()
{this->close(); // 真正关闭主窗口
}
以及最后任意信号 + Lambda表达式 通常简洁高效,适合小型逻辑处理。这个机制是Qt信号槽机制中最灵活也是最现代的的用法之一,非常适合简介逻辑、快速响应、小功能点,无需定义额外的槽函数,直接在connect()中用Lambda表达式表达处理逻辑
Lambda是一种匿名函数(临时的小函数),语法如下,一般用于只想快速响应一下,并不想为了这个操作特地去写某个函数
[]() {// 执行的代码
};
例如。如果只是临时逻辑、简单调试、小按钮响应等首选Lambda,但是如果逻辑复杂、选哟复用、设计多个模块之间的通信还是尽量选择传统的槽函数。但是如果在大型的项目中涉及到多个对象协作、跨模块、需要事件驱动使用自定义信号 + 自定义槽
QPushButton *button = new QPushButton("点我", this);
button->move(100, 100);// 用 lambda 直接响应点击
connect(button, &QPushButton::clicked, [](){qDebug() << "👋 Lambda:按钮被点击啦!";
});
博主去查了一下Lambda的具体原理,它是C++ 11引入的一种匿名函数(inline function)是临时写在某个地方的小函数,不需要在别的地方声明或命名,基本语法如下
[capture](parameter_list) -> return_type {// function body
}
但大部分时候还是选择写成简略的写法
[]() {qDebug() << "Hello Lambda!";
};