python GUI库图形界面开发之PyQt5信号与槽机制、自定义信号基础介绍


Posted in Python onFebruary 25, 2020

信号和槽机制是 QT 的核心机制,要精通 QT 编程就必须对信号和槽有所了解。信号和槽是一种高级接口,应用于对象之间的通信,它是 QT 的核心特性,也是 QT 区别于其它工具包的重要地方。

在linux、windows等 GUI 工具包中,GUI组件都会注册回调函数用于处理组件所触发的动作,通常是注册对应的函数的函数指针。在之前关于Button的文章中提到了信号与槽的机制的使用,通过该机制可以很好的将组件的信号(如button的clocked、toggled、pressed等)和处理该信号的槽关联起来。通过 信号与槽机制,能够让我们很简洁和快速的来完成相关的功能。

信号和槽是用来在对象间传递数据的方法:当一个特定事件发生的时候,signal会被emit出来,slot调用是用来响应相应的signal的。Qt中对象已经包含了许多预定义的 signal(基本组件都有各自特有的预定义的信号),根据使用的场景我们可以添加新的signal。Qt的对象中已经包含了许多预定义的槽函数,但我们也根据使用的场景添加新的槽函数。

信号

当对象的状态发生改变的时候,信号就由该对象发射 (emit) 出去。当一个信号被发射(emit)时候,与其关联的槽函数被立刻执行。其中该对象只负责发送信号,发射该信号的对象并不知道是那个对象在接收这个信号。这样保证了对象与对象之间的低耦合。

如果存在信号和多个槽函数相关联的时候,当信号被发射时,这些槽的执行顺序将会是随机的、不确定的。

用于接受信号,而且槽只是普通的对象成员函数。当和槽连接的信号被发射时,槽会被调用。一个槽并不知道是否有任何信号与自己相连接。

信号和槽的绑定

通过调用 QObject 对象的 connect 函数来将某个对象的信号与另外一个对象的槽函数相关联,这样当发射者发射信号时,接收者的槽函数将被调用。该函数的定义如下::

connect(slot[, type=PyQt5.QtCore.Qt.AutoConnection[, no_receiver_check=False]])

Parameters: 

slot ? the slot to connect to, either a Python callable or another bound signal.

type ? the type of the connection to make.

no_receiver_check ? suppress the check that the underlying C++ receiver instance still exists and deliver the signal anyway.

当信号与槽没有必要继续保持关联时,我们可以使用 disconnect 函数来断开连接。其定义如下:

disconnect([slot])

Parameters: slot ? the optional slot to disconnect from, either a Python callable or another bound signal. If it is omitted then all slots connected to the signal are disconnected.

信号和槽的特点

1、一个信号可以连接到多个槽;

当信号发出后,槽函数都会被调用,但是调用的顺序是随机的,不确定的。

self.slider.valueChanged.connect(self.pBar.setValue) 

self.slider.valueChanged.connect(self.lcdNumber.display)

QSlider数据的变化同时绑定在setValue()和display()两个槽上。

2、多个信号可以连接到同一个槽;

其中任何一个信号发出,槽函数都会被执行。

self.buttonOn.clicked.connect(self.showMessage)

self.buttonOff.clicked.connect(self.showMessage)

showMessage()同时绑定在两个button的clicked信号上

3、信号的参数可以是任何的Python类型;

如list,dict等python独有的类型。自定义信号的时候举例说明。

4、信号和槽的连接可以被移除;

比如断开某个特定信号的关联。

self.buttonOn.clicked.connect(self.showMessage)

5、信号可以和另外一个信号进行关联;

第一个信号发出后,第二个信号也同时发送。比如关闭系统的信号发出之后,同时会发出保存数据的信号。

代码示例:

关于信号和槽的式样代码如下:

#-*- coding:utf-8 -*-
'''
Signal & Slot
'''
__author__ = 'Tony Zhu'

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QWidget, QLCDNumber, QSlider,QGridLayout,QLabel,QHBoxLayout, QGroupBox,
  QVBoxLayout, QApplication,QProgressBar,QPushButton,QMessageBox)


class SignalSlot(QWidget):

  def __init__(self):
    super(SignalSlot,self).__init__()   
    self.initUI()


  def initUI(self):

    self.controlsGroup = QGroupBox("运行样本")
    self.lcdNumber = QLCDNumber(self)
    self.slider = QSlider(Qt.Horizontal, self)
    self.pBar = QProgressBar(self)
    vbox = QVBoxLayout()
    vbox.addWidget(self.pBar)
    vbox.addWidget(self.lcdNumber)
    vbox.addWidget(self.slider)
    self.controlsGroup.setLayout(vbox)

    controlsLayout = QGridLayout()
    self.label1 = QLabel("保存状态:")
    self.saveLabel = QLabel()
    self.label2 = QLabel("运行状态:")
    self.runLabel = QLabel()
    self.buttonSave = QPushButton("保存")
    self.buttonRun = QPushButton("运行")
    self.buttonStop = QPushButton("停止")
    self.buttonDisconnect = QPushButton("解除关联")
    self.buttonConnect = QPushButton("绑定关联")

    controlsLayout.addWidget(self.label1,0,0)
    controlsLayout.addWidget(self.saveLabel,0,1)
    controlsLayout.addWidget(self.label2,1,0)
    controlsLayout.addWidget(self.runLabel,1,1)
    controlsLayout.addWidget(self.buttonSave,2,0)
    controlsLayout.addWidget(self.buttonRun,2,1)
    controlsLayout.addWidget(self.buttonStop,2,2)
    controlsLayout.addWidget(self.buttonDisconnect,3,0)
    controlsLayout.addWidget(self.buttonConnect,3,1)

    layout = QHBoxLayout()
    layout.addWidget(self.controlsGroup)
    layout.addLayout(controlsLayout)
    self.setLayout(layout)

    self.buttonRun.clicked.connect(self.buttonSave.clicked)
    self.slider.valueChanged.connect(self.pBar.setValue)
    self.slider.valueChanged.connect(self.lcdNumber.display)
    self.buttonSave.clicked.connect(self.showMessage)
    self.buttonRun.clicked.connect(self.showMessage)
    self.buttonDisconnect.clicked.connect(self.unbindConnection)
    self.buttonConnect.clicked.connect(self.bindConnection)
    self.buttonStop.clicked.connect(self.stop)

    self.setGeometry(300, 500, 500, 180)
    self.setWindowTitle('信号和槽')

  def showMessage(self):
    if self.sender().text() == "保存":
      self.saveLabel.setText("Saved")
    elif self.sender().text() == "运行":
      self.saveLabel.setText("Saved")
      self.runLabel.setText("Running")

  def unbindConnection(self):
    self.slider.valueChanged.disconnect()
  def bindConnection(self):
    self.slider.valueChanged.connect(self.pBar.setValue)
    self.slider.valueChanged.connect(self.lcdNumber.display)
  def stop(self):
    self.saveLabel.setText("")
    self.runLabel.setText("")

if __name__ == '__main__':

  app = QApplication(sys.argv)
  ex = SignalSlot()
  ex.show()
  sys.exit(app.exec_())

程序运行的结果:

python GUI库图形界面开发之PyQt5信号与槽机制、自定义信号基础介绍

控件说明:

控件类型 控件名称 作用
QLCDNumber lcdNumber 显示slider滑动之后的数据
QProgressBar pBar 显示slider滑动之后的数据(百分比数据)
QSlider slider 滑动块调整数据
QPushButton buttonSave 在saveLabel显示保存的状态”Saved”
QPushButton buttonRun 在runLabel显示运行的状态”Running”
QPushButton buttonDisconnect 解除slider.valueChanged信号的绑定
QPushButton buttonConnect 连接slider.valueChanged信号的绑定
QPushButton buttonStop 清除saveLabel和runLabel的信息

示例说明:

程序样本运行的界面逻辑,先设定运行的程序样本数量,然后先保存后运行的逻辑状态。通过slider的滑动来改变progressBar和LCD的显示数据;“保存”按钮保存运行的样本;“运行”按钮运行程序样本;“解除关联”解除slider.valueChanged信号的绑定,此时slider的滑动,不会改变progressBar和LCD的显示

示例说明:

L22~30:

self.controlsGroup = QGroupBox("运行样本")
self.lcdNumber = QLCDNumber(self)
self.slider = QSlider(Qt.Horizontal, self)
self.pBar = QProgressBar(self)
vbox = QVBoxLayout()
vbox.addWidget(self.pBar)
vbox.addWidget(self.lcdNumber)
vbox.addWidget(self.slider)
self.controlsGroup.setLayout(vbox)

实例化一个QGroupBox,在其中添加QSlider,QProgressBar,QLCDNumber控件。

L32~41:

controlsLayout = QGridLayout()
self.label1 = QLabel("保存状态:")

.....

self.buttonDisconnect = QPushButton("解除关联")
self.buttonConnect = QPushButton("绑定关联")

实例化,界面中右半部分的控件。

L58~65:

self.buttonRun.clicked.connect(self.buttonSave.clicked)
self.slider.valueChanged.connect(self.pBar.setValue)
self.slider.valueChanged.connect(self.lcdNumber.display)
self.buttonSave.clicked.connect(self.showMessage)
self.buttonRun.clicked.connect(self.showMessage)
self.buttonDisconnect.clicked.connect(self.unbindConnection)
self.buttonConnect.clicked.connect(self.bindConnection)
self.buttonStop.clicked.connect(self.stop)

signal和slot进行绑定。

1、一个信号绑定多个槽:

self.slider.valueChanged.connect(self.pBar.setValue)

self.slider.valueChanged.connect(self.lcdNumber.display)

slider控件的valueChanged信号,同时与QProgressBar的setValue(),QLCDNumber的display()槽函数绑定,当valueChanged信号触发的时候,这两个槽函数均会被调用。

2、多个信号绑定到一个槽:

self.buttonSave.clicked.connect(self.showMessage)

self.buttonRun.clicked.connect(self.showMessage)

buttonSave和buttonRun这两个对象的clicked信号,同时绑定到showMessage()这个槽函数。无论哪一个信号被触发,showMessage()这个槽函数均会被调用。

3、信号和槽的连接可以被移除:

self.buttonDisconnect.clicked.connect(self.unbindConnection)

当buttonDisconnect信号触发之后,与其关联的槽函数unbindConnection()中就会执行disconnect()方法,如下:

def unbindConnection(self):
self.slider.valueChanged.disconnect()

其中执行disconnect()的时候可以指定解除与某个特定的slot槽的关联,比如self.slider.valueChanged.disconnect(self.pBar.setValue),此时解除和QProgressBar的setValue()的关联;或者不指定,在不指定slot的场景下这样将解除和这个信号所有关联的槽。

4、信号与信号的关联:

self.buttonRun.clicked.connect(self.buttonSave.clicked)

在示例说明中提到,在运行之前要对样本进行保存,所以为了保证运行的时候执行了保存的操作,所以将buttonRun.clicked信号和buttonSave.clicked信号关联起来。

示例中在没有执行“保存”(buttonSave)的时候,执行“运行”(buttonRun),此时由于两个对象的clicked信号已经关联,所以buttonSave的clicked同样会执行。

PyQt5自定义信号

PyQt5已经自动定义了很多QT内建的信号。但是在实际的使用中为了灵活使用信号与槽机制,我们可以根据需要自定义signal。可以使用pyqtSignal()方法定义新的信号,新的信号作为类的属性。

自定义signal说明:

pyqtSignal()方法原型(PyQt官网的定义):

PyQt5.QtCore.pyqtSignal(types[, name[, revision=0[, arguments=[]]]])

Create one or more overloaded unbound signals as a class attribute.

Parameters: 

types ? the types that define the C++ signature of the signal. Each type may be a Python type object or a string that is the name of a C++ type. Alternatively each may be a sequence of type arguments. In this case each sequence defines the signature of a different signal overload. The first overload will be the default.

name ? the name of the signal. If it is omitted then the name of the class attribute is used. This may only be given as a keyword argument.

revision ? the revision of the signal that is exported to QML. This may only be given as a keyword argument.

arguments ? the sequence of the names of the signal's arguments that is exported to QML. This may only be given as a keyword argument.

Return type:    an unbound signal

新的信号应该定义在QObject的子类中。新的信号必须作为定义类的一部分,不允许将信号作为类的属性在类定义之后通过动态的方式进行添加。通过这种方式新的信号才能自动的添加到QMetaObject类中。这就意味这新定义的信号将会出现在Qt Designer,并且可以通过QMetaObject API实现内省。

通过下面的例子,了解一下关于signal的定义:

from PyQt5.QtCore import QObject, pyqtSignal

class NewSignal(QObject):

  # 定义了一个“closed”信号,该信号没有参数据
  closed= pyqtSignal()

  # 定义了一个"range_changed"信号,该信号有两个int类型的参数
  range_changed = pyqtSignal(int, int, name='rangeChanged')

自定义信号的发射,通过emit()方法类实现,具体参见该函数的原型:

emit(*args)

Parameters: args ? the optional sequence of arguments to pass to any connected slots.

通过下面的例子,了解一下关于emit()的使用:

from PyQt5.QtCore import QObject, pyqtSignal

class NewSignal(QObject):

  # 一个valueChanged的信号,该信号没有参数.
  valueChanged = pyqtSignal()

  def connect_and_emit_valueChanged(self):
    # 绑定信号和槽函数
    self.valueChanged.connect(self.handle_valueChanged)

    # 发射信号.
    self.trigger.emit()

  def handle_valueChanged(self):
    print("trigger signal received")

示例说明:

自定义信号的一般流程如下:

  1. 定义信号
  2. 定义槽函数
  3. 绑定信号和槽
  4. 发射信号

通过代码示例来了解一下信号的自定义过程:

#-*- coding:utf-8 -*-
'''
defined Signal
'''
__author__ = 'Tony Zhu'
import sys
from PyQt5.QtCore import pyqtSignal, QObject, Qt, pyqtSlot
from PyQt5.QtWidgets import QWidget, QApplication, QGroupBox, QPushButton, QLabel, QCheckBox, QSpinBox, QHBoxLayout, QComboBox, QGridLayout


class SignalEmit(QWidget):
  helpSignal = pyqtSignal(str)
  printSignal = pyqtSignal(list)
  #声明一个多重载版本的信号,包括了一个带int和str类型参数的信号,以及带str参数的信号
  previewSignal = pyqtSignal([int,str],[str])
  def __init__(self):
    super().__init__()    
    self.initUI()


  def initUI(self):      

    self.creatContorls("打印控制:")
    self.creatResult("操作结果:")

    layout = QHBoxLayout()
    layout.addWidget(self.controlsGroup)
    layout.addWidget(self.resultGroup)
    self.setLayout(layout)

    self.helpSignal.connect(self.showHelpMessage)
    self.printSignal.connect(self.printPaper)
    self.previewSignal[str].connect(self.previewPaper)
    self.previewSignal[int,str].connect(self.previewPaperWithArgs) 
    self.printButton.clicked.connect(self.emitPrintSignal)
    self.previewButton.clicked.connect(self.emitPreviewSignal)

    self.setGeometry(300, 300, 290, 150)
    self.setWindowTitle('defined signal')
    self.show()

  def creatContorls(self,title):
    self.controlsGroup = QGroupBox(title)
    self.printButton = QPushButton("打印")
    self.previewButton = QPushButton("预览")
    numberLabel = QLabel("打印份数:")
    pageLabel = QLabel("纸张类型:")
    self.previewStatus = QCheckBox("全屏预览")
    self.numberSpinBox = QSpinBox()
    self.numberSpinBox.setRange(1, 100)
    self.styleCombo = QComboBox(self)
    self.styleCombo.addItem("A4")
    self.styleCombo.addItem("A5")

    controlsLayout = QGridLayout()
    controlsLayout.addWidget(numberLabel, 0, 0)
    controlsLayout.addWidget(self.numberSpinBox, 0, 1)
    controlsLayout.addWidget(pageLabel, 0, 2)
    controlsLayout.addWidget(self.styleCombo, 0, 3)
    controlsLayout.addWidget(self.printButton, 0, 4)
    controlsLayout.addWidget(self.previewStatus, 3, 0)
    controlsLayout.addWidget(self.previewButton, 3, 1)
    self.controlsGroup.setLayout(controlsLayout)

  def creatResult(self,title):
    self.resultGroup = QGroupBox(title)
    self.resultLabel = QLabel("")
    layout = QHBoxLayout()
    layout.addWidget(self.resultLabel)
    self.resultGroup.setLayout(layout)

  def emitPreviewSignal(self):
    if self.previewStatus.isChecked() == True:
      self.previewSignal[int,str].emit(1080," Full Screen")
    elif self.previewStatus.isChecked() == False:
      self.previewSignal[str].emit("Preview")

  def emitPrintSignal(self):
    pList = []
    pList.append(self.numberSpinBox.value ())
    pList.append(self.styleCombo.currentText())
    self.printSignal.emit(pList)

  def printPaper(self,list):
    self.resultLabel.setText("Print: "+"份数:"+ str(list[0]) +" 纸张:"+str(list[1]))

  def previewPaperWithArgs(self,style,text):
    self.resultLabel.setText(str(style)+text)

  def previewPaper(self,text):
    self.resultLabel.setText(text)     

  def keyPressEvent(self, event):

    if event.key() == Qt.Key_F1:
      self.helpSignal.emit("help message")

  def showHelpMessage(self,message):
    self.resultLabel.setText(message)
    #self.statusBar().showMessage(message)


if __name__ == '__main__':

  app = QApplication(sys.argv)
  dispatch = SignalEmit()
  sys.exit(app.exec_())

运行该函数之后的效果如下:

python GUI库图形界面开发之PyQt5信号与槽机制、自定义信号基础介绍

示例说明:

通过一个模拟打印的界面来详细说明一下关于信号的自定义,在打印的时候可以设定打印的分数,纸张类型,触发“打印”按钮之后,将执行结果显示到右侧;通过全屏预览QCheckBox来选择是否通过全屏模式进行预览,将执行结果显示到右侧。

通过点击F1快捷键,可以显示helpMessage信息。

代码分析:

L12~15:

helpSignal = pyqtSignal(str)
printSignal = pyqtSignal(list)
#声明一个多重载版本的信号,包括了一个带int和str类型参数的信号,以及带str参数的信号
previewSignal = pyqtSignal([int,str],[str])

通过pyqtSignal()定义了三个信号,helpSignal ,printSignal ,previewSignal 。其中:

  • helpSignal 为str参数类型的信号;
  • printSignal 为list参数类型的信号;
  • previewSignal为一个多重载版本的信号,包括了一个带int和str类型参数的信号,以及str类行的参数。

L31~36:

self.helpSignal.connect(self.showHelpMessage)

self.printSignal.connect(self.printPaper)

self.previewSignal[str].connect(self.previewPaper) 

self.previewSignal[int,str].connect(self.previewPaperWithArgs)

self.printButton.clicked.connect(self.emitPrintSignal)

self.previewButton.clicked.connect(self.emitPreviewSignal)

绑定信号和槽;着重说明一下多重载版本的信号的绑定,previewSignal有两个版本previewSignal(str),previewSignal(int,str)。由于存在两个版本,从因此在绑定的时候需要显式的指定信号和槽的绑定关系。

具体如下:

self.previewSignal[str].connect(self.previewPaper)

self.previewSignal[int,str].connect(self.previewPaperWithArgs)

其中[str]参数的previewSignal信号绑定previewPaper();[int,str]的previewSignal信号绑定previewPaperWithArgs()

L72~76:

def emitPreviewSignal(self):
    if self.previewStatus.isChecked() == True:
      self.previewSignal[int,str].emit(1080," Full Screen")
    elif self.previewStatus.isChecked() == False:
      self.previewSignal[str].emit("Preview")

多重载版本的信号的发射也需要制定对应发射的版本,类似同信号的版定。

L78~82:

def emitPrintSignal(self):
    pList = []
    pList.append(self.numberSpinBox.value ())
    pList.append(self.styleCombo.currentText())
    self.printSignal.emit(pList)

如代码中所示,在信号发射的时候可以传递python数据类型的参数,在本例中传递list类型的参数pList.

L93~96:

def keyPressEvent(self, event):
    if event.key() == Qt.Key_F1:
      self.helpSignal.emit("help message")

通过复写keyPressEvent()方法,将F1快捷键进行功能的拓展。在windows的大部分应用,我们都会使用一些快捷键来快速的完成某些特定的功能。比如F1键,会快速调出帮助界面。那我们就可以复写keyPressEvent()方法来模拟发送所需的信号,来完成我们的对应任务.

注意事项:

  • 自定义的信号在init()函数之前定义;
  • 自定义型号可以传递,str、int、list、object、float、tuple、dict等很多类型的参数;
  • 注意signal和slot的调用逻辑,避免signal和slot之间出现死循环。如在slot方法中继续发射该信号;

更多关于python GUI库图形界面开发之PyQt5文章请查看下面的相关链接

Python 相关文章推荐
Python matplotlib绘图可视化知识点整理(小结)
Mar 16 Python
Python实现线程状态监测简单示例
Mar 28 Python
python print 按逗号或空格分隔的方法
May 02 Python
python实现决策树分类(2)
Aug 30 Python
Django migrations 默认目录修改的方法教程
Sep 28 Python
Django REST framework 分页的实现代码
Jun 19 Python
Python实现自定义读写分离代码实例
Nov 16 Python
利用Python的turtle库绘制玫瑰教程
Nov 23 Python
Python 私有属性和私有方法应用场景分析
Jun 19 Python
Django-celery-beat动态添加周期性任务实现过程解析
Nov 26 Python
python爬虫scrapy框架之增量式爬虫的示例代码
Feb 26 Python
用Python selenium实现淘宝抢单机器人
Jun 18 Python
python模拟点击网页按钮实现方法
Feb 25 #Python
python GUI库图形界面开发之PyQt5动态加载QSS样式文件
Feb 25 #Python
python 计算概率密度、累计分布、逆函数的例子
Feb 25 #Python
python GUI库图形界面开发之PyQt5窗口背景与不规则窗口实例
Feb 25 #Python
python统计函数库scipy.stats的用法解析
Feb 25 #Python
Python Websocket服务端通信的使用示例
Feb 25 #Python
Python GUI库PyQt5样式QSS子控件介绍
Feb 25 #Python
You might like
《PHP边学边教》(04.编写简易的通讯录――视频教程1)
2006/12/13 PHP
某大型网络公司应聘时的笔试题目附答案
2008/03/27 PHP
JS中encodeURIComponent函数用php解码的代码
2012/03/01 PHP
如何离线执行php任务
2017/02/21 PHP
Laravel 5使用Laravel Excel实现Excel/CSV文件导入导出的功能详解
2017/10/11 PHP
理解Javascript_13_执行模型详解
2010/10/20 Javascript
javascript高级学习笔记整理
2011/08/14 Javascript
javascript读取Xml文件做一个二级联动菜单示例
2014/03/17 Javascript
JavaScript中对循环语句的优化技巧深入探讨
2014/06/06 Javascript
JavaScript获得url所有参数键值表的方法
2015/03/21 Javascript
javascript实现checkbox复选框实例代码
2016/01/10 Javascript
微信小程序 video组件详解
2016/10/25 Javascript
本地Bootstrap文件字体图标引入却无法显示问题的解决方法
2020/04/18 Javascript
JS利用cookies设置每隔24小时弹出框
2017/04/20 Javascript
详解Layer弹出层样式
2017/08/21 Javascript
Vue官方文档梳理之全局配置
2017/11/22 Javascript
vue组件详解之使用slot分发内容
2018/04/09 Javascript
微信小程序实现上传图片裁剪图片过程解析
2019/08/22 Javascript
Vue data的数据响应式到底是如何实现的
2020/02/11 Javascript
[02:41]DOTA2亚洲邀请赛小组赛第三日 赛事回顾
2015/02/01 DOTA
Python解析nginx日志文件
2015/05/11 Python
Django中Model的使用方法教程
2018/03/07 Python
python中退出多层循环的方法
2018/11/27 Python
Django中的FBV和CBV用法详解
2019/09/15 Python
浅谈python出错时traceback的解读
2020/07/15 Python
在python中对于bool布尔值的取反操作
2020/12/11 Python
HTML5实时语音通话聊天MP3压缩传输3KB每秒
2019/08/28 HTML / CSS
HTML5之HTML元素扩展(上)—新增加的元素及使用概述
2013/01/31 HTML / CSS
工程地质勘察专业大学生求职信
2013/10/13 职场文书
公司道歉信范文
2014/01/09 职场文书
道路建设实施方案
2014/03/18 职场文书
社区反邪教工作方案
2014/06/16 职场文书
后勤个人工作总结
2015/02/28 职场文书
全国法制宣传日活动总结
2015/05/05 职场文书
毕业论文致谢词
2015/05/14 职场文书
Django中session进行权限管理的使用
2021/07/09 Python