PyQt4訊號與槽詳解

我是傳奇哈哈發表於2019-03-01

GUI 的程式開發人員並非需要甚至根本不需要知道所有的控制元件實現的底層細節,我們只需要知道當按鈕按下時能夠適當的相應即可。基於這一原因,Qt 和 pyqt 提供了兩種通訊機制:低階事件處理機制和高階事件處理機制,前者與其他 GUI 庫提供的功能類似,或者被稱之為 “訊號與槽”。

QT 的一個關鍵特性是它使用訊號和槽來進行物件之間的通訊。當一個元件發出一個訊號時,一個可用的插槽應應該做出相應。如果一個訊號連線到一個插槽,那麼當訊號被髮射時,該插槽應該被呼叫。如果訊號沒有連線,則不會發生任何事情。

訊號與槽機制的特點

  • 訊號可能連線到很多插槽
  • 訊號也可能連線到另外一個訊號
  • 訊號引數可以是任何 python 型別
  • 一個插槽可能連線到許多訊號
  • 連線可以是直接的 (同步) 也可是是排隊的 (非同步)
  • 連線可以跨執行緒
  • 連線可能中斷

繫結和未繫結訊號

訊號(特別是非繫結訊號)是作為 QObject 子類的類的屬性。一個繫結訊號具有connect(),disconnect()和emit()實現相關聯的功能的方法。要擷取一個訊號必須把它連線到槽上。

連線訊號與槽的語法形式

QtCore.QObject.connect(a, QtCore.SIGNAL(`QtSig()`), pyFunction)
QtCore.QObject.connect(a, QtCore.SIGNAL(`QtSig()`), pyClass.pyMethod)
QtCore.QObject.connect(a, QtCore.SIGNAL(`QtSig()`), b, QtCore.SLOT(`QtSlot()`))
QtCore.QObject.connect(a, QtCore.SIGNAL(`PySig()`), b, QtCore.SLOT(`QtSlot()`))
QtCore.QObject.connect(a, QtCore.SIGNAL(`PySig`), pyFunction)
複製程式碼

以上語法可以理解為:連線 a 物件的訊號到槽函式或者某個 b 物件的某個函式。

發射訊號與槽的語法

a.emit(QtCore.SIGNAL(`clicked()`))
a.emit(QtCore.SIGNAL(`pySig`), "Hello", "World")
複製程式碼

發射一個訊號,也可以帶引數。

內建的訊號與槽

大部分的視窗控制元件都提前預置了一些槽,所以很多時候可以直接把預置的訊號和預置的槽相連線,無需做任何事情就可以得到想要的行為效果。

下面我們看兩個簡單的視窗部件 Dial 和 SpinBox。這兩個視窗部件都有 valueChange() 訊號,當這個訊號觸發時就會帶有新值。這兩個視窗部件也都有 setValue() 槽,帶有整數型引數。因此可以將兩個部件的訊號和槽連線起來,無論使用者改變哪一個視窗部件,都會讓另一個值做出改變。

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

# Form implementation generated from reading ui file `D:pyqtSingAndSloftSingSloftDialog.ui`
#
# Created by: PyQt4 UI code generator 4.11.4
#
# WARNING! All changes made in this file will be lost!

from PyQt4 import QtCore, QtGui

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    def _fromUtf8(s):
        return s

try:
    _encoding = QtGui.QApplication.UnicodeUTF8
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
    def _translate(context, text, disambig):
        return QtGui.QApplication.translate(context, text, disambig)

class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName(_fromUtf8("Dialog"))
        Dialog.resize(400, 300)
        Dialog.setSizeGripEnabled(True)
        self.dial = QtGui.QDial(Dialog)
        self.dial.setGeometry(QtCore.QRect(60, 100, 50, 64))
        self.dial.setObjectName(_fromUtf8("dial"))
        self.spinBox = QtGui.QSpinBox(Dialog)
        self.spinBox.setGeometry(QtCore.QRect(190, 120, 54, 25))
        self.spinBox.setObjectName(_fromUtf8("spinBox"))

        self.retranslateUi(Dialog)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        Dialog.setWindowTitle(_translate("Dialog", "Dialog", None))


if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    Dialog = QtGui.QDialog()
    ui = Ui_Dialog()
    ui.setupUi(Dialog)
    Dialog.show()
    sys.exit(app.exec_())


複製程式碼

繼承上面的視窗來編寫我們的訊號與槽:

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

"""
Module implementing Dialog.
"""
from PyQt4 import QtGui
from PyQt4.QtGui import *
from PyQt4.QtCore import *

from Ui_SingSloftDialog import Ui_Dialog

class Dialog(QDialog, Ui_Dialog):
    """
    Class documentation goes here.
    """
    def __init__(self, parent=None):
        """
        Constructor

        @param parent reference to the parent widget
        @type QWidget
        """
        QDialog.__init__(self, parent)

        self.setupUi(self)
        # 連線訊號與槽
        self.connect(self.dial, SIGNAL(`valueChanged(int)`), self.spinBox.setValue)
        self.connect(self.spinBox, SIGNAL(`valueChanged(int)`), self.dial.setValue)

if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    ui = Dialog()
    ui.show()
    sys.exit(app.exec_())
複製程式碼

執行結果:

PyQt4訊號與槽詳解

如果使用者拖動撥號盤為 20,此時撥號盤就會發射一個 valueChange(20) 的訊號,相應的就會對輸入框的 setValue() 槽進行調整,並將 20 作為引數傳遞進去。相反輸入框的值發生改變撥號盤的槽函式也會觸發。貌似會發生死迴圈,其實不用擔心,如果傳遞額值並未發生改變 valueChange() 就不會再次發射訊號。

第二種寫法

在上邊的撥號盤例子中我們用的是 instance.metnodName() 的語法。但是當槽函式實際上是個 Qt 槽而不是 Python 方法時,用 SLOT() 語法可能會更高效。

self.connect(dial,SIGNAL("valueChanged(int)"),spinbox,SLOT("setValue(int)"))
self.connect(spinbox,SIGNAL("valueChanged(int)"),dial,SLOT("setValue(int)"))
複製程式碼

使用 QObject.connect() 可以建立各類連線,也可以使用 QObject.disconnect() 來取消這些連線。實際應用中我們並不需要自己去取消這些連線,這是因為,當刪除一個物件後,PyQt 就會自動斷開改物件的所有連線。

發射自定義訊號的元件

我們已經知道如何連線訊號與槽函式,這些槽就是一些常規的函式或者方法。但是如果我們想建立一個可以發射自定義訊號的元件該怎麼辦呢?使用 QObject.emit() 就可以輕鬆的實現這一點。

class ZeroSpinBox(QSpinBox):
    zeros =0
    def __init__(self, parent=None):
        super(ZeroSpinBox, self).__init__(parent)
        # 首先是把 valueChange 訊號連線到 checkzeor() 函式
        self.connect(self, SIGNAL(`valueChanged(int)`),self.checkzeor )
    def checkzeor(self):
        if self.value() ==0:
            self.zeros += 1
            # 發射一個名為 amout 的訊號,並且帶上引數
            self.emit(SIGNAL(`amount`), self.zeros)
複製程式碼

繼續修改剛才的例子:

class Dialog(QDialog, Ui_Dialog):
    """
    Class documentation goes here.
    """
    def __init__(self, parent=None):
        """
        Constructor

        @param parent reference to the parent widget
        @type QWidget
        """
        QDialog.__init__(self, parent)
        # 初始化剛剛自定義的控制元件
        zerospinbox = ZeroSpinBox()
        layout = QHBoxLayout()
        layout.addWidget(zerospinbox)
        self.setLayout(layout)
        self.setupUi(self)
        self.connect(self.dial, SIGNAL(`valueChanged(int)`), self.spinBox.setValue)
        self.connect(self.spinBox, SIGNAL(`valueChanged(int)`), self.dial.setValue)
        # 把控制元件裡邊定義的 amount 訊號繫結到 announce 函式
        self.connect(zerospinbox, SIGNAL(`amount`), self.announce)

    def announce(self, zeros):
        print `等於0的次數` + str(zeros)


if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    ui = Dialog()
    ui.show()
    sys.exit(app.exec_())
複製程式碼

執行結果:

PyQt4訊號與槽詳解

不同訊號連線到同一個槽上

我們之前的例子把不同的訊號連線到同一個槽上,也不關心是誰發射了這個訊號。有時候我們需要將兩個或者更多的訊號連線到同一個槽上,並需要根據連線的不同訊號做出不同的反應。

如圖我們有 5 個按鈕和 1 個標籤:

PyQt4訊號與槽詳解

定義不同的槽

先說最簡單的連線,這個連線用在 button1 中。下面是 button1 的連線方式:

self.connect(button1, SIGNAL("clicked()"), self.one)
複製程式碼

我們定義一個 one() 方法去改變標籤的值:

def one(self):
        self.label.setText("You clicked button `One`")
複製程式碼

使用 Python2.5 之後的高階函式

高階函式除了可以接受函式作為引數外,還可以把函式作為結果值返回。

def partial(func, arg):
        def callme():
            return func(arg)
        return callme
複製程式碼

使用這個高階函式作為槽函式:

self.button2callback = partial(self.anyButton, "Two")
self.connect(button2, SIGNAL("clicked()"),self.button2callback)
複製程式碼

在我們的定義函式裡邊改變標籤的值:

def anyButton(self, who):
        self.label.setText("You clicked button `%s`" % who)
複製程式碼

連線到同一個槽函式

如果將不同的槽連線到同一個槽函式我們使用 self.sender() 來發現訊號是來自哪個 QObject 物件。

繫結訊號:

self.connect(button4, SIGNAL("clicked()"), self.clicked)
self.connect(button5, SIGNAL("clicked()"), self.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())
複製程式碼

總結

PyQt 訊號與槽的機制需要好好的理解,更重要的是需要大量的實踐才能理解。

關注微信公眾號,獲取更多即時資訊

PyQt4訊號與槽詳解

相關文章