【QT八股文】系列之篇章2 | QT的信号与槽机制及通讯流程
- 前言
- 2. 信号与槽
- 信号与槽机制介绍/本质/原理,什么是Qt信号与槽机制?如何在Qt中使用?
- 信号与槽机制原理,解析流程
- Qt信号槽的调用流程
- 信号与槽机制的优缺点
- 信号与槽机制需要注意的问题
- 信号的注意点
- 信号与槽与回调函数区别
- Qt信号与槽的多种用法
- PYQT5 connect 函数
- Qt connect 函数的连接方式
- PYQT5信号槽的链接方式
- 信号槽同步与异步/多线程下,信号槽分别在什么线程中执行,如何控制——`Qt connect 函数的连接方式`来控制
- 3. 通讯流程
- QT的TCP通讯流程
- QT的UDP通讯流程
- 下一章笔记
- 说明
前言
第一篇章主要是基础定义及QT中重要的事件机制
笔记链接:【QT八股文】系列之篇章1 | QT的基础知识及事件/机制
这里我们在了解了QT的大概后,我们将来了解QT中的核心机制:信号与槽
因为介绍到信号与槽,所以笔者我会讲通讯流程提前在前面来介绍
原创文章,未经同意请勿转载
2. 信号与槽
信号与槽机制介绍/本质/原理,什么是Qt信号与槽机制?如何在Qt中使用?
-
定义
Qt信号与槽机制是一种基于事件机制的编程模型,用于对象之间的通信。信号是由发送方对象发射的事件,而槽是接收方对象用于处理这些事件的函数。在Qt中,我们可以使用QObject类中的信号和槽机制来实现对象间的通信。通过定义信号和槽函数,在信号发射时,会自动调用对应的槽函数进行处理。 -
使用
PyQt的内置信号是自动定义的,使用PyQt5.QtCore.pyqtSignal函数可以为QObject对象创建一个信号,使用pyqtSignal函数可以把信号定义为类的属性。使用connect函数可以将信号绑定到槽函数上,使用disconnect函数可以解除信号与槽函数的绑定,使用emit函数可以发射信号。 -
本质(就是回调函数)
在事件的处理方面,信号槽相比回调函数,具有类型安全、松耦合、任意参数的优势,但执行效率会有一点损失。信号相当于传递参数(指实参,用于传递值/动作变化),槽函数像用于传递函数体(形参/函数体,用于接收值/根据动作变化来做出对应操作。) -
原理
- Qt 中的信号与槽机制是一种事件处理机制,它允许程序在接收到特定事件时执行特定的操作。在 Qt 中,信号与槽机制被广泛应用于组件之间的通信和事件处理。
- 具体来说,Qt 中的信号与槽机制是基于 QObject 类的。任何一个 QObject 对象都可以作为一个信号源,它可以通过 emit() 方法发出信号。同时,任何一个 QObject 对象都可以作为一个槽,它可以接受并处理来自信号源的信号。当一个信号源发出信号时,它会连接到相应的槽。这些槽可以是与信号源同一个对象,也可以是其他 QObject 对象。当信号源接收到信号时,它会将信号传递给所有已经连接到该槽的对象。这些对象会在接收到信号时执行相应的操作。
信号与槽机制原理,解析流程
-
原理
- Qt 中的信号与槽机制是一种事件处理机制,它允许程序在接收到特定事件时执行特定的操作。在 Qt 中,信号与槽机制被广泛应用于组件之间的通信和事件处理。
- 具体来说,Qt 中的信号与槽机制是基于 QObject 类的。任何一个 QObject 对象都可以作为一个信号源,它可以通过 emit() 方法发出信号。同时,任何一个 QObject 对象都可以作为一个槽,它可以接受并处理来自信号源的信号。当一个信号源发出信号时,它会连接到相应的槽。这些槽可以是与信号源同一个对象,也可以是其他 QObject 对象。当信号源接收到信号时,它会将信号传递给所有已经连接到该槽的对象。这些对象会在接收到信号时执行相应的操作。
-
解析流程
- moc查找头文件中的signals,slots,标记出信号和槽。
- 将信号槽信息存储到类静态变量staticMetaObject中,并且按声明顺序进行存放,建立索引。
- 当发现有connect连接时,将信号槽的索引信息放到一个map中,彼此配对。
- 当调用emit时,调用信号函数,并且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数
- 通过active函数找到在map中找到所有与信号对应的槽索引
- 根据槽索引找到槽函数,执行槽函数。
Qt信号槽的调用流程
注意:信号槽的实现:元对象编译器MOC,MOC的本质就是反射器
- MOC(元对象编译器)查找头文件中的signal与slots,标记出信号槽。将信号槽信息储存到类静态变量staticMetaObject中,并按照声明的顺序进行存放,建立索引。
- connect链接,将信号槽的索引信息放到一个双向链表中,彼此配对。
- emit被调用,调用信号函数,且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数。
- active函数在双向链表中找到所有与信号对应的槽索引,根据槽索引找到槽函数,执行槽函数。
信号与槽机制的优缺点
- 优点:
-
类型安全。需要关联的信号槽的签名必须是等同的。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。若信号和槽签名不一致,编译器会报错。
信号的参数可以多于槽,槽参数数量不能大于于信号
。💡 槽函数的参数是否可以比信号的参数多?
也可以。唯一的情况就是槽函数参数带有默认参数,除去默认参数外,槽函数的参数必须小于等于信号的参数。 -
松散耦合。QT的信号槽的建立和解除绑定十分自由。信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是那个对象的那个信号槽接收它发出的信号,它只需在适当的时间发送适当的信号即可,而不需要关心是否被接受和那个对象接受了。Qt就保证了适当的槽得到了调用,即使关联的对象在运行时被删除。程序也不会奔溃。
💡 信号重载了,如何确定连接哪个信号?
采用函数指针确定连接哪个信号。 -
灵活性。一个信号可以关联多个槽,或多个信号关联同一个槽。
-
- 不足:
- 速度较慢。与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢10倍。信号槽同真正的回调函数比起来时间的耗损还是很大的,所以在嵌入式实时系统中应当慎用。
- 原因:
- ①需要定位接收信号的对象。
- ②安全地遍历所有关联槽。
- ③编组、解组传递参数。
- ④多线程的时候,信号需要排队等待。(
然而,与创建对象的new操作及删除对象的delete操作相比,信号和槽的运行代价只是他们很少的一部分。信号和槽机制导致的这点性能损耗,对实时应用程序是可以忽略的。
)
- 原因:
- 不能出现宏定义。信号槽的参数限定很多例如不能携带模板类参数,不能出现宏定义等等。
- 速度较慢。与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢10倍。信号槽同真正的回调函数比起来时间的耗损还是很大的,所以在嵌入式实时系统中应当慎用。
信号与槽机制需要注意的问题
信号与槽机制是比较灵活的,但有些局限性我们必须了解,这样在实际的使用过程中才能够做到有的放矢,避免产生一些错误。下面就介绍一下这方面的情况。
- 信号与槽的效率是非常高的,但是同真正的回调函数比较起来,由于增加了灵活 性,因此在速度上还是有所损失,当然这种损失相对来说是比较小的,通过在一台 i586- 133 的机器上测试是 10 微秒(运行 Linux),可见这种机制所提供的简洁性、灵活性还是 值得的。但如果我们要追求高效率的话,比如在实时系统中就要尽可能的少用这种机制。
- 信号与槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时也有可能 产生死循环。因此,在定义槽函数时一定要注意避免间接形成无限循环,即在槽中再次发射 所接收到的同样信号。
- 如果一个信号与多个槽相关联的话,那么,当这个信号被发射时,与之相关的槽被 激活的顺序将是随机的,并且我们不能指定该顺序。
- 宏定义不能用在 signal 和 slot 的参数中。
- 构造函数不能用在 signals 或者 slots 声明区域内。
- 函数指针不能作为信号或槽的参数。
- 信号与槽不能有缺省参数。
- 信号与槽也不能携带模板类参数。
信号的注意点
- 所有的信号声明都是公有的,所以Qt规定不能在signals前面加public,private, protected。
- 所有的信号都没有返回值,所以返回值都用void。
- 所有的信号都不需要定义。
- 必须直接或间接继承自QOBject类,并且开头私有声明包含Q_OBJECT。
- 在同一个线程中,当一个信号被emit发出时,会立即执行其槽函数,等槽函数执行完毕后,才会执行emit后面的代码,如果一个信号链接了多个槽,那么会等所有的槽函数执行完毕后才执行后面的代码,槽函数的执行顺序是按照它们链接时的顺序执行的。不同线程中(即跨线程时),槽函数的执行顺序是随机的。
- 在链接信号和槽时,可以设置链接方式为:在发出信号后,不需要等待槽函数执行完,而是直接执行后面的代码,是通过connect的第5个参数。
- 信号与槽机制要求信号和槽的参数一致,所谓一致,是参数类型一致。如果不一致,允许的情况是,信号的参数可以比槽函数的参数多,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)。
信号与槽与回调函数区别
-
链接的不同
- 回调函数使用函数指针来实现的,如果多个类都关注一个类的动态变化,这样就会需要写出一个比较长的列表来管理这些类之间的关系。稍微在编码方面不那么灵活,稍显冗余。
- QT使用信号与槽来解决这个连接问题,这种方式比较清晰简单一些,一个类只需要清楚自己有几个槽函数有几个信号,然后将信号与槽进行连接,QT会自己处理函数的调用关系。这样在软件设计角度更加的清晰,灵活,不容易出错。
-
执行顺序/时间的不同
- Qt 信号与槽机制中的槽函数在接收到信号时会自动执行,而回调函数通常是在调用时立即执行。Qt 信号与槽机制可以在信号触发时立即执行槽函数,也可以延迟执行槽函数,而回调函数通常是立即执行的。
- 信号与槽机制中的信号与槽之间的执行顺序是不确定的,可以是任意顺序,也可以是逆序;而回调函数机制中的回调函数之间的执行顺序通常是确定的,按照函数声明的顺序执行。
-
对象绑定
信号与槽机制可以实现对象之间的动态绑定,可以在运行时动态地绑定信号与槽;而回调函数机制通常只能在程序启动时进行绑定。
-
主要用途不同
信号和槽机制是用于在程序运行时传递数据和事件的机制,而回调函数则通常被用于函数或方法的调用。因此,信号和槽机制可以用于模块之间的通信和交互,而回调函数则通常用于函数或方法的调用。
Qt信号与槽的多种用法
- 一个信号可以和多个槽相连
这时槽的执行顺序和在不在同一个线程上有关,同一线程,槽的执行顺序和声明顺序有关,跨线程时,执行顺序是不确定的。
- 多个信号可以连接到一个槽
只要任意一个信号发出,这个槽就会被调用。 - 一个信号可以连接到另外的一个信号
当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。 - 槽可以被取消链接
这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。想主动取消连接就用disconnect()函数中添加任何实现。 - 可以使用Lambda 表达式
在使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的。
PYQT5 connect 函数
注:在Qt中第五个参数用于指定信号与槽的匹配规则。而PYQT5是第四个参数
在 PyQt5 中,connect 函数【connect: PyQt5.QtWidgets.QSignalMapper()
】是一个用于连接信号与槽的函数。它通常被用于将对象的信号与槽函数进行连接。
列子:mapper = Qt.QSignalMapper() mapper.setMapping(button, button.clicked.connect(mapper.setCurrentIndex))
第一个参数是一个可选的参数,用于指定要连接的信号源。如果该参数为 None,则表示连接的是系统提供的信号。如果该参数不为 None,则表示要连接自定义信号。
第二个参数是一个可选的参数,用于指定要连接的槽函数。如果该参数为 None,则表示连接的是默认槽函数。如果该参数不为 None,则表示要连接指定的槽函数。
第三个参数是一个字符串,用于指定信号与槽之间的映射关系。该字符串通常由信号名称和槽函数名称组成。例如,“clicked” 表示将按钮的点击信号与按钮的 clicked 槽函数进行连接。
第四个参数是一个 PyQt5 中的 QSignalMapper 对象,用于指定信号与槽的匹配规则。该对象应该实现 QSignalMapper 类中的方法,例如 setMapping() 和 currentIndex() 等。
第五个参数是一个可选的参数,用于指定信号中断连接的函数。如果连接的信号源对象被删除或重新分配,则连接将被中断。默认情况下,连接不会自动中断。
Qt connect 函数的连接方式
-
自动连接
Qt::AutoConnection
默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用
多线程时为队列连接函数,单线程时为直接连接函数。 -
直接连接
Qt::DirectConnection
== 如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。==
Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数和信号发送者在同一线程。效果看上去就像是直接在信号发送位置调用了槽函数,效果上看起来像函数调用,同步执行。
emit语句后面的代码将在与信号关联的所有槽函数执行完毕后才被执行。
信号/槽在信号发出者所在的线程中执行 -
队列连接
Qt::QueuedConnection
信号发出后,信号会暂时被放到一个消息队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,然后执行和信号关联的槽函数,这种方式既可以在同一线程内传递消息也可以跨线程操作。
emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕
信号在信号发出者所在的线程中执行,槽函数在信号接收者所在的线程中执行 -
Qt::BlockingQueuedConnection
槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。而且接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。 -
Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是为了避免重复连接。
PYQT5信号槽的链接方式
在 PyQt5 中,信号与槽的连接方式有两种:1. 使用 connect() 函数;2. 装饰器@pyqtSlot() 。
@pyqtSlot()
优点是方式书写比较简洁。缺点是但函数名称不能自由定义,在想自定义参数时没有详细说明。
connect()
方式优点是理解和学习起来比较简单,而且函数名称可以自由定义。缺点是但如果信号比较多时,书写就比较混乱。
使用信号处理器的优点是可以在信号发生时执行复杂的操作,而缺点是连接信号处理器需要花费更多的内存和时间,并且连接信号处理器需要手动管理连接关系。因此,使用信号处理器仅适用于需要执行复杂操作的情况。
-
装饰器方法:
@pyqtSlot()
装饰器@pyqtSlot():修饰关键词,表明下面是完整的信号槽函数
# 需要引入 pyqtSlot 库函数 from PyQt5.QtCore import pyqtSlot @pyqtSlot() #装饰器,此函数没有connect直接通过装饰器初始化连接槽函数 def on_pushButton_clicked(self) print("我点击了")
在@pyqtSlot()方式里,函数名称有特殊要求,如下:
def on_(控件对象名)_信号名(self,内置参数):
@pyqtSlot()控制控件的多信号
@pyqtSlot() def on_lineEdit_returnPressed(self): print('触发了信号 returnPressed') def on_lineEdit_textChanged(self): print('触发了信号 textChanged')
注意:
一个控件同时要写多个信号与槽函数时,只需要写一遍@pyqtSlot()关键词
,中间可以有其他函数隔开。一定是一个类里面的,一个控件只写一遍@pyqtSlot(),不是所有控件信号只写一次@pyqtSlot(),有多少控件的信号还是要写。 -
connect连接法
使用 connect() 函数将信号与槽函数连接起来。connect() 函数接受两个参数:要连接的信号和要连接的槽函数。连接成功后,当信号发生时,槽函数将被调用。
# 在初始化函数中信号连接槽函数 self.pushButton.clicked.connect(self.test) # 槽函数 def test(self): print("点击了一下")
规则:
- 语法规则:
self.控件对象名称.信号名称.connect(self.槽函数名称)
- 有参数时,
槽函数名称
部分写成lambda 参数名: 函数名(参数名)
- 没有参数时,槽函数不用写括号
()
- 语法规则:
信号槽同步与异步/多线程下,信号槽分别在什么线程中执行,如何控制——Qt connect 函数的连接方式
来控制
可以通过QT的connect函数的第五个参数(PYQT5中是第四个参数)来控制, 信号槽执行时所在的线程。
通常使用的connect,实际上最后一个参数使用的是Qt::AutoConnection类型:Qt支持6种连接方式,其中3中最主要:
-
Qt::AutoConnection(自动方式)
信号槽在信号发出者所在的线程中执行
Qt的默认连接方式,如果信号的发出和接收这个信号的对象同属一个线程,那个工作方式与直连方式相同(会自动使用Qt::DirectConnection类型);否则工作方式与排队方式相同(会自动使用Qt::QueuedConnection类型)。
即多线程时为队列连接函数,单线程时为直接连接函数。
-
Qt::DirectConnection(直连方式)(信号与槽函数关系类似于函数调用,同步执行)
当信号发出后,相应的槽函数将立即被调用。emit语句后的代码将在所有槽函数执行完毕后被执行。
-
Qt::QueuedConnection(排队方式)(此时信号被塞到信号队列里了,信号与槽函数关系类似于消息通信,异步执行)
信号在信号发出者所在的线程中执行,槽函数在信号接收者所在的线程中执行
当信号发出后,排队到信号队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,调用相应的槽函数。emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕。
-
Qt::BlockingQueuedConnection(信号和槽必须在不同的线程中,否则就产生死锁)
这个是完全同步队列只有槽线程执行完成才会返回,否则发送线程也会一直等待,相当于是
不同的线程可以同步起来执行
。与Qt::QueuedConnection相同,除了信号线程阻塞直到槽返回。如果接收方处于发送信号的线程中,则不能使用此连接,否则应用程序将死锁。
-
Qt::UniqueConnection
与默认工作方式相同,只是不能重复连接相同的信号和槽,因为如果重复连接就会导致一个信号发出,对应槽函数就会执行多次。
这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是为了避免重复连接。
-
Qt::AutoCompatConnection
是为了连接Qt4与Qt3的信号槽机制兼容方式,工作方式与Qt::AutoConnection一样。
3. 通讯流程
QT的TCP通讯流程
QT如果要进行网络编程首先需要在.pro中添加如下代码:QT += network
-
服务端:(QTcpServer)
① 创建QTcpServer对象
② 监听list需要的参数是地址和端口号
③ 当有新的客户端连接成功回发送newConnect信号
④ 在newConnection信号槽函数中,调用nextPendingConnection函数获取新连接QTcpSocket对象
⑤ 连接QTcpSocket对象的readRead信号
⑥ 在readRead信号的槽函数使用read接收数据
⑦ 调用write成员函数发送数据 -
服务器端
-
创建用于监听的套接字
-
给套接字设置监听
-
如果有连接到来, 监听的套接字会发出信号newConnected
-
接收连接, 通过nextPendingConnection()函数, 返回一个QTcpSocket类型的套接字对象(用于通信)
-
使用用于通信的套接字对象通信 1>. 发送数据: write 2>. 接收数据: readAll/read
-
代码
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); tcpServer = new QTcpServer; tcpServer->listen(QHostAddress("192.168.0.111"),1234); connect(tcpServer,SIGNAL(newConnection()),this,SLOT(new_connect())); } Widget::~Widget() { delete ui; } void Widget::new_connect() { qDebug("--new connect--"); QTcpSocket* tcpSocket = tcpServer->nextPendingConnection(); connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(read_data())); socketArr.push_back(tcpSocket); } void Widget::read_data() { for(int i=0; i<socketArr.size(); i++) { if(socketArr[i]->bytesAvailable()) { char buf[256] = {}; socketArr[i]->read(buf,sizeof(buf)); qDebug("---read:%s---",buf); } } }
-
-
-
客户端:(QTcpSocket)
① 创建QTcpSocket对象
② 当对象与Server连接成功时会发送connected 信号
③ 调用成员函数connectToHost连接服务器,需要的参数是地址和端口号
④ connected信号的槽函数开启发送数据
⑤ 使用write发送数据,read接收数据 -
客户端:
- 创建用于通信的套接字
- 连接服务器: connectToHost
- 连接成功与服务器通信
1 >. 发送数据: write 2>. 接收数据: readAll/read
-
代码
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); tcpSocket = new QTcpSocket; connect(tcpSocket,SIGNAL(connected()),this,SLOT(connect_success())); tcpSocket->connectToHost("172.20.10.3",1234); } Widget::~Widget() { delete ui; } void Widget::on_send_clicked() { std::string msg = ui->msg->text().toStdString(); int ret = tcpSocket->write(msg.c_str(),msg.size()+1); qDebug("--send:%d--",ret); } void Widget::connect_success() { ui->send->setEnabled(true); }
QT的UDP通讯流程
UDP(User Datagram Protocol即用户数据报协议)是一个轻量级的,不可靠的,面向数据报的无连接协议。在网络质量令人十分不满意的环境下,UDP协议数据包丢失严重。由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。所以QQ这种对保密要求并不太高的聊天程序就是使用的UDP协议。
在Qt中提供了QUdpSocket 类来进行UDP数据报(datagrams)的发送和接收。Socket简单地说,就是一个IP地址加一个port端口 。
QT下UDP通信服务器端和客户端的关系是对等的, 做的处理也是一样的:
- 创建套接字对象 2. 如果需要接收数据, 必须绑定端口 3. 发送数据: writeDatagram 4. 接收数据: readDatagram
流程:①创建QUdpSocket套接字对象 ②如果需要接收数据,必须绑定端口 ③发送数据用writeDatagram,接收数据用 readDatagram 。
下一章笔记
下篇笔记链接:【QT的多线程以及QThread与QObject】
下篇笔记主要内容:QT的多线程以及QThread与QObject
说明
码字不易,可能当中存在某些字体错误(笔者我没有发现),如果有错误,欢迎大家指正。🤗
另外因为笔记是之前做的,这里我只把我之前做的搬移和重新排版过来,如果有知识上的错误也欢迎大家指正。