【QT】十分钟全面理解 信号与槽的机制

2024-09-06 21:52

本文主要是介绍【QT】十分钟全面理解 信号与槽的机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 从一个定时器开始
  • 全方位简介
      • 1. 基本的信号与槽连接
        • 语法
        • 例子
      • 2. 使用函数指针连接信号与槽(现代 C++ 风格)
        • 语法
        • 例子
      • 3. 使用 Lambda 表达式作为槽
        • 语法
        • 例子
      • 4. 自动连接(`QMetaObject::connectSlotsByName`)
        • 规则
        • 例子
      • 5. 信号与槽的多对多连接
        • 例子(一个信号连接多个槽)
        • 例子(多个信号连接一个槽)
      • 6. 断开信号与槽的连接
        • 语法
        • 例子
      • 7. 信号本身也可以是空的
      • 8. 信号可以连接信号
        • 例子
      • 总结
  • 进一步探讨
    • connect的第三个参数
      • 为什么有时第三个参数是 `this`?
        • 例子: `this` 作为接收对象
      • 总结
    • lambda表达式和捕获
      • 1. 传统的信号与槽连接(四个参数)
      • 2. 使用 lambda 表达式的连接(三个参数)
        • a. Lambda 自带槽的定义
        • b. Lambda 是局部可执行的函数
        • c. Qt 自动处理 lambda 的生命周期
      • 3. 例子对比
        • 传统的四个参数连接
        • Lambda 表达式的三个参数连接
      • 4. 如果需要访问对象时如何处理?
      • 总结
    • 对捕获的理解(捕获上下文)
      • 具体理解为:
      • 举例说明
        • 情况 1:不需要捕获
        • 情况 2:需要捕获 `this` 指针
        • 情况 3:需要捕获局部变量
      • 总结
  • 扩展部分 和 C# 横向对比
      • C# 中的 lambda 表达式上下文
      • 例子:C# 中的 lambda 表达式上下文
      • C# 和 C++/Qt 的比较
      • 进一步理解
        • 例子:C# lambda 捕获局部变量
      • 总结

从一个定时器开始

connect(&timer,&QTimer:timeout,[this](){
});
timer.start();

这段代码是在使用 Qt 的信号与槽机制,特别是 QTimer 类的功能。下面是逐行解释:

  1. connect(&timer, &QTimer::timeout, [this]() { });
    这行代码将 QTimer 对象的 timeout 信号与一个 lambda 表达式(匿名函数)槽连接起来。

    • &timer:指向 QTimer 对象的指针,它是发送 timeout 信号的对象。
    • &QTimer::timeoutQTimer 类的 timeout 信号,在定时器时间到达时发射。
    • [this] () { }:这是一个 lambda 表达式。[this] 表示这个 lambda 捕获当前对象(即类的实例)以便在 lambda 内部使用 this 指针。空的 { } 表示这是一个没有任何操作的槽函数。

    这个连接表示,当 timer 对象的 timeout 信号被触发时,lambda 表达式内的代码会被执行(这里的代码是空的,所以什么也不会发生)。

  2. timer.start();
    这行代码启动了定时器,也就是说,QTimer 开始计时。在经过一定的时间间隔(默认是 0 毫秒或使用 setInterval 设置)后,timeout 信号会被触发。

总的来说,这段代码的作用是创建了一个 QTimer,并设置了当定时器超时时,执行一个空的 lambda 表达式槽函数。

看到这里发现还是有点不够透彻,我们继续往下看~~~

全方位简介

Qt 中的信号与槽机制是其事件处理系统的核心,用于实现对象之间的通信。信号是对象发出的消息,而槽是响应这些消息的函数。Qt 的信号与槽机制非常灵活,允许你使用不同的方式进行连接和处理。以下是详细的讲解:

1. 基本的信号与槽连接

语法
connect(sender, SIGNAL(signalName(arguments)), receiver, SLOT(slotName(arguments)));
  • sender:信号发出的对象。
  • signalName(arguments):要连接的信号的名称,使用 SIGNAL 宏。
  • receiver:槽所在的对象。
  • slotName(arguments):槽的名称,使用 SLOT 宏。
例子
QPushButton *button = new QPushButton("Click me");
connect(button, SIGNAL(clicked()), this, SLOT(onButtonClicked()));
  • button 按钮被点击时,它会发出 clicked() 信号,onButtonClicked() 槽函数会被调用。

2. 使用函数指针连接信号与槽(现代 C++ 风格)

Qt 5 引入了更简洁的信号与槽连接方法,支持使用函数指针来连接。相比传统的 SIGNALSLOT 宏,使用函数指针的方式更安全,且可以检查参数类型。

语法
connect(sender, &SenderClass::signalName, receiver, &ReceiverClass::slotName);
  • SenderClass::signalName:发送信号的函数指针。
  • ReceiverClass::slotName:接收信号的函数指针。
例子
QPushButton *button = new QPushButton("Click me");
connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
  • button 按钮被点击时,MainWindow 中的 onButtonClicked 槽会被调用。

3. 使用 Lambda 表达式作为槽

从 Qt 5.0 开始,可以使用 lambda 表达式作为槽,这使得编写简单的响应代码变得更加方便。

语法
connect(sender, &SenderClass::signalName, [=](){// Lambda 函数体
});
  • [=]:捕获上下文中的变量(值捕获)。
  • SenderClass::signalName:信号的函数指针。
  • Lambda 函数体内可以编写要执行的代码。
例子
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, [=]() {qDebug() << "Timeout!";
});
timer->start(1000);
  • 这个例子每隔 1 秒会输出一次 "Timeout!"

4. 自动连接(QMetaObject::connectSlotsByName

Qt 还支持通过命名约定自动连接信号与槽,通常用于 UI 文件和 QObject 派生类。

规则
  • 信号的格式是:objectName_signalName
  • 槽函数的格式是:on_objectName_signalName
例子
void on_button_clicked();

如果在 UI 文件中有一个 QPushButton,其 objectNamebutton,那么 Qt 会自动将 buttonclicked() 信号连接到 on_button_clicked() 槽。

5. 信号与槽的多对多连接

Qt 的信号与槽机制支持:

  • 一个信号连接多个槽:一个信号可以触发多个槽函数。
  • 多个信号连接同一个槽:不同的信号可以触发同一个槽。
例子(一个信号连接多个槽)
connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
connect(button, &QPushButton::clicked, this, &MainWindow::logButtonClicked);

点击 button 会同时调用 onButtonClicked()logButtonClicked()

例子(多个信号连接一个槽)
connect(button1, &QPushButton::clicked, this, &MainWindow::handleButtonClick);
connect(button2, &QPushButton::clicked, this, &MainWindow::handleButtonClick);

无论点击 button1 还是 button2,都会调用 handleButtonClick()

6. 断开信号与槽的连接

你可以随时断开信号与槽的连接。

语法
disconnect(sender, SIGNAL(signalName()), receiver, SLOT(slotName()));
例子
disconnect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
  • 这将断开 buttonclicked() 信号与 onButtonClicked() 槽的连接。

7. 信号本身也可以是空的

在 Qt 中,信号不需要有槽函数与之连接,它们可以是“空的”。当一个信号发出时,如果没有槽函数接收,也不会有任何错误。这提供了灵活性,使得信号与槽的使用更加松耦合。

8. 信号可以连接信号

在某些情况下,你可能希望一个信号发出时,自动触发另一个信号。在 Qt 中这是允许的。

例子
connect(button, &QPushButton::clicked, anotherButton, &QPushButton::click);

button 被点击时,它将发出 clicked() 信号,anotherButton 将接收到 click() 信号。

总结

Qt 的信号与槽机制非常灵活,支持多种连接方式:

  • 经典的 SIGNALSLOT 宏语法。
  • 使用函数指针的现代 C++ 风格。
  • 使用 lambda 表达式的简洁写法。
  • 自动连接的方便方法。

Qt 的信号与槽机制通过松耦合的方式实现了对象之间的通信,使得 Qt 应用程序具有高度的模块化和可维护性。

进一步探讨

我们发现一般情况下,connect 是四个参数,而使用lambda表达式时是需要三个参数,这是为什么呢?为什么lambda需要捕获呢?

connect的第三个参数

在 Qt 的 connect 函数中,第三个参数指的是“槽函数的接收对象” 或者说是 槽函数的主子,即信号触发时执行槽函数的对象。因此,第三个参数通常是槽所属的对象。例如,在以下代码中:

connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
  • button 是发送信号的对象(QPushButton),即信号发出的源。
  • &QPushButton::clicked 是信号,表示按钮点击时会发出 clicked() 信号。
  • this 是接收信号的对象,也就是槽函数的所属对象。在这种情况下,this 表示当前对象(通常是 MainWindow),也就是槽函数 onButtonClicked 所在的对象。
  • &MainWindow::onButtonClicked 是槽函数的指针,表示当 clicked() 信号被触发时,onButtonClicked 函数会被调用。

为什么有时第三个参数是 this

当槽函数是类的成员函数时,你通常会使用 this 作为接收对象。因为槽函数 onButtonClicked 属于 MainWindow 类,你需要告诉 connect 函数在哪个对象上调用这个槽函数,因此使用 this,指代当前的 MainWindow 实例。

例子: this 作为接收对象
connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);

这里的 thisMainWindow 类的对象,表示当 button 被点击时,MainWindowonButtonClicked 函数会被调用。

总结

  • connect 的第三个参数用于指明接收信号并执行槽函数的对象。当槽函数属于当前类实例时,通常使用 this

lambda表达式和捕获

当使用 lambda 表达式作为槽时,connect 只需要三个参数的原因在于 lambda 本质上就是一个内联的可调用对象,它已经包含了槽函数的定义。因此,不再需要明确地指定槽函数的接收对象。下面详细解释原因。

1. 传统的信号与槽连接(四个参数)

在传统的 Qt 信号与槽机制中,connect 函数的四个参数分别是:

connect(sender, SIGNAL(signalName()), receiver, SLOT(slotName()));
  • sender:信号的发送者。
  • signalName:信号的名称,定义了发送者会触发哪个信号。
  • receiver:槽的接收者,指明哪个对象的槽函数会响应信号。
  • slotName:槽的名称,指明接收者的哪个函数会处理信号。

这种方式需要指定接收对象 receiver,因为 Qt 需要知道在哪个对象上调用槽函数。

2. 使用 lambda 表达式的连接(三个参数)

当使用 lambda 表达式时,connect 只需要三个参数:

connect(sender, &SenderClass::signalName, []() {// Lambda 作为槽
});

原因在于,lambda 表达式本质上是一个可调用对象,而且这个可调用对象已经包含了执行的代码逻辑,因此不需要再指定一个接收对象。具体原因如下:

a. Lambda 自带槽的定义

在传统方式中,槽函数是一个对象的成员函数,因此需要指定在哪个对象上调用槽函数(通过 receiver 参数)。但 lambda 表达式是匿名的,它定义了槽函数的逻辑,因此:

  • 不需要一个额外的接收对象。lambda 自身就是一个可调用的对象,它会在信号触发时直接执行 lambda 中定义的代码。
b. Lambda 是局部可执行的函数

Lambda 表达式是一种轻量的方式来处理简单的事件响应,它既可以捕获局部变量,也可以不捕获任何变量。

c. Qt 自动处理 lambda 的生命周期

当使用 lambda 表达式作为槽时,Qt 内部会管理 lambda 的生命周期。只要信号与槽保持连接,lambda 表达式就会保持有效,直到连接被断开或对象被销毁。这与成员函数不同,成员函数必须绑定到一个对象上(即 receiver),而 lambda 是匿名的,因此不需要 receiver 参数。

3. 例子对比

传统的四个参数连接
connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
  • 这里需要指定 this,因为 onButtonClickedMainWindow 类的成员函数,必须指明在哪个对象上调用该函数。
Lambda 表达式的三个参数连接
connect(button, &QPushButton::clicked, []() {// Lambda 逻辑qDebug() << "Button clicked!";
});
  • 这里不需要指定 receiver,因为 lambda 是一个匿名的、内联的函数,它不属于任何特定的对象。

4. 如果需要访问对象时如何处理?

如果 lambda 需要访问某个对象的成员函数或成员变量,可以通过捕获该对象(通常是 this)来实现。例如:

connect(button, &QPushButton::clicked, [this]() {this->doSomething();
});
  • this 被捕获到 lambda 表达式中,因此可以在 lambda 内部调用当前对象的成员函数 doSomething()
  • 在这种情况下,仍然只需要三个参数,因为 this 已经被捕获进 lambda,而不是通过 connect 的第三个参数显式传递。

总结

使用 lambda 表达式时,connect 只需要三个参数是因为 lambda 本身就是一个可调用对象,已经包含了槽函数的逻辑,因此:

  • 不再需要指定接收对象(第三个参数)。
  • Lambda 的执行上下文可以通过捕获来定义,不需要通过 connectreceiver 参数显式传递。

这使得代码更加简洁灵活,特别适用于处理简单的事件响应逻辑。

对捕获的理解(捕获上下文)

当你使用 lambda 表达式 作为槽函数时,确实没有指定槽函数的接收对象。因此,lambda 表达式本身没有上下文,所以需要显式捕获你希望使用的上下文(如 this 指针或局部变量),以便在 lambda 表达式内访问相关的数据或函数。

具体理解为:

  1. 没有接收对象时:
    在传统的 connect 中,第三个参数(接收对象 receiver)是明确指定的上下文,它告诉 Qt 在哪个对象上调用槽函数。因此,槽函数可以直接访问该对象的成员变量和成员函数。例如:

    connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
    

    在这里,onButtonClicked() 是属于 MainWindow 类的成员函数,this 表示槽函数的接收对象,函数执行时有明确的上下文,即 MainWindow 对象的成员可以被访问。

  2. 使用 lambda 表达式时:
    Lambda 表达式没有天然的上下文,因为它是一个匿名的内联函数,不属于某个对象,因此:

    • 如果需要在 lambda 内部访问外部对象或变量,必须通过捕获机制手动传递这些上下文
    • 捕获的变量(如 this 指针或局部变量)会成为 lambda 的执行上下文,使得 lambda 能够访问这些变量。

举例说明

情况 1:不需要捕获
connect(button, &QPushButton::clicked, []() {qDebug() << "Button clicked!";
});
  • 在这个例子中,lambda 不需要上下文,因为它没有访问任何外部对象或变量,只是简单地打印了一条消息,因此没有必要捕获任何上下文。
情况 2:需要捕获 this 指针
connect(button, &QPushButton::clicked, [this]() {this->doSomething();
});
  • 在这里,lambda 表达式内部需要调用当前对象(this)的成员函数 doSomething()。由于 lambda 没有天然的上下文,因此需要通过 [this] 捕获当前对象的指针,以便能够在 lambda 内访问 this->doSomething()
情况 3:需要捕获局部变量
int counter = 0;
connect(button, &QPushButton::clicked, [=]() mutable {counter++;qDebug() << "Counter: " << counter;
});
  • 在这个例子中,lambda 需要访问局部变量 counter。由于 lambda 默认没有访问外部局部变量的能力,所以通过 [=] 捕获所有外部局部变量(按值捕获),这样 lambda 内部就可以访问 counter 变量,并对其进行修改(需要 mutable 关键字)。

总结

  • 没有上下文:当使用 lambda 表达式作为槽时,没有接收对象,所以默认没有上下文。
  • 通过捕获添加上下文:如果 lambda 需要访问外部对象(如 this)或局部变量,则必须通过捕获机制显式提供上下文。

你需要捕获什么,取决于 lambda 内部需要访问的内容。如果 lambda 不访问任何外部变量或对象,就不需要捕获任何上下文。

扩展部分 和 C# 横向对比

在 C# 中,使用 lambda 表达式时,默认上下文是当前类的实例,即 this 指针。也就是说,在 C# 中,lambda 表达式可以直接访问类的成员变量和成员方法,而不需要显式捕获 this

C# 中的 lambda 表达式上下文

在 C# 中,当你在类中定义一个 lambda 表达式时,lambda 表达式会自动捕获当前的上下文,包括类的实例(即 this),因此你可以直接访问该类的成员变量或成员方法。

例子:C# 中的 lambda 表达式上下文

class MyClass
{private int counter = 0;public void RegisterEvent(Button button){// 在 C# 中,lambda 表达式可以直接访问类的成员变量或方法button.Click += (sender, e) =>{counter++; // 直接访问类的成员变量DoSomething(); // 直接调用类的成员方法};}private void DoSomething(){Console.WriteLine("Counter: " + counter);}
}

在上面的代码中,lambda 表达式直接访问了 counter 成员变量和 DoSomething 方法。无需像在 C++ 或 Qt 中那样显式捕获 this,因为 C# 自动捕获了当前类的上下文

C# 和 C++/Qt 的比较

  • C#:在 lambda 表达式中,类的上下文(即 this自动捕获,不需要显式指定。因此,你可以直接访问当前类的成员变量和方法,代码更加简洁。
  • C++/Qt:lambda 表达式不会自动捕获上下文,如果需要访问 this 或外部变量,必须显式捕获,如 [this][&]

进一步理解

C# 的 lambda 表达式不仅自动捕获 this,还可以自动捕获局部变量。在 C# 中,lambda 表达式会捕获其定义所在方法中的局部变量,并在事件触发时保持这些变量的状态(闭包)。

例子:C# lambda 捕获局部变量
public void RegisterEvent(Button button)
{int localCounter = 0;button.Click += (sender, e) =>{localCounter++; // 捕获局部变量Console.WriteLine("Local Counter: " + localCounter);};
}

在这个例子中,localCounter 是一个局部变量,lambda 表达式在事件中捕获了它,并在每次点击按钮时对其进行修改。

总结

在 C# 中,lambda 表达式的上下文默认就是 this,你不需要像在 C++ 或 Qt 中那样显式捕获当前对象。这使得在 C# 中使用 lambda 表达式更加简洁直观。如果你需要访问局部变量或类的成员,C# 会自动处理捕获工作。

这篇关于【QT】十分钟全面理解 信号与槽的机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1143222

相关文章

Qt中QGroupBox控件的实现

《Qt中QGroupBox控件的实现》QGroupBox是Qt框架中一个非常有用的控件,它主要用于组织和管理一组相关的控件,本文主要介绍了Qt中QGroupBox控件的实现,具有一定的参考价值,感兴趣... 目录引言一、基本属性二、常用方法2.1 构造函数 2.2 设置标题2.3 设置复选框模式2.4 是否

QT进行CSV文件初始化与读写操作

《QT进行CSV文件初始化与读写操作》这篇文章主要为大家详细介绍了在QT环境中如何进行CSV文件的初始化、写入和读取操作,本文为大家整理了相关的操作的多种方法,希望对大家有所帮助... 目录前言一、CSV文件初始化二、CSV写入三、CSV读取四、QT 逐行读取csv文件五、Qt如何将数据保存成CSV文件前言

Qt中QUndoView控件的具体使用

《Qt中QUndoView控件的具体使用》QUndoView是Qt框架中用于可视化显示QUndoStack内容的控件,本文主要介绍了Qt中QUndoView控件的具体使用,具有一定的参考价值,感兴趣的... 目录引言一、QUndoView 的用途二、工作原理三、 如何与 QUnDOStack 配合使用四、自

深入理解Apache Kafka(分布式流处理平台)

《深入理解ApacheKafka(分布式流处理平台)》ApacheKafka作为现代分布式系统中的核心中间件,为构建高吞吐量、低延迟的数据管道提供了强大支持,本文将深入探讨Kafka的核心概念、架构... 目录引言一、Apache Kafka概述1.1 什么是Kafka?1.2 Kafka的核心概念二、Ka

SpringRetry重试机制之@Retryable注解与重试策略详解

《SpringRetry重试机制之@Retryable注解与重试策略详解》本文将详细介绍SpringRetry的重试机制,特别是@Retryable注解的使用及各种重试策略的配置,帮助开发者构建更加健... 目录引言一、SpringRetry基础知识二、启用SpringRetry三、@Retryable注解

SpringKafka错误处理(重试机制与死信队列)

《SpringKafka错误处理(重试机制与死信队列)》SpringKafka提供了全面的错误处理机制,通过灵活的重试策略和死信队列处理,下面就来介绍一下,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、Spring Kafka错误处理基础二、配置重试机制三、死信队列实现四、特定异常的处理策略五

Qt spdlog日志模块的使用详解

《Qtspdlog日志模块的使用详解》在Qt应用程序开发中,良好的日志系统至关重要,本文将介绍如何使用spdlog1.5.0创建满足以下要求的日志系统,感兴趣的朋友一起看看吧... 目录版本摘要例子logmanager.cpp文件main.cpp文件版本spdlog版本:1.5.0采用1.5.0版本主要

java中反射(Reflection)机制举例详解

《java中反射(Reflection)机制举例详解》Java中的反射机制是指Java程序在运行期间可以获取到一个对象的全部信息,:本文主要介绍java中反射(Reflection)机制的相关资料... 目录一、什么是反射?二、反射的用途三、获取Class对象四、Class类型的对象使用场景1五、Class

Qt 中 isHidden 和 isVisible 的区别与使用小结

《Qt中isHidden和isVisible的区别与使用小结》Qt中的isHidden()和isVisible()方法都用于查询组件显示或隐藏状态,然而,它们有很大的区别,了解它们对于正确操... 目录1. 基础概念2. 区别清见3. 实际案例4. 注意事项5. 总结1. 基础概念Qt 中的 isHidd

QT移植到RK3568开发板的方法步骤

《QT移植到RK3568开发板的方法步骤》本文主要介绍了QT移植到RK3568开发板的方法步骤,文中通过图文示例介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录前言一、获取SDK1. 安装依赖2. 获取SDK资源包3. SDK工程目录介绍4. 获取补丁包二