【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
类的功能。下面是逐行解释:
-
connect(&timer, &QTimer::timeout, [this]() { });
这行代码将QTimer
对象的timeout
信号与一个 lambda 表达式(匿名函数)槽连接起来。&timer
:指向QTimer
对象的指针,它是发送timeout
信号的对象。&QTimer::timeout
:QTimer
类的timeout
信号,在定时器时间到达时发射。[this] () { }
:这是一个 lambda 表达式。[this]
表示这个 lambda 捕获当前对象(即类的实例)以便在 lambda 内部使用this
指针。空的{ }
表示这是一个没有任何操作的槽函数。
这个连接表示,当
timer
对象的timeout
信号被触发时,lambda 表达式内的代码会被执行(这里的代码是空的,所以什么也不会发生)。 -
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 引入了更简洁的信号与槽连接方法,支持使用函数指针来连接。相比传统的 SIGNAL
和 SLOT
宏,使用函数指针的方式更安全,且可以检查参数类型。
语法
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
,其 objectName
是 button
,那么 Qt 会自动将 button
的 clicked()
信号连接到 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);
- 这将断开
button
的clicked()
信号与onButtonClicked()
槽的连接。
7. 信号本身也可以是空的
在 Qt 中,信号不需要有槽函数与之连接,它们可以是“空的”。当一个信号发出时,如果没有槽函数接收,也不会有任何错误。这提供了灵活性,使得信号与槽的使用更加松耦合。
8. 信号可以连接信号
在某些情况下,你可能希望一个信号发出时,自动触发另一个信号。在 Qt 中这是允许的。
例子
connect(button, &QPushButton::clicked, anotherButton, &QPushButton::click);
当 button
被点击时,它将发出 clicked()
信号,anotherButton
将接收到 click()
信号。
总结
Qt 的信号与槽机制非常灵活,支持多种连接方式:
- 经典的
SIGNAL
和SLOT
宏语法。 - 使用函数指针的现代 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);
这里的 this
是 MainWindow
类的对象,表示当 button
被点击时,MainWindow
的 onButtonClicked
函数会被调用。
总结
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
,因为onButtonClicked
是MainWindow
类的成员函数,必须指明在哪个对象上调用该函数。
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 的执行上下文可以通过捕获来定义,不需要通过
connect
的receiver
参数显式传递。
这使得代码更加简洁灵活,特别适用于处理简单的事件响应逻辑。
对捕获的理解(捕获上下文)
当你使用 lambda 表达式 作为槽函数时,确实没有指定槽函数的接收对象。因此,lambda 表达式本身没有上下文,所以需要显式捕获你希望使用的上下文(如 this
指针或局部变量),以便在 lambda 表达式内访问相关的数据或函数。
具体理解为:
-
没有接收对象时:
在传统的connect
中,第三个参数(接收对象receiver
)是明确指定的上下文,它告诉 Qt 在哪个对象上调用槽函数。因此,槽函数可以直接访问该对象的成员变量和成员函数。例如:connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
在这里,
onButtonClicked()
是属于MainWindow
类的成员函数,this
表示槽函数的接收对象,函数执行时有明确的上下文,即MainWindow
对象的成员可以被访问。 -
使用 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# 会自动处理捕获工作。