Python Qt GUI設計:5種事件處理機制(提升篇—3)

之前在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(),在實踐中,在調試時才會使用這種方法,實際中基本用不多,這里不再贅述了。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容