pyqt5信号与槽的使用
信号与槽介绍
信号(Signal)和槽(Slot)是Qt中的核心机制,也是在PyQt编程中对象之间进行通信的机制。在Qt中,每一个QObject对象和PyQt中所有继承自QWidget的控件(这些都是QObject的子对象)都支持信号与槽机制。当信号发射时,连接的槽函数将会自动执行。在PyQt 5中信号与槽通过object.signal.connect()方法连接。
PyQt的窗口控件类中有很多内置信号,开发者也可以添加自定义信号。信号与槽具有如下特点。
- 一个信号可以连接多个槽。
- 一个信号可以连接另一个信号。
- 信号参数可以是任何Python类型。
- 一个槽可以监听多个信号。
- 信号与槽的连接方式可以是同步连接,也可以是异步连接。
- 信号与槽的连接可能会跨线程。
- 信号可能会断开。
在GUI编程中,当改变一个控件的状态时(如单击了按钮),通常需要通知另一个控件,也就是实现了对象之间的通信。在早期的GUI编程中使用的是回调机制,在Qt中则使用一种新机制——信号与槽。在编写一个类时,要先定义该类的信号与槽,在类中信号与槽进行连接,实现对象之间的数据传输。
当事件或者状态发生改变时,就会发出信号。同时,信号会触发所有与这个事件(信号)相关的函数(槽)。信号与槽可以是多对多的关系。一个信号可以连接多个槽,一个槽也可以监听多个信号。
简单使用
Qt Designer提供了基本的编辑信号槽方法。首先,新建一个模板名为"Widget"的简单窗口,该窗口文件名为MainWinSignalSlog01.ui。
在Qt Designer窗口左侧有一个Buttons栏,找到QPushButton控件,把它拖到窗体Form中。在属性编辑器中,找到按钮对应的text属性,把属性值改为"关闭窗口",并将objectName属性值改为"closeWinBtn"
单击工具栏上的"编辑信号/槽"(或者通过单击"Edit"(编辑)菜单→"编辑信号/槽"),进入信号槽编辑模式,可以直接在发射者("关闭窗口"按钮)上按住鼠标左键不放,拖动到接收者(Form窗体)上,这样就建立起了连接。
将界面文件转换为Python文件,直接运行tools.py。
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'd:\Github\PyProjects\pyqt5\ui\MainWinSignalSlog01.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(400, 300)
self.closeWinBtn = QtWidgets.QPushButton(Form)
self.closeWinBtn.setGeometry(QtCore.QRect(150, 110, 75, 23))
self.closeWinBtn.setObjectName("closeWinBtn")
self.retranslateUi(Form)
self.closeWinBtn.clicked.connect(Form.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
self.closeWinBtn.setText(_translate("Form", "关闭窗口"))
为了使窗口的显示和业务逻辑分离,再新建一个调用窗口显示的文件CallMainWinSignalSlog01.py,其完整代码如下:
import sys
from PyQt5.QtWidgets import QApplication ,QMainWindow
from ui.MainWinSignalSlog01 import Ui_Form # 我这里把所有的ui文件放在ui文件夹下
class MyMainWindow(QMainWindow,Ui_Form):
def __init__(self,parent=None):
super(MyMainWindow,self).__init__(parent)
self.setupUi(self)
if __name__=="__main__":
app=QApplication(sys.argv)
myWin=MyMainWindow()
myWin.show()
sys.exit(app.exec_())
运行效果:
点击就会关闭窗口。
定义信号
使用PyQt5.QtCore.pyqtSignal()
函数可以为QObject
创建一个信号,使用pyqtSingnal()
函数可以把信号定义为类的属性。
- 为
QObject
对象创建信号
使用pyqtSignal()
函数创建一个或多个重载的未绑定的信号作为类的属性,信号只能在QObject
的子类中定义。
class Foo(QObject):
valueChanged = pyqtSingal([dict], [list])
信号必须在类创建时定义,不能在类创建后作为类的属性动态添加进来。types参数表示定义信号时参数的类型,name参数表示信号名字,该项缺省时使用类的属性名字。
- 为控件创建信号
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QApplication ,QMainWindow
class MyMainWindow(QMainWindow):
btnClickSingal=pyqtSignal() # 创建一个btnClickSingal信号
- 操作信号
使用connect()
函数可以把信号绑定到槽函数上。
使用disconnect()
函数可以解除信号与槽函数的绑定。
使用emit()
函数可以发射信号。
信号与槽的入门应用
信号与槽有三种使用方法。第一种内置信号与槽使用,第二种自定义信号与槽的使用,第三种是装饰器的信号与槽的使用。
- 内置信号与槽的使用
内置信号与槽的使用,就是使用窗口控件的函数,而不是使用自定义的函数。通过QObject.signal.connect
将一个QObject
的信号连接到另一个QObject
的槽函数。
import sys
from PyQt5.QtWidgets import QApplication ,QWidget, QMessageBox, QPushButton
if __name__=="__main__":
app=QApplication(sys.argv)
Widget=QWidget()
def showMsg():
QMessageBox.information(Widget, "信息提示框", "OK, 弹出测试信息")
btn = QPushButton("测试点击按钮", Widget)
btn.clicked.connect(showMsg) # 控件点击信号关联自定义槽函数
Widget.show()
sys.exit(app.exec_())
点击按钮,弹出提示框。
- 自定义信号和槽函数的使用
所谓自定义信号与槽的使用,是指在发射信号时,不使用窗口控件的函数,而是使用自定义的函数。自定义信号与槽使用灵活,可以自定义传递多种数据类型实现不同的功能需求。
import sys
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QApplication ,QWidget, QMessageBox, QPushButton
# 信号对象
class QTypeSignal(QObject):
# 定义一个信号
sendmsg = pyqtSignal(object)
def __init__(self):
super(QTypeSignal, self).__init__()
def run(self):
# 发射信号
self.sendmsg.emit('hello pyqt5')
# 槽对象
class QTypeSlot(QObject):
def __init__(self):
super(QTypeSlot, self).__init__()
# 槽对象中的函数
def get(self, msg):
print("QSlot get msg =>", msg)
if __name__=="__main__":
send = QTypeSignal()
slot = QTypeSlot()
# 1
print('----把信号绑定到槽函数----')
send.sendmsg.connect(slot.get)
send.run()
# 2
print('----把信号和槽函数断开----')
send.sendmsg.disconnect(slot.get)
send.run()
运行结果:
----把信号绑定到槽函数----
QSlot get msg => hello pyqt5
----把信号和槽函数断开----
这里成功将hello pyqt5
从信号传递到槽中,上面实现了一个字符串参数的传递,如果要实现多个参数呢?
import sys
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QApplication ,QWidget, QMessageBox, QPushButton
# 信号对象
class QTypeSignal(QObject):
# 定义一个信号
sendmsg = pyqtSignal(str, str)
def __init__(self):
super(QTypeSignal, self).__init__()
def run(self):
# 发射信号
self.sendmsg.emit("第一个参数", "第二个参数")
# 槽对象
class QTypeSlot(QObject):
def __init__(self):
super(QTypeSlot, self).__init__()
# 槽对象中的函数
def get(self, msg1, msg2):
print(f'QSlot get msg => {msg1} + {msg2}')
if __name__=="__main__":
send = QTypeSignal()
slot = QTypeSlot()
# 1
print('----把信号绑定到槽函数----')
send.sendmsg.connect(slot.get)
send.run()
# 2
print('----把信号和槽函数断开----')
send.sendmsg.disconnect(slot.get)
send.run()
运行结果:
----把信号绑定到槽函数----
QSlot get msg => 第一个参数 + 第二个参数
----把信号和槽函数断开----
高级自定义信号与槽
自定义流程如下:
(1)定义信号
(2)定义槽函数
(3)连接信号与槽
(4)发射信号
1. 定义信号
通过类成员变量定义信号对象
class MyWidget(QWidget):
#无参数的信号
Signal_NoParameters=pyqtSignal()
#带一个参数(整数)的信号
Signal_OneParameter=pyqtSignal(int)
#带一个参数(整数或者字符串)的重载版本的信号
Signal_OneParameter_Overload=pyqtSignal([int],[str])
#带两个参数(整数,字符串)的信号
Signal_TwoParameters=pyqtSignal(int,str)
#带两个参数([整数,整数]或者[整数,字符串])的重载版本的信号
Signal_TwoParameters_Overload=pyqtSignal([int,int],[int,str])
2. 定义槽函数
定义一个槽函数,有不同的输入参数。
class MyWidget(QWidget):
def setValue_NoParameters(self):
'''无参数的槽函数'''
pass
def setValue_OneParameter(self, nIndex):
'''一个参数(整数)的槽函数'''
pass
def setValue_OneParameter_string(self, szIndex):
'''一个参数(字符串)的槽函数'''
pass
def setValue_TwoParameters_String(self, x, szy):
'''带两个参数(整数,字符串)槽函数'''
pass
3. 连接信号与槽函数
通过connect方法连接信号与槽函数或者可调用对象。
app = QApplication(sys.argv)
widget = MyWidget()
# 连接无参数信号
widget.Signal_NoParameters.connect(widget.setValue_NoParameters)
# 连接一个整数参数信号
widget.Signal_OneParameter.connect(widget.setValue_OneParameter)
# 连接两个参数信号
widget.Signal_TwoParameters.connect(widget.setValue_TwoParameters_String)
widget.show()
4. 发射信号
通过emit方法发射信号
信号与槽高级用法
使用自定义参数
在PyQt编程过程中,经常会遇到给槽函数传递自定义参数的情况,比如有一个信号与槽函数的连接是
button1.clicked.connect(show_page)
对于clicked信号来说,它是没有参数的;对于show_page函数来说,希望它可以接收参数。希望show_page函数像如下这样:
def show_page(self,name):
print(name,"点击啦")
于是就产生一个问题——信号发出的参数个数为0,槽函数接收的参数个数为1,由于0﹤1,这样运行起来一定会报错(原因是信号发出的参数个数一定要大于槽函数接收的参数个数)。解决这个问题就是本节的重点:自定义参数的传递。
import sys
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QMainWindow, QApplication ,QWidget, QMessageBox, QPushButton,QHBoxLayout
class WinForm(QMainWindow):
def __init__(self, parent=None):
super(WinForm, self).__init__(parent)
button1 = QPushButton("按钮1")
button2 = QPushButton("按钮2")
button1.clicked.connect(lambda: self.onButtonClick(1))
button2.clicked.connect(lambda: self.onButtonClick(2))
layout = QHBoxLayout()
layout.addWidget(button1)
layout.addWidget(button2)
main_frame = QWidget()
main_frame.setLayout(layout)
self.setCentralWidget(main_frame)
def onButtonClick(self, n):
print(f'按钮{n}被按下了')
QMessageBox.information(self, "信息提示框", f"按钮{n}被按下了")
if __name__=="__main__":
app = QApplication(sys.argv)
win = WinForm()
win.show()
sys.exit(app.exec_())
运行效果:
装饰器信号与槽
所谓装饰器信号与槽,就是通过装饰器的方法来定义信号和槽函数。具体的使用方法如下:
@PyQt5.QtCore.pyqtSolt(参数)
def on_发送者对象名称_发送者对象信号(self, 参数):
pass
这种方法有效的前提是下面的函数已经执行, 这行代码用来将QObject中的子孙对象的某些信号按照其objectName连接到相应的槽函数。:
QMetaObject.connectSlotsByName(QObject)
在上面代码中,“发送者对象名称”就是使用setObjectName函数设置的名称,因此自定义槽函数的命名规则也可以看成:on +使用setObjectName设置的名称+信号名称。接下来看具体的使用方法。
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton
import sys
class CustWidget(QWidget):
def __init__(self, parent=None):
super(CustWidget, self).__init__(parent)
self.okButton = QPushButton("OK", self)
# 使用setObjectName设置对象名称
self.okButton.setObjectName("okButton")
layout = QHBoxLayout()
layout.addWidget(self.okButton)
self.setLayout(layout)
QtCore.QMetaObject.connectSlotsByName(self)
@QtCore.pyqtSlot()
def on_okButton_clicked(self):
print("单击了OK按钮")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = CustWidget()
win.show()
app.exec_()
信号与槽的断开与连接
有时候基于某些原因,想要临时或永久断开某个信号与槽的连接。
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton
import sys
class SignalClass(QtCore.QObject):
# 声明无参数的信号
signal1 = QtCore.pyqtSignal()
# 声明一个int类型参数的信号
signal2 = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super(SignalClass, self).__init__(parent)
# 将信号signal连接到sin1Call和sin2Call这两个槽函数
self.signal1.connect(self.sin1Call)
self.signal1.connect(self.sin2Call)
# 将信号signal2连接到信号signal1
self.signal2.connect(self.signal1)
# 发射信号
self.signal1.emit()
self.signal2.emit(1)
# 断开signal1和signal2信号与各个槽函数连接
self.signal1.disconnect(self.sin1Call)
self.signal1.disconnect(self.sin2Call)
self.signal2.disconnect(self.signal1)
# signal1 signal2连接到同一个槽函数signalCall
self.signal1.connect(self.sin1Call)
self.signal2.connect(self.sin1Call)
# 再次发射信号
self.signal1.emit()
self.signal2.emit(1)
def sin1Call(self):
print("signal-1 emit")
def sin2Call(self):
print("signal-2 emit")
if __name__ == "__main__":
SignalClass()
运行结果如下:
(qtvenv) D:\Github\PyProjects\pyqt5>python singnalSlot02.py
signal-1 emit
signal-2 emit
signal-1 emit
signal-2 emit
signal-1 emit
signal-1 emit
多线程中信号与槽的使用
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton
import sys
class Main(QWidget):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
# 创建一个线程实例并设置名称、变量、信号与槽
self.thread = MyThread()
self.thread.setIdentify("thread1")
self.thread.sinout.connect(self.outText)
self.thread.setVal(6)
def outText(self, text):
print(text)
class MyThread(QThread):
sinout = pyqtSignal(str)
def __init__(self, parent=None):
super(MyThread, self).__init__(parent)
self.identity = None
def setIdentify(self, text):
self.identity = text
def setVal(self, val):
self.times = int(val)
# 执行线程的run方法
self.start()
def run(self):
while self.times > 0 and self.identity:
# 发射信号
self.sinout.emit(self.identity+"==>"+str(self.times))
self.times -= 1
if __name__ == "__main__":
app = QApplication(sys.argv)
main = Main()
main.show()
sys.exit(app.exec_())
运行结果如下:
(qtvenv) D:\Github\PyProjects\pyqt5>python singnalSlot03.py
thread1==>6
thread1==>5
thread1==>4
thread1==>3
thread1==>2
thread1==>1
有时在开发程序时经常会执行一些耗时的操作,这样就会导致界面卡顿,这也是多线程的应用范围之一——为了解决这个问题,我们可以创建多线程,使用主线程更新界面,使用子线程实时处理数据,最后将结果显示到界面上。
from PyQt5.QtCore import QThread, pyqtSignal, QDateTime
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton, QLineEdit, QDialog
import sys
import time
class BackendThread(QThread):
# 通过类成员对象定义信号
update_date = pyqtSignal(str)
# 处理业务逻辑
def run(self):
while True:
data = QDateTime.currentDateTime()
currTime = data.toString("yyyy-MM-dd hh:mm:ss")
self.update_date.emit(str(currTime))
time.sleep(1)
class Window(QDialog):
def __init__(self):
QDialog.__init__(self)
self.setWindowTitle("Pyqt5界面实时更新列子")
self.resize(400, 100)
self.input = QLineEdit(self)
self.input.resize(400, 100)
self.initUI()
def initUI(self):
# 创建线程
self.backend = BackendThread()
# 连接信号
self.backend.update_date.connect(self.handleDisplay)
# 开始线程
self.backend.start()
# 将当前时间输出到文本框
def handleDisplay(self, data):
self.input.setText(data)
if __name__ == "__main__":
app = QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())