QT之信号和槽

在刚刚了解Qt的时候,为了通过按钮显示 hello world 的时候曾说明过信号与槽,虽然没有细说,不过也算是接触过。

而本文就会细细说明什么是 Qt 的信号与槽。

概念初识

在 linux 学进程相关的内容的时候,曾了解过信号是操作系统控制进程的一种方式,可以看作是操作系统和进程通信的方式,而 Qt 的信号与槽实际上也差不多。

Qt中的信号三要素

  • 信号源:发出信号的控件
  • 信号类型:用户进行不同的操作会发送不同的信号,比如按钮被点击了,某个文本被复制了,鼠标光标被移动的信号,这些信号都需要区分
  • 槽:槽实际上就是一个信号到来时所需要执行的函数

在 Qt 中需要先通过 connect 函数将信号和槽关联起来,当特定的信号到来时,就触发特定的槽,执行特定的函数。

而 Qt 中一个类如果使用信号与槽,必须在类内部带一个宏。

这个宏会展开成很长一段的代码,想要使用信号与槽必须带它。

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 : 用于指定的关联方式,不过一般采用缺省的 AutoConnection 方式
小实验

我们做一个实验,即以代码的方式实现通过点击按钮关闭窗口。

在 MainWindow.cpp 中初始化一个按钮,并且通过 connect 函数将 QPushButton 中的 clicked 信号和 QMainWindow 中的 close 槽函数关联起来。

cpp 复制代码
#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 函数绑定信号与槽。

cpp 复制代码
#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 这个对象中获取。

cpp 复制代码
#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 函数会将窗口的标题修改成 "自定义信号"。

cpp 复制代码
#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 信号。

cpp 复制代码
#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 会将信号的参数作为实参发送给槽函数,我们可以试一试。

首先通过图形化界面设置两个按钮,并且通过图形化界面形式自定义槽函数。

然后再自己设置自己的信号和槽函数,它们都带有参数,并且形式符合规则。

cpp 复制代码
#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.

cpp 复制代码
#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 表达式作为槽函数。

cpp 复制代码
#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 函数连接可能比较繁琐,但是它实现了代码的低耦合,虽然也足够优秀,但是对比市场上的可能有点不足,这是由于时代的局限性造成的。

相关推荐
ZSYP-S12 分钟前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos15 分钟前
c++------------------函数
开发语言·c++
程序员_三木27 分钟前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
是小崔啊37 分钟前
开源轮子 - EasyExcel01(核心api)
java·开发语言·开源·excel·阿里巴巴
tianmu_sama43 分钟前
[Effective C++]条款38-39 复合和private继承
开发语言·c++
黄公子学安全1 小时前
Java的基础概念(一)
java·开发语言·python
liwulin05061 小时前
【JAVA】Tesseract-OCR截图屏幕指定区域识别0.4.2
java·开发语言·ocr
jackiendsc1 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
Oneforlove_twoforjob1 小时前
【Java基础面试题027】Java的StringBuilder是怎么实现的?
java·开发语言
羚羊角uou1 小时前
【C++】优先级队列以及仿函数
开发语言·c++