Python Qt GUI設(shè)計:5種事件處理機(jī)制(提升篇—3)
之前在Python Qt GUI設(shè)計:QTimer計時器類、QThread多線程類和事件處理類(基礎(chǔ)篇—8)中,我們已經(jīng)簡單講到,PyQt為事件處理提供了兩種機(jī)制:高級的信號與槽機(jī)制以及低級的事件處理程序,本篇博文將系統(tǒng)講解Qt的事件處理機(jī)類和制。
事件處理機(jī)制本身很復(fù)雜,是PyQt底層的知識點(diǎn),當(dāng)采用信號與槽機(jī)制處理不了時,才會考慮使用事件處理機(jī)制。
信號與槽可以說是對事件處理機(jī)制的高級封裝,如果說事件是用來創(chuàng)建窗口控件的,那么信號與槽就是用來對這個窗口控件進(jìn)行使用的。比如一個按鈕,當(dāng)我們使用這個按鈕時,只關(guān)心clicked信號,至于這個按鈕如何接收并處理鼠標(biāo)點(diǎn)擊事件,然后再發(fā)射這信號,則不用關(guān)心。但是如果要重載一個按鈕,這時就要關(guān)心這個問題了。比如可以改變它的行為:在鼠標(biāo)按鍵按下時觸發(fā)clicked信號,而不是在釋放時。
1
常見事件類型
Qt事件的類型有很多,常見的Qt事件如下所示:
鍵盤事件:按鍵按下和松開。
鼠標(biāo)事件:鼠標(biāo)指針移動、鼠標(biāo)按鍵按下和松開。
拖放事件:用鼠標(biāo)進(jìn)行拖放。
滾輪事件:鼠標(biāo)滾輪滾動。
繪屏事件:重繪屏幕的某些部分。
定時事件:定時器到時。
焦點(diǎn)事件:鍵盤焦點(diǎn)移動。
進(jìn)入和離開事件:鼠標(biāo)指針移入Widget內(nèi),或者移出。
移動事件::Widget的位置改變。
大小改變事件:Widget的大小改變。
顯示和隱藏事件:Widget顯示和隱藏。
窗口事件:窗口是否為當(dāng)前窗口。
還有一些常見的Qt事件,比如Socket事件、剪貼板事件、字體改變事件、布局改變事件等。
具體事件類型和說明可參見說明文檔:
2
事件處理方法
PyQt提供了如下5種事件處理和過濾方法(由弱到強(qiáng)),其中只有前兩種方法使用最頻繁。
2.1、重新實現(xiàn)事件函數(shù)
比如mousePressEvent()、keyPressEvent()、paintEvent()。這是最常規(guī)的事件處理方法。
通過示例了解重新實現(xiàn)事件函數(shù)的使用方法,效果如下所示:
這個示例中包含了多種事件類型,所以比較復(fù)雜。
首先是類的建立,建立text和message兩個變量,使用paintEvent函數(shù)把它們輸出到窗口中。
update函數(shù)的作用是更新窗口,由于在窗口更新過程中會觸發(fā)一次 paintEvent函數(shù)(paintEvent是窗口基類QWidget的內(nèi)部函數(shù)),因此在本例中update函數(shù)的作用等同于paintEvent函數(shù)。
然后是重新實現(xiàn)窗口關(guān)閉事件與上下文菜單事件,對于上下文菜單事件,主要影響message變量的結(jié)果,paintEvent負(fù)責(zé)把這個變量在窗口底部輸出。
繪制事件是代碼的核心事件,它的主要作用是時刻跟蹤text與message這兩個變量的信息,并把 text的內(nèi)容繪制到窗口的中部,把message的內(nèi)容繪制到窗口的底部(保持5秒后就會被清空)。
以及最后一些鼠標(biāo)、鍵盤的點(diǎn)擊操作等。
實現(xiàn)代碼如下所示:
import sysfrom PyQt5.QtCore import (QEvent, QTimer, Qt)from PyQt5.QtWidgets import (QApplication, QMenu, QWidget)from PyQt5.QtGui import QPainterclass Widget(QWidget):def __init__(self, parent=None):super(Widget, self).__init__(parent)self.justDoubleClicked = Falseself.key = ""self.text = ""self.message = ""self.resize(400, 300)self.move(100, 100)self.setWindowTitle("Events")QTimer.singleShot(0, self.giveHelp) # 避免窗口大小重繪事件的影響,可以把參數(shù)0改變成3000(3秒),然后在運(yùn)行,就可以明白這行代碼的意思。def giveHelp(self):self.text = "請點(diǎn)擊這里觸發(fā)追蹤鼠標(biāo)功能"self.update() # 重繪事件,也就是觸發(fā)paintEvent函數(shù)。'''重新實現(xiàn)關(guān)閉事件'''def closeEvent(self, event):print("Closed")'''重新實現(xiàn)上下文菜單事件'''def contextMenuEvent(self, event):menu = QMenu(self)oneAction = menu.addAction("&One")twoAction = menu.addAction("&Two")oneAction.triggered.connect(self.one)twoAction.triggered.connect(self.two)if not self.message:menu.addSeparator()threeAction = menu.addAction("Thre&e")threeAction.triggered.connect(self.three)menu.exec_(event.globalPos())'''上下文菜單槽函數(shù)'''def one(self):self.message = "Menu option One"self.update()def two(self):self.message = "Menu option Two"self.update()def three(self):self.message = "Menu option Three"self.update()'''重新實現(xiàn)繪制事件'''def paintEvent(self, event):text = self.texti = text.find("\n\n")if i >= 0:text = text[0:i]if self.key: # 若觸發(fā)了鍵盤按鈕,則在文本信息中記錄這個按鈕信息。text += "\n\n你按下了: {0}".format(self.key)painter = QPainter(self)painter.setRenderHint(QPainter.TextAntialiasing)painter.drawText(self.rect(), Qt.AlignCenter, text) # 繪制信息文本的內(nèi)容if self.message: # 若消息文本存在則在底部居中繪制消息,5秒鐘后清空消息文本并重繪。painter.drawText(self.rect(), Qt.AlignBottom | Qt.AlignHCenter,self.message)QTimer.singleShot(5000, self.clearMessage)QTimer.singleShot(5000, self.update)'''清空消息文本的槽函數(shù)'''def clearMessage(self):self.message = ""'''重新實現(xiàn)調(diào)整窗口大小事件'''def resizeEvent(self, event):self.text = "調(diào)整窗口大小為:QSize({0}, {1})".format(event.size().width(), event.size().height())self.update()'''重新實現(xiàn)鼠標(biāo)釋放事件'''def mouseReleaseEvent(self, event):# 若鼠標(biāo)釋放為雙擊釋放,則不跟蹤鼠標(biāo)移動# 若鼠標(biāo)釋放為單擊釋放,則需要改變跟蹤功能的狀態(tài),如果開啟跟蹤功能的話就跟蹤,不開啟跟蹤功能就不跟蹤if self.justDoubleClicked:self.justDoubleClicked = Falseelse:self.setMouseTracking(not self.hasMouseTracking()) # 單擊鼠標(biāo)if self.hasMouseTracking():self.text = "開啟鼠標(biāo)跟蹤功能.\n" + \"請移動一下鼠標(biāo)!\n" + \"單擊鼠標(biāo)可以關(guān)閉這個功能"else:self.text = "關(guān)閉鼠標(biāo)跟蹤功能.\n" + \"單擊鼠標(biāo)可以開啟這個功能"self.update()'''重新實現(xiàn)鼠標(biāo)移動事件'''def mouseMoveEvent(self, event):if not self.justDoubleClicked:globalPos = self.mapToGlobal(event.pos()) # 窗口坐標(biāo)轉(zhuǎn)換為屏幕坐標(biāo)self.text = """鼠標(biāo)位置:窗口坐標(biāo)為:QPoint({0}, {1})屏幕坐標(biāo)為:QPoint({2}, {3}) """.format(event.pos().x(), event.pos().y(), globalPos.x(), globalPos.y())self.update()'''重新實現(xiàn)鼠標(biāo)雙擊事件'''def mouseDoubleClickEvent(self, event):self.justDoubleClicked = Trueself.text = "你雙擊了鼠標(biāo)"self.update()'''重新實現(xiàn)鍵盤按下事件'''def keyPressEvent(self, event):self.key = ""if event.key() == Qt.Key_Home:self.key = "Home"elif event.key() == Qt.Key_End:self.key = "End"elif event.key() == Qt.Key_PageUp:if event.modifiers() & Qt.ControlModifier:self.key = "Ctrl+PageUp"else:self.key = "PageUp"elif event.key() == Qt.Key_PageDown:if event.modifiers() & Qt.ControlModifier:self.key = "Ctrl+PageDown"else:self.key = "PageDown"elif Qt.Key_A <= event.key() <= Qt.Key_Z:if event.modifiers() & Qt.ShiftModifier:self.key = "Shift+"self.key += event.text()if self.key:self.key = self.keyself.update()else:QWidget.keyPressEvent(self, event)if __name__ == "__main__":app = QApplication(sys.argv)form = Widget()form.show()app.exec_()
2.2、重新實現(xiàn)QObject.event()
一般用在PyQt沒有提供該事件的處理函數(shù)的情況下,即增加新事件時。
對于窗口所有的事件都會傳遞給event函數(shù),event函數(shù)會根據(jù)事件的類型,把事件分配給不同的函數(shù)進(jìn)行處理。例如,對于繪圖事件,event會交給paintEvent函數(shù)處理;對于鼠標(biāo)移動事件,event會交給mouseMoveEvent函數(shù)處理;對于鍵盤按下事件,event會交給keyPressEvent函數(shù)處理。
有一種特殊情況是對Tab鍵的觸發(fā)行為,event函數(shù)對Tab鍵的處理機(jī)制是把焦點(diǎn)從當(dāng)前窗口控件的位置切換到Tab鍵次序中下一個窗口控件的位置,并返回True,而不是交給keyPressEvent函數(shù)處理。
因此這里需要在event函數(shù)中對按下Tab鍵的處理邏輯重新改寫,使它與鍵盤上普通的鍵沒什么不同。
在2.1、重新實現(xiàn)事件函數(shù)例子中補(bǔ)充以下代碼,實現(xiàn)重新定義:
'''重新實現(xiàn)其他事件,適用于PyQt沒有提供該事件的處理函數(shù)的情況,Tab鍵由于涉及焦點(diǎn)切換,不會傳遞給keyPressEvent,因此,需要在這里重新定義。'''
def event(self, event):if (event.type() == QEvent.KeyPress andevent.key() == Qt.Key_Tab):self.key = "在event()中捕獲Tab鍵"self.update()return True
效果如下所示:
2.3、安裝事件過濾器
如果對QObject調(diào)用installEventFilter,則相當(dāng)于為這個QObject安裝了一個事件過濾器,對于QObject的全部事件來說,它們都會先傳遞到事件過濾函數(shù)eventFilter中,在這個函數(shù)中我們可以拋棄或者修改這些事件,比如可以對自己感興趣的事件使用自定義的事件處理機(jī)制,對其他事件使用默認(rèn)的事件處理機(jī)制。
由于這種方法會對調(diào)用installEventFilter的所有QObject的事件進(jìn)行過濾,因此如果要過濾的事件比較多,則會降低程序的性能。
通過示例,了解事件過濾器的使用方法,效果如下所示:
對于使用事件過濾器,關(guān)鍵是要做好兩步。
對要過濾的控件設(shè)置installEventFilter,這些控件的所有事件都會被eventFilter函數(shù)接收并處理。
示例中,這個過濾器只對label1的事件進(jìn)行處理,并且只處理它的鼠標(biāo)按下事件(MouseButtonPress)和鼠標(biāo)釋放事件(MouseButtonRelease) 。
如果按下鼠標(biāo)鍵,就會對label1裝載的圖片進(jìn)行縮放(長和寬各縮放一半)。
實現(xiàn)代碼如下所示:
-*- coding: utf-8 -*-from PyQt5.QtGui import *from PyQt5.QtCore import *from PyQt5.QtWidgets import *import sysclass EventFilter(QDialog):def __init__(self, parent=None):super(EventFilter, self).__init__(parent)self.setWindowTitle("事件過濾器")self.label1 = QLabel("請點(diǎn)擊")self.label2 = QLabel("請點(diǎn)擊")self.label3 = QLabel("請點(diǎn)擊")self.LabelState = QLabel("test")self.image1 = QImage("images/cartoon1.ico")self.image2 = QImage("images/cartoon1.ico")self.image3 = QImage("images/cartoon1.ico")self.width = 600self.height = 300self.resize(self.width, self.height)self.label1.installEventFilter(self)self.label2.installEventFilter(self)self.label3.installEventFilter(self)mainLayout = QGridLayout(self)mainLayout.addWidget(self.label1, 500, 0)mainLayout.addWidget(self.label2, 500, 1)mainLayout.addWidget(self.label3, 500, 2)mainLayout.addWidget(self.LabelState, 600, 1)self.setLayout(mainLayout)def eventFilter(self, watched, event):if watched == self.label1: # 只對label1的點(diǎn)擊事件進(jìn)行過濾,重寫其行為,其他的事件會被忽略if event.type() == QEvent.MouseButtonPress: # 這里對鼠標(biāo)按下事件進(jìn)行過濾,重寫其行為mouseEvent = QMouseEvent(event)if mouseEvent.buttons() == Qt.LeftButton:self.LabelState.setText("按下鼠標(biāo)左鍵")elif mouseEvent.buttons() == Qt.MidButton:self.LabelState.setText("按下鼠標(biāo)中間鍵")elif mouseEvent.buttons() == Qt.RightButton:self.LabelState.setText("按下鼠標(biāo)右鍵")'''轉(zhuǎn)換圖片大小'''transform = QTransform()transform.scale(0.5, 0.5)tmp = self.image1.transformed(transform)self.label1.setPixmap(QPixmap.fromImage(tmp))if event.type() == QEvent.MouseButtonRelease: # 這里對鼠標(biāo)釋放事件進(jìn)行過濾,重寫其行為self.LabelState.setText("釋放鼠標(biāo)按鈕")self.label1.setPixmap(QPixmap.fromImage(self.image1))return QDialog.eventFilter(self, watched, event) # 其他情況會返回系統(tǒng)默認(rèn)的事件處理方法。if __name__ == '__main__':app = QApplication(sys.argv)dialog = EventFilter()dialog.show()sys.exit(app.exec_())
2.4、在QApplication中安裝事件過濾器
這種方法比2.3、安裝事件過濾器更強(qiáng)大,QApplication的事件過濾器將捕獲所有QObject的所有事件,而且第一個獲得該事件。也就是說,在將事件發(fā)送給其他任何一個事件過濾器之前(就是在第三種方法之前),都會先發(fā)送給QApplication的事件過濾器。
在2.3、安裝事件過濾器示例基礎(chǔ)上修改,屏蔽三個label標(biāo)簽控件的installEventFilter代碼,這種事件處理方法確實過濾了所有事件,而不像第三種方法那樣只過濾三個標(biāo)簽控件的事件。效果如下所示:
實現(xiàn)代碼如下所示:
-*- coding: utf-8 -*-from PyQt5.QtGui import *from PyQt5.QtCore import *from PyQt5.QtWidgets import *import sysclass EventFilter(QDialog):def __init__(self, parent=None):super(EventFilter, self).__init__(parent)self.setWindowTitle("事件過濾器")self.label1 = QLabel("請點(diǎn)擊")self.label2 = QLabel("請點(diǎn)擊")self.label3 = QLabel("請點(diǎn)擊")self.LabelState = QLabel("test")self.image1 = QImage("images/cartoon1.ico")self.image2 = QImage("images/cartoon1.ico")self.image3 = QImage("images/cartoon1.ico")self.width = 600self.height = 300self.resize(self.width, self.height)# self.label1.installEventFilter(self)# self.label2.installEventFilter(self)# self.label3.installEventFilter(self)mainLayout = QGridLayout(self)mainLayout.addWidget(self.label1, 500, 0)mainLayout.addWidget(self.label2, 500, 1)mainLayout.addWidget(self.label3, 500, 2)mainLayout.addWidget(self.LabelState, 600, 1)self.setLayout(mainLayout)def eventFilter(self, watched, event):print(type(watched))if watched == self.label1: # 只對label1的點(diǎn)擊事件進(jìn)行過濾,重寫其行為,其他的事件會被忽略if event.type() == QEvent.MouseButtonPress: # 這里對鼠標(biāo)按下事件進(jìn)行過濾,重寫其行為mouseEvent = QMouseEvent(event)if mouseEvent.buttons() == Qt.LeftButton:self.LabelState.setText("按下鼠標(biāo)左鍵")elif mouseEvent.buttons() == Qt.MidButton:self.LabelState.setText("按下鼠標(biāo)中間鍵")elif mouseEvent.buttons() == Qt.RightButton:self.LabelState.setText("按下鼠標(biāo)右鍵")'''轉(zhuǎn)換圖片大小'''transform = QTransform()transform.scale(0.5, 0.5)tmp = self.image1.transformed(transform)self.label1.setPixmap(QPixmap.fromImage(tmp))if event.type() == QEvent.MouseButtonRelease: # 這里對鼠標(biāo)釋放事件進(jìn)行過濾,重寫其行為self.LabelState.setText("釋放鼠標(biāo)按鈕")self.label1.setPixmap(QPixmap.fromImage(self.image1))return QDialog.eventFilter(self, watched, event) # 其他情況會返回系統(tǒng)默認(rèn)的事件處理方法。if __name__ == '__main__':app = QApplication(sys.argv)dialog = EventFilter()app.installEventFilter(dialog)dialog.show()sys.exit(app.exec_())
2.5、重新實現(xiàn)QApplication的notify()方法
PyQt使用notify()來分發(fā)事件,要想在任何事件處理器之前捕獲事件,唯一的方法就是重新實現(xiàn)QApplication的notify(),在實踐中,在調(diào)試時才會使用這種方法,實際中基本用不多,這里不再贅述了。
往期推薦

