信号和槽
Linux信号 Signal 系统内部的通知机制. 进程间通信的方式.
- 信号源:谁发的信号.
- 信号的类型:哪种类别的信号
- 信号的处理方式:注册信号处理函数,在信号被触发的时候自动调用执行.
Qt中的信号和Linux中的信号,虽然不是一样的概念,但是确实有相似之处
Qt中,谈到信号,也是涉及到三个要素信号源:
- 由哪个控件发出的信号
- 信号的类型:用户进行不同的操作,就可能触发不同的信号
- 点击按钮,触发点击信号.
- 在输入框中移动光标,触发移动光标的信号,勾选一个复选框
- 选择一个下拉框都会触发出不同的信号
咱们写的GUI程序,就是要让用户进行操作.就是要和用户进行交互这个过程中就需要关注,用户当前的操作具体是个什么样的操作
- 信号的处理方式:槽(slot)=>函数
Qt中可以使用connect这样的函数,把一个信号和一个槽关联起来后续只要信号触发了,Qt就会自动的执行槽函数
所谓的"槽函数"本质上也是一种"回调函数"(callback)
最早C语言阶段
C 进阶=>指针进阶=>函数指针.
- 实现转移表,降低代码的"圈复杂度”.
- 实现回调函数效果=〉qsort
后来在C++阶段~
- STL中,函数对象/仿函数
- lambda 表达式.
后来在Linux中
- 信号处理函数
- 线程的入口函数.
- epoll基于回调的机制
一定是先把信号的处理方式准备好,再触发信号~
Qt中,一定是先关联号信号和槽,然后再触发这个信号.顺序不能颠倒,否则信号就不知道如何处理了(错过了).
信号和槽概述
在Qt中,⽤⼾和控件的每次交互过程称为⼀个事件。⽐如"⽤⼾点击按钮"是⼀个事件,"⽤⼾关闭窗⼝"也是⼀个事件。每个事件都会发出⼀个信号,例如⽤⼾点击按钮会发出"按钮被点击"的信号,⽤⼾关闭窗⼝会发出"窗⼝被关闭"的信号。
Qt中的所有控件都具有接收信号的能⼒,⼀个控件还可以接收多个不同的信号。对于接收到的每个信号,控件都会做出相应的响应动作。例如,按钮所在的窗⼝接收到"按钮被点击"的信号后,会做出"关闭⾃⼰"的响应动作;再⽐如输⼊框⾃⼰接收到"输⼊框被点击"的信号后,会做出"显⽰闪烁的光标,等待⽤⼾输⼊数据"的响应动作。在Qt中,对信号做出的响应动作就称之为槽。
信号和槽是Qt特有的消息传输机制,它能将相互独⽴的控件关联起来。⽐如,"按钮"和"窗⼝"本⾝是两个独⽴的控件,点击"按钮"并不会对"窗⼝"造成任何影响。通过信号和槽机制,可以将"按钮"和"窗⼝"关联起来,实现"点击按钮会使窗⼝关闭"的效果。
信号的本质
信号是由于⽤⼾对窗⼝或控件进⾏了某些操作,导致窗⼝或控件产⽣了某个特定事件,这时Qt对应的窗⼝类会发出某个信号,以此对⽤⼾的操作做出反应。因此,信号的本质就是事件。如:
- 按钮单击、双击
- 窗⼝刷新
- ⿏标移动、⿏标按下、⿏标释放
- 键盘输⼊
那么在Qt中信号是通过什么形式呈现给使⽤者的呢? - 我们对哪个窗⼝进⾏操作,哪个窗⼝就可以捕捉到这些被触发的事件。
- 对于使⽤者来说触发了⼀个事件我们就可以得到Qt框架给我们发出的某个特定信号。
- 信号的呈现形式就是函数,也就是说某个事件产⽣了,Qt框架就会调⽤某个对应的信号函数,通知使⽤者。
在Qt中信号的发出者是某个实例化的类对象。
槽的本质
槽(Slot)就是对信号响应的函数。槽就是⼀个函数,与⼀般的C++函数是⼀样的,可以定义在类的任何位置(public、protected或private),可以具有任何参数,可以被重载,也可以被直接调⽤(但是不能有默认参数)。槽函数与⼀般的函数不同的是:槽函数可以与⼀个信号关联,当信号被发射时,关联的槽函数被⾃动执⾏。
说明
(1)信号和槽机制底层是通过函数间的相互调⽤实现的。每个信号都可以⽤函数来表⽰,称为信号函数;每个槽也可以⽤函数表⽰,称为槽函数。例如:"按钮被按下"这个信号可以⽤clicked()函数表⽰,"窗⼝关闭"这个槽可以⽤close()函数表⽰,假如使⽤信号和槽机制-实现:"点击按钮会关闭窗⼝"的功能,其实就是clicked()函数调⽤close()函数的效果。
(2)信号函数和槽函数通常位于某个类中,和普通的成员函数相⽐,它们的特别之处在于:
- 信号函数⽤signals关键字修饰,槽函数⽤public slots、protected slots 或者private slots修 饰。signals和slots是Qt在C++的基础上扩展的关键字,专⻔⽤来指明信号函数和槽函数;
- 信号函数只需要声明,不需要定义(实现),⽽槽函数需要定义(实现)。
信号函数的定义是Qt⾃动在编译程序之前⽣成的.编写Qt应⽤程序的程序猿⽆需关注.
这种⾃动⽣成代码的机制称为元编程(Meta Programming).这种操作在很多场景中都能⻅到.
信号和槽的使⽤
连接信号和槽
在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 )
参数说明:
- sender:信号的发送者;(哪个控件)
- signal:发送的信号(信号函数);(信号的类型)
- receiver:信号的接收者;(控件)
- method:接收信号的槽函数;(要处理信号的对象提供的成员函数)
- type:⽤于指定关联⽅式,默认的关联⽅式为Qt::AutoConnection,通常不需要⼿动设定
代码⽰例:在窗⼝中设置⼀个按钮,当点击"按钮"时关闭"窗⼝"。
#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("关闭");button->move(200, 200);connect(button, &QPushButton::clicked, this, &Widget::close);
}Widget::~Widget()
{delete ui;
}
所谓的信号也就是Qt中的对象,内部提供的一些成员函数
图标带有锯齿,slot函数,click是一个slot函数,作用就是在调用的时候相当于点击了一下按钮
带有类似wifi的图标,就是信号函数,clicked,才是要触发的点击信号
button, &QPushButton::clicked
connect函数要求,这俩参数是匹配的,button的类型如果是QPushButton*,此时第二个参数的信号必须是QPushButton内置的信号或者父类的信号,不能是其他的类的信号
this, &Widget::close
close是QWidget内置的槽函数,Widget继承自QWidget,也就继承了父亲的槽函数
close槽函数功能已经是内部实现好的,具体作用就是关闭当前的窗口/控件
connect(button, &QPushButton::clicked, this, &Widget::close);
针对button,进行点击操作,Widget就会关闭
具体可以查看Qt文档
connect中的char*
参数
但是传入的是&QPushButton::clicked, &Widget::close函数指针
void(*)();
bool(*)();
这两个函数指针的类型也是不同的
这个函数声明,是以前l日版本的Qt的connect函数的声明
以前版本中,传参的写法和现在其实也是有区别的此时,给信号参数传参,要搭配一个SIGNAL宏. 给槽参数传参,搭配一个SLOT宏.
传入的函数指针转成char*
connect(button, SIGNAL(&QPushButton::clicked), this, SLOT(&Widget::close));
Qt 5开始,对上述写法做出了简化.不再需要写SIGNAL 和SLOT宏了.
给connect提供了重载版本.重载版本中,第二个参数和第四个参数成了泛型参数.允许咱们传入任意类型的函数指针了.
QtPrivate::FunctionPointer<Func1>::Object
Qt封装的类型萃取器
此时connect函数就带有了一定的参数检查功能
如果你传入的第一个参数和第二个参数不匹配,或者第三个参数和第四个参数不匹配.(不匹配,2,4参数的函数指针,不是1,3 参数的成员函数)
此时代码编译出错
自定义槽函数1
widget.cpp
#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("按钮");button->move(100, 100);connect(button, &QPushButton::clicked, this, &Widget::handleClicked);
}Widget::~Widget()
{delete ui;
}void Widget::handleClicked()
{//按下按钮,修改窗口标题this->setWindowTitle("按钮已经按下");
}
widget.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();void handleClicked();private:Ui::Widget *ui;
};
#endif // WIDGET_H
按下按钮
所谓的slot就是一个普通的成员函数
所谓的自定义一个槽函数,操作过程和自定义一个普通的成员函数,没啥区别!在以前版本的Qt中,槽函数必须放到public/private/protected slots:
此处的slots是Qt自己扩展的关键字.(不是C++标准中的语法) Qt里广泛使用了元编程技术. (基于代码,生成代码)
qmake 构建Qt项目的时候,就会调用专门的扫描器,扫描代码中特定的关键字.(slots这种) 基于关键字自动生成一大堆相关的代码,
自定义槽函数2
图形化方式拖入一个PushButton
鼠标右键点击PushButton
点击转到槽
这个窗口列出了QPushButton提供的所有信号,包含了QPushButton父类的信号
双击clicked
直接生成好了一个函数,声明也生成好了
可以直接编写代码
void Widget::on_pushButton_clicked()
{this->setWindowTitle("按钮已经按下");
}
在Qt中,除了通过connect来连接信号槽之外,还可以通过函数名字的方式来自动连接
void Widget::on_pushButton_clicked()
⾃动⽣成槽函数的名称有⼀定的规则。槽函数的命名规则为:on_XXX_SSS,其中:
- 以"on"开头,中间使⽤下划线连接起来;
- "XXX"表⽰的是对象名(控件的 objectName 属性)。
- "SSS"表⽰的是对应的信号。
如:“on_pushButton_clicked()”,pushButton代表的是对象名,clicked是对应的信号。
Qt中调用这个函数的时候,就会触发上述自动连接信号槽的规则!!正是在自动生成的ui_widget.h中调用的~
如果我们通过图形化界面创建控件,还是推荐使用这种快速的方式来连接信号槽
如果我们是通过代码的方式来创建控件,还是得手动connect.(你的代码中没有调用connectSlotsByName)
自定义信号
Qt中也充许自定义信号
自定义槽函数,非常关键.开发中大部分情况都是需要自定义槽函数的槽函数,就是用户触发某个操作之后,要进行的业务逻辑
自定义信号,比较少见.实际开发中很少会需要自定义信号.
信号就对应到用户的某个操作~
在GUI,用户能够进行哪些操作,是可以穷举的~~
Qt内置的信号,基本上已经覆盖到了上述所有可能的用户操作
因此,使用Qt内置的信号,就足以应付大部分的开发场景了,
自定义信号,本身代码比较简单的~
Widget虽然还没有定义任何信号,由于继承自QWidget,和QObject,这俩类里面已经提供了一些信号了,可以直接使用.
所谓的Qt的信号,本质上也就是一个"函数
Qt5以及更高版本中,槽函数和普通的成员函数之间,没啥差别了. 但是,信号,则是一类非常特殊的函数,
- 程序员只要写出函数声明,并且告诉Qt,这是一个“信号”即可, 1.
这个函数的定义,是Qt在编译过程中,自动生成的.(自动生成的过程,程序员无法干预)
信号在Qt中是特殊的机制.Qt生成的信号函数的实现,要配合Qt框架做很多既定的操作~~ - 作为信号函数,这个函数的返回值,必须是void.
有没有参数都可以.甚至也可以支持重载
这个也是Qt自己扩展出来的关键字~~
qmake的时候,调用一些代码的分析/生成工具,
扫描到类中包含signals这个关键字的时候,此时,就会自动的把下面的函数声明认为是信号,并且给这些信号函数自动的生成函数定义
widget.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 slots:void handleMySignal();private:Ui::Widget *ui;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);connect(this, &Widget::mySignal, this, &Widget::handleMySignal);//发送自定义信号emit mySignal();
}Widget::~Widget()
{delete ui;
}void Widget::handleMySignal()
{this->setWindowTitle("处理自定义信号");
}
如何才能触发出自定义的信号呢?
Qt内置的信号,都不需要咱们手动通过代码来触发
用户在GUI,进行某些操作,就会自动触发对应信号.(发射信号的代码已经内置到Qt框架中了)
自定义的信号需要通过emit,来发射信号
emit mySignal();
在启动的时候直接触发信号
发送信号的操作,可以在任何合适的代码中,不一定非要在构造函数中
#include "widget.h"
#include "ui_widget.h"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("处理自定义信号");
}void Widget::on_pushButton_clicked()
{//发送自定义信号emit mySignal();
}
通过图形化方式创建一个槽函数
点击按钮
点击按钮->
QPushButton::clicked->
Widget::on_pushButton_clicked()->
emit mySignal();->
void Widget::handleMySignal()
其实在Qt5中emit 现在啥都没做
真正的操作都包含在mySignal内部生成的函数定义了
emit mySignal();
即使不写emit,信号也能发出去!!
即使如此,实际开发中,还是建议大家,把emit都加上
加上代码可读性更高,更明显的标识出,这里是发射自定义的信号了.
自定义的基本语法
在Qt中,允许⾃定义信号的发送⽅以及接收⽅,即可以⾃定义信号函数和槽函数。但是对于⾃定义的
信号函数和槽函数有⼀定的书写规范。
1、⾃定义信号函数书写规范
- ⾃定义信号函数必须写到"signals"下;
- 返回值为void,只需要声明,不需要实现;
- 可以有参数,也可以发⽣重载;
2、⾃定义槽函数书写规范
- 早期的Qt版本要求槽函数必须写到"public slots"下,但是现在⾼级版本的Qt允许写到类的"public"作⽤域中或者全局下;
- 返回值为void,需要声明,也需要实现;
- 可以有参数,可以发⽣重载;
3、发送信号
使⽤"emit"关键字发送信号。"emit"是⼀个空的宏。"emit"其实是可选的,没有什么含义,只是为了提醒开发⼈员。
必须先关联再发射
原因是,⾸先关联信号和槽,⼀旦检测到信号发射之后就会⽴⻢执⾏关联的槽函数。反之,若先发射信号,此时还没有关联槽函数,当信号发射之后槽函数不会响应
带参数的信号和槽
Qt的信号和槽也⽀持带有参数,同时也可以⽀持重载.
此处我们要求,信号函数的参数列表要和对应连接的槽函数参数列表⼀致.
此时信号触发,调⽤到槽函数的时候,信号函数中的实参就能够被传递到槽函数的形参当中
signals:void mySignal(const QString& text);public slots:void handleMySignal(const QString& text);
这里的参数必须一致
一致主要是要求类型一致,个数如果不一致也可以,不一致的时候,信号的参数的个数必须比槽的参数的个数更多
void Widget::handleMySignal(const QString& text)
{this->setWindowTitle(text);
}void Widget::on_pushButton_clicked()
{//发送自定义信号emit mySignal("带参数的信号");
}
点击按钮
传参可以起到复用代码的效果
有多个逻辑,逻辑上整体一致,但是涉及到的数据不同,
就可以通过函数-参数来复用代码,并且在不同的场景中传入不同的参数即可~
新建一个PushButton
转到槽,新建一个clicked函数
void Widget::on_pushButton_clicked()
{//发送自定义信号emit mySignal("把标题设置为标题1");
}void Widget::on_pushButton_2_clicked()
{emit mySignal("把标题设置为标题2");
}
通过这一套信号槽,搭配不同的参数,就可以起到设置不同标题的效果
Qt中很多内置的信号,也是带有参数的.(这些参数不是咱们自己传递的)
clicked信号就带有一个参数~~
这个参数表示当前按钮是否处于“选中”状态这个选中状态对于QPushButton没啥意义,对于QCheckBox复选框,就很有用了,
signals:void mySignal(const QString& text, const QString& text2);public slots:void handleMySignal(const QString& text);
void Widget::on_pushButton_clicked()
{//发送自定义信号emit mySignal("把标题设置为标题1", "");
}void Widget::on_pushButton_2_clicked()
{emit mySignal("把标题设置为标题2", "");
}
信号函数的参数个数,超过了槽函数的参数个数,此时,都是可以正常使用的信号函数的参数个数,少于槽函数的参数个数,此时代码无法编译通过
直观的思考,应该是要求信号的参数个数和槽的参数个数,严格一致
此处为啥允许信号的参数比槽的参数多呢?? 一个槽函数,有可能会绑定多个信号
如果我们严格要求参数个数一致,就意味着信号绑定到槽的要求就变高了,
换而言之,当下这样的规则,就允许信号和槽之间的绑定更灵活了,更多的信号可以绑定到这个槽函数上了,
个数不一致,槽函数就会按照参数顺序,拿到信号的前N个参数
至少需要确保,槽函数的每个参数都是有值的
要求信号给槽的参数,可以有富裕,但是不能少
带有参数的信号,要求信号的参数和槽的参数要一致
类型,个数要满足要求(信号的参数个数要多于槽的参数个数).
widget.h
Qt中如果要让某个类能够使用信号槽(可以在类中定义信号和槽函数)
则必须要在类最开始的地方,写下QOBJECT宏
这个宏能展开成很多额外的代码
如果不加这个宏,这个类编译的时候会报错