Qt 实战(4)信号与槽 | 4.1、信号与槽机制


前言:

Qt信号与槽机制是一种用于处理对象间通信的强大机制,它是Qt框架的核心特性之一。信号与槽机制使得Qt对象可以在不了解彼此的情况下进行通信,这种松耦合的设计思想极大地提高了代码的可重用性和灵活性。

一、信号与槽机制

1、基本概念

在Qt中,一个对象可以发出一个信号,而另一个对象的槽函数可以接收这个信号并作出响应。信号是一个类的成员函数,当某个特定的事件发生时,它会被自动调用。槽函数也是类的成员函数,它可以被信号连接,当信号发出时,槽函数会被自动执行。下面是与信号槽机制相关的基本概念,如下:

1)信号

在Qt中,信号是一个特定的成员函数,用于在某个特定事件发生时被发射(emit)。信号本身并不实现任何功能,它只是表明某个事件已经发生。信号可以被定义为一个类的成员函数,但其实现是由Qt的元对象系统自动完成的。信号的声明通常在类的头文件中进行,使用signals关键字来标记。

2)槽

槽是普通的成员函数,可以被信号触发。当信号被发射时,与之连接的槽函数将被调用。槽函数可以实现具体的功能,比如更新界面、处理数据等。槽函数的声明和普通成员函数一样,但在Qt的元对象系统中需要进行一些特殊的标记,以便与信号进行连接。

2、信号与槽函数连接

信号与槽的连接是通过QObject::connect()函数实现的,需要注意的是,为了使类支持信号与槽机制,需要在类的声明中包含Q_OBJECT宏。下面介绍几种不同的连接方式,如下:

2.1、connect+宏实现信号与槽连接

在Qt4及之前的版本基于connect+宏实现信号与槽绑定,其中发送信号和槽函数需要用 SIGNAL()SLOT() 来进行声明,connect函数声明如下:

QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, 
    const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)

下面是一段示例代码,如下:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    // 登录按键绑定槽函数
    connect(this->ui->btn_ok, SIGNAL(clicked(bool)), this, SLOT(login()));

}

void MainWindow::login()
{
    QString username = ui->lineEdit_username->text();
    QString password = ui->lineEdit_password->text();

    if (username == "jack" && password == "12345") {
        qDebug() << "login success";
    } else {
        qDebug() << "login fail";
    }
}

基于connect+宏实现信号与槽连接,需要注意下面这些问题

  • 声明槽函数要使用private slotspublic slots关键字
  • 信号和槽参数不能包含任何变量名,只能包含类型

2.2、Qt5新connect函数

Qt5推出了新的connect函数,不需要使用SIGNAL()SLOT()宏,可以在编译时做类型检查,connect函数声明如下:

QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, 
    const QObject *context, Functor functor, Qt::ConnectionType type = Qt::AutoConnection)

使用这种方法槽函数的声明不需要放到slots中,只要像普通的函数一样声明就可以了,类型需要与信号保持一致,下面给登录按键绑定槽函数,如下:

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

    // 声明槽函数,与普通的成员函数一样
    void login();

private:
    Ui::MainWindow *ui;
};

绑定槽函数

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
   
    // 登录按键绑定槽函数
    connect(ui->btn_ok, &QPushButton::clicked, this, &MainWindow::login);
}

void MainWindow::login()
{
    qDebug() << "login";
}

2.3、使用函数指针

在Qt 5版本的connect 函数里,信号与槽函数的参数其实都是函数指针,当信号或槽函数有重载时,使用函数指针可以明确告诉编译器使用哪一个重载函数避免歧义

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

    // 声明两个同名的login函数
    void login();
    void login(int state);

通过函数指针绑定槽函数

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

    // 定义函数指针(注意:声明指向成员函数的指针时,要增加类作用域)
    void (MainWindow::*pfnLoginSlot)() = &MainWindow::login;

    // 登录按键绑定无参槽函数
    connect(ui->btn_ok, &QPushButton::clicked, this, pfnLoginSlot);
}

void MainWindow::login()
{
    qDebug() << "login";
}

void MainWindow::login(int state)
{
    qDebug() << "login state";
}

2.4、使用lambda表达式

在connect函数中,槽函数参数可以改用Lambda表达式的方式来进行传参。使用 Lambda表达式的好处是代码的书写更加方便快捷,同时不需要在类中对槽函数做任何的声明了

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

    // 使用Lamdbda表达式作为槽函数
    connect(ui->btn_ok, &QPushButton::clicked, this, [=](){
        qDebug() << "login";
    });
}

2.5、使用Qt Creator添加信号的槽函数

通过Qt Creator 界面来完成发送信号和槽函数的连接,比如右键点击一个按钮,然后选择“转到槽”:

在这里插入图片描述

Qt Creator会自动生成如下代码,首先是槽函数的声明:

// 槽函数声明
private slots:
    void on_btn_cancel_clicked(bool checked);

槽函数实现,如下:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>

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

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

void MainWindow::on_btn_cancel_clicked(bool checked)
{
    
}

使用这种方法不需要使用connect函数将信号与槽函数做连接。 这里槽函数的命名有一定的规则,一般是 on_objectname_signal 这样来命名的。这种方法优点是减少了手动敲代码的工作量,缺点是究竟有哪些信号与槽函数做了连接不易被发现,没有connect 函数看起来直观。

3、链接类型

信号与槽之间的连接类型(Qt::ConnectionType)是一个重要的参数,它决定了信号和槽之间的调用方式以及线程间的交互行为。连接类型在QObject::connect()函数中指定,共有五种不同的连接类型,每种类型都有其特定的使用场景和效果。

static QMetaObject::Connection connect(const QObject *sender, const char *signal,
	const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);

不同连接类型的作用及使用场景,如下:

  • Qt::AutoConnection(自动连接)
    • 作用:这是默认的连接类型。当信号的发送者和接收者位于同一个线程时,等同于Qt::DirectConnection;当它们位于不同的线程时,等同于Qt::QueuedConnection
    • 使用场景: 适用于大多数情况,尤其是当你不确定信号和槽是否在同一线程时。它会自动选择合适的连接类型,以优化性能和响应性。
  • Qt::DirectConnection(直接连接)
    • 作用:槽函数会在信号发射的同一线程中立即执行。这意味着槽函数的执行与信号的发射是同步的。
    • 使用场景:适用于信号和槽在同一线程中的情况,或者当你需要槽函数立即响应信号时。然而,如果槽函数执行耗时操作,可能会导致UI无响应或程序卡顿。
  • Qt::QueuedConnection(队列连接)
    • 作用:槽函数会在接收者对象所在的事件循环中,在适当的时候被调用。如果接收者在不同线程中,信号会被放入接收者线程的事件队列中,等待线程的事件循环处理。
    • 使用场景:适用于信号和槽在不同线程中的情况,或者当你希望槽函数在接收者线程中异步执行时。这种连接方式有助于避免多线程间的竞争条件和数据不一致问题。
  • Qt::BlockingQueuedConnection(阻塞队列连接)
    • 作用:与Qt::QueuedConnection类似,但发送信号的线程会阻塞,直到槽函数执行完毕。
    • 使用场景:适用于需要等待槽函数执行结果的场景,但仅限于信号和槽在不同线程中的情况。如果它们在同一线程中使用此连接方式,可能会导致死锁。
  • Qt::UniqueConnection(唯一连接)
    • 作用:确保信号的发送者和接收者之间只能建立一个连接。如果已经建立了连接,则后续的connect()调用会失败。
    • 使用场景:当你需要确保信号和槽之间的连接唯一性时,可以使用此连接方式。它可以与上述任何连接类型结合使用,以确保连接的唯一性。

注意:使用Qt::UniqueConnection参数,并不意味着信号只能连接一个槽函数。实际上,Qt::UniqueConnection的作用是确保相同的信号和相同的槽之间的连接是唯一的。换句话说,如果某个信号已经连接到了某个槽,并且你尝试再次使用Qt::UniqueConnection来连接它们,那么第二次连接将不会成功,并且connect()函数会返回false

4、断开信号与槽函数之间的连接

在Qt中,断开(disconnect)信号与槽函数之间的连接是一个重要的操作,特别是在对象被销毁、连接不再需要或者需要改变信号与槽的关联关系时。Qt提供了QObject::disconnect函数来断开信号与槽之间的连接。以下是几种常见的断开信号与槽的方法:

4.1、断开所有信号与特定槽的连接

如果你想要断开一个对象上所有信号与某个特定槽的连接,可以使用以下方式:

// Qt4
QObject::disconnect(sender, nullptr, receiver, SLOT(specificSlot()));

// Qt5
QObject::disconnect(sender, nullptr, receiver, &ReceiverClass::specificSlot);

4.2、断开特定信号与所有槽的连接

如果你想要断开某个特定信号与所有槽的连接,可以这样做:

// Qt4
QObject::disconnect(sender, SIGNAL(specificSignal()), nullptr, nullptr);

// Qt5
QObject::disconnect(sender, &SenderClass::specificSignal, nullptr, nullptr);

4.3、断开特定信号与特定槽的连接

如果你想要断开某个特定信号与某个特定槽的连接,可以这样做:

// Qt4
QObject::disconnect(sender, SIGNAL(specificSignal()), receiver, SLOT(specificSlot()));

// Qt5
QObject::disconnect(sender, &SenderClass::specificSignal, receiver, &ReceiverClass::specificSlot);

注意:当QObject被销毁时,Qt会自动断开所有与之相关的信号和槽的连接。但是,如果你想要更早地断开连接,或者想要确保某些连接在对象销毁之前被断开,那么你需要显式调用QObject::disconnect。

5、信号与槽函数类型要匹配

信号与槽函数参数类型要匹配,例如:信号函数带的参数是QString,那么绑定的槽函数要么不带参数(表示不接收数据),要么是跟信号函数一样的参数,下面的代码编译时会报错,因为信号与槽函数参数类型不匹配,如下:

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

    // 登录按键绑定槽函数
    connect(ui->btn_ok, &QPushButton::clicked, this, &MainWindow::login);

    m_pStudent = new Student(this);
    m_pTeacher = new Teacher(this);


    // 信号函数不带参数,槽函数带参数(QString),信号与槽函数参数类型不匹配编译时报错
    void (Teacher::*pfnTeacherSignal)() = &Teacher::hungry;
    void (Student::*pfnStudentSlot)(QString) = &Student::treat;

    QMetaObject::Connection conn = connect(m_pTeacher, pfnTeacherSignal, m_pStudent, pfnStudentSlot);
}

6、总结

Qt的信号与槽机制提供了一种强大且灵活的方式来处理对象间的通信,它是Qt框架中不可或缺的一部分。通过信号和槽,开发者可以轻松地构建出响应式、事件驱动的用户界面和应用程序逻辑。

问题一:调用connect()重复链接同一个信号与槽,有什么副作用

在Qt中,重复链接同一个信号与槽(即将同一个信号连接到同一个槽函数多次)会产生一些副作用,主要包括以下几点:

  • 槽函数被多次调用:当信号被发出时,与之连接的槽函数会被调用。如果同一个信号被连接到同一个槽函数多次,那么每次信号发出时,槽函数都会被调用相应次数。例如,如果信号与槽连接了两次,那么槽函数将被调用两次。这种重复调用可能会导致不必要的计算或操作,影响程序的性能和效率。
  • 可能导致逻辑错误:在某些情况下,槽函数的多次调用可能会引入逻辑错误。例如,如果槽函数执行了某些状态更改或资源分配操作,并且这些操作不应该被重复执行,那么多次调用槽函数就可能导致状态不一致或资源泄漏等问题。
  • 浪费资源:虽然信号和槽机制本身相对轻量级,但重复连接会占用额外的资源(如内存和处理器时间),尤其是在连接数量很多或程序规模很大的情况下。这些资源的浪费可能会影响程序的性能和响应速度。

为了避免上述副作用,Qt提供了Qt::UniqueConnection枚举类型,用于在连接信号和槽时防止重复连接。当使用Qt::UniqueConnection时,如果信号和槽已经连接过了,再次尝试连接就不会生效,从而防止了槽函数的重复调用。

问题二:一个信号只能连接一个槽函数么?

信号与槽之间支持一对一、一对多、多对一等场景,下面是具体的介绍

1)一对多场景

在一对多场景中,一个信号可以被连接到多个槽函数。当该信号被发射时,所有连接到该信号的槽函数都会被调用。但是,槽函数的调用顺序取决于连接时采用的连接类型(即连接策略)以及信号发射和槽函数执行所在的线程。调用顺序归纳如下:

  • Qt::DirectConnection(直接连接):槽函数会在发出信号的同一线程中直接调用,调用顺序与连接的顺序一致。
  • Qt::QueuedConnection(队列连接):槽函数会在接收对象所在线程的事件循环中调用,调用顺序可能受到线程调度和事件队列状态的影响。
    • 如果所有槽函数都在同一个线程的事件队列中执行,那么它们的调用顺序通常也会与连接的顺序一致。
    • 如果槽函数在不同的线程中执行,由于线程调度的复杂性,槽函数的执行顺序可能会变得不确定。

2)多对一场景

多对一场景则指的是多个信号可以连接到同一个槽函数。这意味着,当这些信号中的任何一个被发射时,都会调用该槽函数。这种机制在Qt中同样被支持,并且可以通过不同的方式实现。例如,可以使用QSignalMapper类来将多个信号映射到同一个槽函数,或者简单地通过connect函数将多个信号连接到同一个槽函数。

问题三:一个类如果没有继承QObject如何进行信号与槽的连接?

实际开发过程可能会遇到这样的场景,一个类(注意:没有继承QObject),打开了一个Qt的对话框,这个类需要处理对话框发送的自定义信号,可以使用下面重载一的connect()函数进行连接,如下:

重载一:QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
重载二:QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal,
	const QObject *context, Functor functor, Qt::ConnectionType type = Qt::AutoConnection)

上面是connect()函数的两个重载版本,为了处理上面的场景,只能使用重载一版本的connect()函数。因为,重载二版本第三个参数需要传入一个QObject *指针,前面提到的类并没有继承QObject类,this指针不能作为第三个参数传入进去,因此,只能使用重载一版本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值