pyqt5信号与槽的使用

信号与槽介绍

信号(Signal)和槽(Slot)是Qt中的核心机制,也是在PyQt编程中对象之间进行通信的机制。在Qt中,每一个QObject对象和PyQt中所有继承自QWidget的控件(这些都是QObject的子对象)都支持信号与槽机制。当信号发射时,连接的槽函数将会自动执行。在PyQt 5中信号与槽通过object.signal.connect()方法连接。
PyQt的窗口控件类中有很多内置信号,开发者也可以添加自定义信号。信号与槽具有如下特点。

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

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

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

简单使用

Qt Designer提供了基本的编辑信号槽方法。首先,新建一个模板名为"Widget"的简单窗口,该窗口文件名为MainWinSignalSlog01.ui。

在Qt Designer窗口左侧有一个Buttons栏,找到QPushButton控件,把它拖到窗体Form中。在属性编辑器中,找到按钮对应的text属性,把属性值改为"关闭窗口",并将objectName属性值改为"closeWinBtn"

img

单击工具栏上的"编辑信号/槽"(或者通过单击"Edit"(编辑)菜单→"编辑信号/槽"),进入信号槽编辑模式,可以直接在发射者("关闭窗口"按钮)上按住鼠标左键不放,拖动到接收者(Form窗体)上,这样就建立起了连接。

img

img

将界面文件转换为Python文件,直接运行tools.py。

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

# Form implementation generated from reading ui file 'd:\Github\PyProjects\pyqt5\ui\MainWinSignalSlog01.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# 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, Form):
        Form.setObjectName("Form")
        Form.resize(400, 300)
        self.closeWinBtn = QtWidgets.QPushButton(Form)
        self.closeWinBtn.setGeometry(QtCore.QRect(150, 110, 75, 23))
        self.closeWinBtn.setObjectName("closeWinBtn")

        self.retranslateUi(Form)
        self.closeWinBtn.clicked.connect(Form.close) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.closeWinBtn.setText(_translate("Form", "关闭窗口"))

为了使窗口的显示和业务逻辑分离,再新建一个调用窗口显示的文件CallMainWinSignalSlog01.py,其完整代码如下:

import sys
from PyQt5.QtWidgets import QApplication ,QMainWindow
from ui.MainWinSignalSlog01 import Ui_Form # 我这里把所有的ui文件放在ui文件夹下


class MyMainWindow(QMainWindow,Ui_Form):
    def __init__(self,parent=None):
        super(MyMainWindow,self).__init__(parent)
        self.setupUi(self)
        
        
if __name__=="__main__":
    app=QApplication(sys.argv)
    myWin=MyMainWindow()
    myWin.show()
    sys.exit(app.exec_())

运行效果:

img

点击就会关闭窗口。

定义信号

使用PyQt5.QtCore.pyqtSignal()函数可以为QObject创建一个信号,使用pyqtSingnal()函数可以把信号定义为类的属性。

img

  1. QObject对象创建信号
    使用pyqtSignal()函数创建一个或多个重载的未绑定的信号作为类的属性,信号只能在QObject的子类中定义。
class Foo(QObject):
    valueChanged = pyqtSingal([dict], [list])

信号必须在类创建时定义,不能在类创建后作为类的属性动态添加进来。types参数表示定义信号时参数的类型,name参数表示信号名字,该项缺省时使用类的属性名字。

  1. 为控件创建信号
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QApplication ,QMainWindow


class MyMainWindow(QMainWindow):
    btnClickSingal=pyqtSignal() # 创建一个btnClickSingal信号
  1. 操作信号

使用connect()函数可以把信号绑定到槽函数上。

img

使用disconnect()函数可以解除信号与槽函数的绑定。

img

使用emit()函数可以发射信号。

img

信号与槽的入门应用

信号与槽有三种使用方法。第一种内置信号与槽使用,第二种自定义信号与槽的使用,第三种是装饰器的信号与槽的使用。

  1. 内置信号与槽的使用

内置信号与槽的使用,就是使用窗口控件的函数,而不是使用自定义的函数。通过QObject.signal.connect将一个QObject的信号连接到另一个QObject的槽函数。

import sys
from PyQt5.QtWidgets import QApplication ,QWidget, QMessageBox, QPushButton


if __name__=="__main__":
    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_())

点击按钮,弹出提示框。

img

  1. 自定义信号和槽函数的使用

所谓自定义信号与槽的使用,是指在发射信号时,不使用窗口控件的函数,而是使用自定义的函数。自定义信号与槽使用灵活,可以自定义传递多种数据类型实现不同的功能需求。

import sys
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QApplication ,QWidget, QMessageBox, QPushButton

# 信号对象
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()
    

运行结果:

----把信号绑定到槽函数----
QSlot get msg => hello pyqt5
----把信号和槽函数断开----

这里成功将hello pyqt5从信号传递到槽中,上面实现了一个字符串参数的传递,如果要实现多个参数呢?

import sys
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QApplication ,QWidget, QMessageBox, QPushButton

# 信号对象
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(f'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()

运行结果:

----把信号绑定到槽函数----
QSlot get msg => 第一个参数 + 第二个参数
----把信号和槽函数断开----

高级自定义信号与槽

自定义流程如下:

(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_String(self, x, szy):
              '''带两个参数(整数,字符串)槽函数'''
              pass

3. 连接信号与槽函数

通过connect方法连接信号与槽函数或者可调用对象。

    app = QApplication(sys.argv)
    widget = MyWidget()
    # 连接无参数信号
    widget.Signal_NoParameters.connect(widget.setValue_NoParameters)
    
    # 连接一个整数参数信号
    widget.Signal_OneParameter.connect(widget.setValue_OneParameter)

    # 连接两个参数信号
    widget.Signal_TwoParameters.connect(widget.setValue_TwoParameters_String)

    widget.show()

4. 发射信号

通过emit方法发射信号

信号与槽高级用法

使用自定义参数

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

button1.clicked.connect(show_page)

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

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

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

import sys
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QMainWindow, QApplication ,QWidget, QMessageBox, QPushButton,QHBoxLayout

class WinForm(QMainWindow):
       def __init__(self, parent=None):
           super(WinForm, self).__init__(parent)
           button1 = QPushButton("按钮1")
           button2 = QPushButton("按钮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(f'按钮{n}被按下了')
              QMessageBox.information(self, "信息提示框", f"按钮{n}被按下了")
           

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

运行效果:

img

装饰器信号与槽

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

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

这种方法有效的前提是下面的函数已经执行, 这行代码用来将QObject中的子孙对象的某些信号按照其objectName连接到相应的槽函数。:

QMetaObject.connectSlotsByName(QObject)

在上面代码中,“发送者对象名称”就是使用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()
    app.exec_()

img

信号与槽的断开与连接

有时候基于某些原因,想要临时或永久断开某个信号与槽的连接。

from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton

import sys

class SignalClass(QtCore.QObject):
    # 声明无参数的信号
    signal1 = QtCore.pyqtSignal()
    
    # 声明一个int类型参数的信号
    signal2 = QtCore.pyqtSignal(int)
    
    def __init__(self, parent=None):
        super(SignalClass, self).__init__(parent)
        
        # 将信号signal连接到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连接到同一个槽函数signalCall
        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__":
    SignalClass()

运行结果如下:

(qtvenv) D:\Github\PyProjects\pyqt5>python singnalSlot02.py
signal-1 emit
signal-2 emit
signal-1 emit
signal-2 emit
signal-1 emit
signal-1 emit

多线程中信号与槽的使用

from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton

import sys

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

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

运行结果如下:

(qtvenv) D:\Github\PyProjects\pyqt5>python singnalSlot03.py
thread1==>6
thread1==>5
thread1==>4
thread1==>3
thread1==>2
thread1==>1

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

from PyQt5.QtCore import QThread, pyqtSignal, QDateTime
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton, QLineEdit, QDialog

import sys
import time

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__":
    app = QApplication(sys.argv)
    main = Window()
    main.show()
    sys.exit(app.exec_())

img

posted @ 2022-08-07 00:24  Apostle浩  阅读(1940)  评论(0编辑  收藏  举报