Qt开发——信号和槽
信号和槽介绍
在Linux系统编程中也涉及到 信号(Signal),关于Linux中的信号,我们认为它是系统内部的通知机制,也是进程之间通信的方式。
信号中涉及到信号关键的三个要素:
信号源:谁发的信号
信号类型:哪种类别的信号
信号的处理方式:注册信号处理函数,在信号被触发的时候自动调用执行
Qt 中的信号和Linux中的信号虽然不是一样的概念,但是确实有相似之处
Qt中,谈到信号,通常也涉及到三个要素
信号源:由哪个控件发出的信号
信号类型:用户进行不同的操作,就可能触发不同的信号。
信号的处理方式:槽(slot) ——其实就是一个函数
Qt中可以 使用 connect 这样的函数,把一个信号 和 一个槽关联起来
后续只要信号触发了,Qt 就会自动的执行槽函数
所谓的"槽函数"本质也是一种"回调函数"(callback)
在Qt中,用户和控件的每次交互过程称为一个事件。比如 "用户点击按钮" 是⼀个事件,"用户关闭窗口"也是⼀个事件。每个事件都会发出一个信号,例如用户点击按钮会发出 "按钮被点击" 的信 号,用户关闭窗口会发出 "窗口被关闭" 的信号。Qt中的所有控件都具有接收信号的能力,一个控件还可以接收多个不同的信号。
对于接收到的每 个信号,控件都会做出相应的响应动作。例如,按钮所在的窗口接收到 "按钮被点击" 的信号后,会做出 "关闭自己" 的响应动作;再比如输入框自己接收到 "输入框被点击" 的信号后,会做出"显示闪烁的光标,等待用户输入数据" 的响应动作。在qt中,对信号做出的响应动作就称之为槽。
信号和槽是 Qt 特有的消息传输机制,它能将相互独立的控件关联起来。比如,"按钮"和"窗⼝"本身是两个独立的控件,点击"按钮"并不会对"窗口"造成任何影响。通过信号和槽机制,可以将"按 钮"和"窗口"关联起来,实现"点击按钮会使窗口关闭" 的效果。
和Linux中一样,在遇到信号之前,你已经知道了遇到这个信号的时候该做什么事情了;所以要提前把不同信号的处理方式准备好了,再触发信号;这样才能知道遇到某个信号后处理对应的事情。
Qt 中 ,一定是先关联 信号 和 槽 ,然后再触发这个信号,顺序不能颠倒,否则信号就不知道如何处理了(错过了)
connect函数的用法
在Qt中,QObject类提供了⼀个静态成员函数connect(),该函数专门用来关联指定的信号函数和槽 函数
QObject是Qt内置的父类.Qt中提供的很多类都是直接或者间接继承自QObject
connect具体的使用方式
connect (const QObject *sender, const char * signal ,const QObject * receiver , const char * method , Qt::ConnectionType type = Qt::AutoConnection )
connect中由5个参数,其中最后一个参数提供了一个默认参数,最后一个参数暂时不考虑,大部分情况下也很少用到这个参数
sender:信号的发送者;
signal:发送的信号(信号函数)
receiver:信号的接收者;
method:接收信号的槽函数;
type:指定关联方式,默认的关联方式为Qt::AutoConnection,通常不需要手动设定。
一个简单的例子:
界面上包含一个按钮,用户点击按钮,则关闭窗口
![]()
注意:
&QPushButton::中click是一个slot函数,作用是在调用的时候相当于点击了一下按钮
clicked函数,才是要触发的点击信号
connect要求,前面这两参数是匹配的,mybutton的类型如果是QPushButton*,此时第二个参数的信号必须是QPushButton内置的信号(或者它父类的信号),不能是一个其他类型的信号(比如:QLineEdit信号)
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);QPushButton* myButton = new QPushButton(this);myButton->setText("close");connect(myButton,&QPushButton::clicked,this,&Widget::close);
}Widget::~Widget()
{delete ui;
}close是QWidget内置的槽函数,由于Widget继承自QWidget,也就继承了父亲的槽函数。close具体的作用:关闭当前的窗口/控件
翻译一下这个connect:
针对 myButton 进行点击 Widget 就会关闭
![]()
这份代码中至少还存在两个重要的问题:
1.你怎么知道 QPushButton 有个 clicked 信号?
或者说你怎么知道QWidget有一个close槽?
其实也就是Qt到底提供了哪些信号和槽可以让我们直接使用?
多看文档

查阅文档的时候,在当前类中没有找到对应的线索,可以看看这个类的父类

QAbstractButton其中就有click槽函数

而信号函数中就包含了clicked

点击此函数名可以查看,函数的解释

查阅文档中信号的时候,最重点的就是关注信号的发送时机(用户进行什么操作会触发此信号)
connect的第一个参数和第三个参数都是QObject* 这没什么问题,问题是第二个参数和第四个参数都是以char*来作为一个函数的类型,实际上我们填写这个参数的时候填写的char*吗?
connect(myButton,&QPushButton::clicked,this,&Widget::close);在实际的 代码中,我们填写的是两个函数指针
char*和函数指针是同一个东西吗?
那当然不是了 clicked 的 指针应该是void(*)() close的指针是 bool(*)()
C++中是不允许使用两个不同的指针类型相互赋值的(传参本质就是赋值)
在文档中查找connect的声明和我们刚才的也是一样的啊

这个文档的函数声明是以前旧版本Qt 的connect 函数的声明,以前版本中,传参的写法和我们现在的写法也是有区别的。给信号参数传参需要搭配一个 SIGNAL 宏。给槽参数传参,搭配一个SLOT宏。这两个宏就能分别把你的两个函数指针转为char*
connect(myButton,SIGNAL(&QPushButton::clicked),this,SLOT(&Widget::close));这种写法从Qt5开始就不这么写了,对上述写法进行了简化,不再需要写SIGNAL和SLOT宏了,给connect 提供了重载版本,在重载版本中,第二个参数和第四个参数成了泛型参数,允许咱们传入任意类型的函数指针了。

自定义槽函数
所谓的slot就是一个普通的成员函数。
在Widget类中声明
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);void handleclick();~Widget();private:Ui::Widget *ui;
};
#endif // WIDGET_H
.cpp中定义然后在构造的时候调用connect
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);QPushButton* button = new QPushButton(this);//自定义槽函数button->setText("按钮");connect(button,&QPushButton::clicked,this,&Widget::handleclick);}void Widget::handleclick()
{//按下按钮修改窗口标题this->setWindowTitle("按钮已经按下");
}Widget::~Widget()
{delete ui;
}
另一种自定义槽函数的方式
转到设计界面,拖过去一个push button 按钮,右键此按钮有一个转到槽的选项

点击转到槽之后就会弹出一个窗口来告诉我们这个按钮可以和哪些信号进行关联

这个窗口就列出了 QPushButton 给我们提供的所有的信号(还包含了它父类的信号)
如果双击这个clicked那么就会跳转到代码编辑的界面,并且帮我们生成好了一个函数的定义和声明,然后我们就可以在里边编写代码
定义

声明

但是找不到connect的代码,在Qt中出了通过 connect 来连接信号槽之外,还可以通过函数名字的方式来自动连接。仔细看一下函数名on_pushButton_clicked,中间是按钮的objectname,到时候就可以ui->pushButton了,后边是信号的名字。
当函数名符合上述规则之后,Qt就能自动的把信号和槽建立连接。
验证一下:将声明和定义改一下函数名,

再次点击按钮发现没有任何响应,应用程序输出了一个错误

QMetaObject::connectSlotsByName,QT中调用这个函数的时候,就会触发自动连接信号槽的规则,这个函数在哪呢——正是在自动生成的ui_widget.h中调用的。
自定义信号
Qt中也允许自定义信号,自定义槽函数非常关键,毕竟开发的过程中大部分情况下都是需要自定义槽函数的。槽函数就是用户触发某个操作之后要执行的业务逻辑。
自定义信号比较少见,实际开发中很少会需要自定义信号,信号就对应了用户的某个操作。在GUI中,用户能够进行哪些操作,是可以穷举的,QT中内置的信号,基本上已经覆盖到了上述所有可能的用户操作。因此,使用QT中内置的信号,就足以应付大部分的开发场景了。
Widget虽然还没有定义任何信号,由于继承自QWidget和QObject,这两类里边已经提供了一些信号了,可以直接使用。
所谓的qt的信号,本质也就是一个"函数"
QT 5 以及更高版本中,槽函数和普通的成员函数之间,没啥差别了
但是信号是一类十分特殊的函数:
要点1:
程序员只需要写出函数声明,并且告诉QT这是一个"信号"即可。这个函数的定义是Qt 在编译过程中自动生成的(自动生成的过程程序员无法干预)
信号在Qt中是特殊的机制,Qt生成的信号函数的实现,需要配合Qt框架做很多既定的操作
要点2:
作为信号函数,这个函数的返回值,必须是void
有没有参数都可以,甚至支持重载

这个也是qt自己扩展出来的关键字,qmake的时候,调用一些代码的分析/生成工具——扫描到 signals 这个关键字的时候,此时就会自动的把下面的函数声明认为是信号,并且给这些信号函数自动的生成函数定义。
.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();
//声明信号
signals:void mySignal();
//槽函数
public:void handleMySignal();private:Ui::Widget *ui;
};
#endif // WIDGET_H
.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);connect(this,&Widget::mySignal,this,&Widget::handleMySignal);
}Widget::~Widget()
{delete ui;
}void Widget::handleMySignal()
{this->setWindowTitle("自定义信号");
}
此时运行代码,窗口标题并没有改变

connect(this,&Widget::mySignal,this,&Widget::handleMySignal);
我们只是建立了连接,还不代表着信号已经发出来了。
那么这个自定义信号怎么触发呢?Qt内置的信号,不需要我们手动通过代码来触发,用户在GUI进行某些操作时,就会自动触发对应的信号(发射信号的代码已经内置到QT框架中了)
所以怎么发射信号呢?Qt中提供了一个关键字emit
通过emit 信号函数调用 ——emit mysignal();
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);connect(this,&Widget::mySignal,this,&Widget::handleMySignal);//发送自定义信号emit mySignal();
}窗口标题被改变了
带参数的信号和槽
函数声明

定义和调用

传参可以起到复用代码的效果,有多个逻辑,逻辑整体一致,但是涉及到的数据不同。就可以通过函数-参数来复用代码,并且在不同的场景中传入不同的参数。

通过这一套信号槽,搭配不同的参数就可以起到设置不同标题的效果。

信号和槽带参数的时候,参数类型必须一致,但是参数的数量可以不一致(信号的参数不能少于槽的参数)

调用的函数也带参,不过我们暂时用不到第二个参数就给个空串吧
信号函数的参数个数超过槽函数的参数个数是可以正常使用的

直观的思考,应该是要求信号的参数个数和槽的参数个数严格保持一致的,此处为什么允许信号的参数比槽的参数更多呢
一个槽函数,有可能会绑定多个信号。如果我们严格要求参数个数一致的话,就意味着信号绑定到槽的要求就变高了。换而言之,当下这样的规则,就允许信号和槽之间的绑定更加灵活,就能够把更多的信号绑定到槽函数中。
如果个数不一致,槽函数就会按照参数顺序,拿到信号的前n个参数
至少需要确保,槽函数的每个参数都是有值的——所以信号给槽的参数只能多不能少
Q_OBJECT
widget.h文件中,有个Q_OBJECT宏

Qt中如果要让某个类能够使用信号槽(可以在类中定义信号和槽函数)则必须要在类的最开始的地方,写入Q_OBJECT宏
信号和槽存在的意义
Qt信号槽,connect这个机制,设想是很美好的
1.解耦合,把触发 用户操作的控件 和 处理对应用户的操作逻辑 解耦合
2.实现 "多对多" 的效果
一个信号可以connect到多个槽函数上
一个槽函数,也可以被多个信号connect
这个多对多是不是在哪见过?
数据库(MySQL)中,设计数据库的表结构,就需要理清楚实体和实体(实体==对象)之间的关系。
Qt中信号和槽 的"多对多" 是 类似于 数据库的多对多
比如我现在有一张学生表和一张课程表
一个学生,可以选择多门课程来学习
一门课程,也可以被多个同学来选择
而connect就相当于数据库中的关联表
实际上,随着程序开发这个事情大家的经验越来越多,其实在GUI开发的过程中,"多对多"这件事,其实是一个"伪需求"。绝大部分情况一对一就够用了。
信号和槽断开连接
可以使用disconnect来断开信号和槽的连接,disconnect和connect的用法是非常类似的
disconnect用到的情况是比较少的,大部分情况下,把信号和槽连上了就不用管了,主动断开往往是把信号重新绑到另一个槽函数上
.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private slots:void on_pushButton_clicked();void on_pushButton_2_clicked();void newHandle();
private:Ui::Widget *ui;
};
#endif // WIDGET_H
.cpp
#include "widget.h"
#include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);connect(ui->pushButton,&QPushButton::clicked,this,&Widget::on_pushButton_clicked);
}Widget::~Widget()
{delete ui;
}void Widget::on_pushButton_clicked()
{this->setWindowTitle("窗口标题");
}void Widget::on_pushButton_2_clicked()
{disconnect(ui->pushButton,&QPushButton::clicked,this,&Widget::on_pushButton_clicked);connect(ui->pushButton,&QPushButton::clicked,this,&Widget::newHandle);}void Widget::newHandle()
{this->setWindowTitle("新标题");
}使用lambda表达式定义槽函数
lambda本质就是一个"匿名函数"主要是应用在"回调函数"场景中
Lambda表达式的语法格式如下:
[ capture ] ( params ) opt -> ret { Function body; };
capture: 捕获列表
params: 参数表
opt: 函数选项
ret: 返回值类型
Function body: 函数体

而当我想移动控件时发现,报错:找不到buttom的定义

lambda表达式是一个回调函数,这个函数无法直接获取到上层作用域的变量的,lambda为了解决上述问题,引入了"变量捕获",通过变量捕获获取外层作用域的变量
在[ ]中捕获就行了

捕获一下this

如果当前lambda 里边想使用更多的外层变量咋办?——直接写[=],就是把上层作用域的变量全捕获
后续如果我们对应的槽函数比较简单,而且是一次性使用的,就经常写作这种lambda的形式
lambda语法是C++11引入的,如果是QT5及更高版本那么默认按照C++11来编译,如果是QT4或者更老的版本就需要在.pro文件中加上C++11 的编译选项
