详解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 SQLite3数据库操作类分享
Jun 10 Python
wxPython中listbox用法实例详解
Jun 01 Python
python3 pillow生成简单验证码图片的示例
Sep 19 Python
python SSH模块登录,远程机执行shell命令实例解析
Jan 12 Python
利用Python进行数据可视化常见的9种方法!超实用!
Jul 11 Python
利用Python如何批量修改数据库执行Sql文件
Jul 29 Python
如何基于python实现画不同品种的樱花树
Jan 03 Python
TensorFlow实现批量归一化操作的示例
Apr 22 Python
Python接口测试结果集实现封装比较
May 01 Python
如何利用Python 进行边缘检测
Oct 14 Python
分享unittest单元测试框架中几种常用的用例加载方法
Dec 02 Python
python os.rename实例用法详解
Dec 06 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
详细介绍PHP应用提速面面观
2006/10/09 PHP
PHP 代码规范小结
2012/03/08 PHP
PHP获取mysql数据表的字段名称和详细信息的方法
2014/09/27 PHP
PHP实现一个多功能购物网站的案例
2017/09/13 PHP
Laravel框架使用技巧之使用url()全局函数返回前一个页面的地址方法详解
2020/04/06 PHP
ThinkPHP3.1.2 使用cli命令行模式运行的方法
2020/04/14 PHP
js 多种变量定义(对象直接量,数组直接量和函数直接量)
2010/05/24 Javascript
jQuery实现等比例缩放大图片让大图片自适应页面布局
2013/10/16 Javascript
javascript创建数组之联合数组的使用方法示例
2013/12/26 Javascript
JS创建自定义表格具体实现
2014/02/11 Javascript
JavaScript中模拟实现jsonp
2015/06/19 Javascript
JavaScript Array对象详解
2016/03/01 Javascript
js实现表单及时验证功能 用户信息立即验证
2016/09/13 Javascript
js中创建对象的几种方式
2017/02/05 Javascript
微信小程序实现动态设置placeholder提示文字及按钮选中/取消状态的方法
2017/12/14 Javascript
监听element-ui table滚动事件的方法
2019/03/26 Javascript
layui实现checkbox的目录树tree的例子
2019/09/12 Javascript
微信小程序自定义波浪组件使用方法详解
2019/09/21 Javascript
Python字符串拼接六种方法介绍
2017/12/18 Python
Python tkinter label 更新方法
2018/10/11 Python
python tkinter界面居中显示的方法
2018/10/11 Python
Window环境下Scrapy开发环境搭建
2018/11/18 Python
pandas读取csv文件,分隔符参数sep的实例
2018/12/12 Python
Django restframework 源码分析之认证详解
2019/02/22 Python
详解Python 多线程 Timer定时器/延迟执行、Event事件
2019/06/27 Python
django ManyToManyField多对多关系的实例详解
2019/08/09 Python
python脚本后台执行方式
2019/12/21 Python
适合Python初学者的一些编程技巧
2020/02/12 Python
localStorage 设置过期时间的方法实现
2018/12/21 HTML / CSS
HTML5 本地存储和内容按需加载的思路和方法
2011/04/07 HTML / CSS
韩国三星集团旗下时尚品牌官网:SSF SHOP
2016/08/02 全球购物
Skyscanner波兰:廉价航班
2017/11/07 全球购物
西班牙购买行李箱和背包网站:Maletas Greenwich
2019/10/08 全球购物
怎么写自荐书范文
2014/02/12 职场文书
《画家乡》教学反思
2014/04/22 职场文书
食品安全承诺书
2014/05/22 职场文书