一、简介
信号和槽是 Qt 特有的消息传输机制,它能将相互独立的控件关联起来。
举个简单的例子,按钮和窗口本是两个独立的控件,点击按钮并不会对窗口造成任何影响。通过信号和槽机制,我们可以将按钮和窗口关联起来,实现“点击按钮会使窗口关闭”的效果。
在 Qt 中,用户和控件的每次交互过程称为一个事件,比如“用户点击按钮”是一个事件,“用户关闭窗口”也是一个事件。每个事件都会发出一个信号,例如用户点击按钮会发出“按钮被点击” 的信号,用户关闭窗口会发出“窗口被关闭”的信号。 Qt 中的所有控件都具有接收信号的能力,一个控件还可以接收多个不同的信号。对于接收到的每个信号,控件都会做出相应的响应动作。例如,按钮所在的窗口接收到“按钮被点击”的信 号后,会做出“关闭自己”的响应动作;再比如输入框自己接收到“输入框被点击”的信号后,会做出“显示闪烁的光标,等待用户输入数据”的响应动作。在 Qt 中,对信号做出的响应动作就称为槽 。
信号和槽机制底层是通过函数间的相互调用实现的。每个信号都可以用函数来表示,称为“信号函数”;
每个槽也可以用函数表示,称为“槽函数”。
例如,“按钮被按下”这个信号可以用 clicked() 函数表示,“窗口关闭”这个槽可以用 close() 函数表示,信号和槽机制实现“点击按钮 会关闭窗口”的功能,其实就是 clicked() 函数调用 close() 函数的效果。 信号函数和槽函数通常位于某个类中,和普通的成员函数相比,它们的特别之处在于:
- 信号函数用 signals 关键字修饰,槽函数用 public slots、protected slots 或者 private slots 修饰。signals 和 slots 是 Qt 在 C++ 的基础上扩展的关键字,专门用来指明信号函数和槽函数。
- 信号函数只需要声明,不需要定义(实现),而槽函数需要定义(实现)。
实际开发中,可以使用 Qt 提供的信号函数和槽函数,也可以根据需要自定义信号函数和槽函数。
自定义信号和槽函数
和信号函数不同,槽函数必须手动定义(实现)。槽函数可以在程序中直接调用,但主要用来响应某个信号。自定义一个槽函数时,需要注意以下几点:
- 槽函数的返回值必须和信号函数相同,由于信号函数的返回值一定是 void,所以槽函数的 返回值也必须为 void;
- 对于带参的信号函数,槽函数可以选择接收所有参数,则参数的类型、顺序、个数都必须与信号函数相同; 也可以选择接收前几个参数,这些参数的类型、顺序都必须与信号函数相同; 还可以选择不接受任何参数。
- 槽函数的参数个数只能比信号函数少,不能比信号函数多;
- 槽函数的参数不能有默认值。
slots 关键字可以和 public、protected、private 搭配使用,它们的区别是:
- public slots:该关键字修饰的槽函数,既可以在当前类及其子类的成员函数中调用,也可以在类外部的其它函数(比如 main() 函数)中调用;
- protected slots:该关键字修饰的槽函数,仅允许在当前类及其子类的成员函数内调用,不 能在类外部的其它函数内调用;
- private slots:该关键字修饰的槽函数,只允许在当前类的成员函数内调用,不能在子类中调用,也不能在类外部的其它函数内调用。
通常情况下,槽函数使用 public slots 修饰。
在 Qt 中信号和槽函数都是独立的个体,本身没有任何联系,但是由于某种特性需求我们可以将二者连接到一起,在 Qt 中我们需要使用 QOjbect类中的 connect 函数进行二者的关联。
注意,并非所有的控件之间都能通过信号和槽关联起来,信号和槽机制只适用于满足以下条件 的控件:
控件类必须直接或者间接继承自 QObject 类,Qt 提供的控件类都满足这一条件。 控件类中必须包含 private 属性的 Q_OBJECT 宏。
将某个信号函数和某个槽函数关联起来,需要借助 QObject 类提供的 connect() 函数。
QT4中的连接方式:
QObject::connect(const QObject *sender, SIGNAL(signal()),
const QObject *receiver, SLOT(slot()));
而在QT5中,连接方式如下:
connect(const QObject *sender, &QObject::signal,
const QObject *receiver, &QObject::method);
拿点击but按钮关闭窗口举例:
connect( &But, SIGNAL(clicked()), &widget, SLOT(close()) ); //QT4
connect( &But, &QPushButton::clicked, &widget, &QWidget::close ); //QT5
和旧版本相比,新版的 connect() 函数改进了指定信号函数和槽函数的方式,不再使用 SIGNAL() 和 SLOT() 宏。
注意:QT5中代替Qt4的信号槽连接,在编译时即对连接的函数指针进行检测,提高了安全性。而QT4的连接方式是基于宏的,只能在运行时进行检测。
最好使用QT5的连接方式,因为这样能检查出错误
一个 connect() 函数只能关联一个信号函数和一个槽函数,程序中可以包含多个 connect() 函数,能实现以下几种效果:
- 关联多个信号函数和多个槽函数;
- 一个信号函数可以关联多个槽函数,当信号发出时,与之关联的槽函数会一个接一个地执行,但它们执行的顺序是随机的,无法人为指定哪个先执行、哪个后执行;
- 多个信号函数可以关联同一个槽函数,无论哪个信号发出,槽函数都会执行。
此外,connect() 函数的 method 参数还可以指定一个信号函数,也就是说,信号之间也可以 相互关联,这样当信号发出时,会随之发出另一个信号。
二、回调函数
QT的信号与槽的本质就是个回调函数
1.什么是回调函数?
简而言之,回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。
#include <iostream>
using namespace std;
typedef int (*pFun)(int,int); //定义一个函数指针
int sub(int a, int b) {
return a-b;
}
//回调函数
int callBack(int a, int b, pFun F) {
return F(a,b);
}
int test(int a, int b) {
return callBack(a,b,sub);
}
int main() {
cout << "函数调用:" << test(2,3) << endl;
return 0;
}
回调函数的本质是“你想让别人的代码执行你的代码,而别人的代码你又不能动”这种需求下产生的。
2.为什么要使用回调函数?
因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。
信号和槽与回调函数的区别:
①回调函数使用函数指针来实现的,如果多个类都关注一个类的动态变化,这样就会需要写出一个比较长的列表来管理这些类之间的关系。稍微在编码方面不那么灵活,稍显冗余。
②QT使用信号与槽来解决这个连接问题,这种方式比较清晰简单一些,一个类只需要清楚自己有几个槽函数有几个信号,然后将信号与槽进行连接,QT会自己处理函数的调用关系。这样在软件设计角度更加的清晰,灵活,不容易出错。
③Qt信号与槽机制降低了Qt对象的耦合度。发信号的对象不需要知道有几个槽函数,也不需要关系是否收到信号,或者谁收到了,谁没收到。同样的槽函数也不需要知道谁是信号的发出者。信号只需要在合适的时机发出即可,降低了对象之间的耦合度。