初识Qt——信号和槽

1. 信号和槽函数

之前我们在Linux中也学过信号。虽然Qt的信号和Linux中的信号是完全不同的概念,但两者在抽象层面上确实有异曲同工之处。

Linux中的信号是系统内部的通知机制或进程间的通信方式,其本质是操作系统层面上的异步中断机制。一旦信号被触发,内核会中断当前进程的执行流,直接跳转执行预先注册的信号处理器函数。

Qt信号则是应用程序框架层面的通信机制,它基于Qt的事件循环。当信号被emit触发时,Qt会根据连接类型决定是立即同步调用槽函数(本质上也是一种回调函数),还是将信号封装为事件放入队列中异步等待处理。

这两种信号机制都遵循一个共同的抽象模型------三个核心要素:

三个核心要素:

信号源:谁发出的信号。Linux中是内核、硬件或其他进程;Qt中是QObject派生类的实例对象。

信号类型:信号的标识。Linux中是固定的31个标准信号编号(如SIGINT、SIGSEGV);Qt中是由开发者任意定义的可携带复杂参数的信号。

信号处理方式:谁来处理、如何处理。Linux中通过signal()或sigaction()注册处理器函数;Qt中通过connect()将信号与槽函数绑定。

2. connect函数

cpp 复制代码
connect(sender, &Sender::signal, receiver, &Receiver::slot);
  • sender:发送信号的对象指针(必须是QObject派生类的指针)------谁发的信号

  • &Sender::signal:发送者类中的信号类型指针(本质是成员函数指针)------发送的什么类型的信号

  • receiver:接收信号的对象指针(也必须是QObject派生类指针,且该对象必须存活到信号触发时)------谁来处理这个信号

  • &Receiver::slot:接收者类中的槽函数指针(可以是普通槽函数、信号、Lambda表达式、全局函数)------怎么处理这个信号

注意:Qt中的connect函数和Linux中 TCP socket中建立连接的函数,没有任何关系,只是名字恰巧一样了

下面我们在widget.cpp的构造函数中实现一个可以关闭窗口的按钮:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    
    QPushButton* close = new QPushButton(this);
    close->setText("关闭窗口");
    close->move(200,200);
    
    connect(close,&QPushButton::clicked,this,&QWidget::close);
}

Widget::~Widget()
{
    delete ui;
}

运行后,界面出现一个"关闭窗口"的按钮,点击按钮就可以实现窗口的关闭:

那么问题来了,我们怎么知道有哪些信号和槽函数呢?就像这里的clicked和close

答案就是------看官方文档!磨刀不误砍柴工,用的多了记住的也就多了,不记得的就去看看文档。

有时候在对应的类中没有找到相对应的信号或者槽,就找找这个类的父类或者祖宗类。

3. 自定义槽函数

3.1 普通成员函数定义

自定义槽函数其实就和自定义一个普通的函数没什么区别

cpp 复制代码
//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:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget() override;
    
    //自定义一个槽函数
    void handlePush();

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H
cpp 复制代码
//widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
#include <QMessageBox>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    QPushButton* push = new QPushButton(this);
    push->setText("按下");
    push->move(200,200);
    //使用自定义的槽函数
    connect(push,&QPushButton::clicked,this,&Widget::handlePush);
}

Widget::~Widget()
{
    delete ui;
}

void Widget::handlePush()
{
    //设计一个弹出窗口提示,显示"按钮被按下!"
    QMessageBox::information(this, "提示", "按钮被按下!");
}

运行后,点击按钮:

之前的版本中,槽函数必须放到public/private/protected slots:下,Qt5之后就可以不用这样写了。但是在使用图形化界面自动生成槽函数时,也会自动生成。

3.2 图形化界面自动生成

下面我们来尝试图形化界面自动生成槽函数。双击.ui文件进入图形化界面,拖拽一个按钮到界面中,修改按钮显示文字,右键单击按钮

选择"转到槽",可以看到和按钮(QPushButton)及其父类和祖宗类相关的所有信号

我们选中第一个clicked并确定或者双击clicked,就会自动生成槽函数声明和定义并直接跳转到对应槽函数定义中,我们可以直接在这里写相关的代码。

但是我们会发现这样操作后,并没有自动生成connect函数,那么信号和槽怎么连接呢?

我们观察一下,代码自动生成的槽函数的函数名(on_控件名_信号类型):

当函数名符合上述规则后,Qt就能自动把信号和槽建立上联系。

所以,如果是通过代码手动创建控件并自定义槽函数时,我们还是要使用connect;如果是使用图形化界面的方式来实现自定义槽函数,就可以不用管connect。

3.3 Lambda表达式

lambda表达式比较常用于回调函数,而槽函数本质上也是一种回调函数,所以也常用lambda表达式写槽函数,而且lambda表达式写槽函数会更简洁。

cpp 复制代码
//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:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget() override;

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H
cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
#include <QMessageBox>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    connect(ui->pushButton,&QPushButton::clicked,this,[this](){
        QMessageBox::information(this, "提示", "lambda表达式实现槽函数");
    });
}

Widget::~Widget()
{
    delete ui;
}

点击界面按钮后:

如果我们的槽函数只是一次性使用,并且比较简短就可以采用槽函数的形式。

另外也要确认捕获到lambda内部的变量是有意义的,因为槽函数执行的时机是不确定的(无法确定用户什么时候触发),我们要确保无论何时用户点击了按钮,捕获到的变量都能正确使用。

4. 自定义信号

自定义信号是你在自己的类中声明的、由moc自动生成实现的特殊成员函数。它的作用是在特定事件发生时通知其他对象。

class MyClass : public QObject {

Q_OBJECT

signals: // 信号区域

void mySignal();

};

自定义信号使用关键规则:

  • 信号必须在signals: 关键字下声明(这是Qt的特殊关键字)

  • 信号只需要声明,不需要实现(moc会自动生成实现)

  • 信号的返回值必须是void

  • 信号可以有任意数量和类型的参数

  • 在 signals: 下默认是protected

  • 信号被触发时要用到emit关键字(也是Qt特有的关键字),但是emit什么实际作用都没有,去掉emit代码也不会有任何影响,但是一般在开发过程中还是会加上它,增加代码的可读性

下面我们来演示一下,通过自定义信号的方式也实现一个点击按钮出现弹窗的效果:

cpp 复制代码
//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:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget() override;
    //自定义一个槽函数
    void handlePush();

signals:
    //自定义一个信号
    void MySignal();

private slots:
    void on_pushButton_clicked();

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H
cpp 复制代码
//widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
#include <QMessageBox>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    connect(this,&Widget::MySignal,this,&Widget::handlePush);
}

Widget::~Widget()
{
    delete ui;
}

void Widget::handlePush()
{
    QMessageBox::information(this, "提示", "按钮被按下!");
}

void Widget::on_pushButton_clicked()
{
    //触发自定义的MySignal信号
    emit MySignal();
    //MySignal();
}

在widget类中自定义一个MySignal信号,将这个信号和handlePush连接起来,按下按钮时触发该信号,从而实现弹窗效果。这里只是为了讲解自定义信号才这样设计,实际开发中肯定不会自定义一个Qt本身已经有相同效果的信号,能用Qt中自带的信号就用已有的。

5. 带参数的信号和槽

信号和槽其实都是可以带参数的,此时发射信号的时候,就可以给信号函数传递实参,与之对应的这个参数就会被传递到相应的槽函数中。

传参可以起到复用代码的效果------有多个逻辑整体一致,但是涉及到的数据不同,就可以通过函数(参数)来实现复用代码,在不同的场景传入不同的参数即可。

Qt中很多内置信号也是带有参数的,大家也可以去了解一下。

还是以我们前面的代码为例,来实现一下通过传参来设计窗口弹出的内容:

cpp 复制代码
//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:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget() override;
    //自定义一个槽函数
    void handlePush(const QString&);

signals:
    //自定义一个信号
    void MySignal(const QString&);

private slots:
    void on_pushButton_clicked();

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H
cpp 复制代码
//widget.cpp

#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
#include <QMessageBox>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    connect(this,&Widget::MySignal,this,&Widget::handlePush);
}

Widget::~Widget()
{
    delete ui;
}

void Widget::handlePush(const QString& context)
{
    QMessageBox::information(this, "提示", context);
}

void Widget::on_pushButton_clicked()
{
    //触发自定义的MySignal信号
    emit MySignal("实现带参信号和槽");
}

点击按钮后的效果:

信号(int)和槽(int)的参数类型必须一致,但是个数可以不一致。如果个数不一致,要求信号(int, QString)的参数个数必须大于槽(int)的参数个数。信号传过来的参数,槽可以选择接收前几个,忽略后面的。

在Qt中可以实现一个信号和多个槽函数绑定,一个槽函数也能被多个信号绑定,所以这样设计参数的传递机制可以让信号和槽的绑定更加灵活。

但是这个"多对多"的功能,在实际开发中不怎么用到,绝大多数情况"一对一"就够用了,所以现在很多GUI开发框架都不支持这种"多对多了"。

6. 信号和槽断开

cpp 复制代码
disconnect(sender, &Sender::signal, receiver, &Receiver::slot);

将绑定的信号和槽断开要用到disconnect函数,用法和connect函数差不多。实际开发中很少会用到,一般只有要将某个信号重新绑定到新的槽函数上才需要先disconnect再重新connect,否则就会出现信号和槽"一对多"的情况,虽然不会报错,但是可能导致功能混乱。

相关推荐
不正经学生4 小时前
C语言字符串函数进阶:安全版函数 + 错误处理 + 子串查找
c语言·开发语言·c++·算法·面试
ITKEY_4 小时前
neovim基于nvim-jdtls 开发java报错
java·开发语言
無限進步D5 小时前
Java 代码块
java·开发语言
吃好睡好便好5 小时前
MATLAB仿真框图4
开发语言·matlab·仿真·分段函数
TheBestRucy5 小时前
基于Dify的旅游攻略&王者荣耀攻略智能助手项目
服务器·开发语言·人工智能·python·算法·旅游
一水5 小时前
Pi :内置四工具源码拆解——文件锁、变更快照与 bash 隔离
开发语言·chrome·ai·bash
梅菲塔亚5 小时前
Keepalived高可用全解析
linux·运维·服务器·开发语言·网络·php
依然鸣5 小时前
PTA团体程序设计天梯赛L2真题讲解L2-005-008
开发语言·数据结构·c++·算法·深度优先·pat考试·图论
君顾16 小时前
代驾跑腿业务系统实战开发指南
java·开发语言·代驾跑腿
夏幻灵6 小时前
从语法坑点到 JS 内存底层:Vue 3 watch 侦听器深度全解
开发语言·javascript·vue.js