目录
一、认识信号和槽
概述
在Qt中,用户和控件的每次交互过程称为一个事件。如"用户点击按钮"是一个事件,"用户关闭窗口"也是一个事件。每个事件都会发出一个信号,如用户点击按钮会发出"按钮被点击"的信号,用户关闭窗口会发出"窗口被关闭"的信号
Qt中的所有控件都具有接收信号的能力,⼀个控件还可以接收多个不同的信号。对于接收到的每个信号,控件都会做出相应的响应动作。如:按钮所在的窗口接收到"按钮被点击"的信号后,会做出"关闭自己"的响应动作;再如:输入框自己接收到"输入框被点击"的信号后,会做出"显示闪烁的光标,等待用户输入数据"的响应动作。在Qt中,对信号做出的响应动作就称之为槽
信号和槽是Qt特有的消息传输机制,它能将相互独立的控件关联起来。如:"按钮"和"窗口"本身是两个独立的控件,点击"按钮"并不会对"窗口"造成任何影响。通过信号和槽机制,可以将"按钮"和"窗口"关联起来,实现"点击按钮会使窗口关闭"的效果
信号的本质
信号是由于用户对窗口或控件进行了某些操作,导致窗口或控件产生了某个特定事件,这时Qt对应的窗口类会发出某个信号,以此对用户的操作做出反应。因此,信号的本质就是事件
槽的本质
槽(Slot)就是对信号响应的函数。槽就是⼀个函数,与一般的C++函数是一样的,可以定义在类的任何位置(public、protected或private),可以具有任何参数,可以被重载,也可以被直接调用(但是不能有默认参数)。槽函数与一般的函数不同的是:槽函数可以与信号关联,当信号被发射时,关联的槽函数被自动执行
说明
- 信号和槽机制底层是通过函数间的相互调用实现的。每个信号都可以用函数来表示,即信号函数;每个槽也可以用函数表示,即槽函数
- 信号函数和槽函数通常位于某个类中,和普通的成员函数相比,它们的特别之处在于:
信号函数用signals关键字修饰,槽函数用public slots、protected slots或者private slots修饰(使用普通成员函数的方式修饰也可)。signals和slots是Qt在C++的基础上扩展的关键字,专门用来指明信号函数和槽函数;信号函数只需要声明,不需要定义(实现),而槽函数需要定义(实现)
Q_OBJECT
若一个类要使用信号和槽机制,必须在类中添加Q_OBJECT这个宏
二、connect函数
在Qt中,QObject类提供了一个静态成员函数connect(),该函数专门用来关联指定的信号函数和槽
函数。QObject是Qt内置的父类,Qt中提供的很多类都是直接或者间接继承自QObject
Qt Assistant中connect函数原型:
cpp
//旧版
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,通常不需要手动设定
但是C++不允许使用两个不同的指针类型相互赋值,使用const char*明显是不行的。因为Qt Assistant中的函数声明,是以前旧版本的Qt的connect函数的声明
在以前的版本中,给信号参数传参需要要搭配一个SIGNAL宏,给槽参数传参需要搭配一个SLOT宏。从Qt5开始,对上述写法进行了简化,给connect重载版本,第二个参数和第四个参数成了泛型函数,允许传入任意参数了
此时connect函数就带有了一定的参数检查的功能,若传入的第一个参数和第二个参数,或者第三个参数和第四个参数不匹配,代码出现编译错误。不匹配是指:2、4参数传入的函数指针,不是1、3参数的成员函数的指针
connect函数使用案例:点击按钮关闭窗口
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* btn = new QPushButton("按钮", this);
btn->move(200, 200);
connect(btn, &QPushButton::clicked, this, &QWidget::close);
}
Widget::~Widget()
{
delete ui;
}
三、自定义槽函数
- 早期的Qt版本要求槽函数必须写到"public slots"下,但是现在高级版本的Qt允许写到类的"public"作用域中或者全局下
- 返回值为void,需要声明,也需要实现
- 可以有参数,可以发生重载
代码编写槽函数
widget.h:
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QPushButton>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void HandleClicked();//槽函数声明
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* btn = new QPushButton("按钮", this);
btn->move(200, 200);
connect(btn, &QPushButton::clicked, this, &Widget::HandleClicked);
}
Widget::~Widget()
{
delete ui;
}
//槽函数定义
void Widget::HandleClicked()
{
setWindowTitle("按钮已按下");
}
ui创建槽函数
自动生成的槽函数的名字是on_pushButton_clicked,其中on是固定的,pushButton是ui中的objectName,clicked写明了是哪种信号。ui自动生成的槽函数不需要connect函数就能在触发信号时被回调。(ui_widget.h中调用了QMetaObject::connectSlotsByName,它会触发自动连接信号槽的规则)
四、自定义信号
自定义信号很少用到。因为在GUI中,用户的操作行为是可以穷举的,Qt内置的信号已经覆盖到了大部分可能的用户操作
- 信号是一种特殊的函数,程序员只需写出函数声明,并告诉Qt,这是一个信号即可。这个函数的定义,是Qt在编译过程中,自动生成的(无法干预)
- 信号函数的返回值必须是void,有没有参数都可以,也支持函数重载
- 信号可以使用emit关键字进行发射(Qt5 emit不写也行)
五、带参数的信号和槽
信号和槽也可以带参数。发射信号时,就可以给信号函数传递实参,这个参数就会被传递到对应的槽函数中。信号和槽函数的参数类型必须一致,个数可以不一致,但是信号的参数个数必须大于槽函数的参数个数(当个数不一致时,就会按顺序拿到信号的前N个参数)
一个信号可以通过connect关联多个槽函数,一个槽函数也能被多个信号关联
六、信号和槽断开连接
使用disconnect断开信号槽的连接(主动断开往往是把信号重新绑定到另一个槽函数上)
widget.cpp:
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
btn = new QPushButton("按钮", this);
btn->move(200, 200);
connect(btn, &QPushButton::clicked, this, &Widget::HandleClicked_1);
QPushButton* changeBtn = new QPushButton("修改按钮功能", this);
changeBtn->move(200, 400);
connect(changeBtn, &QPushButton::clicked, this, &Widget::ChangeButtonRole);
}
Widget::~Widget()
{
delete ui;
}
void Widget::HandleClicked_1() { setWindowTitle("修改窗口标题1"); }
void Widget::HandleClicked_2() { setWindowTitle("修改窗口标题2"); }
void Widget::ChangeButtonRole()
{
disconnect(btn, &QPushButton::clicked, this, &Widget::HandleClicked_1);
connect(btn, &QPushButton::clicked, this, &Widget::HandleClicked_2);
qDebug() << "修改成功";
}
widget.h:
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QPushButton>
#include <QDebug>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void HandleClicked_1();
void HandleClicked_2();
void ChangeButtonRole();
private:
Ui::Widget *ui;
QPushButton* btn;
};
#endif // WIDGET_H
若这里没有disconnect,会使一个信号绑定两个槽函数,触发点击按钮,同时执行两个槽函数
七、信号和槽存在的意义
- **解耦合:**信号发送者不需要知道发出的信号被哪个对象的槽函数接收,槽函数也不需要知道哪些信号关联了自己,Qt的信号槽机制保证了信号与槽函数的调用。支持信号槽机制的类或者父类必须继承于QObject类
- **实现"多对多"的效果:**一个信号可以connect到多个槽函数上,一个槽函数也可以被多个信号connect(实际开发中,这种情况极少)
缺点
与回调函数相比,信号和槽稍微慢⼀些,因为它们提供了更高的灵活性,尽管在实际应用程序中差别不大。通过信号调用的槽函数比直接调用的速度慢约10倍(这是定位信号的接收对象所需的开销;遍历所有关联;编组/解组传递的参数;多线程时,信号可能需要排队),这种调用速度对性能要求不是非常高的场景是可以忽略的,是可以满足绝大部分场景
八、Lambda表达式定义槽函数
- 注意被捕获变量的生命周期
- 尽量传值捕获,传引用捕获可能会捕获到已经被释放的变量,造成程序崩溃
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
setFixedSize(1000, 1000);
QPushButton* button = new QPushButton("点击移动", this);
button->move(400, 800);
connect(button, &QPushButton::clicked, this, [=](){
qDebug() << "Lambda";
button->move(800, 800);
});
}
Widget::~Widget()
{
delete ui;
}
上述代码传值捕获没问题,传引用捕获会崩溃。原因是button是局部变量(它指向的空间位于堆区,但其本身是一个局部变量的指针),构造函数结束后button变量即被销毁,造成程序崩溃