信号与槽的简介
基本介绍
GUI 之间的通信
在GUI编程中,经常涉及控件之间通信的情况,如控件 B 依赖于控件 A,当控件A的参数发生变化时,通常希望控件 B 能够立刻知道这个情况,并做出相应的变化,这就是控件之间的相互通信。
一般的 GUI 框架使用回调实现这种通信。回调是指向函数的指针,因此,如果希望某个 func 可以及时通知某个事件,则可以在 func 中调用回调,这个回调指向另一个函数的指针。
尽管确实存在使用此方法的成功框架,但回调可能不直观,并且在确保回调参数的类型正确性(type-correctness)方面可能会遇到问题。
信号/槽机制
Qt使用了一种代替回调技术的方法,即信号/槽机制。
信号/槽机制的基本原理是当特定事件发生时发出信号,并传递给槽函数。
Qt 的小部件有许多预定义的信号,可以将小部件子类化并添加自定义信号。槽是响应特定信号而调用的函数,Qt 的小部件有许多预定义的插槽,但通常会对小部件子类化并添加自己的槽,以方便灵活处理感兴趣的信号。
信号/槽机制是类型安全的,Qt 的信号/槽机制可以确保如果将信号连接到槽,那么槽将在正确的时间接收信号的参数并且进行调用。信号/槽机制可以接收任意数量的任意类型的参数。
信号/槽是 Qt中的核心机制,也是在 PySide/PyQt 编程中对象之间进行通信的机制从QObject 或其子类之一(如 QWidget)继承的所有类都可以包含信号与槽。
当对象更改状态时,它会根据需要发射信号,而这个信号会被绑定的槽函数捕捉并执行结果,这就是Qt 中的通信机制。信号只负责发射,不关心是否有槽函数接收。同样,槽函数只用来接收信号,不知道是否有链接到它的信号发射,这体现了 Qt 通信机制的灵活性和独立性。
信号/槽机制的特点
PySide/PyQt的窗口控件中有很多内置信号,也可以添加自定义信号。信号/槽机制具有如下特点。
- 一个信号可以连接多个槽,在发射信号时,插槽将按照它们连接的顺序一个接一个地执行。
- 一个信号可以连接另一个信号(在发射第一个信号时立即发射第二个信号)。
- 信号的参数可以是任意 Python 类型。
- 信号永远不能有返回类型。
- 一个槽可以监听多个信号。
- 信号/槽机制完全独立于任何 GUI事件循环。
- 信号与槽的连接方式既可以是同步的,也可以是异步的
- 信号与槽的连接可能会跨线程。
- 信号可能会断开。
两种通信机制之间的差别
与回调相比,信号/槽机制稍微慢一些,因为它提供了更高的灵活性,但是在实际的应用程序中两种机制的差异微不足道。
一般来说,发送连接到某些插槽的信号比直接调用接收器的性能差 10 倍。
这是定位连接对象、安全代所有连接(即检查后续接收器在发射期间没有被破坏),以及以通用方式编组任何参数所需的开销。
但是,考虑到字符串、向量、列表操作、新建实例或删除实例等操作,信号/槽机制的开销只占完整函数调用的很小一部分。信号/槽机制的简单性和灵活性非常值得这部分开销,这些开销甚至不会被注意到。
创建信号
Qt 提供了很多内置信号,如 QPushButton 的 clicked 信号、toggled 信号等,这些信号系统已经定义好,可以满足绝大多数需求。如果需要其他信号,则可以自己定义信号。使用 QtCoreSignal0函数可以创建信号,也可以为 QObject 及其子类(包括 QWidget 等)创建信号。
from PySide6.QtCore import Signal
__init__(self,
*types: type,
name: Union[str,NoneType] = None,
arguments: Union[str,NoneType] = None)-> None
使用Signal()函数可以创建一个或多个重载的未绑定的信号作为类的属性。信号必须在创建类时定义,不能在创建类以后作为类的属性动态添加进来。
- types 表示定义信号时参数的类型;
- name表示信号的名字,该项在默认情况下使用类的属性的名字。
信号可以传递多个参数,并指定信号传递参数的类型,参数类型是标准的 Python 数据类型(如字符串、日期、布尔类型、数字、列表、元组和字典)。
一般的创建方式如下这是一个可以传递4 种参数(str、int、list、dict)的信号:
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QWidget
class winForm(QWidget):
# Signal(*types: type,name: Union[str,NoneType] = None,arguments: Union[str,NoneType] = None)
btnClickedsignal = Signal(str,int,list,dict)
注意:PySide 6和PyQt 6对信号与槽的命名稍有不同,PySide6命名为Signal与slot,而在PyQt6中对应为pyqtSignal与 pyqtslot,它们只是名字不同而已,使用方式没有区别。
因此,可以将PyQt6代码和 PySide6代码尽量统一起来,减少后面的麻烦,对PyQt6代码可以尝试做如下修改:
from PyQt6.QtCore import pygtsignal as Signal
from PyQt6.QtCore import pyqtslot as slot
当然,对PySide6代码的修改也是一样的。
自定义重载信号:
signal1 = QtCore.Signal()# 无参数的信号
signal2 = QtCore.Signal(int)# 带一个参数(整数)的信号
signal3 = QtCore.Signal(int,str)# 带两个参数(整数,字符串)的信号
signal4 = QtCore.Signal(list)# 带一个参数(列表)的信号
signal5 = QtCore.Signal(dict)# 带一个参数(字典)的信号
signal6 = QtCore.Signal((int,str,),(str,))# 带(整数 字符串)或者(字符串)的信号. 部分教程讲的是用方括号pyside6会有问题,单个参数的逗号按照官方示例最好也加上
例子:
import sys
from PySide6.QtCore import Signal
from PySide6.QtGui import Qt
from PySide6.QtWidgets import QPushButton,QApplication,QWidget
class Btn(QPushButton):
"""自定义按钮类,重写事件以实现自定义信号"""
# 自定义信号
rightClicked = Signal((str,),(int,))# 每次只发送一个str或int类型的数据,应用于重载等情况
leftClicked = Signal((str,),(int,str,))# 可以传出多个参数
def mousePressEvent(self,e)-> None:
super().mousePressEvent(e)
if e.button()== Qt.MouseButton.RightButton: # 当按下的键是鼠标右键时
self.rightClicked.emit("str1")# 发送信号,传递参数
self.rightClicked[str].emit("str2")# 发送信号,指明了传递的参数为str类型
self.rightClicked[int].emit(8881)# 发送信号,指明了传递的参数为int类型
if e.button()== Qt.MouseButton.LeftButton:
self.leftClicked.emit("str3")
self.leftClicked[int,str].emit(8882,"str4")
self.leftClicked[int,str].emit(8883,"str5")# 指明传递参数的数量和对应类型
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("信号")
self.resize(500,500)
self.setup_ui()
def setup_ui(self):
btn = Btn("测试按钮",self)
btn.move(100,100)
btn.rightClicked.connect(lambda content: print(f"右键点击1 {content}:{type(content)}"))# 没有指明需要的参数类型,默认取第一种
btn.rightClicked[str].connect(lambda content: print(f"右键点击2 {content}:{type(content)}"))# 指明参数类型str
btn.rightClicked[int].connect(lambda content: print(f"右键点击3 {content}:{type(content)}"))# 指明参数类型int
# 传递多个参数
btn.leftClicked.connect(lambda c1: print(f"左键点击1 {c1}:{type(c1)}"))# 默认传递
btn.leftClicked[int,str].connect(lambda c1,c2: print(f"左键点击2 {c1}:{type(c1)},{c2}:{type(c2)}"))# 指定类型int,str
btn.leftClicked[str].connect(lambda c1: print(f"左键点击3 {c1}:{type(c1)}"))# 默认传递 # 指定类型str
if __name__ =="__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())
"""
左键点击1 str3:<class 'str'>
左键点击3 str3:<class 'str'>
左键点击2 8882:<class 'int'>,str4:<class 'str'>
左键点击2 8883:<class 'int'>,str5:<class 'str'>
右键点击1 str1:<class 'str'>
右键点击2 str1:<class 'str'>
右键点击1 str2:<class 'str'>
右键点击2 str2:<class 'str'>
右键点击3 8881:<class 'int'>
"""
可以发现走默认参数往往会同时触发指定参数类型的槽,重载型参数 实际运用建议还是指定类型触发 不要直接用默认。
操作信号
- 使用 connect()函数可以把信号绑定到槽函数上。
signaName[type].connect()
可用于连接重载型信号 - 使用 disconnect()函数可以解除信号与槽函数的绑定。使用
signaName.disconnect()
断开重载型信号连接 - 使用emit()函数可以发射信号。
- 当使用自定义信号时,不仅需要手动触发信号,还需要用emit())函数。
- 使用内置信号会自动触发,不需要执行emit0函数。
重载型信号连接
查询控件的信号时,会发现有些控件有多个名字相同但是参数不同的信号。
例如对于按钮有clicked()和 clicked(bool)两种信号,一种不需要传递参数的信号,另一种传递布尔型参数的信号。
这种信号名称相同、参数不同的信号称为重载(overload)型信号。
对于重载型信号定义自动关联槽函数时,需要在槽函数前加修饰符@slot(type)
声明是对哪个信号定义槽函数,其中type
是信号传递的参数类型。
- 如果对按钮的clicked(bool)信号定义自动关联槽函数,需要在槽函数前加入
@slot(bool)
进行修饰; - 如果对按钮的clicked()信号定义自动关联槽函数,需要在槽函数前加人@slot()进行修饰。
需要注意的是,在使用@slot(type)
修饰符前,应提前用from PySide6.QtCore import slot
语句导人槽函数。
定义一个信号后就有连接connect()
、发送emit()
、断开disconnect()
属性
重载型信号断开
已连接的信号非重载类型的直接使用signaName.disconnect()
断开连接即可
重载类型的使用signaName[type].disconnect()
断开连接
手动关联内置信号的自定义槽函数
除了使用控件内置信号定义自动连接的槽函数外,还可以将控件内置信号手动连接到其他函数上,这时需要用到信号的 connect()
方法。
btnCalculate.clicked.connect(self.method)
语句将按钮的单击信号clicked
与method()
函数进行连接
也可以在主程序中,在消息循环语句前用myWindow.ui.btnCalculate.clicked.connect(myWindow.method)
语句进行消息与槽函数的连接
槽函数
槽函数用来接收信号并执行相应的操作。槽函数可以是任何函数,也包括 lambda 表达式,主要作用是执行一些与信号匹配的操作。
可灵活使用lambda表达式链接一些自定义的信号和槽函数,尤其是在槽函数要求的参数和实际传递不一致时