zoukankan      html  css  js  c++  java
  • PyQt5:PyQt5 信号与槽(PyQt5的事件处理机制)

    一、事件

      在事件模型,有三个参与者:事件源、事件目标、事件对象。

    •   事件源:状态发生改变的对象,它产生事件 Source_Obj
    •   事件目标:是想要被通知的对象 Target_Obj
    •   事件对象:封装了事件源中的状态变化 Evnet_Obj

      PyQt5有一个独一无二的信号和槽机制来处理事件。信号和槽用于对象之间的通信。当指定事件发生,一个事件信号会被发射。槽可以被任何Python脚本调用。当和槽连接的信号被发射时,槽会被调用。调用示意图如图1所示:

    图1

     二、信号和槽(或槽函数)

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

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

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

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

    图2

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

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

    三、高级自定义信号与槽

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

      (1)定义信号。

      (2)定义槽函数。

      (3)连接信号与槽函数。

      (4)发射信号。

    (1)定义信号

      通过类成员变量定义信号对象。使用 pyqtSignal()方法

    import sys
    import PyQt5.QtWidgets as PQW
    import PyQt5.QtCore as PQC

    class MyWidget(PQW.QWidget):
    # 无参数的信号
    Signal_NoParameters = PQC.pyqtSignal()
    # 带一个参数(整数)的信号
    Signal_OneParameter = PQC.pyqtSignal(int)
    # 带一个参数(整数或者字符串)的重载版本的信号
    Signal_OneParameter_Overload = PQC.pyqtSignal([int],[str])
    # 带两个参数(整数,字符串)的信号
    Signal_TwoParameters = PQC.pyqtSignal(int,str)
    # 带两个参数([整数,整数]或者[整数,字符串])的重载版本的信号
    Signal_TwoParameters_Overload = PQC.pyqtSignal([int,int],[int,str])

    (2)定义槽函数

      定义一个槽函数,它有多个不同的输入参数。槽函数就是普通类中的函数或方法。

    class MyWidget(PQW.QWidget):  #接上例程序,同一个类MyWidget。
        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)实例

     1 from PyQt5.QtCore import QObject , pyqtSignal
     2 
     3 class CustSignal(QObject):
     4 
     5     #声明无参数的信号
     6     signal1 = pyqtSignal()
     7 
     8     #声明带一个int类型参数的信号
     9     signal2 = pyqtSignal(int)
    10 
    11     #声明带int和str类型参数的信号
    12     signal3 = pyqtSignal(int,str)
    13 
    14     #声明带一个列表类型参数的信号
    15     signal4 = pyqtSignal(list)
    16 
    17     #声明带一个字典类型参数的信号
    18     signal5 = pyqtSignal(dict)
    19 
    20     #声明一个多重载版本的信号,包括带int和str类型参数的信号和带str类型参数的信号
    21     signal6 = pyqtSignal([int,str], [str])
    22 
    23     def __init__(self,parent=None):
    24         super(CustSignal,self).__init__(parent)
    25 
    26         #将信号连接到指定槽函数
    27         self.signal1.connect(self.signalCall1)
    28         self.signal2.connect(self.signalCall2)
    29         self.signal3.connect(self.signalCall3)
    30         self.signal4.connect(self.signalCall4)
    31         self.signal5.connect(self.signalCall5)
    32         self.signal6[int,str].connect(self.signalCall6)
    33         self.signal6[str].connect(self.signalCall6OverLoad)
    34 
    35         #发射信号
    36         self.signal1.emit()
    37         self.signal2.emit(1)
    38         self.signal3.emit(1,"text")
    39         self.signal4.emit([1,2,3,4])
    40         self.signal5.emit({"name":"wangwu","age":"25"})
    41         self.signal6[int,str].emit(1,"text")
    42         self.signal6[str].emit("text")
    43 
    44     def signalCall1(self):
    45         print("signal1 emit")
    46 
    47     def signalCall2(self,val):
    48         print("signal2 emit,value:",val)
    49 
    50     def signalCall3(self,val,text):
    51         print("signal3 emit,value:",val,text)
    52 
    53     def signalCall4(self,val):
    54         print("signal4 emit,value:",val)
    55 
    56     def signalCall5(self,val):
    57         print("signal5 emit,value:",val)
    58 
    59     def signalCall6(self,val,text):
    60         print("signal6 emit,value:",val,text)
    61 
    62     def signalCall6OverLoad(self,val):
    63         print("signal6 overload emit,value:",val)
    64 
    65 if __name__ == '__main__':
    66     custSignal = CustSignal()
    View Code

    运行结果如下:

    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

     四、使用自定义参数

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

    button1.clicked.connect(show_page)

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

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

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

      有两种解决方法,其中一种解决方法是使用lambda表达式。其完整代码如下:

    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.setGeometry(300,300,600,400)
        form.show()
        sys.exit(app.exec_())

     运行效果如下:

    图3

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

      另一种解决方法是使用functools中的partial函数。实例代码如下:

    from PyQt5.QtWidgets import QMainWindow, QPushButton , QWidget , QMessageBox, QApplication, QHBoxLayout
    import sys
    from functools import partial

    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))
    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.setGeometry(300,300,600,400)
    form.show()
    sys.exit(app.exec_())

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

    五、装饰器信号与槽

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

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

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

    QtCore.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.setWindowTitle('装饰器信号和槽')
        win.setGeometry(300,300,600,400)
        win.show()
        app.exec_()

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

    图4

       有的读者可能注意到,我们一直没有解释下面这行代码的含义:QtCore.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按钮")

     实例如下:

     1 from PyQt5 import QtCore 
     2 from PyQt5.QtWidgets import QApplication ,QWidget ,QHBoxLayout , QPushButton
     3 import sys
     4 
     5 class CustWidget( QWidget ):
     6 
     7     def __init__(self, parent=None):
     8         super(CustWidget, self).__init__(parent)
     9 
    10         self.okButton = QPushButton("OK", self)
    11         #使用setObjectName设置对象名称
    12         self.okButton.setObjectName("okButton")
    13         layout =  QHBoxLayout()
    14         layout.addWidget(self.okButton)
    15         self.setLayout(layout)
    16         QtCore.QMetaObject.connectSlotsByName(self)
    17         self.okButton.clicked.connect(self.okButton_clicked)
    18 
    19     def okButton_clicked(self):
    20         print( "单击了OK按钮")
    21 
    22 if __name__ == "__main__":
    23     app =  QApplication(sys.argv)
    24     win = CustWidget()
    25     win.show()
    26     sys.exit(app.exec_())
    View Code

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

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

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

    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
    

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

    1、简单多线程中信号与槽的使用   

      最简单的多线程使用方法是利用QThread函数,如下代码展示了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

    2、多线程处理显示和逻辑运算分开

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

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

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

    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))   #通过sleep(1),每秒发射一个信号
                time.sleep(1)
    
    # class Window(QDialog):  #界面类,用于显示
    class Window(PQW.QWidget):
        def __init__(self):
            # QDialog.__init__(self)
            super().__init__()
            self.setWindowTitle('PyQt 5界面实时更新例子')
            self.resize(400, 100)
            self.input = QLineEdit(self)
            self.input.resize(400, 30)
            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

    参考博文:https://blog.csdn.net/broadview2006/article/details/78475842

  • 相关阅读:
    mysq 日期相减
    说说时间观与时间管理——北漂18年(71)
    ionic之切换开关
    ionic之单选框
    SELECT ... LOCK IN SHARE MODE和SELECT ... FOR UPDATE locks在RR模式下可以看到最新的记录
    14.5.2.3 Consistent Nonlocking Reads 一致性非锁定读
    14.5.2.2 autocommit, Commit, and Rollback
    14.5.2 事务隔离级别
    对于唯一索引使用唯一条件搜索, InnoDB 只锁定找到的index record,不是它之前的区间
    mysql explain 解释
  • 原文地址:https://www.cnblogs.com/chenhaiming/p/9930628.html
Copyright © 2011-2022 走看看