(PyQt5信号与槽(三)高级玩法)

三、PyQt5信号与槽高级玩法

1、高级自定义信号与槽

  • 自定义信号的一般流程:
    • 定义信号
    • 定义槽函数
    • 连接信号与槽函数
    • 发射信号

(1)定义信号

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


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)定义槽函数

  • 定义槽函数,它有多个不同的输入参数
    def setValue_NoParameters(self):
        """无参槽函数"""
        print("""无参槽函数""")

    def setValue_OneParameter(self, nIndex):
        """带一个参数(整数)的槽函数"""
        print("""带一个参数(整数)的槽函数""", nIndex)

    def setValue_OneParameter_String(self, snIndex):
        """带一个参数(字符串)的槽函数"""
        print("""带一个参数(字符串)的槽函数""", snIndex)

    def setValue_TwoParameters(self, x, y):
        """带两个参数(整数,整数)的槽函数"""
        print("""带两个参数(整数,整数)的槽函数""", x, y)

    def setValue_TwoParameters_String(self, x, szY):
        """带两个参数(整数,字符串)的槽函数"""
        print("""带两个参数(整数,字符串)的槽函数""", x, szY)

(3)连接信号与槽函数

		# 连接无参信号
        self.Signal_NoParameters.connect(self.setValue_NoParameters)

        # 连接带一个整数参数的信号
        self.Signal_OneParameter.connect(self.setValue_OneParameter)

        # 连接带一个整数参数,经过重载的信号
        self.Signal_OneParameter_Overload.connect(self.setValue_OneParameter_String)

        # 连接一个信号,有两个整数参数
        self.Signal_TwoParameters.connect(self.setValue_TwoParameters)

        # 连接带两个参数(整数,整数)的重载版本的信号
        self.Signal_TwoParameters_Overload[int, int].connect(self.setValue_TwoParameters)

        # 连接带两个参数(整数,字符串)的重载版本的信号
        self.Signal_TwoParameters_Overload[int, str].connect(self.setValue_TwoParameters_String)

(4)发射信号

    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)实例

  • 上面例子完整代码
# -*- coding:utf-8 -*-
"""
    # @Time:2022/12/11 0011 14:18
    # @Author:晚秋拾叶
    # @File:senior.py
    # @PyCharm之Python
"""
import sys
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import pyqtSignal


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

    def __init__(self):
        super().__init__()
        
        """------------------一大片信号连接---------------------"""
        # 连接无参信号
        self.Signal_NoParameters.connect(self.setValue_NoParameters)

        # 连接带一个整数参数的信号
        self.Signal_OneParameter.connect(self.setValue_OneParameter)

        # 连接带一个整数参数,经过重载的信号
        self.Signal_OneParameter_Overload.connect(self.setValue_OneParameter_String)

        # 连接一个信号,有两个整数参数
        self.Signal_TwoParameters.connect(self.setValue_TwoParameters)

        # 连接带两个参数(整数,整数)的重载版本的信号
        self.Signal_TwoParameters_Overload[int, int].connect(self.setValue_TwoParameters)

        # 连接带两个参数(整数,字符串)的重载版本的信号
        self.Signal_TwoParameters_Overload[int, str].connect(self.setValue_TwoParameters_String)

    """------------------一大片槽函数---------------------"""
    def setValue_NoParameters(self):
        """无参槽函数"""
        print("""无参槽函数""")

    def setValue_OneParameter(self, nIndex):
        """带一个参数(整数)的槽函数"""
        print("""带一个参数(整数)的槽函数""", nIndex)

    def setValue_OneParameter_String(self, snIndex):
        """带一个参数(字符串)的槽函数"""
        print("""带一个参数(字符串)的槽函数""", snIndex)

    def setValue_TwoParameters(self, x, y):
        """带两个参数(整数,整数)的槽函数"""
        print("""带两个参数(整数,整数)的槽函数""", x, y)

    def setValue_TwoParameters_String(self, x, szY):
        """带两个参数(整数,字符串)的槽函数"""
        print("""带两个参数(整数,字符串)的槽函数""", x, szY)

    """------------------发射信号函数---------------------"""
    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')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = MyWidget()
    widget.show()
    sys.exit(app.exec_())
  • 运行过程中,需要点击窗口空白处。

  • 运行结果 在这里插入图片描述

  • 新建另一实例

# -*- coding:utf-8 -*-
"""
    # @Time:2022/12/11 0011 15:51
    # @Author:晚秋拾叶
    # @File:qt07_signalSlot02.py
    # @PyCharm之Python
"""
from PyQt5.QtCore import QObject, pyqtSignal


class CustSignal(QObject):
    """  1 定义信号-------------------------------"""
    # 声明一个无参数的信号
    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)

        """  2 信号连槽-------------------------------"""
        # 信号连接到指定槽
        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)

        """  4 信号发送-------------------------------"""
        # 信号发射
        self.signal1.emit()
        self.signal2.emit(1)
        self.signal3.emit(1, "text")
        self.signal4.emit([1, 2, 3, 4])
        self.signal5.emit({"name": "晚秋拾叶", "age": "28"})
        self.signal6[int, str].emit(1, "text")
        self.signal6[str].emit("text")

    """  3 定义槽函数-------------------------------"""

    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函数来说,希望它可以接收参数。

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

  • 本节提供了两种解决方法,其中一种方法就是使用lambda表达式。例子如下:

# -*- coding:utf-8 -*-
"""
    # @Time:2022/12/11 0011 16:41
    # @Author:晚秋拾叶
    # @File:qt07_winSignalSlot04.py
    # @PyCharm之Python
"""
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_())

  • 代码分析
    • 单击按钮1,将弹出信息提示框,提示信息为“Button 1 clicked”。控制台信息为 Button 1被按下了
    • 这里重点解释onButtonClick()函数是怎样处理从两个按钮传来的信号的。使用lambda表达式传递按钮数字给槽函数,当然也可以传递其他任何信息,甚至是按钮控件本身(假设槽函数打算把传递信号的按钮修改为不可用的话)。
    • 另一种解决方法是使用functools中的partial函数。例子如下:
from functools import partial

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("信号和槽传递额外参数例子使用partial函数")
        button1 = QPushButton('Button 1')
        button2 = QPushButton('Button 2')

        # button1.clicked.connect(lambda: self.onButtonClick(1))
        # button2.clicked.connect(lambda: self.onButtonClick(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(QObject)
    
  • 发送者对象名称 → 使用setObjectName函数设置的名称。自定义槽函数的命名规则也可以看成 : on _使用setObjectName设置的名称_信号名称。具体例子如下:

# -*- coding:utf-8 -*-
"""
    # @Time:2022/12/11 0011 17:08
    # @Author:晚秋拾叶
    # @File:qt07_connSlotsByName.py
    # @PyCharm之Python
"""
from PyQt5 import QtCore
from PyQt5.QtWidgets import *
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)
        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_())

  • 结果如图 在这里插入图片描述
  • 代码分析
    • 解释下面代码的意思

      QtCore.QMetaObject.connectSlotsByName(self)
      
    • 事实上,这是在PyQt5中根据信号名称自动连接到槽函数的核心代码。通过使用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按钮")
        
      • 事实上,以下代码实现的效果是相同的。

        • 代码一
        @QtCore.pyqtSlot()
        def on_okButton_clicked(self):
        	print("单击了ok按钮")
        
        • 代码二
        	    self.okButton.clicked.connect(self.okButton_clicked)
        	def okButton_clciked(self):
        		print("单击了OK按键")
        

4、信号与槽的断开和连接

  • 有时候基于某些原因 ,想要临时或永久断开某个信号与槽的连接。看下面例子
# -*- coding:utf-8 -*-
"""
    # @Time:2022/12/11 0011 18:10
    # @Author:晚秋拾叶
    # @File:qt07_signalSlot03.py
    # @PyCharm之Python
"""
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.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.emit()
        self.signal1.connect(self.sin1Call)
        self.signal2.connect(self.sin1Call)

        # 信号再次发射
        self.signal1.emit()  # signal-1 emit
        self.signal2.emit(1)  # signal-1 emit

    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结合
    • 案例:
    • 通过一个模拟打印的界面来详细说明信号的使用,在打印时可以设置打印的份数、纸张类型,触发“打印”按钮后,将执行结果显示在右侧;通过QCheckBox (“全屏预览”复选框)来选择是否通过全屏模式进行预览,将执行结果显示在右侧。
  • 运行QtDesigner工具,新建一个模板名为“Widget”的简单窗口,命名为MainWinSignalSlog02.ui。通过将Widget Box区域的控件拖曳到窗口中,实现下图界面。
    • 其中,打印和预览功能需要编写信号和槽。 在这里插入图片描述
  • 窗口控件简要说明
控件类型 控件名称 作用
QGroupBox(组合布局器) controlsGroup 布局整个窗口左侧的控件
QHBoxLayout(打印垂直布局器) controlGroup 打印控制布局
QHBoxLayout(结果垂直布局器) resultGroup 操作结果布局
QSpinBox(计数器) numberSpinBox 显示打印的份数
QComboBox(下拉列表框) styleCombo 显示打印的纸张类型,纸张类型包括A3、A4和A5
QPushButton(打印按钮) printButton 连接emitPrintSignal函数的绑定,触发自定义信号printSignal的发射
QCheckBox(复选框) previewStatus 是否全屏预览
QPushButton(预览按钮) previewButton 连接emitPrintSignal函数的绑定,触发自定义信号previewSignal的发射
QLabel(显示标签) resultLabel 显示执行结果
  • 打开Designer,建一个Form窗体,然后按照这个预览图,从左侧工具中拖曳各种布局和控件即可,其中QComboBox中的的选项,需要双击这个控件然后再添加。 在这里插入图片描述

  • 将界面文件转换成Python文件。命令行如下:

     pyuic5.exe MainWinSignalSlog02.ui -o MainWinSignalSlog02.py
    
  • 生成的MainWinSignalSlog02.py文件,完整代码如下。

    # -*- coding: utf-8 -*-
    
    # Form implementation generated from reading ui file 'MainWinSignalSlog02.ui'
    #
    # Created by: PyQt5 UI code generator 5.15.4
    #
    # 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, PrintWin):
    
            PrintWin.setObjectName("PrintWin")
            PrintWin.resize(794, 183)
    
            # “打印控制”组合布局设计
            self.controlGroup = QtWidgets.QGroupBox(PrintWin)
            self.controlGroup.setGeometry(QtCore.QRect(10, 10, 461, 161))
            self.controlGroup.setObjectName("controlGroup")
    
            #
            self.horizontalLayoutWidget = QtWidgets.QWidget(self.controlGroup)
            self.horizontalLayoutWidget.setGeometry(QtCore.QRect(9, 20, 431, 80))
            self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
    
            # 打印份数等控件所在的垂直布局
            self.HLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
            self.HLayout.setContentsMargins(0, 0, 0, 0)
            self.HLayout.setObjectName("HLayout")
    
            # “打印份数”标签
            self.label = QtWidgets.QLabel(self.horizontalLayoutWidget)
            self.label.setObjectName("label")
            self.HLayout.addWidget(self.label)
    
            # “计数器”标签
            self.numberSpinBox = QtWidgets.QSpinBox(self.horizontalLayoutWidget)
            self.numberSpinBox.setObjectName("numberSpinBox")
            self.HLayout.addWidget(self.numberSpinBox)
    
            # “纸张类型”标签
            self.label_2 = QtWidgets.QLabel(self.horizontalLayoutWidget)
            self.label_2.setObjectName("label_2")
            self.HLayout.addWidget(self.label_2)
    
            # “下拉列表框”标签
            self.styleCombo = QtWidgets.QComboBox(self.horizontalLayoutWidget)
            self.styleCombo.setObjectName("styleCombo")
            self.styleCombo.addItem("")
            self.styleCombo.addItem("")
            self.styleCombo.addItem("")
            self.HLayout.addWidget(self.styleCombo)
    
            # ”打印“按键
            self.printButton = QtWidgets.QPushButton(self.horizontalLayoutWidget)
            self.printButton.setObjectName("printButton")
            self.HLayout.addWidget(self.printButton)
    
            #
            self.horizontalLayoutWidget_2 = QtWidgets.QWidget(self.controlGroup)
            self.horizontalLayoutWidget_2.setGeometry(QtCore.QRect(9, 110, 321, 41))
            self.horizontalLayoutWidget_2.setObjectName("horizontalLayoutWidget_2")
    
            # ”全屏预览“复选框所在的垂直布局器
            self.HLayout_2 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_2)
            self.HLayout_2.setContentsMargins(0, 0, 0, 0)
            self.HLayout_2.setObjectName("HLayout_2")
            # ”全屏预览“复选框
            self.previewStatus = QtWidgets.QCheckBox(self.horizontalLayoutWidget_2)
            self.previewStatus.setObjectName("previewStatus")
            self.HLayout_2.addWidget(self.previewStatus)
    
            # ”预览”按键
            self.previewButton = QtWidgets.QPushButton(self.horizontalLayoutWidget_2)
            self.previewButton.setObjectName("previewButton")
            self.HLayout_2.addWidget(self.previewButton)
    
            # “操作结果”组合布局设计
            self.resultGroup = QtWidgets.QGroupBox(PrintWin)
            self.resultGroup.setGeometry(QtCore.QRect(480, 10, 311, 161))
            self.resultGroup.setObjectName("resultGroup")
            self.resultLabel = QtWidgets.QLabel(self.resultGroup)
            self.resultLabel.setGeometry(QtCore.QRect(25, 63, 261, 21))
            self.resultLabel.setObjectName("resultLabel")
    
            # 调用再编译函数,对窗口进行展现
            self.retranslateUi(PrintWin)
            # 遇到了上节课的那种对PrintWin函数的绑定
            QtCore.QMetaObject.connectSlotsByName(PrintWin)
    
        def retranslateUi(self, PrintWin):
            _translate = QtCore.QCoreApplication.translate
            PrintWin.setWindowTitle(_translate("PrintWin", "打印窗口"))
            self.controlGroup.setTitle(_translate("PrintWin", "打印控制"))
            self.label.setText(_translate("PrintWin", "打印份数"))
            self.label_2.setText(_translate("PrintWin", "纸张类型"))
            self.styleCombo.setItemText(0, _translate("PrintWin", "A3"))
            self.styleCombo.setItemText(1, _translate("PrintWin", "A4"))
            self.styleCombo.setItemText(2, _translate("PrintWin", "A5"))
            self.printButton.setText(_translate("PrintWin", "打印"))
            self.previewStatus.setText(_translate("PrintWin", "全屏预览"))
            self.previewButton.setText(_translate("PrintWin", "预览"))
            self.resultGroup.setTitle(_translate("PrintWin", "操作结果"))
            self.resultLabel.setText(_translate("PrintWin", "<html><head/><body><p><br/></p></body></html>"))
    	
    		
    
  • 为了实现窗口显示和业务逻辑分离,再建一个调用对话窗口的文件CallMainWinSignalSlog02.py,代码如下:

    # -*- coding:utf-8 -*-
    """
        # @Time:2022/12/11 0011 21:06
        # @Author:晚秋拾叶
        # @File:callMainWinSignalSlog02.py
        # @PyCharm之Python
    """
    import sys
    from PyQt5.QtWidgets import QApplication, QMainWindow
    from MainWinSignalSlog02 import Ui_Form
    from PyQt5.QtCore import pyqtSignal, Qt
    
    
    class MyMainWindow(QMainWindow, Ui_Form):
        # 帮助信号和打印信号
        helpSignal = pyqtSignal(str)
        printSignal = pyqtSignal(list)
        # 预览信号,声明一个多重载版本的信号,包括了一个带int和str类型参数的信号,以及带str参数的信号
        previewSignal = pyqtSignal([int, str], [str])
    
        def __init__(self, parent=None):
            super(MyMainWindow, self).__init__(parent)
            self.setupUi(self)
            self.initUI()
    
        def initUI(self):
            self.helpSignal.connect(self.showHelpMessage)                       # 1 信号一,显示帮助消息
            self.printSignal.connect(self.printPaper)                           # 2 信号二,点击打印按键后显示的信息
            self.previewSignal[str].connect(self.previewPaper)                  # 3 信号三,多重载中的一个参数情况下的预览Preview
            self.previewSignal[int, str].connect(self.previewPaperWithArgs)     # 4 信号三,多重载中的多个参数情况下的全屏预览1080 Full Screen
            self.printButton.clicked.connect(self.emitPrintSignal)              # 5 打印按钮,发射打印信号
            self.previewButton.clicked.connect(self.emitPreviewSignal)          # 6 预览按钮,发射预览信号
    
        # 1 显示帮助消息
        def showHelpMessage(self, message):
            self.resultLabel.setText(message)
            self.statusBar().showMessage(message)
    
        # 2 点击打印按键后显示的信息
        def printPaper(self, listview):
            self.resultLabel.setText("打印: " + "份数:" + str(listview[0]) + " 纸张:" + str(listview[1]))
    
        # 3 多重载中的一个参数情况下的预览Preview
        def previewPaper(self, text):
            self.resultLabel.setText(text)
    
        # 4 多重载中的多个参数情况下的全屏预览1080 Full Screen
        def previewPaperWithArgs(self, style, text):
            self.resultLabel.setText(str(style) + text)
    
        # 5 发射打印信号
        def emitPrintSignal(self):
            pList = [self.numberSpinBox.value(), self.styleCombo.currentText()]
            self.printSignal.emit(pList)
    
        # 6 发射预览信号
        def emitPreviewSignal(self):
            if self.previewStatus.isChecked():
                self.previewSignal[int, str].emit(1080, " Full Screen")
            elif not self.previewStatus.isChecked():
                self.previewSignal[str].emit("Preview")
    
        # 0 重载点击键盘事件
        def keyPressEvent(self, event):
            if event.key() == Qt.Key_F1:
                self.helpSignal.emit("help message")
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        win = MyMainWindow()
        win.show()
        sys.exit(app.exec_())
    
    
  • 效果如图

    • 这是我按下了F1键后的效果图,出现help message的帮助字样。 在这里插入图片描述
    • 这是我选择了打印份数和纸张类型后,再点打印的效果图。 在这里插入图片描述
    • 窗体代码需要改动的地方,也可以直接在窗体改 在这里插入图片描述
  • 代码分析

    • 这个例子非常好,既学习了信号与槽,还复习了Designer工具的使用。
    • 代码分离的好处,那就是所见即所得,每次改动用Designer太方便了,生成的界面图,保存后,用pyuic命令重新生成一次,很省事。
    • 本例通过pyqtSignal()定义了三个信号,即helpSignal、printSignal、previewSignal,参数类型分别为str、list和一个多重载版本的信号,其中previewSignal的参数包括一个带int和str类型参数的信号,以及带str类型参数的信号
      # 帮助信号和打印信号
      helpSignal = pyqtSignal(str)
      printSignal = pyqtSignal(list)
      # 预览信号,声明一个多重载版本的信号,包括了一个带int和str类型参数的信号,以及带str参数的信号
      previewSignal = pyqtSignal([int, str], [str])
      
    • 对于绑定信号与槽,这里着重说明多重载版本的信号绑定。因为previewSignal有两个版本,因此,在绑定时需要显式指定信号与槽的绑定关系。
      self.helpSignal.connect(self.showHelpMessage)
      self.printSignal.connect(self.printPaper)
      self.previewSignal[str].connect(self.previewPaper)
      self.previewSignal[int, str].connect(self.previewPaperWithArgs)
      
    • [str]参数的previewSignal信号绑定previewPaper(),[int,str]参数previewSignal信号绑定previewWithArgs(),多重载版本的信号发射时,要注意发射信号传递的参数类型和个数,在Qt参数的通信机制中,根据所传递信号的参数类型和个数,连接到不同的槽函数。
      def emitPreviewSignal(self):
          if self.previewStatus.isChecked():
              self.previewSignal[int, str].emit(1080, " Full Screen")
          elif not self.previewStatus.isChecked():
              self.previewSignal[str].emit("Preview")
      
    • 信号发射时可以传递Python数据类型的参数,本例中的printSignal信号可以传递list类型的参数pList。
      def emitPrintSignal(self):
          pList = [self.numberSpinBox.value(), self.styleCombo.currentText()]
          self.printSignal.emit(pList)
      
    • 通过复写keyPressEvent()方法,对F1键进行功能扩展。在Windows的大部分应用中,都会使用一些快捷键来快速完成某些特定的功能。这里通过复写keyPressEvent()方法模拟发射所需的信号,来完成对应的任务。
      def keyPressEvent(self, event):
         if event.key() == Qt.Key_F1:
             self.helpSignal.emit("help message")
      
  • 特别注意

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

6、多线程中信号与槽的使用

  • 最简单的多线程使用方法是利用QTread函数,下例展现了QTread函数和信号/槽的简单结合方法。

    # -*- coding:utf-8 -*-
    """
        # @Time:2022/12/12 0012 0:01
        # @Author:晚秋拾叶
        # @File:qt07_signalSlot04.py
        # @PyCharm之Python
    """
    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_())
    
  • 运行结果 在这里插入图片描述

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

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

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

  • 本例文件名为PyQt5/Chapter07/qt07_signalSIotThreaadpy,其完整代码如下:

    # -*- coding:utf-8 -*-
    """
        # @Time:2022/12/12 0012 0:10
        # @Author:晚秋拾叶
        # @File:qt07_signalSlotThread.py
        # @PyCharm之Python
    """
    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('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__':
        from pyqt5_plugins.examples.exampleqmlitem import QtCore
    
        QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
        app = QApplication(sys.argv)
        win = Window()
        win.show()
        sys.exit(app.exec_())
    
    
  • 运行结果 在这里插入图片描述