【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

相关文章

深入理解Mysql OnlineDDL的算法

《深入理解MysqlOnlineDDL的算法》本文主要介绍了讲解MysqlOnlineDDL的算法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小... 目录一、Online DDL 是什么?二、Online DDL 的三种主要算法2.1COPY(复制法)

JAVA实现Token自动续期机制的示例代码

《JAVA实现Token自动续期机制的示例代码》本文主要介绍了JAVA实现Token自动续期机制的示例代码,通过动态调整会话生命周期平衡安全性与用户体验,解决固定有效期Token带来的风险与不便,感兴... 目录1. 固定有效期Token的内在局限性2. 自动续期机制:兼顾安全与体验的解决方案3. 总结PS

详解Spring中REQUIRED事务的回滚机制详解

《详解Spring中REQUIRED事务的回滚机制详解》在Spring的事务管理中,REQUIRED是最常用也是默认的事务传播属性,本文就来详细的介绍一下Spring中REQUIRED事务的回滚机制,... 目录1. REQUIRED 的定义2. REQUIRED 下的回滚机制2.1 异常触发回滚2.2 回

QT Creator配置Kit的实现示例

《QTCreator配置Kit的实现示例》本文主要介绍了使用Qt5.12.12与VS2022时,因MSVC编译器版本不匹配及WindowsSDK缺失导致配置错误的问题解决,感兴趣的可以了解一下... 目录0、背景:qt5.12.12+vs2022一、症状:二、原因:(可以跳过,直奔后面的解决方法)三、解决方

基于Redis自动过期的流处理暂停机制

《基于Redis自动过期的流处理暂停机制》基于Redis自动过期的流处理暂停机制是一种高效、可靠且易于实现的解决方案,防止延时过大的数据影响实时处理自动恢复处理,以避免积压的数据影响实时性,下面就来详... 目录核心思路代码实现1. 初始化Redis连接和键前缀2. 接收数据时检查暂停状态3. 检测到延时过

Redis中哨兵机制和集群的区别及说明

《Redis中哨兵机制和集群的区别及说明》Redis哨兵通过主从复制实现高可用,适用于中小规模数据;集群采用分布式分片,支持动态扩展,适合大规模数据,哨兵管理简单但扩展性弱,集群性能更强但架构复杂,根... 目录一、架构设计与节点角色1. 哨兵机制(Sentinel)2. 集群(Cluster)二、数据分片

Python 字符串裁切与提取全面且实用的解决方案

《Python字符串裁切与提取全面且实用的解决方案》本文梳理了Python字符串处理方法,涵盖基础切片、split/partition分割、正则匹配及结构化数据解析(如BeautifulSoup、j... 目录python 字符串裁切与提取的完整指南 基础切片方法1. 使用切片操作符[start:end]2

SpringBoot加载profile全面解析

《SpringBoot加载profile全面解析》SpringBoot的Profile机制通过多配置文件和注解实现环境隔离,支持开发、测试、生产等不同环境的灵活配置切换,无需修改代码,关键点包括配置文... 目录题目详细答案什么是 Profile配置 Profile使用application-{profil

Python自定义异常的全面指南(入门到实践)

《Python自定义异常的全面指南(入门到实践)》想象你正在开发一个银行系统,用户转账时余额不足,如果直接抛出ValueError,调用方很难区分是金额格式错误还是余额不足,这正是Python自定义异... 目录引言:为什么需要自定义异常一、异常基础:先搞懂python的异常体系1.1 异常是什么?1.2

深入理解go中interface机制

《深入理解go中interface机制》本文主要介绍了深入理解go中interface机制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前言interface使用类型判断总结前言go的interface是一组method的集合,不