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