Bootstrap

[Qt从入门到精通] 信号和槽

一、信号和槽

1.1 概念
  • 什么是信号?

        在Qt中,用户和控件每次交互的一个过程称为一个事件。比如用户点击一个按钮,用户关闭窗口等等。每个事件都会发出一个信号,例如用户点击按钮会发出 "按钮被点击" 的信号,用户关闭窗口会发出 "窗口被关闭" 的信号

  • 什么是槽?

        Qt中的所有控件都具有接收信号的能力,⼀个控件还可以接收多个不同的信号。对于接收到的每个信号,控件都会做出相应的响应动作。例如,按钮所在的窗口接收到 "按钮被点击" 的信号后,会做出 "关闭自己" 的响应动作;再如输入框自己接收到 "输入框被点击" 的信号后,会做出 "显示闪烁的光标,等待用户输入数据" 的响应动作。在Qt中,对信号做出的响应动作就称之为槽

        而信号和槽是 Qt 特有的消息传输机制,它能将相互独立的控件关联起来。比如,"按钮" 和 "窗口"  本身是两个独立的控件,点击 "按钮" 并不会对 "窗口" 造成任何影响。通过信号和槽机制,可以将 "按钮" 和 "窗口" 关联起来,实现 "点击按钮会使窗口关闭" 的效果。 

对于信号和槽我们关心三个元素:

发送信号的人、信号类型(怎么发送的信号)、信号的处理方式

1.2 信号的本质

        信号是由于用户对窗口或控件进行了某些操作,导致窗口或控件产生了某个特定事件,这时 Qt 对 应的窗口类会发出某个信号,以此对用户的操作做出反应。因此,信号的本质就是事件。如:

  • 按钮单击、双击
  • 窗口刷新
  • 鼠标移动、鼠标按下、鼠标释放
  • 键盘输入

那么在 Qt 中信号是通过什么形式呈现给使用者的呢?

我们对哪个窗口进行操作,哪个窗口就可以捕捉到这些被触发的事件。

  • 对于使用者来说触发了⼀个事件我们就可以得到 Qt 框架给我们发出的某个特定信号。
  • 信号的呈现形式就是函数,也就是说某个事件产生了,Qt 框架就会调用某个对应的信号函数,通知使用者。
  • 在 Qt 中信号的发出者是某个实例化的类对象。
1.3 槽的本质

        槽(Slot)就是对信号响应的函数。槽就是⼀个函数,与⼀般的 C++ 函数是⼀样的,可以定义在类的任何位置,可以具有任何参数,可以被重载,也可以被直接调用(但是不能有默认参数)。槽函数与一般的函数不同的是:槽函数可以与⼀个信号关联,当信号被发射时,关联的槽函数被自动执行。

二、信号和槽的使用

2.1 连接信号和槽

在 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"
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::close);
}

Widget::~Widget()
{
    delete ui;
}

当我们点击按钮时,就会触发槽函数,这个窗口就会被关闭

注意:

connect要求发送者和信号函数必须是匹配的,如果发送信号的对象是QPushButton类型,那第二个函数必须是QPushButton类内置的函数,或者其继承的函数,不能是别的类的函数,例如QLineEdit内置的函数

2.2 查看Qt内置信号和槽

系统自带的信号和槽通常是通过 "Qt 帮助文档" 来查询。 如上述示例,要查询 "按钮" 的信号,在帮助文档中输入:QPushButton

  • 首先可以在 "Contents" 中寻找关键字 signals
  • 如果没有找到,继续去父类中查找.因此我们去他的父类 QAbstractButton 中继续查找关键字signals

这里的 clicked() 就是要找的信号。槽函数的寻找方式和信号⼀样,只不过它的关键字是 slot。

2.3 自定义信号和槽函数

1. 自定义信号函数书写规范 

  1. 自定义信号函数必须写到 "signals" 下;
  2. 返回值为 void,只需要声明,不需要实现;
  3. 可以有参数,也可以发生重载

2. 自定义槽函数

  1. 早期的 Qt 版本要求槽函数必须写到 "public slots" 下,但是现在高级版本的 Qt 允许写到类的 "public" 作用域中或者全局下
  2. 返回值为 void,需要声明,也需要实现;
  3. 可以有参数,可以发生重载;

3. 发送信号

使用 "emit" 关键字发送信号。"emit" 是⼀个空的宏。"emit" 其实是可选的,没有什么含义,不写也可以,但是建议写上,因为可以提高代码的可阅读性

示例:
1、在 widget.h 中声明自定义的信号和槽,如图所示

2、在 widget.cpp 中实现槽函数,并且关联信号和槽

3. 结果

注意:

自定义信号函数只需要声明并不需要定义,函数的实现是由Qt自己实现的,程序员无法干预,当程序进行编译时,检测到signals关键字,编译器就会自动将下面的函数实现为信号函数

 我们也可以通过可视化操作的方式快速生成信号和槽代码

1. 首先我们先创建一个按钮,右击按钮点转到槽

2.对于普通按钮来说, 使用 clicked 信号即可. clicked(bool) 没有意义的,具有特殊状态的按钮(比如复选按钮)才会用到 clicked(bool) 

3. 选择完成后

说明: 

自动生成槽函数的名称有⼀定的规则。槽函数的命名规则为:on_XXX_SSS,其中:

1、以 "on" 开头,中间使用下划线连接起来;

2、" XXX " 表示的是对象名(控件的objectName 属性)。

3、" SSS " 表示的是对应的信号。 如:"on_pushButton_clicked() ",pushButton代表的是对象名,clicked是对应的信号

按照上述的命名的槽函数,就会被Qt自动与对应的信号进行,关联,这也就是为什么此时代码并没有显示的给我们补充connect处理信号与槽。在后续的代码编写中除非是编译器自动生成,我们还是建议显示的调用connect进行关联,防止命名错误导致关联失败。

2.4 带参数的信号和槽

        Qt的信号和槽也支持带有参数,同时也可以支持重载,此处我们要求,信号函数的参数列表要和对应连接的槽函数参数列表⼀致,此时信号触发,调用到槽函数的时候,信号函数中的实参就能够被传递到槽函数的形参当中。通过这样的机制,就可以让信号给槽传递数据了

示例1:重载信号槽

(1)在 "widget.h" 头文件中声明信号函数以及槽函数;如下图所示:

(2)在"Widget.cpp"中实现槽函数,并在合适位置发出信号

其实信号的参数个数可以多于槽函数的参数个数,但是槽的参数个数不能多于信号参数个数,因为至少要保证槽函数的参数都是有值的,但是实际开发中最好还是保持参数个数也能匹配⼀致.

三、信号与槽的连接方式

3.1 一对一

主要有两种形式,分别是:⼀个信号连接⼀个槽和⼀个信号连接⼀个信号。

上述代码就是一个信号连接一个槽的方式,接下来我们举一个一个信号连接一个信号的例子:

这样当我们点击按钮,就会调用我们自己定义的信号函数,而这个信号函数被触发就会调用MySlots函数

3.2 一对多
  • ⼀个信号连接多个槽

  • 一个槽对应多个信号

其实其他的GUI开发对于这些都搞得比较简单,例如:

button.click=handle;

function QtMessageHandler{
    ........
}

这样就可以把一个事件的触发关联一个回调函数,那为什么Qt还要单独设计一套信号和槽机制来满足这些呢?

其实Qt不仅想要通过connect连接一个信号和槽,他更主要的原因是想将用户操作的控件和对应用户处理业务的操作进行解耦合,并且想要实现多对多的的效果,一个信号可以关联多个槽函数,一个槽函数也可以对应多个信号,尽管这个需求在实际开发中几乎不会遇到

四、信号和槽的其他说明

4.1 信号与槽的断开

使用 disconnect 即可完成断开.disconnect 的用法和 connect 基本⼀致.

4.2 使用 Lambda 表达式定义槽函数

Qt5在Qt4的基础上提高了信号与槽的灵活性,允许使用任意函数作为槽函数。但如果想方便的编写槽函数,比如在编写函数时连函数名都不想定义,则可以通过 Lambda表达式来 达到这个目的。

不了解lambda表达式的可以看一看博主的这一篇文章:

C++11新特性【下】{lambda表达式、可变模板参数、包装器}-CSDN博客

示例:点击按钮关闭窗口

4.3 信号和槽的优缺点 

优点:

松散耦合:信号发送者不需要知道发出的信号被哪个对象的槽函数接收,槽函数也不需要知道哪些信号关联了自己,Qt的信号槽机制保证了信号与槽函数的调用。支持信号槽机制的类或者父类必须继承于 QObject 类。

缺点: 

效率较低:与回调函数相比,信号和槽稍微慢⼀些,因为它们提供了更高的灵活性,尽管在实际应用程序中差别不大。通过信号调用的槽函数比直接调用的速度慢约10倍(这是定位信号的接收对象所需的开销;遍历所有关联;编组/解组传递的参数;多线程时,信号可能需要排队),这种调用速度对性能要求不是非常高的场景是可以忽略的,是可以满足绝大部分场景。

;