QT 事件與事件過濾器
事件
在Qt中,事件是作為物件處理的,所有事件物件繼承自抽象類QEvent。此類用來表示程式內部發生或者來自於外部但應用程式應該知道的動作。事件能夠能過被 QObject 的子類接受或者處理,但是通常用在與元件有關的應用中。本文主要闡述了在一個典型應用中的事件接收與處理。
事件的傳遞傳送
當一個事件產生時,Qt 通過例項化一個 QEvent 的合適的子類來表示它,然後通過呼叫 event() 函式傳送給 QObject 的例項(或者它的子類)。
event() 函式本身並不會處理事件,根據事件型別,它將呼叫相應的事件處理函式,並且返回事件被接受還是被忽略。
一些事件,比如 QMouseEvent 和 QKeyEvent,來自視窗系統;有的,比如 QTimerEvent,來自於其他事件源;另外一些則來自應用程式本身。
事件的型別
大部分事件型別有專門的類,比如 QResizeEvent, QPaintEvent, QMouseEvent, QKeyEvent 和 QCloseEvent。它們都是 QEvent 的子類,並且添加了自己特定的事件處理函式。比如 QResizeEvent 事件添加了 size()和 oldSize() 函式,使元件獲知自身大小的改變。
有些事件支援不止一個事件型別。比如 QMouseEvent 滑鼠事件,可以表示滑鼠的按下,雙擊,移動,以及其它的一些操作。
每一個事件都有其相關聯的型別,由 QEvent::Type 定義。我們能夠很方便地在執行時用這些型別來判斷該事件是哪一個子類。
因為程式響應方式的多樣性和複雜性,Qt 的事件傳遞機制是富有彈性很靈活的。QCoreApplication::notify() 的相關文件闡述大部分內容;Qt Quarterly 中的文章 Another Look at Events 也進行了簡要描述。在這裡我們的闡述對於 95% 的程式而言來說已經足夠了。
事件的處理
通常事件的處理需要呼叫一個虛擬函式。比如,QPaintEvent 事件的處理需要呼叫 QWidget::paintEvent() 函式。這個虛擬函式負責做出適當的響應,通常是用來重繪元件。如果你在自己的函式中並不打算實現所有的處理,你可以呼叫基類的實現。
例如,下面的程式碼用來處理滑鼠左鍵點選一個自定義的選擇框的操作,而其他的點選事件則被傳遞給基類 QCheckBox 處理。
void MyCheckBox::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
// handle left mouse button here
} else {
// pass on other buttons to base class
QCheckBox::mousePressEvent(event);
}
}
如果你想代替基類的處理,你必須自己實現所有的功能。但是,如果你只想擴充套件子基類的功能,你只需要實現你自己需要的那部分,剩下的讓基類來替你處理。
少數情況下,Qt 可能沒有指定專門的處理函式,或者指定的處理函式不能滿足要求。通常對 Tab 鍵的處理就會發生這種情況。一般地,Tab 鍵用來移動焦點,但是一些控制元件需要 Tab 鍵作其它的事情。
這些物件可以通過重新實現 QObject::event() 來滿足需要,它們可以在通用處理呼叫之前或之後來加入自己的處理,或者完全將事件處理替換為自己的事件處理函式。一個非常罕見的控制元件或許既要處理 Tab 鍵,又要呼叫程式特定的事件型別。那麼,我們就可以使用以下程式碼實現。
bool MyWidget::event(QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
QKeyEvent *ke = static_cast<QKeyEvent *>(event);
if (ke->key() == Qt::Key_Tab) {
// special tab handling here
return true;
}
} else if (event->type() == MyCustomEventType) {
MyCustomEvent *myEvent = static_cast<MyCustomEvent *>(event);
// custom event handling here
return true;
}
return QWidget::event(event);
}
注意,QWidget::event() 在那些沒有被處理的事件仍然要被呼叫,並且通過返回值表示事件是否被處理,返回 true 表示事件被阻止傳送到其他的物件。
事件過濾器
有時,並不存在一個特定事件函式,或者特定事件功能不足。最普通的例如按下tab鍵。正常情況下,被QWidget看成是去移動 鍵盤焦點,但少數視窗部件需要自行解釋。
讓我們試著設想已經有了一個CustomerInfoDialog的小部件。CustomerInfoDialog 包含一系列QLineEdit. 現在,我們想用空格鍵來代替Tab,使焦點在這些QLineEdit間切換。
一個解決的方法是子類化QLineEdit,重新實現keyPressEvent(),並在keyPressEvent()裡呼叫focusNextChild()。像下面這樣:
void MyLineEdit::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Space)
{
focusNextChild();
}
else
{
QLineEdit::keyPressEvent(event);
}
}
但這有一個缺點。如果CustomerInfoDialog裡有很多不同的控制元件(比如QComboBox,QEdit,QSpinBox),我們就必須子類化這麼多控制元件。這是一個煩瑣的任務。
一個更好的解決辦法是: 讓CustomerInfoDialog去管理他的子部件的按鍵事件,實現要求的行為。我們可以使用事件過濾器。
一個事件過濾器的安裝需要下面2個步驟:
1, 呼叫installEventFilter()註冊需要管理的物件。
2,在eventFilter() 裡處理需要管理的物件的事件。
一般,推薦在CustomerInfoDialog的建構函式中註冊被管理的物件。像下面這樣:
CustomerInfoDialog::CustomerInfoDialog(QWidget *parent)
: QDialog(parent)
{
...
firstNameEdit->installEventFilter(this);
lastNameEdit->installEventFilter(this);
cityEdit->installEventFilter(this);
phoneNumberEdit->installEventFilter(this);
}
一旦,事件管理器被註冊,傳送到firstNameEdit,lastNameEdit,cityEdit,phoneNumberEdit的事件將首先發送到eventFilter()。
下面是一個 eventFilter()函式的實現:
bool CustomerInfoDialog::eventFilter(QObject *target, QEvent *event)
{
if (target == firstNameEdit || target == lastNameEdit
|| target == cityEdit || target == phoneNumberEdit)
{
if (event->type() == QEvent::KeyPress)
{
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Space)
{
focusNextChild();
return true;
}
}
}
return QDialog::eventFilter(target, event);
}
在上面的函式中,我們首先檢查目標部件是否是 firstNameEdit,lastNameEdit,cityEdit,phoneNumberEdit。接著,我們判斷事件是否是按鍵事件。如果事件是按鍵事件,我們把事件轉換為QKeyEvent。接著,我們判斷是否按下了空格鍵,如果是,我們呼叫focusNextChild(),把焦點傳遞給下一個控制元件。然後,返回,true通知Qt,我們已經處理了該事件。
如果返回false的話,Qt繼續將該事件傳送給目標控制元件,結果是一個空格被插入到QLineEdit中。
如果目標控制元件不是 QLineEdit,或者按鍵不是空格鍵,我們將把事件傳遞給基類的eventFilter()函式。
Qt提供5個級別的事件處理和過濾:
1,重新實現事件函式。 比如: mousePressEvent(), keyPress-Event(), paintEvent() 。
這是最常規的事件處理方法。
2,重新實現QObject::event().
這一般用在Qt沒有提供該事件的處理函式時。也就是,我們增加新的事件時。
3,安裝事件過濾器
4,在 QApplication 上安裝事件過濾器。
這之所以被單獨列出來是因為: QApplication 上的事件過濾器將捕獲應用程式的所有事件,而且第一個獲得該事件。也就是說事件在傳送給其它任何一個event filter之前傳送給QApplication的event filter。
5,重新實現QApplication 的 notify()方法.
Qt使用 notify()來分發事件。要想在任何事件處理器捕獲事件之前捕獲事件,唯一的方法就是重新實現QApplication 的 notify()方法。
Qt建立了QEvent事件物件之後,會呼叫QObject的event()函式做事件的分發。有時候,你可能需要在呼叫event()函式之前做一些另外的操作,比如,對話方塊上某些元件可能並不需要響應回車按下的事件,此時,你就需要重新定義元件的event()函式。如果元件很多,就需要重寫很多次event()函式,這顯然沒有效率。為此,你可以使用一個事件過濾器,來判斷是否需要呼叫event()函式。
QOjbect有一個eventFilter()函式,用於建立事件過濾器。這個函式的簽名如下:
virtual bool QObject::eventFilter ( QObject * watched, QEvent * event )
如果watched物件安裝了事件過濾器,這個函式會被呼叫並進行事件過濾,然後才輪到元件進行事件處理。在重寫這個函式時,如果你需要過濾掉某個事件,例如停止對這個事件的響應,需要返回true
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (obj == textEdit) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast(event);
qDebug() << "Ate key press" << keyEvent->key();
return true;
} else {
return false;
}
} else {
// pass the event on to the parent class
return QMainWindow::eventFilter(obj, event);
}
}
上面的例子中為MainWindow建立了一個事件過濾器。為了過濾某個元件上的事件,首先需要判斷這個物件是哪個元件,然後判斷這個事件的型別。例如,我不想讓textEdit元件處理鍵盤事件,於是就首先找到這個元件,如果這個事件是鍵盤事件,則直接返回true,也就是過濾掉了這個事件,其他事件還是要繼續處理,所以返回false。對於其他元件,我們並不保證是不是還有過濾器,於是最保險的辦法是呼叫父類的函式。
在建立了過濾器之後,下面要做的是安裝這個過濾器。安裝過濾器需要呼叫installEventFilter()函式。這個函式的簽名如下:
void QObject::installEventFilter ( QObject * filterObj )
這個函式是QObject的一個函式,因此可以安裝到任何QObject的子類,並不僅僅是UI元件。這個函式接收一個QObject物件,呼叫了這個函式安裝事件過濾器的元件會呼叫filterObj定義的eventFilter()函式。例如,textField.installEventFilter(obj),則如果有事件傳送到textField元件是,會先呼叫obj->eventFilter()函式,然後才會呼叫textField.event()。
當然,你也可以把事件過濾器安裝到QApplication上面,這樣就可以過濾所有的事件,已獲得更大的控制權。不過,這樣做的後果就是會降低事件分發的效率。
如果一個元件安裝了多個過濾器,則最後一個安裝的會最先呼叫,類似於堆疊的行為。
注意,如果你在事件過濾器中delete了某個接收元件,務必將返回值設為true。否則,Qt還是會將事件分發給這個接收元件,從而導致程式崩潰。
事件過濾器和被安裝的元件必須在同一執行緒,否則,過濾器不起作用。另外,如果在install之後,這兩個元件到了不同的執行緒,那麼,只有等到二者重新回到同一執行緒的時候過濾器才會有效。
事件的呼叫最終都會呼叫QCoreApplication的notify()函式,因此,最大的控制權實際上是重寫QCoreApplication的notify()函式。由此可以看出,Qt的事件處理實際上是分層五個層次:重定義事件處理函式,重定義event()函式,為單個元件安裝事件過濾器,為QApplication安裝事件過濾器,重定義QCoreApplication的notify()函式。這幾個層次的控制權是逐層增大的。