一、信号和槽介绍
信号(Signal)和槽(Slot)是Qt中的核心机制,也是在PyQt编程中对象之间进行通信的机制。在Qt中,每个QObject对象和PyQt中所有继承自QWidget的控件(这些都是QObject的子对象)都支持信号与槽机制。当信号发射时,连接的槽函数将会自动执行。在PyQt5中信号与槽通过object.signal.connect()方法连接。
PyQt的窗口控件类中有很多内置信号,开发者也可以添加自定义信号。信号与槽具有如下特点:
*一个信号可以连接多个槽
*一个信号可以连接另一个信号
*信号参数可以使任何Python类型
*一个槽可以监听多个信号
*信号与槽的连接方式可以是同步连接,也可以是异步连接
*信号与槽的连接可能会跨线程
*信号可能会断开
当事件或者状态发生改变时,就会发出信号。同时,信号会触发所有与这个事件(信号)相关的函数(槽)。信号与槽可以使多对多的关系。一个信号可以连接多个槽,一个槽也可以监听多个信号。
1、定义信号
PyQt的内置信号时自动定义的。使用PyQt5.QtCore.pyqtSIGNAL()函数可以为QObject创建一个信号,使用pyqtSingnal()函数可以把信号定义为类的属性。
1.1为QObject对象创建信号
使用pqtSignal()函数创建一个或多个重载的未绑定的信号作为类的属性,信号只能在QObject的子类中定义。信号必须在类创建时定义,不能在累创建后作为类的属性动态添加进来。types参数表示定义信号时参数的类型,name参数表示信号名字,该项缺省时使用类的属性名字。使用pyqtSignal()函数创建信号时,信号可以传递多个参数,并指定信号传递参数的类型,参数类型是标准的Python数据类型(字符串、日期、布尔类型、数字、列表、元组和字典)
1.2为控件创建信号
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QMainWindow
class WinForm(QMainWindow):
btnClickedSignal = pyqtSignal()
上面代码为自定义控件WinForm创建了一个btnClickedSignal信号
2、操作信号
使用connect()函数可以把信号绑定到槽函数上。使用disconnect()函数可以解除信号与槽函数的绑定。使用emit()函数可以发射信号。
3、信号与槽的入门应用
信号与槽有三种使用方法,第一种是内置信号与槽的使用,第二种是自定义信号与槽的使用,第三种是装饰器的信号与槽的使用。
3.1内置信号与槽的使用
所谓内置信号与槽的使用,是指在发射信号时,使用窗口控件的函数,而不是自定义的函数。在信号与槽中,可以通过QObject.signal.connect将一个QObject的信号连接到另一个QObject的槽函数。例子如下:
""" | |
【简介】 | |
信号和槽例子 | |
""" | |
from PyQt5.QtWidgets import QPushButton , QApplication, QWidget | |
from PyQt5.QtWidgets import QMessageBox | |
import sys | |
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_()) |
这个例子将一个按钮对象的内置clicked信号连接到自定义的槽函数showMsg(),也可以说showMsg()函数响应了一个按钮单击事件。单击“测试点击按钮”按钮,就会弹出一个信息提示框。运行结果如下:
3.2自定义信号与槽的使用
指在发射信号时,不使用窗口控件的函数,而是使用自定义的函数(简单地说,就是使用pyqtSignal类实例发射信号)。之所以要使用自定义信号与槽,是因为通过内置函数发射信号有自身的缺陷。首先,内置函数值包含一些常用的信号,有些信号的发射找不到对应的内置函数;其次,只有在特定情况下(如按钮的点击事件)才能发射这种信号;最后,内置函数传递的参数是特定的,不可以自定义。使用自定义的信号函数则没有这些缺陷。
在PyQt5编程中,自定义信号与槽的适用范围很灵活,比如因为业务需求,在程序中的某个地方需要发射一个信号,传递多种数据类型(实际上就是传递参数),然后在槽函数中接收传递过来的数据,这样就可以非常灵活地实现一些业务逻辑。以下是Python风格的写法举例:
""" | |
【简介】 | |
内置信号槽示例 | |
""" | |
from PyQt5.QtCore import QObject, pyqtSignal | |
# 信号对象 | |
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() |
运行结果:
信号与槽连接的主要步骤如下:
(1)生成一个信号
sendmsg = pyqtSignal(object)
(2)将信号与槽函数绑起来
send.sendmsg.connect(slot.get)
(3)槽函数接收数据
def get(self,msg):
print(“QSlot get msg =>”+msg)
(4)发射信号的实现
self.sendmsg.emit('Hello Pyqt5')
(5)把信号绑定到槽对象中的槽函数get()上,所以槽函数能接收到所发射的信号-----字符串‘Hello Pyqt5’。至此,数据传递成功,就这么简单。
send = QTypeSignal()
slot = QTypeSlot()
print(‘---把信号绑定到槽函数上---’)
send.sendmsg.connect( slot.get)
send.run()
同理,断开信号与槽函数get()的连接,那么槽函数肯定就接收不到所发射的信号了
print(‘---把信号与槽函数的连接断开---’)
send.sendmsg.disconnect( slot.get)
send.run()
以上演示的是传递一个参数,如果要传递两个参数,也可以用这个思路,例子如下:
from PyQt5.QtCore import QObject , pyqtSignal | |
#信号对象 | |
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("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() | |
二、信号与槽再细分
1、内置信号和槽函数
以下的例子演示单击按钮时关闭窗口,使用内置的信号和槽函数。完整代码如下:
""" | |
【简介】 | |
内置的信号/槽示例 | |
""" | |
from PyQt5.QtWidgets import * | |
import sys | |
class Winform(QWidget): | |
def __init__(self,parent=None): | |
super().__init__(parent) | |
self.setWindowTitle('内置的信号/槽示例') | |
self.resize(330, 50 ) | |
btn = QPushButton('关闭', self) | |
btn.clicked.connect(self.close) | |
if __name__ == '__main__': | |
app = QApplication(sys.argv) | |
win = Winform() | |
win.show() | |
sys.exit(app.exec_()) |
运行结果如下:
在上面的代码中,单击按钮时触发按钮内置的信号(clicked),绑定窗口(QWidget)内置的槽函数(self.close)
2、内置信号和自定义槽函数
下面的代码是使用内置的信号和自定义的槽函数:
""" | |
【简介】 | |
内置的信号,自定义槽函数示例 | |
""" | |
from PyQt5.QtWidgets import * | |
import sys | |
class Winform(QWidget): | |
def __init__(self,parent=None): | |
super().__init__(parent) | |
self.setWindowTitle('内置的信号和自定义槽函数示例') | |
self.resize(330, 50 ) | |
btn = QPushButton('关闭', self) | |
btn.clicked.connect(self.btn_close) | |
def btn_close(self): | |
# 自定义槽函数 | |
self.close() | |
if __name__ == '__main__': | |
app = QApplication(sys.argv) | |
win = Winform() | |
win.show() | |
sys.exit(app.exec_()) |
运行结果:
3、自定义信号和内置槽函数:
完整代码如下:
""" | |
【简介】 | |
自定义信号和内置槽函数 示例 | |
""" | |
from PyQt5.QtWidgets import * | |
from PyQt5.QtCore import pyqtSignal | |
import sys | |
class Winform(QWidget): | |
# 自定义信号,不带参数 | |
button_clicked_signal = pyqtSignal() | |
def __init__(self,parent=None): | |
super().__init__(parent) | |
self.setWindowTitle('自定义信号和内置槽函数示例') | |
self.resize(330, 50 ) | |
btn = QPushButton('关闭', self) | |
# 连接 信号和槽 | |
btn.clicked.connect(self.btn_clicked) | |
# 接收信号,连接到槽 | |
self.button_clicked_signal.connect(self.close) | |
def btn_clicked(self): | |
# 发送自定义信号,无参数 | |
self.button_clicked_signal.emit() | |
if __name__ == '__main__': | |
app = QApplication(sys.argv) | |
win = Winform() | |
win.show() | |
sys.exit(app.exec_()) |
运行结果如下:
4、自定义信号和槽函数
完整代码如下:
""" | |
【简介】 | |
自定义信号和槽函数 示例 | |
""" | |
from PyQt5.QtWidgets import * | |
from PyQt5.QtCore import pyqtSignal | |
import sys | |
class Winform(QWidget): | |
# 自定义信号,不带参数 | |
button_clicked_signal = pyqtSignal() | |
def __init__(self,parent=None): | |
super().__init__(parent) | |
self.setWindowTitle('自定义信号和槽函数示例') | |
self.resize(330, 50 ) | |
btn = QPushButton('关闭', self) | |
# 连接 信号和槽 | |
btn.clicked.connect(self.btn_clicked) | |
# 接收信号,连接到自定义槽函数 | |
self.button_clicked_signal.connect(self.btn_close) | |
def btn_clicked(self): | |
# 发送自定义信号,无参数 | |
self.button_clicked_signal.emit() | |
def btn_close(self): | |
self.close() | |
if __name__ == '__main__': | |
app = QApplication(sys.argv) | |
win = Winform() | |
win.show() | |
sys.exit(app.exec_()) |
运行结果:
在上面的代码中,单击按钮时触发自定义信号(button_clicked_signal),绑定自定义的槽函数(self.btn_close)
三、信号与槽的高级玩法
1、高级自定义信号与槽
所谓高级自定义信号与槽,指的是我们可以以自己信号的方式定义信号与槽函数,并传递参数。自定义信号的一般流程如下:
(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(QWidget):
def setValue_OneParameter(self):
'''无参数的槽函数'''
pass
def setValue_OneParameter(self,nIndex):
'''带一个参数(整数)的槽函数'''
pass
def setValue_OneParameter_String(self,szIndex):
'''带一个参数(字符串)的槽函数'''
pass
def setValue_TwoParameters(self,x,y):
'''带两个参数(整数,整数)的槽函数'''
pass
def setValue_TwoParameters_String(self,x,szY):
'''带两个参数(整数,字符串)槽函数'''
pass
(3)连接信号与槽函数
通过connect方法连接信号与槽函数或者可调用对象。
app = QApplication(sys.argv)
widget = MyWidget()
#连接无参数的信号
widget.Signal_NoParameters.connect(self.setValue_NoParameters)
#连接带一个整数参数的信号
widget.Signal_OneParameter.connect(self.setValue_OneParameter)
#连接带一个整数参数,经过重载的信号
widget.Signal_OneParameter_Overload[str].connect(self.setValue_OneParameter_String)
#连接一个信号,它有两个整数参数
widget.Signal_TwoParameters.connect(self.setValue_TwoParameters)
#连接两个参数(整数,整数)的重载版本的信号
widget.Signal_TwoParameters_Overload[int,int].connect(self.setValue_TwoParameters)
#连接带两个参数(整数,字符串)的重载版本的信号
widget.Signal_TwoParameters_Overload[int,str].connect(self.setValue_TwoParameters_String)
widget.show()
(4)发射信号
通过emit方法发射信号
class MyWidget(QWidget):
def mousePressEvent(self,event):
#发射无参数的信号
self.Signal_NoParameters.emit()
#发射带一个参数(整数)的信号
self.Signal_OneParameter.emit(1)
#发射带一个参数(整数)的重载版本的信号
self.Signal_OneParameter_Overload.emit(1)
#发射带一个参数(字符串)的重载版本的信号
self.Signal_OneParameter_Overload.emit("abc")
#发射带两个参数(整数,字符串)的信号
self.Signal_TwoParameters_Overload.emit(1,2)
#发射带两个参数(整数,字符串)的重载版本的信号
self.Signal_TwoParameters_Overload.emit(1,"abc")
(5)实例:
""" | |
【简介】 | |
内置信号槽信号槽示例 | |
""" | |
from PyQt5.QtCore import QObject , pyqtSignal | |
class CustSignal(QObject): | |
# 声明一个无参数的信号 | |
signal1 = pyqtSignal() | |
# 声明带一个int类型参数的信号 | |
signal2 = pyqtSignal(int) | |
# 声明带一个int和str类型参数的信号 | |
signal3 = pyqtSignal(int,str) | |
# 声明带一个列表类型参数的信号 | |
signal4 = pyqtSignal(list) | |
# 声明带一个字典类型参数的信号 | |
signal5 = pyqtSignal(dict) | |
# 声明一个多重载版本的信号,包括了一个带int和str类型参数的信号或着带str参数的信号 | |
signal6 = pyqtSignal([int,str], [str]) | |
def __init__(self,parent=None): | |
super(CustSignal,self).__init__(parent) | |
# 信号连接到指定槽 | |
self.signal1.connect(self.signalCall1) | |
self.signal2.connect(self.signalCall2) | |
self.signal3.connect(self.signalCall3) | |
self.signal4.connect(self.signalCall4) | |
self.signal5.connect(self.signalCall5) | |
self.signal6[int,str].connect(self.signalCall6) | |
self.signal6[str].connect(self.signalCall6OverLoad) | |
# 信号发射 | |
self.signal1.emit() | |
self.signal2.emit(1) | |
self.signal3.emit(1,"text") | |
self.signal4.emit([1,2,3,4]) | |
self.signal5.emit({"name":"wangwu","age":"25"}) | |
self.signal6[int,str].emit(1,"text") | |
self.signal6[str].emit("text") | |
def signalCall1(self): | |
print("signal1 emit") | |
def signalCall2(self,val): | |
print("signal2 emit,value:",val) | |
def signalCall3(self,val,text): | |
print("signal3 emit,value:",val,text) | |
def signalCall4(self,val): | |
print("signal4 emit,value:",val) | |
def signalCall5(self,val): | |
print("signal5 emit,value:",val) | |
def signalCall6(self,val,text): | |
print("signal6 emit,value:",val,text) | |
def signalCall6OverLoad(self,val): | |
print("signal6 overload emit,value:",val) | |
if __name__ == '__main__': | |
custSignal = CustSignal() |
运行结果:
2、使用自定义参数
在PyQt编程中,经常会遇到给槽函数传递自定义参数的情况,比如有一个信号与槽函数的连接是
button1.clicked.connect(show_page)
我们知道对于clicked信号来说,它是没有参数的;对于show_page函数来说,希望它可以接收参数。希望show_page函数像如下这样: def show_page(self,name):
print(name," 点击啦")
于是就产生了一个问题-----信号发出的参数个数为0,槽函数接收的参数个数为1,由于0<1,这样运行起来一定会报错(原因是信号发出的参数个数一定要大于槽函数接收的参数个数)。解决这样问题就要用到自定义参数的传递了。下面的例子是使用lambda表达式来解决这个问题的完整代码:
""" | |
【简介】 | |
部件中的信号槽传递,使用lambda表达式传参数示例 | |
""" | |
from PyQt5.QtWidgets import QMainWindow, QPushButton , QWidget , QMessageBox, QApplication, QHBoxLayout | |
import sys | |
class WinForm(QMainWindow): | |
def __init__(self, parent=None): | |
super(WinForm, self).__init__(parent) | |
self.setWindowTitle("信号和槽传递额外参数例子") | |
button1 = QPushButton('Button 1') | |
button2 = QPushButton('Button 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('Button {0} 被按下了'.format(n)) | |
QMessageBox.information(self, "信息提示框", 'Button {0} clicked'.format(n)) | |
if __name__ == "__main__": | |
app = QApplication(sys.argv) | |
form = WinForm() | |
form.show() | |
sys.exit(app.exec_()) |
运行结果:
另一种解决方法是使用functools中的partial函数。例子如下:
""" | |
【简介】 | |
部件中的信号槽传递,使用partial函数传参数示例 | |
""" | |
from PyQt5.QtWidgets import QMainWindow, QPushButton , QWidget , QMessageBox, QApplication, QHBoxLayout | |
import sys | |
from functools import partial | |
class WinForm(QMainWindow): | |
def __init__(self, parent=None): | |
super(WinForm, self).__init__(parent) | |
self.setWindowTitle("信号和槽传递额外参数例子") | |
button1 = QPushButton('Button 1') | |
button2 = QPushButton('Button 2') | |
button1.clicked.connect(partial(self.onButtonClick, 1)) | |
button2.clicked.connect(partial(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('Button {0} 被按下了'.format(n)) | |
QMessageBox.information(self, "信息提示框", 'Button {0} clicked'.format(n)) | |
if __name__ == "__main__": | |
app = QApplication(sys.argv) | |
form = WinForm() | |
form.show() | |
sys.exit(app.exec_()) |
运行结果:
3、装饰器信号与槽
所谓装饰器信号与槽,就是通过装饰器的方法来定义信号和槽函数。具体使用方法如下:
@PyQt5.QtCore.pyqtSlot(参数)
def on_发送者对象名称_发射信号名称(self,参数)
pass
这种方法有效的前提是下面的函数已经执行:
QMetaObject.connectSlotsByName(QOJbject)
在上面的代码中,“发送者对象名称”就是使用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() | |
sys.exit(app.exec_()) |
运行结果:
4、信号与槽的断开与连接
""" | |
【简介】 | |
信号槽N对N连接、断开连接示例 | |
""" | |
from PyQt5.QtCore import QObject , pyqtSignal | |
class SignalClass(QObject): | |
# 声明一个无参数的信号 | |
signal1 = pyqtSignal() | |
# 声明带一个int类型参数的信号 | |
signal2 = pyqtSignal(int) | |
def __init__(self,parent=None): | |
super(SignalClass,self).__init__(parent) | |
# 信号sin1连接到sin1Call和sin2Call这两个槽 | |
self.signal1.connect(self.sin1Call) | |
self.signal1.connect(self.sin2Call) | |
# 信号sin2连接到信号sin1 | |
self.signal2.connect(self.signal1) | |
# 信号发射 | |
self.signal1.emit() | |
self.signal2.emit(1) | |
# 断开sin1、sin2信号与各槽的连接 | |
self.signal1.disconnect(self.sin1Call) | |
self.signal1.disconnect(self.sin2Call) | |
self.signal2.disconnect(self.signal1) | |
# 信号sin1和sin2连接同一个槽sin1Call | |
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__': | |
signal = SignalClass() |
运行结果:
5、Qt Designer神助攻:界面显示与业务逻辑的分离
在实战应用中,由于Qt Designer可以更好地实现界面显示与业务逻辑的分离,所以能帮我们解决大量的代码。如果能使用Qt Designer自动创建一些信号与槽机制,那就更好了。下面就介绍信号和槽是如何和Qt Designer结合的。