【QT】信号与槽:自定义信号、参数传递与Lambda

自定义信号

从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在这里都可以使用。

相关推荐
jiaguangqingpanda2 小时前
Day23-20260119
java·开发语言
Java程序员威哥2 小时前
Spring Boot 3.x 云原生终极适配:GraalVM 原生镜像构建 + Serverless 生产级部署(完整实战+最优模板)
java·开发语言·spring boot·后端·云原生·serverless·maven
黛玉晴雯子0012 小时前
Kubernets-Helm&发布模式(持续更新)
java·开发语言
工业甲酰苯胺2 小时前
2026 年 PHP 函数式编程 优势与实际应用
开发语言·php
leaves falling10 小时前
C语言内存函数-
c语言·开发语言
至为芯12 小时前
IP6537至为芯支持双C口快充输出的45W降压SOC芯片
c语言·开发语言
小羊羊Python12 小时前
SoundMaze v1.0.1正式发布!
开发语言·c++
浩瀚地学12 小时前
【Java】JDK8的一些新特性
java·开发语言·经验分享·笔记·学习
l1t12 小时前
利用DeepSeek将python DLX求解数独程序格式化并改成3.x版本
开发语言·python·算法·数独