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

相关推荐
用户805533698035 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner5 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz10 天前
QML Hello World 入门示例
qt
xcyxiner13 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner14 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner14 天前
DicomViewer (添加模型类)3
qt
xcyxiner15 天前
DicomViewer (目录调整) 2
qt
xcyxiner15 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00617 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术17 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript