Qt中的信号与槽机制
什么是信号和槽?
在生活中,我们会收到各种各样的信号,交通信号灯就是一个很好的信号与槽的示例:红灯、黄灯和绿灯是信号灯发出的信号,这些信号告诉行人和车辆何时停止、何时准备停止、何时通过。车辆和行人对这些信号的响应(如停车、行走)就是槽。
在Qt中,信号(signals)和槽(slots)是一种用于对象间通信的机制。信号是对象发出的消息,而槽是用于处理这些消息的方法。当一个对象的状态发生变化时,它可以发出一个信号,其他对象可以连接到这个信号并执行相应的操作。
解析connect
函数
在Qt框架中,connect
函数用于将对象的信号(signal)连接到另一个对象的槽(slot)上。当信号被发射(emit)时,与之连接的槽就会被调用。Qt的connect
函数有多个重载版本,但最常见的形式接受四个或五个参数:
发送者(Sender):
- 这是发出信号的对象。在Qt中,这通常是一个QObject或其子类的实例。
- 参数类型:
QObject *
信号(Signal):
- 这是发送者对象中定义的一个信号。信号是类的成员函数,使用
signals
关键字在类的声明中定义。 - 参数类型:通常是一个指向成员函数指针的类型,如
void (SenderClass::*)(Args...)
,其中SenderClass
是发送者的类名,Args...
是信号的参数类型列表。 - 注意:在Qt 5及以后版本中,你也可以使用
&SenderClass::signalName
这样的语法来指定信号。
- 这是发送者对象中定义的一个信号。信号是类的成员函数,使用
接收者(Receiver):
- 这是接收信号并调用槽的对象。它同样是一个QObject或其子类的实例。如果接收者为
nullptr
(或QCoreApplication::instance()
),则槽函数将在全局范围内调用(即,作为静态成员函数或全局函数调用)。 - 参数类型:
QObject *
- 这是接收信号并调用槽的对象。它同样是一个QObject或其子类的实例。如果接收者为
槽(Slot):
- 这是接收者对象中定义的一个槽。槽是类的成员函数,使用
slots
关键字在类的声明中定义(但在Qt 5中,slots
关键字是可选的)。 - 参数类型:与信号相同的成员函数指针类型。
- 注意:在Qt 5及以后版本中,你也可以使用
&ReceiverClass::slotName
这样的语法来指定槽。
- 这是接收者对象中定义的一个槽。槽是类的成员函数,使用
连接类型(Connection Type)(可选参数):
- 这个参数定义了信号和槽之间的连接类型。Qt支持几种不同的连接类型,如
Qt::DirectConnection
(直接连接)、Qt::QueuedConnection
(排队连接)等。在Qt 5中,这个参数是可选的,并且默认是Qt::AutoConnection
。 - 参数类型:
Qt::ConnectionType
- 这个参数定义了信号和槽之间的连接类型。Qt支持几种不同的连接类型,如
下面是一个使用connect
函数的示例:
// 假设有两个类:Sender 和 Receiver,它们都是 QObject 的子类
// Sender 类有一个信号 signalEmitted()
// Receiver 类有一个槽 slotCalled()
Sender *sender = new Sender;
Receiver *receiver = new Receiver;
// 连接信号和槽
QObject::connect(sender, &Sender::signalEmitted, receiver, &Receiver::slotCalled);
// 在某个地方,当 signalEmitted() 被调用时,slotCalled() 也会被调用
sender->emit signalEmitted();
请注意,Qt 5引入了一种新的信号和槽语法,它使用函数指针而不是字符串来指定信号和槽,这提高了类型安全性和编译时检查。在上面的示例中,我们使用了这种新语法。如果你正在使用较旧的Qt版本,你可能需要使用基于字符串的旧语法。但是,新语法更受欢迎,因为它更安全且易于使用。
一、槽(Slots)
1. 基本概念
槽是Qt对象中的一个成员函数,它可以被信号调用。当信号被发出时,与之关联的槽函数将被执行。槽函数可以有返回值和参数,这使得它们比信号更加灵活。槽函数可以被任何对象调用,但通常它们是由信号触发的。
也可以采用ui创建
实现和connect一样的效果,名称必须一致!
2. 自定义槽
自定义槽的定义与普通成员函数类似,但需要满足一些特定要求:
- 参数:槽函数的参数个数和类型必须与它所连接的信号的参数相匹配或更少。也就是说,槽函数的参数可以少于信号的参数,但不能多于。
- 返回值类型:槽函数的返回值类型可以为任何类型,但通常建议返回
void
。这是因为槽函数通常是由信号触发的,而信号没有返回值。 - 访问权限:槽函数可以是
public slots:
、protected slots:
或普通的成员函数。但是,槽函数的访问权限决定了谁可以和它进行连接。例如,public slots:
可以被任何信号连接,而private slots:
只能被该类本身的信号连接。
示例:
class MyWidget : public QWidget
{
Q_OBJECT
public:
// ...
public slots: //对于Qt5之前的版本 需要显示写public/private/protect slots
//Qt5后可以直接声明槽函数
void mySlot() {
// 处理信号的代码
} // 自定义槽,处理int参数
// ...
};
点击后widgetTitle被修改
二、信号(Signals)
1. 基本概念
信号是Qt对象在特定事件发生时发出的。它们可以由Qt框架中的类定义,也可以由用户自定义的类定义。信号是事件驱动的,即它们只在特定事件发生时发出,并且不包含返回类型(即它们是单向的)。信号的发出类似于广播,没有特定的接收者,只要有对象对这个信号感兴趣并进行了连接,就可以接收到这个信号。
依旧采用上述示例,此时我们发送的信号为QPushButton
下的clicked
信号
tips
:click() vs clicked 的语义区别:
click()是一个方法调用,它会直接模拟按钮的点击行为并可能触发相应的信号。而 clicked
是一个信号,它在按钮被用户或程序点击时自动发射,你需要通过 connect 函数将它连接到槽函数上以便处理点击事件。
2. 自定义信号
自定义信号的定义遵循以下规则:
- 返回值类型:信号的返回值类型必须为
void
。这是因为信号是事件驱动的,不需要返回值。 - 参数:信号可以有参数,但参数类型必须是Qt元类型系统所支持的。这包括Qt的核心类型(如int、QString等)以及用户自定义的类型(但需要注册到Qt元类型系统中)。
- 声明方式:在类的声明中,使用
signals:
关键字来声明信号。这告诉Qt编译器这个类将包含一些信号。
示例:
class MyWidget : public QWidget
{
Q_OBJECT
public:
// ...
signals:
void mySignal();
// ...
};
发出信号用emit关键字,Qt5后可以不显示写出
因为发送信号在构造函数中,所以在构造过程中完成了信号和槽的操作。
有参的信号和槽
信号和槽都有参
信号有参槽无参
信号无参槽有参
信号函数参数>槽函数参数
综上可知:自定义信号的函数参数个数必须 >= 自定义槽函数的参数
三、信号与槽的关联
在Qt中,使用QObject::connect()
函数可以将信号与槽进行关联。当信号被发出时,与之关联的槽函数将被自动调用。Qt提供了两种连接信号与槽的方式:使用SIGNAL和SLOT宏(Qt 4及更早版本)和使用新语法(Qt 5及更高版本)。
1. 使用SIGNAL和SLOT宏
这种方式在Qt 4及更早版本中广泛使用,但在Qt 5及更高版本中已被视为过时。使用这种方式时,需要传入信号和槽的字符串标识符,这可能会导致一些类型安全问题。
QObject::connect(sender, SIGNAL(mySignal(int)), receiver, SLOT(mySlot(int)));
2. 使用新语法
Qt 5引入了基于函数指针的新语法来连接信号与槽。这种方式更加类型安全且易于使用。它允许在编译时检查信号和槽的参数类型是否匹配。
QObject::connect(sender, &SenderClass::mySignal, receiver, &ReceiverClass::mySlot);
此外,Qt还支持将Lambda表达式作为槽函数进行连接,这使得代码更加简洁和灵活。
四、信号和槽的参数传递
信号和槽可以传递任意数量和类型的参数。当连接信号和槽时,它们的参数必须兼容。例如,如果信号有一个int参数,那么槽函数也必须接受一个int参数。使用QObject::connect函数连接信号和槽时,Qt会在参数类型不匹配时进行编译时检查。
上述可知,
connect
的第2、4个参数为char*
类型的指针,但是我们传参时传的确是函数指针,那么这两个是一个类型吗?
const char *signal:这个参数指定了信号的名称。在C++中,信号实际上是一个普通的C字符串,用来指示发送者正在发出的信号的名称。信号的名称通常是一个在发送者类中声明的函数名,但它们不是普通的C++函数,而是使用Qt宏来声明的,比如signals。因此,它们以字符串的形式传递给connect()函数。
const char *member:这个参数指定了槽函数的名称。与信号类似,槽函数也是作为普通的C字符串传递给connect()函数的。槽函数是接收者类中的一个普通函数,用于处理信号发出的动作。
总的来说,const char *signal和const char *member参数中使用字符串表示信号和槽函数的名称是因为在C++中,无法直接通过函数指针来传递函数的名称。因此,Qt 使用字符串来标识信号和槽函数的名称,然后在运行时使用反射机制来建立连接。
- 如上述第三点所说,采用
QObject::connect(sender, SIGNAL(mySignal(int)), receiver, SLOT(mySlot(int)));
宏编程将传入的函数指针转成char* ,这样是不安全的:
类型检查缺失:在编译时,编译器不会检查信号和槽的参数类型和数量是否匹配。这可能导致运行时错误,如果信号和槽的参数不匹配,但在编译时却不会报错。
字符串拼写错误:由于
SIGNAL
和SLOT
宏接收的是字符串字面量,因此如果信号或槽的名称拼写错误,编译器也不会报错,直到运行时才会出现问题。不易读和维护:使用字符串字面量来表示信号和槽的名称不如直接使用函数指针或成员函数指针来得直观和易于维护。
在 Qt 5 中,引入了新的基于函数指针的信号和槽连接语法,这种语法具有以下优势:
类型安全:新的连接语法在编译时会检查信号和槽的参数类型和数量是否匹配,从而避免了因类型不匹配而导致的运行时错误。
(假如传入的第一个参数和第二个参数不匹配,或者第三个和第四个参数不匹配(不匹配指的是:2、4参数的函数指针,不是1、3参数的成员函数)会编译出错)
易读和易维护:使用函数指针或成员函数指针来指定信号和槽使得代码更加直观和易于理解。此外,当信号或槽的签名发生更改时,编译器会立即报错,从而更容易发现和修复问题。
支持 lambda 表达式:新的连接语法还支持使用 lambda 表达式作为槽,这使得可以在连接时直接定义槽的行为,而无需单独定义一个槽函数。
使用Lambda表达式连接信号和槽
总结
通过信号和槽,Qt提供了一种简单而强大的对象间通信机制。它使得编写灵活、可扩展的应用程序变得更加容易。通过本文的介绍,希望您对Qt中的信号和槽有了更深入的理解,并能够在实际项目中灵活运用它们。