自定义信号
从Qt 5开始,自定义槽函数与定义普通成员函数已无区别,不再需要使用Slot宏。在实际开发中,我们通常需要自定义槽函数来处理用户操作触发的信号。
信号与槽函数有所不同:信号主要用于区分用户操作,而用户操作是可穷举的,因此我们通常直接使用QT提供的信号,很少需要自定义。虽然QT支持自定义信号,但定义信号与定义槽函数存在明显差异。
在QT中,信号是一种特殊的函数类型。我们只需声明函数并告诉QT其为"信号"即可,这个函数的定义,QT会在编译过程中会配合很多既定的框架来实现(我们无法干预)。作为信号函数,返回值必须是void,有没有参数都支持重载。
signal
我们该怎么告诉QT我们定义的是一个信号呢?使用槽函数就可以了,QT自己扩展一个关键字signals。
在.h文件中声明:
signals:
void mySignal();
public:
void handlemySignal();
在cpp中定义handlemySignal函数,然后使用connect函数连接信号与槽:
#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("你好!");
}
运行之后我们发现:

什么也没有!我们把信号与槽连接起来了,但是我们根本就没有发送信号,QT只是自动定义了这个信号,并不会自动触发这个信号。QT内置的信号都不需要我们自己在代码中选择什么时候触发,用户在GUI操作后自动就触发了。我们自己定义的信号要自己选择在什么时候触发。发送信号要使用到一个关键字------emit。
emit
直接使用emit就可以发送我们的信号。
#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("你好!");
}
信号成功发送,标题成功更改。

当然我们可以自由选择在什么时候发送我们的信号。就比如说在按钮被点击的时候发送信号:
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void handlebutton();
signals:
void mySignal();
public:
void handlemySignal();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
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("发送自定义信号");
connect(button,&QPushButton::clicked,this,&Widget::handlebutton);
connect(this,&Widget::mySignal,this,&Widget::handlemySignal);
//发送自定义的信号
//emit mySignal();
}
Widget::~Widget()
{
delete ui;
}
void Widget::handlebutton()
{
emit mySignal();
}
void Widget::handlemySignal()
{
this->setWindowTitle("你好!");
}
运行,点击按钮后,我们定义的信号被成功触发,标题成功修改。

补充:其实QT发展到现在,emit其实背后什么都没干,所有的操作都包含到signals这个关键字里了,不写emit也是可以运行的,但是这样并不规范,可读性差。
带参数的信号槽
信号和槽的函数也是可以带参数的,先看一个例子:
.h文件:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void handlebutton();
signals:
void mySignal(const QString& text);
public:
void handlemySignal(const QString& text);
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);
QPushButton * button = new QPushButton(this);
button->setText("发送自定义信号");
connect(button,&QPushButton::clicked,this,&Widget::handlebutton);
connect(this,&Widget::mySignal,this,&Widget::handlemySignal);
//发送自定义的信号
//emit mySignal();
}
Widget::~Widget()
{
delete ui;
}
void Widget::handlebutton()
{
emit mySignal("改变标题为文本一");
}
void Widget::handlemySignal(const QString &text)
{
this->setWindowTitle(text);
}
运行结果:标题被设置成了文本的内容。

现在我们来总结要点,信号和槽是可以传参数的,当然也是可以重载的,那么我们在传参时要注意什么呢?要保证信号和槽的参数类型要一致,槽函数的参数由信号来传,类型不一样会报错。且信号的参数数量要大于等于槽函数的参数数量。
现在我们来看一看使用了参数的信号槽和不带参数的有什么区别,我们发现参数的引入只做了一件事,将要设置的文本与Widget::handlemySignal()函数解耦了,要设置什么文本直接在发送信号时设置就行了。解耦了有什么好处呢?解耦的好处有很多,这里主要体现的是提高了代码的复用率。
看下面一个例子:
为了简便,我这里使用图形化的方式定义按钮。
#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::handlebutton);
connect(this,&Widget::mySignal,this,&Widget::handlemySignal);
//发送自定义的信号
//emit mySignal();
}
Widget::~Widget()
{
delete ui;
}
// void Widget::handlebutton()
// {
// emit mySignal("改变标题为文本一");
// }
void Widget::handlemySignal(const QString &text)
{
this->setWindowTitle(text);
}
void Widget::on_pushButton_clicked()
{
emit mySignal("改变标题为文本一");
}
void Widget::on_pushButton_2_clicked()
{
emit mySignal("改变标题为文本二");
}
按下按钮pushButton,标题改变:

按下pushButton_2标题改变:

我们用自定义的信号mySignal和自定义的槽函数handlemySignal实现了改变标题这个功能,并且
signals:
void mySignal(const QString& text);
public:
void handlemySignal(const QString& text);
与其它函数都解耦了,要使用这个功能,直接用我们自定义的这个信号槽就可以了。
QT中很多内置的信号也是带有参数的,这个参数不需要我们来传,QT会根据用户的操作自己来传。例如:clicked

这个参数表示当前按钮是不是选中状态,例如平时我在做选择题的时候选中的方框。
参数个数问题
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void handlebutton();
signals:
void mySignal(const QString& text,const QString& text2);
public:
void handlemySignal(const QString& text);
private slots:
void on_pushButton_clicked();
void on_pushButton_2_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#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::handlebutton);
connect(this,&Widget::mySignal,this,&Widget::handlemySignal);
//发送自定义的信号
//emit mySignal();
}
Widget::~Widget()
{
delete ui;
}
// void Widget::handlebutton()
// {
// emit mySignal("改变标题为文本一");
// }
void Widget::handlemySignal(const QString &text)
{
this->setWindowTitle(text);
}
void Widget::on_pushButton_clicked()
{
emit mySignal("改变标题为文本一","");
}
void Widget::on_pushButton_2_clicked()
{
emit mySignal("改变标题为文本二","");
}
运行结果:

当信号的参数个数多余槽函数的参数的参数个数时,编译也可以成功;如果信号的参数个数小于槽函数的参数个数,编译是无法通过的。按常理来讲,信号和槽的参数个数不是应该完全一致吗,为什么QT会这样设计呢?
这样多个信号可以绑定同一个槽函数,让信号与槽函数之间的绑定更灵活了。
Q_OBJECT

QT中硬性规定了,如果某个类要使用信号和槽就必须在类的开头声明Q_OBJTECT这个宏,这个宏会自动为我们生成很多代码。
信号和槽存在的意义
信号槽,最终要解决的问题都是响应用户的操作。信号槽,在各种GUI开发框架中是一个比较独特的存在,例如在前端开发中,不需使用connect函数连接信号和槽,直接使用会调函数就行了
// 前端事件处理
button.addEventListener('click', () => {
console.log('按钮被点击');
});
// Qt信号槽
QObject::connect(button, &QPushButton::clicked,
[](){ qDebug() << "按钮被点击"; });
事件和处理函数被一对一的绑定到一起了,一个事件只能对应一个处理函数,一个处理函数也只能对应一个事件。
而信号槽机制则是为了实现两个目的:
1.将信号和槽解耦合(即把用户的触发操作和处理机制解耦合)
2.实现"多对多"的效果,一个信号可以connect多个槽函数,一个槽函数也可以被多个信号connect。
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
signals:
void mysignal1();
void mysignal2();
void mysignal3();
public:
void mySlot1();
void mySlot2();
void mySlot3();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
信号mysignal1既能与槽函数mySlot1绑定,也能与槽函数mySlot2绑定;
#include "widget.h"
#include "ui_widget.h"
#include<QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(this, &Widget::mysignal1,this,&Widget::mySlot1);
connect(this, &Widget::mysignal1, this, &Widget::mySlot2);
connect(this, &Widget::mysignal2, this, &Widget::mySlot1);
connect(this, &Widget::mysignal2, this, &Widget::mySlot3);
}
Widget::~Widget()
{
delete ui;
}
void Widget::mySlot1()
{
qDebug() << "mySlot1";
}
void Widget::mySlot2()
{
qDebug() << "mySlot2";
}
void Widget::mySlot3()
{
qDebug() << "mySlot3";
}
void Widget::on_pushButton_clicked()
{
emit mysignal1();
}
运行结果,按下后成功打印日志,两个槽函数都被同一个信号绑定。

QT的设计时间非常早,为了将来能处理更多的问题,设计了信号槽机制来实现"多对多"的效果;但是后来大家发现,一对一就可以解决99%的问题;所以后来的框架都没有沿用这个机制。
补充
disconnect
在QT中,disconnect用于断开信号与槽的连接,其用法与connect类似。不过这个功能并不常用,通常建立连接后我们会保持其状态。只有在需要主动断开连接,或将信号重新绑定到新槽函数时,才会用到disconnect方法。
我们先用图形化的方式创建一个按钮:

自定义槽函数并手动连接信号与槽;
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void handleclick();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#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::handleclick);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handleclick()
{
this->setWindowTitle("改变标题");
}
运行结果:

现在我们再创建一个控件,转到槽:

#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void handleclick();
void handleclick2();
private slots:
void on_pushButton_change_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include<QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(ui->pushButton,&QPushButton::clicked,this,&Widget::handleclick);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handleclick()
{
this->setWindowTitle("改变标题");
qDebug()<<"click1";
}
void Widget::handleclick2()
{
this->setWindowTitle("一一");
qDebug()<<"click2";
}
void Widget::on_pushButton_change_clicked()
{
//1.断开信号和原来槽函数的连接
disconnect(ui->pushButton,&QPushButton::clicked,this,&Widget::handleclick);
//2.重新绑定新的信号和槽
connect(ui->pushButton,&QPushButton::clicked,this,&Widget::handleclick2);
}
点击按钮pushButton_change后再点击pushButton,标题变为"一一"。

继续点击按钮pushButton,只打印日志"click2"

如果我们不断开信号槽的连接,直接连接新的槽函数会怎么样呢?
#include "widget.h"
#include "ui_widget.h"
#include<QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(ui->pushButton,&QPushButton::clicked,this,&Widget::handleclick);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handleclick()
{
this->setWindowTitle("改变标题");
qDebug()<<"click1";
}
void Widget::handleclick2()
{
this->setWindowTitle("一一");
qDebug()<<"click2";
}
void Widget::on_pushButton_change_clicked()
{
//1.断开信号和原来槽函数的连接
//disconnect(ui->pushButton,&QPushButton::clicked,this,&Widget::handleclick);
//2.重新绑定新的信号和槽
connect(ui->pushButton,&QPushButton::clicked,this,&Widget::handleclick2);
}
一个信号绑定两个槽函数,两个槽函数都被触发

lambda
Lambda表达式是C++11引入的匿名函数对象,允许你在代码中直接定义函数,无需单独声明。
// 传统方式:需要声明和定义分离的槽函数
class MyClass : public QObject {
Q_OBJECT
public slots:
void onButtonClicked() { // ① 在头文件声明
qDebug() << "按钮被点击";
}
};
// Lambda方式:直接在connect中定义
connect(button, &QPushButton::clicked,
[]() { // 无需声明,直接定义
qDebug() << "按钮被点击";
});
下面我们来看一个具体的例子。
#include "widget.h"
#include "ui_widget.h"
#include<QPushButton>
#include<QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* button = new QPushButton(this);
button->setText("按钮");
button->move(300,300);
connect(button,&QPushButton::clicked,this,[](){
qDebug()<<"lambda被触发!";
}
);
}
Widget::~Widget()
{
delete ui;
}
运行程序:

变量捕获
c++中lambda表达式直接无法获取到上层作用域中的变量,直接使用会报错。

为了解决这个问题,引入了"变量捕获"的语法,通过变量捕获可以获取外层作用域中的变量。
#include "widget.h"
#include "ui_widget.h"
#include<QPushButton>
#include<QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* button = new QPushButton(this);
button->setText("按钮");
button->move(300,300);
connect(button,&QPushButton::clicked,this,[button](){
qDebug()<<"lambda被触发!";
button->move(123,321);
}
);
}
Widget::~Widget()
{
delete ui;
}
成功运行:

如果说,我们要用到的外部变量很多呢,也必须要一个一个的写出来吗?其实是不需要的,直接在[ ]内填入一个'='就可以捕获外部作用域的所有变量。
#include "widget.h"
#include "ui_widget.h"
#include<QPushButton>
#include<QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* button = new QPushButton(this);
button->setText("按钮");
button->move(300,300);
connect(button,&QPushButton::clicked,this,[=](){
qDebug()<<"lambda被触发!";
button->move(123,321);
this->move(100,100);
}
);
}
Widget::~Widget()
{
delete ui;
}
注意:要注意捕获变量的生命周期,如果变量被提前释放,程序会崩溃或者出现未定义的行为!
这里我们的button是new出来的并且加到了对象树上,窗口关闭时才会释放。而this是widget在main函数中定义,main函数关闭时才会释放。所有button和this在这里都可以使用。