年龄已近半百,大学有幸学习计算机专业,可惜从事了二十多年非IT工作,现闲下来学习Python,弥补一下遗憾,今借阅《Pyside6/PyQt6快速开发与实战》一书学习,摘抄其中内容,作为学习笔记,帮助自己理解和记忆,若对有缘阅读者有帮助,不胜荣幸,特此说明。
信号和槽是Qt中的核心机制,也是在PySide/PyQt编程中对象之间进行通信的机制,从QObject成员或其子类之一(如QWidget)继承的所有类都可以包含信号和槽。当对象更改状态时,它会更具需要发射信号,而这个信号会被绑定的槽函数捕捉并执行结果,这就是Qt中的通信机制。信号只负责发射,不关心是否有槽函数接收。同样,槽函数只用来接收信号,不知道是否有链接到它的信号发射,这体现了Qt通信机制的灵活性和独立性。
Qt信号/槽机制的特点
PySide/PyQt的窗口控件中有很多内置信号,也可以添加自定义信号。信号/槽机制具有u如下的特点:
- 一个信号可以连接多个槽,在发射信号时,插槽将按照它们连接的顺序一个接一个地执行。
- 一个信号可以连接另一个信号(在发射第一个信号时,立即发射第二个信号)。
- 信号的参数可以是任意的Python类型。
- 信号永远不能有返回类型。
- 一个槽可以监听多个信号。
- 信号/槽机制完全独立于任何GUI事件循环。
- 信号与槽的连接方式既可以是同步的,也可以是异步的。
- 信号与槽的连接可以跨线程。
- 信号可能会断开。
创建信号
Qt提供了很多内置信号,比如QPushButton的clicked信号、toggled信号等,这些信号系统已经定义好,可以满足绝大多数需求。如果需要其他信号,则可以自己定义信号。使用QtCode.Signal()函数可以创建信号,也可以为QObject及其子类(包括QWidget等)创建信号。
使用Signal()函数可以创建一个或多个重载的未绑定的信号作为类的属性。信号必须在创建类时定义,不能在创建类之后作为类的属性动态添加进来。
from Pyside6.QtCode import Signal
from PySide6.QtWidget import QWidget
class WinForm(QWidget):
btnClickedSignal = Signal(str,int,list,dict)
信号可以传递多个参数,并指定信号传递参数的类型,参数类型是标准的Python数据类型。如上创建了一个btnClickedSignal信号,传递了4个参数,类型分别是字符串,整数,列表,字典。
操作信号
使用connect()函数可以把信号绑定到槽函数上,使用disconnect()函数可以解除信号与槽函数的绑定,使用emit()函数发射信号。
注意:当使用自定义信号时,需要手动触发信号,使用emit()函数。内置信号会自动发射,不需要执行emit()函数来操作执行。
槽函数
槽函数用来接收信号并执行相应的操作。槽函数可以是任何函数,也包括lamdba表达式,主要作用是执行一些与信号匹配的操作。简单的信号与槽的连接方法如下(clicked信号是QPushButton的内置信号):
btn = QPushButton("按钮控件")
# 把按钮控件btn的单击信号(clicked)连接到槽函数self.buttonClick
# 说明,这里的槽函数不能有括号和参数
btn.clicked.connect(self.buttonClick)
def buttonClick(self):
# do something
pass
内置信号+内置槽函数案例:
layout = QVBoxLayout()
self.setLayout(layout)
self.checkBox = QCheckBox('显示点击状态')
layout.addWidget(self.checkBox)
self.label = QLabel('用来显示信息', self)
layout.addWidget(self.label)
button1 = QPushButton("1-内置信号+内置槽", self)
layout.addWidget(button1)
'''
把button1的clicked内置信号,绑定到复选框对象的toggle内置槽函数
复选框的内置槽函数toggle实现状态切换
单击按钮,则实现了复选框状态切换
'''
button1.clicked.connect(self.checkBox.toggle)
内置信号+自定义槽函数
自定义的槽函数可以理解为自定义函数,在自定义函数中可以实现更复杂的功能。比如:信号
self.button2 = QPushButton("2-内置信号+自定义槽", self)
layout.addWidget(self.button2)
# 把button2的clicked信号连接到一个自定义函数button2Click
self.connect1 = self.button2.clicked.connect(self.button2Click)
def button2Click(self):
self.checkBox.toggle()
# 其它的功能
pass
信号与槽的参数
通过为槽函数传递特定的参数,可以实现更复杂的功能。可以传递Qt的内置参数(不同控件信号,可能会有特定的内置参数),也可以传递自定义参数,也可以一起传递。自定义参数一般使用lambda表达式传递,也可以通过partial()函数传递。
1)内置信号+默认参数
layout = QVBoxLayout()
self.setLayout(layout)
self.label = QLabel('用来显示信息', self)
layout.addWidget(self.label)
self.button1 = QPushButton("1-内置信号+默认参数", self)
self.button1.setCheckable(True)
layout.addWidget(self.button1)
self.button1.clicked[bool].connect(self.button1Click)
def button1Click(self,bool1):
if bool1 == True:
self.label.setText("time:%s,触发了'1-内置信号+默认参数',传递一个信号的默认参数:%s',表示该按钮被按下"%(time.strftime('%H:%M:%S'),bool1))
else:
self.label.setText("time:%s,触发了'1-内置信号+默认参数',传递一个信号的默认参数:%s',表示该按钮没有被按下"%(time.strftime('%H:%M:%S'),bool1))
上面代码,点击按钮(QPushButton),激发内置信号单击信号,单击信号传递了一个布尔类型参数,在按钮连接的槽函数中button1Click(self,bool1)接收了布尔型参数,在槽函数中予以使用。
上面的代码,按钮对象单击信号传递参数的理解:
(a)在PyQt中,对于clicked
信号,通常传递的参数是按钮的checked状态,即一个布尔值(True
或False
)。这个参数表示按钮在被点击后是否处于选中状态。
对于其他控件,如果它们具有可选中的(checkable)状态,通常也具有类似的信号传递布尔参数。例如,QCheckBox
和QRadioButton
也有clicked
信号,当它们被点击时,也会传递一个布尔参数,表示是否被选中。
需要注意的是,并非所有控件在被点击时都会传递布尔参数。一些控件可能只发出clicked
信号而不带参数,或者带有其他类型的参数。具体传递什么参数,取决于控件的类型和信号的定义。
因此,在设计槽函数时,需要根据连接的信号的参数类型来定义槽函数的参数。如果信号传递的是一个布尔参数,那么槽函数应该接受一个布尔类型的参数。如果信号没有参数,那么槽函数也可以不接受任何参数。
对于PyQt或PySide中的信号与槽机制,当信号带有参数时,通常需要在信号的后面使用方括号来指定参数的类型。这是为了确保信号与槽之间的参数类型匹配,以避免出现类型错误或不可预期的行为。
(b)在前面的例子中,按钮的clicked
信号带有一个布尔类型的参数,表示按钮的选中状态。因此,在连接信号与槽时,我们在clicked
后面使用方括号,并在其中指定参数类型为bool
,即clicked[bool]
。这样做是为了确保将正确的参数类型传递给槽函数。
一般情况下,如果信号带有参数,都需要使用这种方式来指定参数的类型。然而,也有一些信号不带参数,这时就不需要在方括号中指定参数类型。
总结来说,使用方括号来指定参数类型是为了确保信号与槽之间的类型匹配和正确的参数传递。如果信号带有参数,通常应该按照这种方式来传递参数。
(c)经测试,不使用方括号说明参数类型,程序运行正常,在响应的槽函数中参数,就可以接收该参数及使用。
内置信号+自定义参数
在PySide/PyQt中,经常需要为槽函数传递自定义的参数,一般两种方式:一种是使用lamdba表达式,一种使用partial()函数。
(1)下面代码使用lambda表达式:
self.button3 = QPushButton("3-内置信号+自定义参数lambda", self)
self.button3.setCheckable(True)
layout.addWidget(self.button3)
self.button3.clicked[bool].connect(lambda bool1:self.button3Click(bool1,button=self.button3,a=5,b='botton3'))
# 槽函数,接收1个内置参数,3个自定义参数
def button3Click(self,bool1,button,a,b):
if bool1 == True:
_str = f"time:{time.strftime('%H:%M:%S')},触发了'{button.text()}',传递一个信号的默认参数:{bool1}',表示该按钮被按下。\n三个自定义参数button='{button}',a={a},b='{b}'"
else:
_str = f"time:{time.strftime('%H:%M:%S')},触发了'{button.text()}',传递一个信号的默认参数:{bool1}',表示该按钮没有被按下。\n三个自定义参数button='{button}',a={a},b='{b}'"
self.label.setText(_str)
上面代码通过lambda表达式对button3Click()函数进行封装,这个函数传递了4个参数,分别是内置参数boll1,以及自定义的3个参数button/a/b。内置参数表示按钮按下的状态,自定义参数传递额外的信息。
(2)使用partial()函数:
# 使用partial()函数,要从functools模块先导入
from functools import partial
self.button4 = QPushButton("4-内置信号+自定义参数partial", self)
self.button4.setCheckable(True)
layout.addWidget(self.button4)
self.button4.clicked[bool].connect(partial(self.button4Click,*args,button=self.button4,a=7,b='button4'))
# 自定义槽函数接受按钮控件单击信号传递的参数
def button4Click(self,bool1,button,a,b):
if bool1 == True:
_str = f"time:{time.strftime('%H:%M:%S')},触发了'{button.text()}',传递一个信号的默认参数:{bool1}',表示该按钮被按下。\n三个自定义参数button='{button}',a={a},b='{b}'"
else:
_str = f"time:{time.strftime('%H:%M:%S')},触发了'{button.text()}',传递一个信号的默认参数:{bool1}',表示该按钮没有被按下。\n三个自定义参数button='{button}',a={a},b='{b}'"
self.label.setText(_str)
使用partial()函数,必须从functools模块导入。这里通过*args捕获内置参数bool。这个*args需要在主窗口类的初始化函数中定义,如下:
class SignalSlotDemo(QWidget):
signal1 = Signal()
signal2 = Signal(str)
signal3 = Signal(str,int,list,dict)
signal4 = Signal(str,int,list,dict)
# 下面使用partial()函数中使用了*args参数,需要在初始化函数中定义
def __init__(self, *args, **kwargs):
# 在执行super()的初始化__init__()可以不使用,程序都正常执行
# super(SignalSlotDemo, self).__init__()
super(SignalSlotDemo, self).__init__(*args, **kwargs)
如下是一个比较完整的代码(摘自《PySide6/PyQt6快速开发与实践》)
# -*- coding: utf-8 -*-
'''
【简介】
PySide6中 信号与槽 例子
'''
import sys
from PySide6.QtWidgets import *
from PySide6.QtCore import Signal,Slot,QMetaObject,QThread, QDateTime
from PySide6.QtGui import *
import time
class SignalSlotDemo(QWidget):
signal1 = Signal()
signal2 = Signal(str)
def __init__(self, *args, **kwargs):
super(SignalSlotDemo, self).__init__(*args, **kwargs)
self.setWindowTitle('信号与槽案例')
self.resize(400, 300)
layout = QVBoxLayout()
self.setLayout(layout)
self.checkBox = QCheckBox('显示点击状态')
layout.addWidget(self.checkBox)
self.label = QLabel('用来显示信息', self)
layout.addWidget(self.label)
button1 = QPushButton("1-内置信号+内置槽", self)
layout.addWidget(button1)
button1.clicked.connect(self.checkBox.toggle)
self.button2 = QPushButton("2-内置信号+自定义槽", self)
layout.addWidget(self.button2)
self.connect1 = self.button2.clicked.connect(self.button2Click)
button3 = QPushButton("3-自定义信号+内置槽", self)
self.signal1.connect(self.checkBox.toggle)
layout.addWidget(button3)
button3.clicked.connect(lambda: self.signal1.emit())
button4 = QPushButton("4-自定义信号+自定义槽", self)
self.signal2[str].connect(self.button4Click)
layout.addWidget(button4)
button4.clicked.connect(lambda: self.signal2.emit('我是参数'))
button5 = QPushButton("5-断开连接'2-内置信号+自定义槽'", self)
layout.addWidget(button5)
button5.clicked.connect(self.button5Click)
button6 = QPushButton("6-恢复连接'2-内置信号+自定义槽'", self)
layout.addWidget(button6)
button6.clicked.connect(self.button6Click)
self.button7 = QPushButton("7-装饰器信号与槽", self)
self.button7.setObjectName("button7Slot")
layout.addWidget(self.button7)
QMetaObject.connectSlotsByName(self)
self.button8 = QPushButton("8-多线程信号与槽", self)
layout.addWidget(self.button8)
self.button8.clicked.connect(self.button8Click)
def button2Click(self):
self.checkBox.toggle()
sender = self.sender()
self.label.setText('time:%s,触发了 %s'% (time.strftime('%H:%M:%S'),sender.text()))
def button4Click(self, _str):
self.checkBox.toggle()
self.label.setText('time:%s,触发了 4-内置信号+自定义槽,并传递了一个参数:“%s”' %(time.strftime('%H:%M:%S'),_str))
def button5Click(self):
try:
self.button2.clicked.disconnect()
# self.button2.disconnect(self.connect1)
# self.button2.clicked.disconnect(self.button2Click)
self.label.setText("time:%s,断开连接:'2-内置信号+自定义槽'" % time.strftime('%H:%M:%S'))
except:
self.label.setText("time:%s,'2-内置信号+自定义槽'已经断开连接,不用重复断开" % time.strftime('%H:%M:%S'))
def button6Click(self):
if self.isSignalConnect_(self.button2,'clicked()'):
self.button2.clicked.disconnect(self.button2Click)
# try:
# self.button2.clicked.disconnect(self.button2Click)
# except:
# pass
self.button2.clicked.connect(self.button2Click)
self.label.setText("time:%s,重新连接了:'2-内置信号+自定义槽'"%time.strftime('%H:%M:%S'))
@Slot()
def on_button7Slot_clicked(self):
self.checkBox.toggle()
self.label.setText('time:%s,触发了 7-装饰器信号与槽' %time.strftime('%H:%M:%S'))
def button8Click(self):
self.checkBox.toggle()
if hasattr(self,'backend'):
self.label.setText(f"time:{time.strftime('%H:%M:%S')},已经开启线程,不用重复开启")
else:
# 创建线程
self.backend = BackendThread()
# 连接信号
self.backend.update_date.connect(self.display_time)
# 开始线程
self.backend.start()
def display_time(self,tim):
self.button8.setText(f'8-多线程,time:{tim}')
def isSignalConnect_(self, obj, name):
"""判断信号是否连接
:param obj: 对象
:param name: 信号名,如 clicked()
"""
index = obj.metaObject().indexOfMethod(name)
if index > -1:
method = obj.metaObject().method(index)
if method:
return obj.isSignalConnected(method)
return False
class BackendThread(QThread):
# 通过类成员对象定义信号对象
update_date = Signal(str)
# 处理要做的业务逻辑
def run(self):
while True:
self.update_date.emit(time.strftime('%H:%M:%S'))
time.sleep(1)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = SignalSlotDemo()
demo.show()
app.exec()