在刚刚了解Qt的时候,为了通过按钮显示 hello world 的时候曾说明过信号与槽,虽然没有细说,不过也算是接触过。
而本文就会细细说明什么是 Qt 的信号与槽。
概念初识
在 linux 学进程相关的内容的时候,曾了解过信号是操作系统控制进程的一种方式,可以看作是操作系统和进程通信的方式,而 Qt 的信号与槽实际上也差不多。
Qt中的信号三要素
- 信号源:发出信号的控件
- 信号类型:用户进行不同的操作会发送不同的信号,比如按钮被点击了,某个文本被复制了,鼠标光标被移动的信号,这些信号都需要区分
- 槽:槽实际上就是一个信号到来时所需要执行的函数
在 Qt 中需要先通过 connect 函数将信号和槽关联起来,当特定的信号到来时,就触发特定的槽,执行特定的函数。
而 Qt 中一个类如果使用信号与槽,必须在类内部带一个宏。
这个宏会展开成很长一段的代码,想要使用信号与槽必须带它。
connect函数
connect( const QObject* sender, const char * signal, const QObject* receiver, const char * method, Qt::ConnectionType type = Qt::AutoConnection() );
- sender : 信号源,即发出信号的控件
- signal :信号,即信号源发出的信号类型
- receiver : 接收者,即处理信号的控件
- method : 槽,即接收者处理信号的动作/函数
- type : 用于指定的关联方式,不过一般采用缺省的 AutoConnection 方式
小实验
我们做一个实验,即以代码的方式实现通过点击按钮关闭窗口。
在 MainWindow.cpp 中初始化一个按钮,并且通过 connect 函数将 QPushButton 中的 clicked 信号和 QMainWindow 中的 close 槽函数关联起来。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QPushButton* button = new QPushButton(this);
button->setText("closed");
button->move(300,300);
connect(button,&QPushButton::clicked,this,&QMainWindow::close);
}
MainWindow::~MainWindow()
{
delete ui;
}
其中,在控件中都有类似的情况,比如 QPushButton 中出现了 click 和 clicked,这二者有什么不同呢?
实际上第一个 click 是一个槽函数,它的作用就是模拟一次点击的动作。
而第二个 clicked 则是一个信号,它是该按钮被点击之后所发送的信号。
而区别二者就是通过前面的图标
这个图标表示一个槽函数。
这个图标表示一个信号。
使用 connect 函数可以通过这个图标区别是信号还是槽函数。
QT5之后的connect函数
上述的connect函数第二个参数和第四个参数都是一个 const char* 类型的,但是我们使用时传入的却是函数,这实际上会引发类型错误。
实际上上述的 connect 函数是 QT5 之前的函数,当时传入信号和槽还需要通过 SIGNAL 和 SLOT 两个宏函数来进行转化。
现在更新后就可以直接传了。
自定义槽函数
作为一个前端工具,只有类自带的槽函数是不够用的,因此我们需要自定义槽函数。
而自定义槽函数的连接有两种方式:通过 connect 函数进行连接和通过 图形化界面生成一个槽函数,QT自己通过 connectSlotByName 进行连接。
通过代码形式
可以自己初始化一个控件,然后定义一个函数后通过connect 函数绑定信号与槽。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QPushButton* button = new QPushButton(this);
button->setText("hell world");
connect(button,&QPushButton::clicked,this,&MainWindow::Handlerclicked);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::Handlerclicked()
{
this->setWindowTitle("clicked!");
}
如果这个空间是在 ui 界面直接通过拖拽方式得到的话,就需要从 ui 这个对象中获取。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->pushButton,&QPushButton::clicked,this,&MainWindow::Handlerclicked);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::Handlerclicked()
{
ui->pushButton->setText("hellworld");
}
这样就能够通过代码形式 connect 自定义的槽函数。
通过图形化界面形式
当我们拖拽一个控件到画布上时,通过右键点击可以发现有一个 “转到槽” 的选项。
点击 “转到槽” 之后即可选择这个槽函数所连接的信号。
选择信号后,发现 QT 自动生成了一个函数。
其名字由发出信号的控件名以及发出的信号组成。
通过这种方式也可以将控件的信号与槽函数连接起来,而不用通过connect 函数连接。
并且这个生成的函数名不可修改,因为QT就是通过这个函数名和信号进行连接的,我们可以实验一下。
其中 connectSlotsByName 是一个函数,表示通过函数名来连接槽函数与信号,因此函数名不可随便修改。
自定义信号
在QT中,我们也可以自定义信号,虽然在开发场景中很少用,不过还是需要了解一下。
QT中的信号实际上就是一个函数,我们看一看如何实现。
在 MainWindow.h 中声明一个函数,其中这个函数前面需要用 signals 关键字修饰一下。
然后在 MainWindow.cpp 中绑定即可。
绑定的 HandlerSignal 函数会将窗口的标题修改成 "自定义信号"。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(this,&MainWindow::mySignal,this,&MainWindow::HandlerSignal);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::HandlerSignal()
{
this->setWindowTitle("自定义信号");
}
但是这里只是将信号和槽绑定了而已,但是这个信号并没有发出。
因此我们可以通过间接的方式来发送信号。
这里将 pushButton 的 clicked 信号和 HandlerPush 槽函数绑定,而这个槽函数内部会发送一个 mySigal 信号。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(this,&MainWindow::mySignal,this,&MainWindow::HandlerSignal);
QPushButton* pushbutton = new QPushButton(this);
pushbutton->setText("发送 mySignal 信号");
connect(pushbutton,&QPushButton::clicked,this,&MainWindow::HandlerPush);
pushbutton->move(200,200);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::HandlerSignal()
{
this->setWindowTitle("自定义信号");
}
void MainWindow::HandlerPush()
{
emit mySignal();
}
虽然QT5之后可以不用带 emit 关键字就能发送信号了,但是一般还是带上,防止出现错误。
emit : 发送信号的关键字。
点击之后,发现 window 的标题改变了,证明确实发送了 mySignal 信号了。
带参数的信号与槽
Qt 的信号与槽函数都能够带参数,就像函数传参一样。
无论自定义的信号与槽还是 Qt 自带的信号与槽都有参数,不过它们都需要遵守一定的规则。
- 信号的参数和槽的参数类型必须相同,即信号的参数1和槽的参数1类型必须相同
- 信号的参数个数可以和槽的参数个数可以不同,即信号的参数个数可以比槽的参数个数多
当一个信号被发送时,如果槽函数在遵守上面的规则的情况下需要信号传参,那么 Qt 会将信号的参数作为实参发送给槽函数,我们可以试一试。
首先通过图形化界面设置两个按钮,并且通过图形化界面形式自定义槽函数。
然后再自己设置自己的信号和槽函数,它们都带有参数,并且形式符合规则。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QString>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void MyHandlerSignal(const QString& text);
signals:
void mysignal(const QString& text);
private slots:
void on_pushButton_clicked();
void on_pushButton_2_clicked();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
两个按钮都会发送一个带有参数的信号,而这个信号对应的函数会修改窗口的名称。
接着我们运行一下看看会发生什么?
可以看到窗口的标题确实修改了。并且修改成了信号所带的参数。
除了自定义的槽函数和信号有参数之外,一些自带的槽函数和信号也有参数。
比如这个 PushButton 的控件,其 clikced 信号既有没参数的,也有带参数的。
取消信号槽的连接
信号和槽不仅可以通过 connect 连接,也能够通过 disconnect 函数取消连接。
因为 Qt 的信号槽机制支持多对多连接,即一个信号可以绑定多个槽函数,一个槽函数也能够绑定多个信号,虽然这个机制不常用,但是如果忽略可能会出现错误。
因此如果有需求的话,可以通过 disconnect 先取消连接,再重新连接其他的信号与槽。
我们可以实验一下,这里有两个按钮,第一个按钮和 Handler1 建立连接,Handler1 会修改窗口名称为 Handler1.
第二个按钮会将第一个按钮和 Handler1 的连接断开,然后和 Handler3 建立连接。
Handler3 会修改窗口的名称为 Handler3.
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->pushButton,&QPushButton::clicked,this,&MainWindow::Handler1);
connect(ui->pushButton_2,&QPushButton::clicked,this,&MainWindow::Handler2);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::Handler1()
{
this->setWindowTitle("Handler1");
}
void MainWindow::Handler2()
{
//取消第一个按钮的连接
disconnect(ui->pushButton,&QPushButton::clicked,this,&MainWindow::Handler1);
//连接其他的槽
connect(ui->pushButton,&QPushButton::clicked,this,&MainWindow::Handler3);
}
void MainWindow::Handler3()
{
this->setWindowTitle("Handler3");
}
可以看到结果正如预期所料。
如果这里不提前 disconnect 的话,那么按钮1 就会同时和 Handler1 和 Handler3 建立连接,导致一对多的情况出现。
采用 lambada 表达式作为槽函数
在 Qt 5 以及更高版本的 Qt 下, 一般默认采用的 C++11 编译,因此可以使用 lambada 表达式。
而如果是 Qt4 以及更低的版本,就需要添加指令来采用 C++11 编译。
这里我们就是用 lambada 表达式作为槽函数。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPushButton>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
QPushButton* button = new QPushButton(this);
button->setText("按钮");
connect(button,&QPushButton::clicked,this,[=](){
button->move(300,300);
});
}
MainWindow::~MainWindow()
{
delete ui;
}
发现确实执行了 lambada 表达式函数的内容。
总结
本文讲解了什么是信号与槽,也讲了信号槽的使用方法,比如自定义信号和槽函数,带参数的信号与槽,disconnect 的使用和 lambada 表达式的槽函数。
总的来说信号槽这个机制挺优秀,但是其他的 GUI 工具并没有使用信号槽的机制,比如 java 的GUI开发就是通过类似赋值的手段来将某一个函数与信号关联起来,而不是用 connect 函数来连接。
connect 函数连接可能比较繁琐,但是它实现了代码的低耦合,虽然也足够优秀,但是对比市场上的可能有点不足,这是由于时代的局限性造成的。