【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开发记录,qt,开发语言)