1. 信号和槽函数
之前我们在Linux中也学过信号。虽然Qt的信号和Linux中的信号是完全不同的概念,但两者在抽象层面上确实有异曲同工之处。
Linux中的信号是系统内部的通知机制或进程间的通信方式,其本质是操作系统层面上的异步中断机制。一旦信号被触发,内核会中断当前进程的执行流,直接跳转执行预先注册的信号处理器函数。
Qt信号则是应用程序框架层面的通信机制,它基于Qt的事件循环。当信号被emit触发时,Qt会根据连接类型决定是立即同步调用槽函数(本质上也是一种回调函数),还是将信号封装为事件放入队列中异步等待处理。
这两种信号机制都遵循一个共同的抽象模型------三个核心要素:
三个核心要素:
信号源:谁发出的信号。Linux中是内核、硬件或其他进程;Qt中是QObject派生类的实例对象。
信号类型:信号的标识。Linux中是固定的31个标准信号编号(如SIGINT、SIGSEGV);Qt中是由开发者任意定义的可携带复杂参数的信号。
信号处理方式:谁来处理、如何处理。Linux中通过signal()或sigaction()注册处理器函数;Qt中通过connect()将信号与槽函数绑定。
2. connect函数
cpp
connect(sender, &Sender::signal, receiver, &Receiver::slot);
sender:发送信号的对象指针(必须是QObject派生类的指针)------谁发的信号
&Sender::signal:发送者类中的信号类型指针(本质是成员函数指针)------发送的什么类型的信号
receiver:接收信号的对象指针(也必须是QObject派生类指针,且该对象必须存活到信号触发时)------谁来处理这个信号
&Receiver::slot:接收者类中的槽函数指针(可以是普通槽函数、信号、Lambda表达式、全局函数)------怎么处理这个信号
注意:Qt中的connect函数和Linux中 TCP socket中建立连接的函数,没有任何关系,只是名字恰巧一样了
下面我们在widget.cpp的构造函数中实现一个可以关闭窗口的按钮:
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* close = new QPushButton(this);
close->setText("关闭窗口");
close->move(200,200);
connect(close,&QPushButton::clicked,this,&QWidget::close);
}
Widget::~Widget()
{
delete ui;
}
运行后,界面出现一个"关闭窗口"的按钮,点击按钮就可以实现窗口的关闭:

那么问题来了,我们怎么知道有哪些信号和槽函数呢?就像这里的clicked和close
答案就是------看官方文档!磨刀不误砍柴工,用的多了记住的也就多了,不记得的就去看看文档。
有时候在对应的类中没有找到相对应的信号或者槽,就找找这个类的父类或者祖宗类。
3. 自定义槽函数
3.1 普通成员函数定义
自定义槽函数其实就和自定义一个普通的函数没什么区别
cpp
//widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget() override;
//自定义一个槽函数
void handlePush();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
cpp
//widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
#include <QMessageBox>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* push = new QPushButton(this);
push->setText("按下");
push->move(200,200);
//使用自定义的槽函数
connect(push,&QPushButton::clicked,this,&Widget::handlePush);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handlePush()
{
//设计一个弹出窗口提示,显示"按钮被按下!"
QMessageBox::information(this, "提示", "按钮被按下!");
}
运行后,点击按钮:

之前的版本中,槽函数必须放到public/private/protected slots:下,Qt5之后就可以不用这样写了。但是在使用图形化界面自动生成槽函数时,也会自动生成。
3.2 图形化界面自动生成
下面我们来尝试图形化界面自动生成槽函数。双击.ui文件进入图形化界面,拖拽一个按钮到界面中,修改按钮显示文字,右键单击按钮

选择"转到槽",可以看到和按钮(QPushButton)及其父类和祖宗类相关的所有信号

我们选中第一个clicked并确定或者双击clicked,就会自动生成槽函数声明和定义并直接跳转到对应槽函数定义中,我们可以直接在这里写相关的代码。
但是我们会发现这样操作后,并没有自动生成connect函数,那么信号和槽怎么连接呢?
我们观察一下,代码自动生成的槽函数的函数名(on_控件名_信号类型):

当函数名符合上述规则后,Qt就能自动把信号和槽建立上联系。

所以,如果是通过代码手动创建控件并自定义槽函数时,我们还是要使用connect;如果是使用图形化界面的方式来实现自定义槽函数,就可以不用管connect。
3.3 Lambda表达式
lambda表达式比较常用于回调函数,而槽函数本质上也是一种回调函数,所以也常用lambda表达式写槽函数,而且lambda表达式写槽函数会更简洁。
cpp
//widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget() override;
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
#include <QMessageBox>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(ui->pushButton,&QPushButton::clicked,this,[this](){
QMessageBox::information(this, "提示", "lambda表达式实现槽函数");
});
}
Widget::~Widget()
{
delete ui;
}
点击界面按钮后:

如果我们的槽函数只是一次性使用,并且比较简短就可以采用槽函数的形式。
另外也要确认捕获到lambda内部的变量是有意义的,因为槽函数执行的时机是不确定的(无法确定用户什么时候触发),我们要确保无论何时用户点击了按钮,捕获到的变量都能正确使用。
4. 自定义信号
自定义信号是你在自己的类中声明的、由moc自动生成实现的特殊成员函数。它的作用是在特定事件发生时通知其他对象。
class MyClass : public QObject {
Q_OBJECT
signals: // 信号区域
void mySignal();
};
自定义信号使用关键规则:
-
信号必须在signals: 关键字下声明(这是Qt的特殊关键字)
-
信号只需要声明,不需要实现(moc会自动生成实现)
-
信号的返回值必须是void
-
信号可以有任意数量和类型的参数
-
在 signals: 下默认是protected
-
信号被触发时要用到emit关键字(也是Qt特有的关键字),但是emit什么实际作用都没有,去掉emit代码也不会有任何影响,但是一般在开发过程中还是会加上它,增加代码的可读性
下面我们来演示一下,通过自定义信号的方式也实现一个点击按钮出现弹窗的效果:
cpp
//widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget() override;
//自定义一个槽函数
void handlePush();
signals:
//自定义一个信号
void MySignal();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
cpp
//widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
#include <QMessageBox>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(this,&Widget::MySignal,this,&Widget::handlePush);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handlePush()
{
QMessageBox::information(this, "提示", "按钮被按下!");
}
void Widget::on_pushButton_clicked()
{
//触发自定义的MySignal信号
emit MySignal();
//MySignal();
}

在widget类中自定义一个MySignal信号,将这个信号和handlePush连接起来,按下按钮时触发该信号,从而实现弹窗效果。这里只是为了讲解自定义信号才这样设计,实际开发中肯定不会自定义一个Qt本身已经有相同效果的信号,能用Qt中自带的信号就用已有的。
5. 带参数的信号和槽
信号和槽其实都是可以带参数的,此时发射信号的时候,就可以给信号函数传递实参,与之对应的这个参数就会被传递到相应的槽函数中。
传参可以起到复用代码的效果------有多个逻辑整体一致,但是涉及到的数据不同,就可以通过函数(参数)来实现复用代码,在不同的场景传入不同的参数即可。
Qt中很多内置信号也是带有参数的,大家也可以去了解一下。
还是以我们前面的代码为例,来实现一下通过传参来设计窗口弹出的内容:
cpp
//widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget() override;
//自定义一个槽函数
void handlePush(const QString&);
signals:
//自定义一个信号
void MySignal(const QString&);
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
cpp
//widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
#include <QMessageBox>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(this,&Widget::MySignal,this,&Widget::handlePush);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handlePush(const QString& context)
{
QMessageBox::information(this, "提示", context);
}
void Widget::on_pushButton_clicked()
{
//触发自定义的MySignal信号
emit MySignal("实现带参信号和槽");
}
点击按钮后的效果:

信号(int)和槽(int)的参数类型必须一致,但是个数可以不一致。如果个数不一致,要求信号(int, QString)的参数个数必须大于槽(int)的参数个数。信号传过来的参数,槽可以选择接收前几个,忽略后面的。
在Qt中可以实现一个信号和多个槽函数绑定,一个槽函数也能被多个信号绑定,所以这样设计参数的传递机制可以让信号和槽的绑定更加灵活。
但是这个"多对多"的功能,在实际开发中不怎么用到,绝大多数情况"一对一"就够用了,所以现在很多GUI开发框架都不支持这种"多对多了"。
6. 信号和槽断开
cpp
disconnect(sender, &Sender::signal, receiver, &Receiver::slot);
将绑定的信号和槽断开要用到disconnect函数,用法和connect函数差不多。实际开发中很少会用到,一般只有要将某个信号重新绑定到新的槽函数上才需要先disconnect再重新connect,否则就会出现信号和槽"一对多"的情况,虽然不会报错,但是可能导致功能混乱。