信號和槽(Signals and Slots)
Qt庫第一個認識到在幾乎所有情況下,程序員不需要或甚至不想知道所有的底層細節:他們不在乎按鈕是如何按下,他們只是想知道它是按下的以便他們能夠適當地做出反應。因此,Qt和PyQt提供了兩種通信機制:底層事件處理機制,類似于所有其他GUI庫提供的機制,以及Trolltech(Qt的制造商)所稱的高級機制 “信號和槽”。
??每個QObject,包括所有的PyQt的小部件,因為它們從QWidget(一個QObject的子類)派生,都支持信號和槽機制。特別地,它們能夠通知狀態的改變,例如當復選框變為選中或未選中時,以及其他重要的事件,例如當點擊按鈕(通過任何方式)時。 所有的PyQt的小部件有一組預定義的信號。
??每當一個信號發射,默認情況下PyQt只是拋出它!要注意到信號,我們就必須將其連接到槽。在C ++ / Qt中,槽是必須用特殊語法聲明的方法; 但在PyQt中,它們可以是任何我們喜歡的可調用的對象(例如,任何函數或方法),并且在定義它們時不需要特殊的語法。
??大多數小部件也有預定義的槽,因此在某些情況下,我們可以將預定義的信號連接到預定義的槽,而不必執行任何其他操作來獲得我們想要的行為。PyQt在這方面比C ++ / Qt更通用,因為我們不僅可以連接到槽,而且可以連接到任何可調用,并且從PyQt4.2開始,可以動態添加“預定義”信號和插槽到QObject中。讓我們來看看信號和槽在實踐中如何適用于如圖所示4.6的信號和槽程序。
QDial和QSpinBox小部件都有valueChanged()信號,當發出時,它們攜帶新值。 它們都有setValue()槽,它們取整數值。 因此,我們可以將這兩個小部件彼此連接,使得無論用戶改變什么,都將導致另一個相應地改變:
#!/usr/bin/python
#-*- coding: utf8-*-
'''
Created on 2016年12月10日
@author: shiyi
'''
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
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")
app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()
由于兩個小部件以這種方式連接,如果用戶把Dial移動到20,Dial將發出一個valueChanged(20)的信號,這將反過來導致調用Spinbox的setValue()槽,以20作為參數。 但是,因為它的值現在已經改變,Spinbox將發出一個valueChanged(20)信號,這反過來將調用Dial的setValue()插槽,以20作為參數。所以看起來我們會得到一個無限循環。但是實際上如果值沒有實際改變,valueChanged()信號不會被發射。這是因為寫入值改變槽的標準方法是通過將新值與現有值進行比較開始的。如果值相同,我們什么都不做,并返回; 否則,我們應用改變并發出信號以宣布狀態的改變。連接如圖4.7所示。
現在讓我們看看連接的一般語法。 我們假設PyQt模塊已經使用**from ... import * **語法導入,并且s和w是QObjects,通常是小部件,s通常是self:
s.connect(w, SIGNAL("signalSignature"), functionName)
s.connect(w, SIGNAL("signalSignature"), instance.methodName)
s.connect(w, SIGNAL("signalSignature"),instance, SLOT("slotSignature"))
signalSignature是信號的名稱,以及括號中的逗號分隔的參數類型名稱列表(可能為空)。 如果信號是一個Qt信號,類型名稱必須是C++類型名稱,如int和QString。 C++類型名稱可能相當復雜,每個類型名稱可能包括一個或多個const,*和&。當我們將它們寫為信號(或槽)簽名時,我們可以丟棄任何consts和&s,但必須保留任何*s。例如,通過QString的幾乎每個Qt信號都使用const QString的參數類型,但是在PyQt中,僅僅使用QString就足夠了。 另一方面,QListWidget具有帶有簽名itemActivated(QListWidgetItem *)的信號,并且我們必須寫入與這個完全一樣的信號。
??PyQt信號在它們被實際發射時被定義,并且可以具有任何數量的任何類型的參數,如我們將很快看到的。
??slotSignature與signalSignature具有相同的形式,除了名稱是Qt時隙。 插槽可以不具有比連接到其的信號更多的自變量,但是可以具有更少的自變量; 那么將丟棄額外的參數。相應的信號和槽參數必須具有相同的類型,例如,我們無法將QDial的valueChanged(int)信號連接到QLineEdit的setText(QString)槽。
??在我們的dial和spinbox示例中,我們使用instance.methodName語法,就像我們對本章前面所示的示例應用程序一樣。 但是當槽實際上是一個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()。 例如,這里是一個完整的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)
我們連接到Spinbox自己的valueChanged()信號,并將其調用我們的checkzero()插槽。 如果值恰好為0,則checkzero()槽發出零值信號,以及它已經為零的次數的計數; 傳遞這樣的附加數據是可選的。 缺少信號的括號很重要:它告訴PyQt這是一個“短路”信號。
??沒有參數(因此沒有括號)的信號是短路Python信號。 當發出這樣的信號時,任何數據都可以作為附加參數傳遞給emit()方法,并且它們作為Python對象傳遞。 這避免了將參數轉換為C ++數據類型和從C ++數據類型轉換的開銷,并且還意味著可以傳遞任意Python對象,甚至不能轉換為C ++數據類型的對象。 具有至少一個自變量的信號是Qt信號或非短路Python信號。 在這些情況下,PyQt將檢查信號是否是Qt信號,如果不是,則會假定它是一個Python信號。 在任何一種情況下,參數都將轉換為C ++數據類型。
下面是我們如何連接到窗體的_init_()方法中的信號:
zerospinbox = ZeroSpinBox()
...
self.connect(zerospinbox, SIGNAL("atzero"), self.announce)
同樣,我們不能使用括號,因為它是一個短路信號。 為了完整性,這里是它連接到的形式:
def announce(self, zeros):
print "ZeroSpinBox has been at zero %d times" % zeros
如果我們使用帶有標識符但沒有括號的SIGNAL()函數,我們如前所述指定短路信號。 我們可以使用這種語法來發出短路信號,并連接到它們。 這兩種用法都在示例中顯示。
??如果我們使用SIGNAL()函數和signalSignature(一個可能是空的大寫逗號分隔的PyQt類型列表),我們指定一個Python或Qt信號。 (Python信號是在Python代碼中發出的; Qt信號是從底層C ++對象發出的信號。)我們可以使用這種語法來發出Python和Qt信號,并連接到它們。 這些信號可以連接到任何可調用的,即任何功能或方法,包括Qt槽; 它們也可以使用SLOT()語法與slotSignature連接。 PyQt檢查信號是否是Qt信號,如果不是,則假定它是一個Python信號。 如果我們使用括號,即使對于Python信號,參數必須可以轉換為C ++數據類型。
??現在我們來看另一個例子,一個小的自定義非GUI類,它有一個信號和一個槽,并顯示該機制不限于GUI類 - 任何QObject子類可以使用信號和槽。
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 %.2f%%" % 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更改為8.50%”。在前面的示例中,我們將多個信號連接到同一個插槽,我們不在乎誰發出信號。 但有時我們想要將兩個或多個信號連接到同一個插槽,并且根據調用者的不同,插槽的行為會有所不同。 在本節的最后一個例子中,我們將解決這個問題。
??圖4.8中顯示的Connections程序有五個按鈕和一個標簽。
??當點擊其中一個按鈕時,信號和插槽機制用于更新標簽的文本。 下面是如何在窗體的__init__()方法中創建第一個按鈕:
button1 = QPushButton("One")
所有其他按鈕都以相同的方式創建,不同之處僅在于它們的變量名稱和傳遞給它們的文本。
??我們將從最簡單的連接開始,這是由button1使用。 這里是__init__()方法的connect()調用:
self.connect(button1, SIGNAL("clicked()"), self.one)
我們為此按鈕使用了專用方法:
def one(self):
self.label.setText("You clicked button 'One'")
將按鈕的clicked()信號連接到適當響應的單個方法可能是最常見的連接情況。但是如果大多數處理是相同的,只是一些參數化取決于按下哪個特定按鈕? 在這種情況下,通常最好將每個按鈕連接到同一個插槽。 有兩種方法可以做到這一點。 一個是使用部分函數應用程序來包裝具有參數的插槽,以便在調用插槽時,使用調用它的按鈕對其進行參數化。 另一個是要求PyQt告訴我們哪個按鈕稱為插槽。 我們將展示這兩種方法,從偏函數應用(partial function application)開始。
??回到第65頁,我們創建了一個包裝器函數,它使用Python 2.5的functools.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
不幸的是,這將不適用于4.3之前的PyQt版本。 包裝器函數是在connect()調用中創建的,但是一旦connect()調用完成,包裝器就會超出范圍并被垃圾回收。 從PyQt 4.3開始,用functools.partial()制作的包裝器在用于像這樣的連接時會被特別處理。 這意味著連接的函數不會被垃圾回收,因此前面顯示的代碼將正常工作。
??對于PyQt 4.0,4.1和4.2,我們仍然可以使用partial():我們只需要保留對包裝器的引用 - 除了connect()調用之外,我們不會使用引用,但事實上它是一個屬性 的表單實例將確保包裝器函數在表單存在時不會超出范圍,因此將工作。 所以連接實際上是這樣:
self.button2callback = partial(self.anyButton, "Two")
self.connect(button2, SIGNAL("clicked()"),
self.button2callback)
當按鈕2被點擊時,將使用包含文本“Two”的字符串參數來調用anyButton()方法。 以下是此方法的外觀:
def anyButton(self, who):
self.label.setText("You clicked button '%s'" % who)
我們可以使用這個插槽的所有按鈕使用我們剛剛顯示的partial()函數。 事實上,我們可以避免使用partial(),并得到相同的結果:
self.button3callback = lambda who="Three": self.anyButton(who)
self.connect(button3, SIGNAL("clicked()"),self.button3callback)
這里我們創建了一個lambda函數,該函數由按鈕的名稱參數化。 它的工作原理與partial()技術相同,并且調用相同的anyButton()方法,只有使用lambda來創建包裝器。
??button2callback()和button3callback()調用anyButton(); 它們之間的唯一區別是第一個通過“Two”作為其參數,第二個通過“Three”。
??如果我們使用PyQt 4.1.1或更高版本,并且我們使用lambda回調,我們不必保留對它們的引用。 這是因為PyQt在用于在連接中創建包裝時專門處理lambda。 (這是同樣的特殊處理,期望擴展到functools.partial()在PyQt 4.3)。因此,我們可以直接使用lambda在connect()調用。 例如:
self.connect(button3, SIGNAL("clicked()"),lambda who="Three": self.anyButton(who))
包裝技術工作得很好,但是有一個替代方法稍微更多地涉及,但在某些情況下可能是有用的,特別是當我們不想包裝我們的調用。 這種其他技術用于響應button4和button5。 這里是他們的聯系:
self.connect(button4, SIGNAL("clicked()"), 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(),因為他們覺得它不是很好的面向對象的風格,所以他們傾向于使用部分功能應用程序,當這樣的需求出現。
??實際上有一種其他技術可以用來獲得包裝函數和參數的效果。它使用QSignalMapper類,其使用的示例在第9章中顯示。
??在一些情況下,可以將槽稱為信號的結果,并且在槽中直接或間接執行的處理導致最初稱為槽的信號被再次調用,導致無限循環。這樣的循環在實踐中是罕見的。兩個因素有助于減少循環的可能性。首先,只有在發生真實變化時才發射一些信號。例如,如果QSpinBox的值由用戶更改或通過setValue()調用以編程方式更改,則只有當新值與當前值不同時,才會發出其valueChanged()信號。第二,一些信號僅作為用戶動作的結果而發射。例如,QLineEdit僅在用戶更改文本時發出其textEdited()信號,而不是在通過setText()調用在代碼中更改時發出它的textEdited()信號。
??如果一個信號-槽循環已經發生,自然,首先要檢查的是代碼的邏輯是正確的:我們實際上是在做我們想要的處理?如果邏輯是正確的,并且我們還有一個循環,我們可以通過改變我們連接的信號來打破循環-例如,將由于程序化變化而發出的信號替換為被發射的信號僅作為用戶交互的結果。如果問題仍然存在,我們可以使QObject.blockSignals()在我們的代碼中的某些地方發出信號,QObject.blockSignals()被所有QWidget類繼承,并傳遞一個Boolean-True以停止對象發出信號,而False則恢復信號。
??這完成了我們對信號和時隙機制的正式覆蓋。在本書其余部分所示的幾乎所有示例中,我們將看到更多的信號和時隙示例。大多數其他GUI庫已經以某種形式或其他形式復制了機制。這是因為信號和槽機制是非常有用和強大的,并且讓程序員自由地專注于它們的應用程序的邏輯,而不必關心用戶如何調用特定操作的細節。
翻譯自Rapid GUI Programming with Python and Qt Chapter04 Signals and Slots