详解PyQt5信号与槽的几种高级玩法


Posted in Python onMarch 24, 2020

信号(Signal)和槽(Slot)是Qt中的核心机制,也是在PyQt编程中对象之间进行通信的机制。本文介绍了几种PyQt 5信号与槽的几级玩法。

在Qt中,每一个QObject对象和PyQt中所有继承自QWidget的控件(这些都是QObject的子对象)都支持信号与槽机制。当信号发射时,连接的槽函数将会自动执行。在PyQt 5中信号与槽通过object.signal.connect()方法连接。

PyQt的窗口控件类中有很多内置信号,开发者也可以添加自定义信号。信号与槽具有如下特点。

  • 一个信号可以连接多个槽。
  • 一个信号可以连接另一个信号。
  • 信号参数可以是任何Python类型。
  • 一个槽可以监听多个信号。
  • 信号与槽的连接方式可以是同步连接,也可以是异步连接。
  • 信号与槽的连接可能会跨线程。
  • 信号可能会断开。

在GUI编程中,当改变一个控件的状态时(如单击了按钮),通常需要通知另一个控件,也就是实现了对象之间的通信。在早期的GUI编程中使用的是回调机制,在Qt中则使用一种新机制——信号与槽。在编写一个类时,要先定义该类的信号与槽,在类中信号与槽进行连接,实现对象之间的数据传输。信号与槽机制示意图如图1所示。

详解PyQt5信号与槽的几种高级玩法

当事件或者状态发生改变时,就会发出信号。同时,信号会触发所有与这个事件(信号)相关的函数(槽)。信号与槽可以是多对多的关系。一个信号可以连接多个槽,一个槽也可以监听多个信号。

关于PyQt API中信号与槽的更详细解释,可以参考官方网站: http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html

1 高级自定义信号与槽

所谓高级自定义信号与槽,指的是我们可以以自己喜欢的方式定义信号与槽函数,并传递参数。自定义信号的一般流程如下:

(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(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[int].
  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.emit(1,"abc")
    # 发射带两个参数(整数,整数)的重载版本的信号
    self.Signal_TwoParameters_Overload.emit(1,2)
    # 发射带两个参数(整数,字符串)的重载版本的信号
    self.Signal_TwoParameters_Overload.emit (1,"abc")

5.实例

本例文件名为PyQt5/Chapter07/qt07_signalSlot02.py,其完整代码如下:

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()

运行结果如下:

signal1 emit
signal2 emit,value: 1
signal3 emit,value: 1 text
signal4 emit,value: [1, 2, 3, 4]
signal5 emit,value: {'name': 'wangwu', 'age': '25'}
signal6 emit,value: 1 text
signal6 overload emit,value: text

2 使用自定义参数

在PyQt编程过程中,经常会遇到给槽函数传递自定义参数的情况,比如有一个信号与槽函数的连接是

button1.clicked.connect(show_page)

我们知道对于clicked信号来说,它是没有参数的;对于show_page函数来说,希望它可以接收参数。希望show_page函数像如下这样:

def show_page(self, name):
print(name," 点击啦")

于是就产生一个问题——信号发出的参数个数为0,槽函数接收的参数个数为1,由于0<1,这样运行起来一定会报错(原因是信号发出的参数个数一定要大于槽函数接收的参数个数)。解决这个问题就是本节的重点:自定义参数的传递。

本书提供了两种解决方法,其中一种解决方法是使用lambda表达式。本例文件名为PyQt5/Chapter07/qt07_ winSignalSlot04.py ,其完整代码如下:

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) 
    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_())

运行脚本,显示效果如图2和图3所示。

详解PyQt5信号与槽的几种高级玩法

详解PyQt5信号与槽的几种高级玩法

代码分析:

单击“Button 1”按钮,将弹出一个信息提示框,提示信息为“Button 1 clicked”。Python控制台的输出信息为:

Button 1 被按下了

这里重点解释onButtonClick()函数是怎样处理从两个按钮传来的信号的。使用lambda表达式传递按钮数字给槽函数,当然也可以传递其他任何东西,甚至是按钮控件本身(假设槽函数打算把传递信号的按钮修改为不可用的话)。

另一种解决方法是使用functools中的partial函数。本例文件名为PyQt5/Chapter07/qt07_winSignalSlot05.py,其核心代码如下:

button1.clicked.connect(partial(self.onButtonClick, 1))     
button2.clicked.connect(partial(self.onButtonClick, 2))

采用哪种方法好一点呢?这属于风格问题,笔者比较喜欢使用lambda表达式,因为其条理清晰,而且灵活。

3 装饰器信号与槽

所谓装饰器信号与槽,就是通过装饰器的方法来定义信号和槽函数。具体的使用方法如下:

@PyQt5.QtCore.pyqtSlot(参数)
def on_发送者对象名称_发射信号名称(self, 参数):
    pass

这种方法有效的前提是下面的函数已经执行:

QMetaObject.connectSlotsByName(QObject)

在上面代码中,“发送者对象名称”就是使用setObjectName函数设置的名称,因此自定义槽函数的命名规则也可以看成:on + 使用setObjectName设置的名称 + 信号名称。接下来看具体的使用方法。

本例文件名为PyQt5/Chapter07/qt07_connSlotsByName.py,其完整代码如下:

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_()

运行脚本,显示效果如图4所示。单击“OK”按钮,控制台打印出预期的调试信息。

详解PyQt5信号与槽的几种高级玩法

有的读者可能注意到,我们一直没有解释下面这行代码的含义:

QMetaObject.connectSlotsByName(QObject)

事实上,它是在PyQt 5中根据信号名称自动连接到槽函数的核心代码。通过前面章节中的例子可以知道,使用pyuic5命令生成的代码中会带有这么一行代码,接下来对其进行解释。

这行代码用来将QObject中的子孙对象的某些信号按照其objectName连接到相应的槽函数。这句话读起来有些拗口,这里举个例子进行简单说明。以上面例子中的代码为例:

假设代码QtCore.QMetaObject.connectSlotsByName(self)已经执行,则下面的代码:

@QtCore.pyqtSlot()  
def on_okButton_clicked(self):
  print( "单击了OK按钮")

会被自动识别为下面的代码(注意,函数中去掉了on,因为on会受到connectSlotsByName的影响,加上on运行时会出现问题):

def __init__(self, parent=None):
    self.okButton.clicked.connect(self.okButton_clicked)
  def okButton_clicked(self):
    print("单击了OK按钮")

这部分代码放在PyQt5/Chapter07/qt07_connSlotsByName_2.py文件中:

# -*- coding: utf-8 -*-

"""
  【简介】
  信号与槽的自动连接例子
"""

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)
    self.okButton.clicked.connect(self.okButton_clicked)

  def okButton_clicked(self):
    print( "单击了OK按钮")

if __name__ == "__main__":    
  app = QApplication(sys.argv)
  win = CustWidget()
  win.show()
  sys.exit(app.exec_())

运行上述代码,发现结果和图4一样。

4 信号与槽的断开和连接

有时候基于某些原因,想要临时或永久断开某个信号与槽的连接。这就是本节案例想要达到的目的。

本例文件名为PyQt5/Chapter07/qt07_signalSlot03.py,其完整代码如下:

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)

    # 将信号signal1连接到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连接到同一个槽函数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()

运行结果如下:

signal-1 emit
signal-2 emit
signal-1 emit
signal-2 emit
signal-1 emit
signal-1 emit

5 多线程中信号与槽的使用

最简单的多线程使用方法是利用QThread函数,如下代码(见PyQt5/Chapter07/ qt07_signalSlot04.py)展示了QThread函数和信号与槽简单的结合方法。其完整代码如下:

from PyQt5.QtWidgets import QApplication ,QWidget
from PyQt5.QtCore import QThread , pyqtSignal
import sys

class Main(QWidget):
  def __init__(self, parent = None):
    super(Main,self).__init__(parent)

    # 创建一个线程实例并设置名称、变量、信号与槽
    self.thread = MyThread()
    self.thread.setIdentity("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 setIdentity(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_())

运行结果如下:

thread1==>6
thread1==>5
thread1==>4
thread1==>3
thread1==>2
thread1==>1

有时在开发程序时经常会执行一些耗时的操作,这样就会导致界面卡顿,这也是多线程的应用范围之一——为了解决这个问题,我们可以创建多线程,使用主线程更新界面,使用子线程实时处理数据,最后将结果显示到界面上。

本例中,定义了一个后台线程类BackendThread来模拟后台耗时操作,在这个线程类中定义了信号update_date。使用BackendThread线程类在后台处理数据,每秒发射一次自定义信号update_date。

在初始化窗口界面时,定义后台线程类BackendThread,并把线程类的信号update_date连接到槽函数handleDisplay()。这样后台线程每发射一次信号,就可以把最新的时间值实时显示在前台窗口的QLineEdit文本对话框中。

本例文件名为PyQt5/Chapter07/qt07_signalSlotThreaad.py,其完整代码如下:

from PyQt5.QtCore import QThread , pyqtSignal, QDateTime 
from PyQt5.QtWidgets import QApplication, QDialog, QLineEdit
import time
import sys

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('PyQt 5界面实时更新例子')
    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)
  win = Window()
  win.show() 
  sys.exit(app.exec_())

运行脚本,显示效果如图5所示。

详解PyQt5信号与槽的几种高级玩法

到此这篇关于详解PyQt5信号与槽的几种高级玩法的文章就介绍到这了,更多相关PyQt5信号与槽内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
Python学习笔记(二)基础语法
Jun 06 Python
详解Python中映射类型(字典)操作符的概念和使用
Aug 19 Python
Python批处理删除和重命名文件夹的实例
Jul 11 Python
python 在屏幕上逐字显示一行字的实例
Dec 24 Python
python如何读取bin文件并下发串口
Jul 05 Python
python绘图模块matplotlib示例详解
Jul 26 Python
python的pyecharts绘制各种图表详细(附代码)
Nov 11 Python
python matplotlib 画dataframe的时间序列图实例
Nov 20 Python
python如何获取apk的packagename和activity
Jan 10 Python
使用Python实现微信拍一拍功能的思路代码
Jul 09 Python
pip 20.3 新版本发布!即将抛弃 Python 2.x(推荐)
Dec 16 Python
Python爬虫新手入门之初学lxml库
Dec 20 Python
Windows10+anacond+GPU+pytorch安装详细过程
Mar 24 #Python
Python多进程multiprocessing、进程池用法实例分析
Mar 24 #Python
PyCharm Anaconda配置PyQt5开发环境及创建项目的教程详解
Mar 24 #Python
Python多线程通信queue队列用法实例分析
Mar 24 #Python
pycharm部署、配置anaconda环境的教程
Mar 24 #Python
PyQt5 如何让界面和逻辑分离的方法
Mar 24 #Python
Python3 中sorted() 函数的用法
Mar 24 #Python
You might like
一台收音机,让一家人都笑逐颜开!
2020/08/21 无线电
thinkphp 一个页面使用2次分页的实现方法
2013/07/15 PHP
使用phpQuery采集网页的方法
2013/11/13 PHP
php截取视频指定帧为图片
2016/05/16 PHP
PHP文件及文件夹操作之创建、删除、移动、复制
2016/07/13 PHP
php使用str_replace替换多维数组的实现方法分析
2017/06/15 PHP
Laravel框架实现redis集群的方法分析
2017/09/14 PHP
基于jQuery实现点击同时更改两个iframe的网址
2010/07/01 Javascript
jQuery实现带滚动导航效果的全屏滚动相册实例
2015/06/19 Javascript
Javascript removeChild()删除节点及删除子节点的方法
2015/12/27 Javascript
javascript实现checkbox复选框实例代码
2016/01/10 Javascript
Angular实现form自动布局
2016/01/28 Javascript
jquery自适应布局的简单实例
2016/05/28 Javascript
关于Function中的bind()示例详解
2016/12/02 Javascript
Ionic3 UI组件之Gallery Modal详解
2017/06/07 Javascript
详解用Node.js写一个简单的命令行工具
2018/03/01 Javascript
解决vue js IOS H5focus无法自动弹出键盘的问题
2018/08/30 Javascript
解决layui下拉框监听问题(监听不到值的变化)
2019/09/28 Javascript
js回调函数仿360开机
2019/12/26 Javascript
微信小程序实现下拉加载更多商品
2020/12/29 Javascript
[01:12]DOTA2 2015年秋季互动指南
2015/11/10 DOTA
使用IPython下的Net-SNMP来管理类UNIX系统的教程
2015/04/15 Python
python使用matplotlib绘制柱状图教程
2017/02/08 Python
python 中random模块的常用方法总结
2017/07/08 Python
Python pandas.DataFrame调整列顺序及修改index名的方法
2019/06/21 Python
python matplotlib画盒图、子图解决坐标轴标签重叠的问题
2020/01/19 Python
详解Python yaml模块
2020/09/23 Python
python代码实现猜拳小游戏
2020/11/30 Python
Cotton On美国网站:澳洲时装连锁品牌
2016/10/25 全球购物
美体小铺英国官网:The Body Shop英国
2017/01/24 全球购物
新闻编辑求职信
2014/04/09 职场文书
五一劳动节慰问信
2015/02/14 职场文书
2014年度个人总结范文
2015/03/09 职场文书
无罪辩护词范文
2015/05/21 职场文书
三八红旗手主要事迹材料
2015/11/04 职场文书
Mybatis-Plus 使用 @TableField 自动填充日期
2022/04/26 Java/Android