Qt开发:信号与槽的介绍和使用
文章目录
- 一、信号与槽的概念
- 二、信号与槽的优点
- 三、信号与槽的连接方式
- 1.connect()函数的不同参数形式
- 2.使用sender()获得信号发射者
- 3.信号与槽机的连接方式
- 四、信号与槽的实现原理
一、信号与槽的概念
信号(Signal)就是在特定情况下被发射的事件,例如PushButton最常见的信号就是鼠标单击时发送的clicked()信号。GUI程序设计的主要内容就是对界面上各组件的信号的响应,只需要知道什么情况下发射哪些信号,合理地去响应和处理这些信号就可以了。
槽(Slot)就是对信号响应的函数。槽就是一个函数,与一般的C++函数的声明定义是一样的,可以定义在类的任何部分(public、private或protected),可以具有任何参数,也可以被直接调用。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,管理的槽函数被自动执行。
信号与槽关联是用QObject::connect()函数实现的,其基本格式是:
QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
connect()是QObject类的一个静态函数,而QObject是所有Qt类的基类,在实际调用时可以忽略前面的限定符,所以可以直接写为:
QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
其中sender是发射信号的对象的名称,signal()是信号名称,信号可以看作是特殊的函数,需要带括号,有参数时还需要指明参数,receiver是接收信号的对象名称,slot()是槽函数的名称,需要带括号,有参数时还需要指明参数。
关于信号与槽的使用,有以下一些规则需要注意:
- 一个信号可以连接多个槽函数,当一个信号与多个槽函数关联时,槽函数按照建立连接时的顺序依次执行。当信号和槽函数带有参数时,在connect()函数里,要写明参数的类型,但可以不写参数名称。
- 多个信号可以连接同一个槽函数
- 一个信号可以连接另一个信号;当一个信号发射时,也会发射另一个信号,从而是新特殊的功能。
- 信号与槽的参数个数和类型需要一致,至少信号的参数不能少于槽的参数,如果不匹配,会出现编译错误或运行错误。
- 在使用信号和槽的类中,必须在类的定义中加入宏Q_OBJECT。
- 当一个信号被发射时,与其关联的槽函数通常被立即执行,就像正常调用一个函数一样。
二、信号与槽的优点
在Qt中,信号与槽(Signals and Slots)是一种用于实现对象间通信的重要机制。信号与槽机制的本质是一种事件处理和消息传递系统,它允许对象在特定事件发生时通信并响应这些事件,而不需要对象直接调用彼此的方法。这种机制具有以下关键特征和本质:
- 解耦性(Decoupling):信号与槽机制将发射信号的对象与接收信号的槽函数的对象解耦。这意味着对象之间不需要直接知道彼此的存在或具体的实现细节,从而使代码更加灵活和可维护。
- 事件驱动(Event-Driven):信号与槽机制是一种事件驱动的编程范式。当一个对象发射信号时,其他对象可以注册槽函数来监听和响应这些信号,实现了异步和事件驱动的编程模型。
- 多对多连接:一个信号可以连接到多个槽,一个槽也可以连接到多个信号。这种多对多的连接允许广泛的对象之间进行通信,无需复杂的管理。
- 支持跨线程通信:Qt的信号与槽机制支持线程间通信,使得多线程应用程序中的异步消息传递更容易。
- 类型安全:信号与槽机制在编译时进行类型检查,以确保信号与槽之间的连接是类型安全的,从而减少运行时错误。
- 可扩展性:开发人员可以自定义信号和槽,从而为自定义对象添加新的事件和行为,使得Qt应用程序可以轻松地扩展和定制。
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f8abd4c7490247f2a38768b47788c0db.png#pic_center
三、信号与槽的连接方式
信号与槽是Qt的一个核心特点,也是它区别于其他框架的重要特性。信号与槽是对象进行通信的机制,也需要由Qt的元对象系统支持才能实现的。Qt使用信号与槽的机制实现对象间通信,它隐藏了复杂的底层实现,完成信号与槽的关联后,发射信号时不需要知道Qt是如何找到槽函数的,Qt的信号与槽机制与C++ Builder的”事件—响应“比较类似,但是更加灵活。
某些开发架构使用回调函数(callback)实现对象间通信。与回调函数相比,信号与槽的执行速度稍微慢一点,因为需要查找连接的对象和槽函数,但是这种差别在应用程序运行时是感觉不到的,而其提供的灵活性却比回调函数强很多。
1.connect()函数的不同参数形式
QObject::connect()函数由多重参数形式,一种参数形式的函数原型是:
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
使用这种参数形式的connect()进行信号与槽函数的连接时,传递的是函数签名的字符串形式。一般语法如下所示:
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
这里使用了宏SIGNAL()和SLOT()指定信号和槽函数,而且如果信号和槽函数带有参数,还需要注明参数类型。如:
connect(sender, SIGNAL(signal(int)), receiver, SLOT(slot(int)));
缺点:
字符串方式缺乏类型检查,容易出现拼写错误而导致连接失败。
编译时无法验证信号和槽的参数是否匹配,错误只能在运行时发现。
另一种参数形式的connect()函数的原型是:
QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection)
优点:
函数指针语法更清晰,便于代码维护。
编译时进行类型检查,减少运行时错误。
对于具有默认参数的信号与槽(即信号和名称是唯一的,没有参数不同而同名的两个信号),可以使用这种函数指针形式进行关联。如:
connect(lineEdit, &QLineEdit::textChanged, this, &widget::on_textChanged);
但是对于具有不同参数的同名信号就不能采用函数指针的方式进行信号与槽的关联,例如QSpinBox有两个valueChanged()信号,分别是:
void QSpinBox::valueChanged(int i);
void QSpinBox::valueChanged(const QString &text);
如果还使用上述的方式进行关联,编译就会报错。在Qt5.7版本以上,解决信号与槽函数重载的问题可以通过使用QOverload类,它是Qt提供的一个工具类,用于指定特定的重载函数,通过使用QOverload类,可以确保连接到正确的重载函数。
// 连接到 int 参数版本的 on_valueChanged
connect(sender, QOverload<int>::of(&QSpinBox::valueChanged), receiver, QOverload<int>::of(&widget::on_valueChanged));
// 连接到 QString 参数版本的 on_valueChanged
connect(sender, QOverload<const QString &>::of(&valueChanged::valueChanged), receiver, QOverload<const QString &>::of(&widget::on_valueChanged));
如果使用的 Qt 版本低于 5.7,可以用 static_cast 明确指定信号的类型。我们有如下的信号和槽函数:
信号:
class SenderClass : public QObject {
Q_OBJECT
signals:
void mySignal(int value);
void mySignal(const QString &text);
};
槽:
class ReceiverClass : public QObject {
Q_OBJECT
public slots:
void mySlot(int value);
void mySlot(const QString &text);
};
SenderClass *sender = new SenderClass;
ReceiverClass *receiver = new ReceiverClass;
// 连接 int 参数的信号和槽
connect(sender, static_cast<void (SenderClass::*)(int)>(&SenderClass::mySignal),
receiver, static_cast<void (ReceiverClass::*)(int)>(&ReceiverClass::mySlot));
// 连接 QString 参数的信号和槽
connect(sender, static_cast<void (SenderClass::*)(const QString &)>(&SenderClass::mySignal),
receiver, static_cast<void (ReceiverClass::*)(const QString &)>(&ReceiverClass::mySlot));
如果连接过程需要中间层处理,或者使用的代码环境中槽函数比较复杂,不想使用QOverload类或者static_cast 进行连接,可以使用 Lambda 表达式。
SenderClass *sender = new SenderClass;
ReceiverClass *receiver = new ReceiverClass;
// 连接 int 参数的信号
connect(sender, static_cast<void (SenderClass::*)(int)>(&SenderClass::mySignal), this, [receiver](int value) {
receiver->mySlot(value);
});
// 连接 QString 参数的信号
connect(sender, static_cast<void (SenderClass::*)(const QString &)>(&SenderClass::mySignal), this, [receiver](const QString &text) {
receiver->mySlot(text);
});
2.使用sender()获得信号发射者
在槽函数里,使用QObject::sender()可以获取信号发射者的指针,如果知道信号发射者的类型,可以将指针投射为确定的类型,然后使用这个确定类的接口函数。
例如在QSpinBox的valueChanged(int)信号的槽函数里,可以通过sender()和qobject_cast获得信号发射者的指针,从而对信号发射者进行操作。
QSpinBox *spinBox = qobject_cast<QSpinBox*>(sender());
3.信号与槽机的连接方式
Qt中的信号与槽机制通过不同方式的连接实现了对象之间的通信。以下是几种连接方式:
- 自动连接(Auto Connection):这是最常见的连接方式,通常在Qt Designer中进行。当你使用Qt Designer将信号与槽连接时,Qt会自动选择适当的连接类型。例如,当你在UI设计器中将按钮的clicked()信号与槽函数连接时,Qt通常会自动选择Qt::AutoConnection,这意味着它会在事件循环中自动将信号传递给槽。
- 直接连接(Direct Connection):使用Qt::DirectConnection可以创建直接连接。在这种连接中,信号会立即调用槽函数,不经过事件队列。这通常用于在同一线程内的对象之间建立连接。
- 队列连接(Queued Connection):使用Qt::QueuedConnection可以创建队列连接。在这种连接中,信号会被发送到接收者的事件队列,稍后由事件循环处理。这对于在不同线程中的对象之间建立连接很有用。
- 阻塞队列连接(BlockingQueuedConnection):使用 Qt::BlockingQueuedConnection 可以创建阻塞队列连接。与 Qt::QueuedConnection 类似,信号会被发送到接收者的事件队列中,但不同的是,在Qt::BlockingQueuedConnection中,发射信号的线程会等待槽函数执行完成后再继续执行。这对于需要等待槽函数完成后才能继续执行的线程间通信非常有用。
四、信号与槽的实现原理
在 Qt 中,信号是通过元对象系统来与指定的槽函数进行关联和调用的。元对象系统使得 Qt 能够在运行时动态地查找和连接信号与槽,确保信号触发时正确地找到槽函数并执行它。
信号和槽的实现原理基于 C++ 的元对象系统,具体包括以下几个关键部分:
- 代码编译阶段:在编译时,Qt 使用元对象编译器(moc,Meta-Object Compiler)处理包含信号和槽声明的源代码文件。moc 生成了元对象代码,其中包含了类的元对象信息,包括信号和槽的描述。
- 元对象信息:每一个继承自 QObject 的类都有一个与之相关的元对象。元对象包含了该类的属性、方法和信号槽的描述。对于信号,元对象记录了它们的名称、参数类型以及它们的标识符。
- QObject 类的实例:在运行时,每个 QObject 类的实例都包含一个指向元对象的指针,这个元对象描述了该实例的类信息和信号槽。
- 信号发射:当一个对象发射一个信号(使用 emit 关键字),Qt 通过元对象系统来查找与该信号相关联的槽函数。元对象系统根据信号的名称和参数类型在元对象中查找匹配的槽函数。
- 信号和槽的连接:在使用 QObject::connect 函数将信号与槽连接时,Qt 会将信号的描述与槽函数的描述进行匹配。这通常是在应用程序的初始化阶段完成的。
- 信号触发:当信号被触发时,元对象系统会遍历与该信号相关联的槽函数,并按照它们的连接顺序调用这些槽函数。
- 参数传递:如果信号有参数,元对象系统会确保传递正确的参数给槽函数。