Python Qt GUI与数据可视化编程
上QQ阅读APP看书,第一时间看更新

2.4 自定义信号的使用

2.4.1 信号与槽的一些特点和功能

在PyQt5中,信号与槽的使用有如下一些特点。

· 一个信号可以关联多个槽函数。

· 一个信号也可以关联其他信号。

· 信号的参数可以是任何Python数据类型。

· 一个槽函数可以和多个信号关联。

· 关联可以是直接的(同步)或排队的(异步)。

· 可以在不同线程之间建立关联。

· 信号与槽也可以断开关联。

2.3节的示例使用的信号都是类的内建信号,在自定义类中还可以自定义信号。使用自定义信号在程序的对象之间传递信息是非常方便的,例如在多窗体应用程序中,通过信号与槽在窗体之间传递数据。

使用PyQt5.QtCore.pyqtSignal()为一个类定义新的信号。要自定义信号,类必须是QObject类的子类。pyqtSignal()的句法是:

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

信号可以带有参数types,后面的参数都是一些可选项,基本不使用。

信号需要定义为类属性,这样定义的信号是未绑定(unbound)信号。当创建类的实例后,PyQt5会自动将类的实例与信号绑定,这样就生成了绑定的(bound)信号。这与Python语言从类的函数生成绑定的方法的机制是一样的。

一个绑定的信号(也就是类的实例对象的信号)具有connect()、disconnect()和emit()这3个函数,分别用于关联槽函数、断开与槽函数的关联、发射信号。

2.4.2 自定义信号使用示例

下面是示例Demo2_4目录下的程序human.py的完整代码,这个程序演示了自定义信号的使用,以及信号与槽的使用中一些功能的实现方法。

        ## 自定义信号与槽的演示
        import sys
        from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal


        class Human(QObject):
            ##定义一个带str类型参数的信号
            nameChanged = pyqtSignal(str)
            ## overload型信号有两种参数,一种是int,另一种是str
            ageChanged = pyqtSignal([int], [str])


            def __init__(self, name='Mike', age=10, parent=None):
              super().__init__(parent)
              self.setAge(age)
              self.setName(name)


            def   setAge(self, age):
              self.__age= age
              self.ageChanged.emit(self.__age)    #发射int参数信号
              if age<=18:
                  ageInfo="你是 少年"
              elif (18< age <=35):
                  ageInfo="你是 年轻人"
              elif (35< age <=55):
                  ageInfo="你是 中年人"
              elif (55< age <=80):
                  ageInfo="您是 老人"
              else:
                  ageInfo="您是 寿星啊"
              self.ageChanged[str].emit(ageInfo)    #发射str参数信号


            def setName(self, name):
              self.__name = name
              self.nameChanged.emit(self.__name)


        class Responsor(QObject):
          @pyqtSlot(int)
          def do_ageChanged_int(self, age):
              print("你的年龄是:"+str(age))


          @pyqtSlot(str)
          def do_ageChanged_str(self, ageInfo):
              print(ageInfo)


        ##   @pyqtSlot(str)
          def do_nameChanged(self, name):
              print("Hello, "+name)


        if  __name__ == "__main__":    ##测试程序
          print("**创建对象时**")
          boy=Human("Boy",16)
          resp=Responsor()
          boy.nameChanged.connect(resp.do_nameChanged)


          ## overload的信号,两个槽函数不能同名,关联时需要给信号加参数区分
          boy.ageChanged.connect(resp.do_ageChanged_int)    #默认参数,int型
          boy.ageChanged[str].connect(resp.do_ageChanged_str)    #str型参数


          print("\n **建立关联后**")
          boy.setAge(35)        #发射两个ageChanged信号
          boy.setName("Jack")   #发射nameChanged信号


          boy.ageChanged[str].disconnect(resp.do_ageChanged_str)    #断开关联
          print("\n **断开ageChanged[str]的关联后**")
          boy.setAge(10)        #发射两个ageChanged信号

(1)信号的定义

定义的类Human是从QObject继承而来的,它定义了两个信号,两个信号都需要定义为类的属性。nameChanged信号是带有一个str类型参数的信号,定义为:

        nameChanged = pyqtSignal(str)

ageChanged信号是具有两种类型参数的overload型的信号,信号的参数类型可以是int,也可以是str。ageChanged信号定义为:

        ageChanged = pyqtSignal([int], [str])

(2)信号的发射

通过信号的emit()函数发射信号。在类的某个状态发生变化,需要通知外部发生了这种变化时,发射相应的信号。如果信号关联了一个槽函数,就会执行槽函数,如果信号没有关联槽函数,就不会产生任何动作。

例如在Human.setName()函数中,当变量self.__name发生变化时发射nameChanged信号,并且传递参数,即

        self.nameChanged.emit(self.__name)

变量self.__name作为信号的参数,关联的槽函数可以从参数中获得当前信号的名称,从而进行相应的处理。

Human.setAge()函数中发射了两次ageChanged信号,但是使用了不同的参数,分别是int型参数和str型参数,即

        self.ageChanged.emit(self.__age)      #int参数信号
        self.ageChanged[str].emit(ageInfo)    #str参数信号

(3)信号与槽的关联

另外定义的一个类Responsor也是从QObject继承而来的,它定义了三个函数,分别用于与Human类实例对象的信号建立关联。

因为信号ageChanged有两种参数类型,要与两种参数的ageChanged信号都建立关联,两个槽函数的名称必须不同,所以定义的两个槽函数名称分别是do_ageChanged_int和do_ageChanged_str。

需要在创建类的具体实例后再进行信号与槽的关联,所以,程序在测试部分先创建两个具体的对象。

        boy=Human("Boy",16)
        resp=Responsor()

如果一个信号的名称是唯一的,即不是overload型信号,那么关联时无须列出信号的参数,例如,nameChanged信号的连接为:

        boy.nameChanged.connect(resp.do_nameChanged)

对于overload型的信号,定义信号时的第一个位置的参数是默认参数。例如,ageChanged信号的定义是:

        ageChanged = pyqtSignal([int], [str])

所以,ageChanged信号的默认参数就是int型。默认参数的信号关联无须标明参数类型,所以有:

        boy.ageChanged.connect(resp.do_ageChanged_int)        #默认参数,int型

但是,对于另外一个非默认参数,必须在信号关联时在信号中注明参数,即

        boy.ageChanged[str].connect(resp.do_ageChanged_str)    #str型参数

(4)@pyqtSlot修饰符的作用

在PyQt5中,任何一个函数都可以作为槽函数,但有时也需要使用@pyqtSlot修饰符说明函数的参数类型,以使信号与槽之间能正确关联。

@pyqtSlot()修饰符用于声明槽函数的参数类型,例如在2.3节的示例中,为了使函数on_chkBoxItalic_clicked(self, checked)与窗体上chkBoxItalic复选框的clicked(bool)信号自动建立关联,就使用了@pyqtSlot(bool)进行修饰。

在本例Responsor类的3个槽函数前的@pyqtSlot()修饰符都可以被注释掉,不影响程序的运行结果。因为overload型信号的两个槽函数名称不同,在建立关联时也指定了参数类型。

(5)断开信号与槽的关联

在程序中可以使用disconnect()函数断开信号与槽的关联,例如,程序中用下面的代码断开了一个关联。

        boy.ageChanged[str].disconnect(resp.do_ageChanged_str)    #断开关联

运行程序human.py,在Python Shell中显示如下的运行结果:

        **创建对象时**


          **建立关联后**
        你的年龄是:35
        你是 年轻人
        Hello, Jack


          **断开ageChanged[str]的关联后**
        你的年龄是:10

从运行结果中可以看到:

· 创建对象时虽然也发射信号,但还未建立关联,所以无响应;

· 建立关联后,3个信号关联的槽函数都响应了;

· 断开关联后,断开关联的槽函数无响应了。

2.4.3 使用信号与槽的一些注意事项

通过这两节的示例讲解,信号与槽使用中涉及的一些用法基本都介绍了。信号与槽机制是非常好用的,特别是为GUI程序各对象之间的信息传递提供了很方便的处理方法,但是在PyQt5中使用信号与槽时也要注意以下问题。

(1)对于PyQt5中的类的内建overload型信号,一般只为其中一种信号编写槽函数。例如QCheckBox组件有clicked()和clicked(bool)两种信号,可以有针对性地只选择其中一种参数类型的信号编写槽函数。如果使用的overload型信号不是默认参数类型的信号,那么槽函数还需要使用@pyqtSlot()修饰符声明参数类型。

(2)在自定义信号时,尽量不要定义overload型信号。因为Python的某些类型转换为C++的类型时,对于C++来说可能是同一种类型的参数,例如,若定义一个overload型的信号:

        valueChanged = pyqtSignal([dict], [list])

dict和list在Python中是不同的数据类型,但是转换为C++后可能就是相同的数据类型了,这可能会出现问题。