PyQt4信号和槽

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PyQt4信号和槽相关的知识,希望对你有一定的参考价值。

每个GUI库都提供了事件发生的不少细节,如鼠标点击、键盘按键等。例如,如果用户点击了一个写有Click Me的按钮后,按钮所附带的信息就会变成可用。GUI库可以告知我们 鼠标点击时与按钮的相对坐标,与按钮相应的父窗口部件,还有与屏幕相关的信息;GUI库还 会给出ShiftCtrlAlt以及NumLock键在当时的状态;也会给出按下松开按钮的精确时间等。如果用户通过非鼠标点击的其他方式按下按钮,也应该能够提供相类似的信息。用户也有可能通过多次使用Tab键来把光标移动到按钮上,之后再按下空格,或者是用Alt +C等快捷键。尽管所有这些不同方法的结果一样,但按下按钮时这些方法中的每一种都会产生不同的事件和不同的信息。

Qt库曾经是最早认识到几乎在每一种情况下,程序开发人员并非需要甚至根本就不需要知道所有这些底层细节:他们并不关心按钮是如何按下的,他们只是想知道在按钮按下时能够 适当响应即可。基于这一原因,QtPyQt提供了两种通信机制:低级事件处理机制(low-level event-handling mechanism)和高级机制(high-level mechanism),前者与所有其他GUI库提供的功能类似,后者被奇趣科技(Qt的提出者)称之为“信号和槽”。

每个QObject包括PyQt的全部窗口部件,因为它们都派生自QWidget,这也是QObject的一个子类——都会支持信号和槽机制。特别是,它们都有声明状态转换的能力,比如当选中或者不选中复选框(checkbox)时,或者是其他重要事件发生的时候,例如按钮按下 (无论是哪种按下方式)。PyQt的所有窗口部件都有一系列的预定义信号。无论信号何时发射,默认情况下,PyQt都只是简单地将其扔掉!要截取一个信号,就必须 将它连接到槽上去。在C++/Qt中,槽是必须用特殊语法声明的一些方法,不过在PyQt中,则可以是我们想调用的任何东西(比如,任意的函数或者方法),并且在定义的时候也不需要特殊的语法声明。

大部分的窗口部件也都提前预置了一些槽,所以有些时候可以直接把预置的信号与预置的槽相连接,无须做任何事就可以得到想要的行为效果。从这个方面来看,PyQt要比C++/Qt更为灵便些,因为在PyQt中可以连接的不仅只有槽,还可以是任何可被调用的对象,且从PyQt 4.2以后,还可以向QObject中动态添加一些预定义的信号和槽。

无论是QDial还是QSpinBox窗口部件都有valueChanged()信号,当这个信号触发时,就会带着新值。这两个窗口部件也都有setValue()槽,带有整数型参数值。因此,可以将这两个窗口部件的这两个信号和槽相互连接起来,无论用户改变了哪一个窗口部件,都会让另一个值做出相应的响应。

class Form(QDialog):

    def __init__(self, parent=None):
        super(Form, self).__init__(parent)

        dial = QDial()
        dial.setNotchesVisible(True)
        spinbox = QSpinBox()

        layout = QHBoxLayout()
        layout.addWidget(dial)
        layout.addWidget(spinbox)
        self.setLayout(layout)

        self.connect(dial, SIGNAL("valueChanged(int)"), spinbox.setValue)
        self.connect(spinbox, SIGNAL("valueChanged(int)"), dial.setValue)
        self.setWindowTitle("Signals and Slots")

由于这两个窗口部件是以这种方式连接的,所以如果用户拖动了拨号盘,比如说拨动值为20,此时拨号盘就会发射一个valueChanged(20)这样的信号,相应地,就会对微调框的setValue槽进行调用,并将20作为其参数。不过,在此之后,由于微调框的值被改变了, 所以它也会发射一个valueChanged(20)信号,相应地,拨号盘的setValue槽也会将20 作为参数接受。这样看起来貌似会陷人一个无限的死循环。不过,如果传递的值并未真正改 变,valueChanged就不会再次发射信号。这是因为,编写值变化型槽代码的标准方法就 是在执行该方法之前会先检测目标值于当前值之间是否真的存在差异。如果值一直是相同的, 那么就什么也不做直接返回;否则,才会执行变化,发射一个声明状态改变的信号。

技术分享图片

现在来看一下连接信号和槽的一般语法形式。假设PyQt的各个模块已导人使用有from. . .import *的语法,并假定sW都是QObject对象,一般情况下,这些对象都是窗口部件,通常会用self来代表s。

s.connect(w,SIGNAL("signalSignature"),functionName)
s.connect(w,SIGNAL("signalSignature"),instance.methodName)
s.connect(w,SIGNAL("signalSignature"),
        instance,SLOT("slotSignature"))

这里的signaJSignature是信号的名字,也是一个(也有可能是空的)用逗号分隔的参数类型名的列表。如果该信号是一个Qt信号,那么这些类型名必须是C++型类型,比如intQString等。C++型类型名可能会相当复杂,对于每一个类型名都可能会含有一个或者多个const&的关键字字符。在将这些字符作为信号(槽)名时,可以省略这些const型和 &型的参数,但必须保留型的参数。例如,几乎每一个传递QString参数的Qt信号,都会 用到一个const QString &型参数,不过在PyQt中,仅是使用一个QString就足够了。另一方面,QListWidget有个itemActivated(QListWidgetltem * )型信号,在编写代码时则必须准确。

PyQt的各个信号在真正发射时就要定义过,可以带有任意数量和任意类型的参数,这一点稍后就可以看到。

siotSignaturesignaiSignature拥有相同的形式,只是前者的名字是一个Qt槽。 一个槽所拥有的参数数量可能并不比与其相连信号的参数数量多,实际上可能会更少;对于那些多余的参数则会被忽略掉。相互对应的信号和槽则必须具有相同的参数类型,因此可以举一个例子,就不能把QDialvalueChanged(int)信号连接到QLineEditsetText(QString)槽上去。

在这个拨号盘和微调框的例子里,在本章之前给出的例子中用到过instance.methodName的语法。但当该槽实际是个Qt槽而不是Python方法时,用SL0T()语法可能就会更高效些

self.connect(dial, SIGNAL("valueChanged(int)"), spinbox.setValue)
self.connect(spinbox, SIGNAL("valueChanged(int)"), dial.setValue)

正如之前早就看到的那样,一个槽可以被多个信号连接。因而也有可能让一个信号与多个槽 相连接。虽然这种情况比较少见,但却有可能会将一个信号与另一个信号相连接:在这些情况 下,当第一个信号发射时,就会造成与之相连的信号也发射信号。

使用QObject.connect()可以建立各类连接,也可以用QObject.disconnect()来取 消这些连接。在实际应用中,很少需要我们自己去取消连接,这是因为,比方说,当删除一个对象后,PyQt就会自动断开与该对象相关联的所有连接。

迄今为止,我们已经看到了如何连接信号和写槽函数,这些槽就是一些常规的函数或者方法。也知道在说明状态转换或者其他重要事件发生时会发射信号。但如果想创建一个可以发射自定义信号的组件,该怎么办呢?使用QObject.emit()就可以轻松实现这一点。例如,以下就是一个完整的QSpinBox子类,它会发射自定义的atzero信号,还会传递一个数字:

class ZeroSpinBox(QSpinBox):

    zeros = 0

    def __init__(self, parent=None):
        super(ZeroSpinBox, self).__init__(parent)
        self.connect(self, SIGNAL("valueChanged(int)"), self.checkzero)

    def checkzero(self):
        if self.value() == 0:
            self.zeros += 1
            self.emit(SIGNAL("atzero"), self.zeros)

连接微调框自己的valueChanged()信号,使其调用这里的checkzero()槽。如果value的值刚好为0checkzero()槽就会发射atzero信号,这样就可以计算出一共有多少次 0; 像这样来传递一些额外的数据并非都是强制性的。对于信号来说,缺失圆括号非常重要:这会告诉PyQt,这是一个“短路”信号。

无参数的信号(不带圆括号的信号)是一个短路Python的信号(short-circuit Python signal)。 在这种信号发射的时候,可以向emit()方法传递任何额外参数,且这些参数会作为Python对象来进行传递。这样就可以避免对参数进行C++类型相互转换时所带来的风险,但也就意味着可以传递任意Python对象,即使这些Python对象无法在C++数据类型之间相互转换也可以传递。带有至少一个参数的信号就要么是Qt信号,要么是Python非短路信号(no-short-circuit Python signal)。在这些情况下,PyQt会检查该信号是否是Qt信号,如果不是,就会假定是Python信号。无论何种情况,这些参数都会被转换成C++数据类型。

下面就来看看该如何与表单方法中的信号相连接:

zerospinbox = ZeroSpinBox()
...
self.connect(zerospinbox, SIGNAL(“atzero”), self.announce)

再说一遍,务必不要带圆括号,因为这是一个短路信号。基于完整性考虑,以下就是与表单相连接的槽:

def announce(self, zeros):
    print "ZeroSpinBox has been at zero %d times" % zeros

如果在SIGNAL()方法中使用了不带圆括号的标识符,那么就意味着给定的是一个如前所述 的短路信号。使用这种语法既可以用来发射短路信号,也可以用来向其提供连接。例中这两种方法都用到了。

如果用带一个signalSignature()可能是一个不带括号的、逗号分隔的PyQt类型列表) 的SIGNAL()函数,可以给定一个Python信号或者Qt信号(Python信号会以Python代码的形式发射;Qt信号会以隐式C++对象的形式发射)。使用这种语法就可以发射Python或者Qt信号了,也可以连接它们。这些信号可以与任何可调用的函数或者方法相连接,也可以与槽相连接;也可以使用带有slotSignatrure参数的SLOT()语法对其予以连接。PyQt会查看该 信号是否是Qt信号,如果不是,它会认为该信号是个Python信号。如果使用带有括号的指示符,即使是对Python信号,这些参数也会被转换成C++数据类型。

现在看另外一个例子,一个很小的非GUI自定义类,有一个信号和一个槽,这里所探讨的机制并不仅限于GUI类,任何QObejet子类均可以使用这些信号和槽。

class TaxRate(QObject):

    def __init__(self):
        super(TaxRate, self).__init__()
        self.__rate = 17.5

    def rate(self):
        return self.__rate

    def setRate(self, rate):
        if rate != self.__rate:
            self.__rate = rate
            self.emit(SIGNAL("rateChanged"), self.__rate)

无论是rate()方法还是setRate()方法,都可以被连接,因为任何Python可调用对象都可以用做槽。如果汇率变动,就可以更新私有的_rate的值,然后发射自定义的rateChanged信号,以便能够把新汇率当成参数。也可以使用更为快速的短路语法。如果打算使用标准语法,那么唯一的区别可能就是信号会被写成SIGNAL( "rateChanged (float)")。如果将rateChanged信号与setRate()槽连接,因为有if语句,也就不会发生死循环的情况。回 头看一下正在使用的这个类。首先,定义了一个可在汇率发生变动时调用的方法:

def rateChanged(value):
    print("TaxRate changed to {0:.2f}%".format(value))

现在,可以对其测试一下:

vat = TaxRate()
    vat.connect(vat, SIGNAL("rateChanged"), rateChanged)
    vat.setRate(17.5)    # No change will occur (new rate is the same)
    vat.setRate(8.5)     # A change will occur (new rate is different)

这会导致在控制台里仅输出一行文字:“TaxRate changed to 8.50%”

在之前的各个例子里,曾经将不同的信号连接在同一个槽上,也并不关心是谁发射了信 号。不过有的时候,打算将两个或者更多的信号连接到同一个槽上,并需要根据连接的不同信号做出不同的反应。

下图中给出的连接程序Connections有5个按钮和一个标签。当按下任意一个按钮时, 就会用信号和槽机制来更新标签上的文本。这里给出的是用表单中的_init_()方法创建第一个按钮的代码:

button1 = QPushButton("One")

除了变量的名字和所需传送文本的不同之外,所有其他按钮的创建方式与此类似。

技术分享图片

就先从最简单的连接开始,这个连接是用在buttonl中的。以下给出的是_init_()方法的connect()调用方式:

self.connect(button1, SIGNAL("clicked()"), self.one)

对于这个按钮,使用了一种特殊的方法:

def one(self):
        self.label.setText("You clicked button 'One'")

将按钮的clicked()信号连接到单一的方法上,以便能够适当地响应该信号,这种做法可能 是最常用的一种连接模式。

不过,既然大部分的处理过程都一样,那是否就意味着只需用一些参数就能够按下某个特定的按钮呢?在这些情况下,每个按钮最好都能连接到同一个槽上。实现这一做法可以用两 种方法。第一种方法是用偏函数应用程序(partial function application)来封装带一个参数的槽以便在调用参数按钮时能够调用该槽。另外一种做法是通过PyQt来了解是哪个按钮调用了该槽。这里会全部给出这两种方法,先从偏函数应用程序方法开始。

我们曾使用Python 2.5functools.partial()函数或者是用自己简单的partial()函数创建过一个封装函数:

import sys
if sys.version_info[:2] < (2,5):
    def partial(func, arg):
        def callme():
            return func(arg)
        return callme
else:
    from functools import partial

使用partial()可以把一个槽和一个按钮名字封装到一起。所以可以试着这样做:

self.connect(button2, SIGNAL("clicked()"),
            partial(self.anyButton, "Two")) # WRONG for PyQt 4.0-4.2

遗憾的是,在PyQt4.3之前的版本中尚无法实现这一点。该封装函数是在connect()调用中创建的,不过在connect()—调用完,就会超出作用范围(go out of scope)并作为垃圾回收掉。从PyQt4.3之后,如果连接时再像这样使用functools.partial()封装函数,那么就会对它们进行特殊处理。这就意味着连接的函数就不会被垃圾回收,因而之前给出的代码就可以正确执行了。

对于PyQt4.04.14.2这几个版本,依然可以使用partial():只需保持一个对封装 函数的引用即可,除非在是调用connect(),否则不会使用这个引用,但实际上表单(form) 实例的属性会确保在其存续期间该引用函数不会超出作用范围,从而也就可以正常工作。因此,该连接看起来可能会是这样的:

self.button2callback = partial(self.anyButton, "Two") 
self.connect(button2, SIGNAL("clicked()"), self.button2callback)

当点击button2后,就会调用anyButton()方法,会带一个参数,其中含有文本字符串 “Two”。该方法的代码看起来是这样的:

def anyButton(self, who):
    self.label.setText("You clicked button *%s'" % who)

本来也可以把这个槽像之前所给出的partial()函数那样应用到所有的按钮上。但事实上,一点不用partial函数也可以得到完全相同的结果:

self.button3callback = lambda who = "Three": self.anyButton(who) 
self.connect(buttons, SIGNAL("clicked()"), self.button3callback)

这里会创建一个lambda函数,用按钮的名字来作为其参数。该函数也可以像partial函数那样相同工作,调用同样的anyButton()方法,仅用lambda表达式来创建封装函数。无论是button2callback()还是button3callback都会调用 anyButton()方法;两者的唯一区别是它们的参数不同,一个参数传递“Two”,另一个参数传递“Three”。如果PyQt用的是4.1.1或者更高级的版本,就不必保留对自己的引用。这是因为在连接中使用lambda表达式创建各个封装函数时,PyQt会做特殊处理(这与PyQt4.3中扩展functools.partial()的特殊处理方式一致)。基于这一原因,可以在各connect()调用中直接使用lambda表达式。例如:

self. connect (button3, SIGNAL (?? clicked ()"),
                lambda who="Three": self.anyButton(who))

封装技术可以非常不错地工作,不过这里还会再稍微引入另外一种方法,但确实在某些时候会非常有用,特别是当我们不想封装自己的方法时这种技术会用在button4和button5上。 这里给出的是它们的连接:

self.connect(button4, SIGNAL(Hclicked()"), self.clicked) 
self.connect(button5, SIGNAL("clicked()"), self.clicked)

需要注意的是,这里并没有对这两个按钮所连接的clicked()方法进行封装,所以第一眼看上去,好像无法区分到底是哪个按钮调用了clicked()方法。然而如果确实想要区分出来,只需仔细看下面的实现代码就可以了:

def clicked(self):
    button = self.sender()
    if button is None or not isinstance(button, QPushButton): return
    self.label.setText("You clicked button '%s' " % button.text())

在一个槽的内部总是可以通过调用sender()发现到底调用信号是来自哪个QObject对象 的(如果通过使用常规调用方法,调用的槽可能会返回None。尽管知道连接到这个槽上的仅 仅是些按钮,但仍需小心为好。我们曾用过isinstance(),但当时本可以使用hasattr(button, "text")来替代的。如果把所有的按钮都连接到这个槽上,应该也都是可以正确工作的。

有些编程人员不喜欢使用sender。因为觉得这不是面向对象的编程风格,所以在需要出现这种类似的情况时,他们会更愿意使用偏函数应用程序的方法。事实上,确实还有其他的封装技术也可获得对函数和参数的封装。这就是使用QSignal-Mapper类,会在第9章中给出一个使用该类的例子

在某些情况下,槽会执行一定的操作,操作结果是调用该槽的信号,无论这种循环调用是 直接调用还是间接调用,都会造成调用该槽的信号再次被重复调用,从而造成死循环。在实际 应用中,这种循环模式一般较为少见。可以用两种方法降低这种循环发生的可能性。第一种方法是,只在信号真正发生改变的时候才发射该信号。例如,如果用户改变了一个QSpinBox的值,或者程序通过调用setValue()改变其值,那么只有当新数值与当前现有数值不同时, 才会发射valueChanged()信号。第二种方法是,只有是用户的动作才会发射某些信号。例如,只有在用户更改了QLineEdit的文本时,它才会发射textEdited()信号,而如果是在代码中通过调用setText()更改了文本,则仍旧不会发射textEdited()信号。

如果看起来确实形成了一个信号-槽循环,通常情况下,要做的第一件事就是去检査代码的逻辑是否正确:是不是像我们想象的那样确实进行了处理?如果逻辑是正确的,也确实是出现了信号-槽循环,或许就需要通过修改那些与其连接的信号来打断这种循环链,例如,将那些程序发生改变就将发射的信号替换为用户出现交互后才发射的信号。如果问题依然存在, 可以在代码的某些特定位置用QObject.blockSignals()来阻止这些信号,所有的QWidget类都继承有QObject.blockSignals(),它会传递一个布尔值True,对象停止发射信号;False,对象恢复信号发射能力。

以上是关于PyQt4信号和槽的主要内容,如果未能解决你的问题,请参考以下文章

PyQt4信号与槽

PyQt4关闭窗口

初见QT---信号和槽

Qt信号和槽的问题

QT信号和槽

PyQt5——信号和槽初探