Qt的信号槽基础介绍

概述

Qt 的信号槽(Signal-Slot)机制是其最为强大的特性之一,它处理的是类对象之间的关系问题。信号槽机制可以让不同对象直接或间接地通信,以达到高度解耦、模块化的目的。

在信号槽机制中,一个对象发出一个信号,另一个对象处理这个信号,它们彼此之间并不互相知晓。信号槽机制采用观察者模式来实现,即当一个对象的状态改变时,会自动通知所有已经注册过的观察者。

信号(signals)

信号是一个特殊的成员函数,只有声明没有具体的实现,它类似于事件,表示某些事情的发生。当这些事情发生时,信号被发送出去,同时带上相应的参数。

信号的定义格式如下:

signals:
    returnType signalName(argument);
  • returnType:表示信号的返回类型,通常为void,也可以为其他类型。
  • signalName:表示信号的名称,习惯上使用驼峰命名法,形式可以是任意合法C++标识符。
  • argument:表示信号的参数列表,可以为空或包含多个参数,参数与参数之间用逗号隔开。

例如:

signals:
    void mySignal();
    void valueChanged(int newValue);
    bool enableChanged(bool isEnabled);

上面三个信号分别声明了没有参数的mySignal()信号、带有一个整型参数的valueChanged(int)信号和带有一个bool类型参数并返回一个bool类型值的enableChanged(bool)信号。

信号通常被定义在类的 signals:部分中,表示这些成员函数是用来发出信号的,并不需要提供实现。我们称之为“信号函数”。

槽(slots)

槽是相对于信号而言的,用来处理信号传递过来的信息,执行具体操作。每个槽都是普通的成员函数,例如:

public slots:
    void mySlot();
    void setValue(int newValue);
  • mySlot():一个没有参数和返回值的槽函数。
  • setValue(int newValue):一个带有一个整型参数的槽函数。

槽也需要在类体中进行声明,格式如下:

public slots:
    returnType slotName(argument);

其中,

  • returnType:为槽的返回值类型,一般实现时不需要返回值,就使用void。
  • slotName:为槽函数名,用来标示槽。
  • argument:为槽的输入参数。

我们可以通过以下代码将一个信号与一个相应的槽连接起来:

connect(sender, SIGNAL(signalFunction()), receiver, SLOT(slotFunction()));

其中,

  • sender:为发送信号的对象指针,该对象需要含有已经声明过的信号函数signalFunction()
  • SIGNAL(signalFunction()):为发送信号的信号函数,使用SIGNAL()宏定义来表示。该宏可以将字符串转换成一个特定的int值,与指定对象以及指定的变量类型绑定在一起构成一个监听者,用于监听发射的事件。
  • receiver:为接收信号的对象指针,该对象需要包含槽函数slotFunction()
  • SLOT(slotFunction()):为接收信号的槽函数,使用SLOT()宏定义来表示。该宏也可以将字符串转换成一个特定的int值,与指定对象以及指定的变量类型绑定在一起构成一个触发器,用于响应发射出的事件。

例如:

// 建立连接,当button被单击时会触发handleButtonClicked()。
connect(button, SIGNAL(clicked()), this, SLOT(handleButtonClicked()));

信号与槽的连接

当信号和槽在同一线程中进行连接时,它们会在同一个线程中处理,这样可以确保线程安全性和信号的及时处理。当信号和槽在不同线程中进行连接时,需要考虑跨线程信号和槽连接。

同一线程中的信号和槽连接:

在同一线程中使用 connect() 进行信号和槽的连接时,Qt 会自动地将信号传递到槽函数中,无需担心锁和互斥。这种方式下,信号和槽的传递是同步的,即信号发射后,对应的槽函数能够立即执行。由于它们在同一线程中执行,因此不需要考虑线程安全问题。

不同线程中的信号和槽连接:

在不同线程中使用 connect() 进行信号和槽的连接时,需要考虑线程安全性问题。在这种情况下,必须选择正确的连接方式,以确保信号和槽跨线程连接的正确性。

a. Qt::AutoConnection 连接方式:该连接方式仅适用于同线程和跨线程的信号与槽连接的场景,是connect函数第五个参数的默认值。同线程时,相当于Qt::DirectConnection 连接;跨线程时,相当于Qt::QueuedConnection 连接方式。

b. Qt::DirectConnection 连接方式:该连接方式仅适用于同线程信号与槽连接的场景,并且需要在连接信号与槽时指定。使用这种方式时,槽函数会直接在发射信号的线程中执行,而不是在接收信号的对象所在的线程中执行。由于没有加锁和同步操作,因此容易导致线程安全问题。

c. Qt::QueuedConnection 连接方式:该连接方式适用于跨线程信号与槽连接的场景,并且也需要在连接信号与槽时指定。使用这种方式时,Qt 会将信号放入接收者对象的事件队列中,由接收者对象所在线程中的事件循环执行槽函数。因为槽函数在接收者对象所在线程的事件循环中依次执行,所以这个过程是线程安全的。但是要注意,该连接方式下,如果槽函数过于耗时或被阻塞,将会影响事件循环和其他事件的处理。

d. Qt::BlockingQueuedConnection 连接方式:该连接方式的线程处理方式与QueuedConnection 连接方式相同,但是信号发射线程会阻塞的等待槽函数执行完成。

e. Qt::UniqueConnection方式:该连接方式可以通过或的形式与上面的几种连接方式组合使用。如果一个连接被设置为UniqueConnection且连接成功,则后面出现的相同信号槽连接会失败。

f. QThread 和 signal-slot 方式:将接收信号的对象封装成一个线程类,并将该线程类作为子线程运行后,再通过 emit() 方法来发射信号即可。这类线程和信号槽的交互方法与主线程与子线程之间控制UI交互方式相似。具体实现方法可以继承 QThread 类或使用 QtConcurrent 并发框架等。

在进行跨线程信号槽的连接时,需要选取合适的连接方式来确保线程安全。在使用 Qt::QueuedConnection 方式时,也需要注意槽函数的耗时和阻塞情况,以保证应用程序的良好响应性。

信号和槽的参数传递

在信号和槽机制中,信号和槽函数的参数可以有多种类型,包括基本数据类型、Qt 类型、用户自定义类型等。Qt 支持以下三种方式传递参数:

  1. 直接值传递:用于传递小型的基本类型(如 bool、int、float)或 Qt 内置类型(如 QString、QVariant)。在这种情况下,当信号触发时会将参数按值的形式传递给槽函数,而不是传递指针或引用。

  2. 指针传递:用于传递大型数据结构或对象(例如 QByteArray 或 QPainter)。在这种情况下,Qt 将仅传递指向数据或对象的指针,而不是复制整个对象或数据结构。这种方法避免了内存消耗和不必要的复杂性。

  3. 引用传递:用于传递只读数据结构。在引用传递中,将参数作为常量引用进行声明,这表示只读访问。如果需要修改参数,则应该使用指针传递方式。

除了这些基本的方式,还可以在 Qt 中自定义任何类型来传递到信号和槽之间。首先,你需要使用 Q_DECLARE_METATYPE 宏将自定义类型注册到 Qt 的元对象系统中。第二,你需要使用 qRegisterMetaType() 函数将自定义类型对应到类中。为了支持传递到信号和槽函数之间的任何类型,Qt 带有 QVariant 类型。该类模板化,可以容纳下可能是承载多种不同数据类型的值。

因此,在使用信号和槽时,一般考虑参数的传递方式和类型的兼容性,选择合适的传递方式以及跨线程是否要使用 Qt::QueuedConnection 等等。

使用中的注意事项

QVariant类型传递

在Qt信号槽机制中传递QVariant类型的参数较为常见,但需要注意数据类型进行正确解析、内存管理以及程序版本的兼容性问题:

  1. 实际数据类型的转换:由于QVariant可以包含多种类型的数据,因此必须确保信号发送者和接收者正确解析了QVariant传递的值,并将其转换为预期数据类型。使用 Q_DECLARE_METATYPE 宏和 qRegisterMetaType() 函数,可以在类定义中声明自定义类型(如结构体、枚举等),并使qt元对象系统意识到这些类型。

  2. QVariant数据与程序版本:如果是与比较老的应用程序通信,则可能会有某些类型之前不存在或更新变得不兼容的情况。在这种情况下,建议使用序列化技术(例如使用QDataStream在不同平台上传递二进制数据)来传递自定义数据类型。

  3. 内存管理:当向槽函数中传递堆内存分配的数据类型时,需要避免内存泄漏和不必要的拷贝操作。建议使用QSharedPointer或std::shared_ptr等智能指针处理共享或需追踪生命周期的指针数据,避免手动处理动态分配的指针而潜在导致的内存安全问题。

  4. 性能考虑:如果内容不多,使用 QVariant 传输可以更清晰易读,但对于更大数据集,可以通过直接传递指针和长度的方式,避免不必要的类型转换和二进制数据复制。

跨线程的信号槽绑定

跨线程的信号槽绑定在Qt中是支持的,跨线程的信号槽绑定能够方便地在不同线程之间进行通信,但是需要注意线程安全问题,尤其是要避免竞争条件和内存泄漏等问题:

  1. 信号发送和接收的线程必须是不同的。如果在同一线程内发射并处理相同信号,则信号槽机制将表现为阻塞调用同一个函数。

  2. 槽函数需要在正确的目标线程中执行,以避免可能的竞态条件和线程安全问题。如果连接方法为Queued Connection,则槽函数将使用目标线程的事件循环进行调度。

  3. 如果信号的参数是指针类型,则要确保其对应的对象在接收信号时是有效的,因为在排队连接时,可能在发出信号后已经删除该对象。

  4. 对于跨线程的连接,最好使用在QObject派生类中定义的 signals 和 slots ,而非普通函数。

  5. 如果操作非常密集,那么Queued Connection带来了额外的线程间调用延迟,这可能导致接收者在已经运行完任务后,才会处理队列中的挂起操作。

  6. 当您在连接信号和槽时,可以使用Qt::AutoConnection连接类型。当发射信号的线程与接收者线程相同时,它将表现为直接连接方式,而在线程不同时,则表现为Queued Connection方式。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zw_ggr_2017

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值