Qt 5——Qt消息機制和事件(二)

3 事件過濾器

有時候,對象需要查看、甚至要攔截髮送到另外對象的事件。例如,對話框可能想要攔截按鍵事件,不讓別的組件接收到;或者要修改回車鍵的默認處理。

通過前面的章節,我們已經知道,Qt 創建了QEvent事件對象之後,會調用QObject的event()函數處理事件的分發。顯然,我們可以在event()函數中實現攔截的操作。由於event()函數是 protected 的,因此,需要繼承已有類。如果組件很多,就需要重寫很多個event()函數。這當然相當麻煩,更不用說重寫event()函數還得小心一堆問題。好在 Qt 提供了另外一種機制來達到這一目的:事件過濾器。

QObject有一個eventFilter()函數,用於建立事件過濾器。函數原型如下:
virtual bool QObject::eventFilter ( QObject * watched, QEvent * event );
這個函數正如其名字顯示的那樣,是一個“事件過濾器”。所謂事件過濾器,可以理解成一種過濾代碼。事件過濾器會檢查接收到的事件。如果這個事件是我們感興趣的類型,就進行我們自己的處理;如果不是,就繼續轉發。這個函數返回一個 bool 類型,如果你想將參數 event 過濾出來,比如,不想讓它繼續轉發,就返回 true,否則返回 false。事件過濾器的調用時間是目標對象(也就是參數裏面的watched對象)接收到事件對象之前。也就是說,如果你在事件過濾器中停止了某個事件,那麼,watched對象以及以後所有的事件過濾器根本不會知道這麼一個事件。

我們來看一段簡單的代碼:

class MainWindow : public QMainWindow
 {
 public:
     MainWindow();
 protected:
     bool eventFilter(QObject *obj, QEvent *event);
 private:
     QTextEdit *textEdit;
 };
 
 MainWindow::MainWindow()
 {
     textEdit = new QTextEdit;
     setCentralWidget(textEdit);
 
     textEdit->installEventFilter(this);
 }
 
 bool MainWindow::eventFilter(QObject *obj, QEvent *event)
 {
     if (obj == textEdit) {
         if (event->type() == QEvent::KeyPress) {
             QKeyEvent *keyEvent = static_cast<QKeyEvent *>(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是我們定義的一個類。我們重寫了它的eventFilter()函數。爲了過濾特定組件上的事件,首先需要判斷這個對象是不是我們感興趣的組件,然後判斷這個事件的類型。在上面的代碼中,我們不想讓textEdit組件處理鍵盤按下的事件。所以,首先我們找到這個組件,如果這個事件是鍵盤事件,則直接返回 true,也就是過濾掉了這個事件,其他事件還是要繼續處理,所以返回 false。對於其它的組件,我們並不保證是不是還有過濾器,於是最保險的辦法是調用父類的函數。
eventFilter()函數相當於創建了過濾器,然後我們需要安裝這個過濾器。安裝過濾器需要調用QObject::installEventFilter()函數。函數的原型如下:
void QObject::installEventFilter ( QObject * filterObj )
這個函數接受一個QObject *類型的參數。記得剛剛我們說的,eventFilter()函數是QObject的一個成員函數,因此,任意QObject都可以作爲事件過濾器(問題在於,如果你沒有重寫eventFilter()函數,這個事件過濾器是沒有任何作用的,因爲默認什麼都不會過濾)。已經存在的過濾器則可以通過QObject::removeEventFilter()函數移除。
我們可以向一個對象上面安裝多個事件處理器,只要調用多次installEventFilter()函數。如果一個對象存在多個事件過濾器,那麼,最後一個安裝的會第一個執行,也就是後進先執行的順序。

還記得我們前面的那個例子嗎?我們使用event()函數處理了 Tab 鍵:
bool CustomWidget::event(QEvent *e)
{
if (e->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
if (keyEvent->key() == Qt::Key_Tab) {
qDebug() << “You press tab.”;
return true;
}
}
return QWidget::event(e);
}
現在,我們可以給出一個使用事件過濾器的版本:
bool FilterObject::eventFilter(QObject *object, QEvent *event)
{
if (object == target && event->type() == QEvent::KeyPress)
{
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Tab) {
qDebug() << “You press tab.”;
return true;
} else {
return false;
}
}
return false;
}
事件過濾器的強大之處在於,我們可以爲整個應用程序添加一個事件過濾器。記得,installEventFilter()函數是QObject的函數,QApplication或者QCoreApplication對象都是QObject的子類,因此,我們可以向QApplication或者QCoreApplication添加事件過濾器。這種全局的事件過濾器將會在所有其它特性對象的事件過濾器之前調用。儘管很強大,但這種行爲會嚴重降低整個應用程序的事件分發效率。因此,除非是不得不使用的情況,否則的話我們不應該這麼做。
注意,
事件過濾器和被安裝過濾器的組件必須在同一線程,否則,過濾器將不起作用。另外,如果在安裝過濾器之後,這兩個組件到了不同的線程,那麼,只有等到二者重新回到同一線程的時候過濾器纔會有效。
9.4 總結
Qt 的事件是整個 Qt 框架的核心機制之一,也比較複雜。說它複雜,更多是因爲它涉及到的函數衆多,而處理方法也很多,有時候讓人難以選擇。現在我們簡單總結一下 Qt 中的事件機制。
Qt 中有很多種事件:鼠標事件、鍵盤事件、大小改變的事件、位置移動的事件等等。如何處理這些事件,實際有兩種選擇:
所有事件對應一個事件處理函數,在這個事件處理函數中用一個很大的分支語句進行選擇,其代表作就是 win32 API 的WndProc()函數:
LRESULT CALLBACK WndProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
在這個函數中,我們需要使用switch語句,選擇message參數的類型進行處理,典型代碼是:
switch(message)
{
case WM_PAINT:
// …
break;
case WM_DESTROY:
// …
break;

}
每一種事件對應一個事件處理函數。Qt 就是使用的這麼一種機制:
mouseEvent()
keyPressEvent()
…
Qt 具有這麼多種事件處理函數,肯定有一個地方對其進行分發,否則,Qt 怎麼知道哪一種事件調用哪一個事件處理函數呢?這個分發的函數,就是event()。顯然,當QMouseEvent產生之後,event()函數將其分發給mouseEvent()事件處理器進行處理。
event()函數會有兩個問題:
event()函數是一個 protected 的函數,這意味着我們要想重寫event(),必須繼承一個已有的類。試想,我的程序根本不想要鼠標事件,程序中所有組件都不允許處理鼠標事件,是不是我得繼承所有組件,一一重寫其event()函數?protected 函數帶來的另外一個問題是,如果我基於第三方庫進行開發,而對方沒有提供源代碼,只有一個鏈接庫,其它都是封裝好的。我怎麼去繼承這種庫中的組件呢?
event()函數的確有一定的控制,不過有時候我的需求更嚴格一些:我希望那些組件根本看不到這種事件。event()函數雖然可以攔截,但其實也是接收到了QMouseEvent對象。我連讓它收都收不到。這樣做的好處是,模擬一種系統根本沒有那個事件的效果,所以其它組件根本不會收到這個事件,也就無需修改自己的事件處理函數。這種需求怎麼辦呢?
這兩個問題是event()函數無法處理的。於是,Qt 提供了另外一種解決方案:事件過濾器。事件過濾器給我們一種能力,讓我們能夠完全移除某種事件。事件過濾器可以安裝到任意QObject類型上面,並且可以安裝多個。如果要實現全局的事件過濾器,則可以安裝到QApplication或者QCoreApplication上面。這裏需要注意的是,如果使用installEventFilter()函數給一個對象安裝事件過濾器,那麼該事件過濾器只對該對象有效,只有這個對象的事件需要先傳遞給事件過濾器的eventFilter()函數進行過濾,其它對象不受影響。如果給QApplication對象安裝事件過濾器,那麼該過濾器對程序中的每一個對象都有效,任何對象的事件都是先傳給eventFilter()函數。
事件過濾器可以解決剛剛我們提出的event()函數的兩點不足:
首先,事件過濾器不是 protected 的,因此我們可以向任何QObject子類安裝事件過濾器;
其次,事件過濾器在目標對象接收到事件之前進行處理,如果我們將事件過濾掉,目標對象根本不會見到這個事件。
事實上,還有一種方法,我們沒有介紹。Qt 事件的調用最終都會追溯到QCoreApplication::notify()函數,因此,最大的控制權實際上是重寫QCoreApplication::notify()。這個函數的聲明是:
virtual bool QCoreApplication::notify ( QObject * receiver,
QEvent * event );
該函數會將event發送給receiver,也就是調用receiver->event(event),其返回值就是來自receiver的事件處理器。注意,這個函數爲任意線程的任意對象的任意事件調用,因此,它不存在事件過濾器的線程的問題。不過我們並不推薦這麼做,因爲notify()函數只有一個,而事件過濾器要靈活得多。

現在我們可以總結一下 Qt 的事件處理,實際上是有五個層次:
重寫paintEvent()、mousePressEvent()等事件處理函數。這是最普通、最簡單的形式,同時功能也最簡單。
重寫event()函數。event()函數是所有對象的事件入口,QObject和QWidget中的實現,默認是把事件傳遞給特定的事件處理函數。
在特定對象上面安裝事件過濾器。該過濾器僅過濾該對象接收到的事件。
在QCoreApplication::instance()上面安裝事件過濾器。該過濾器將過濾所有對象的所有事件,因此和notify()函數一樣強大,但是它更靈活,因爲可以安裝多個過濾器。全局的事件過濾器可以看到 disabled 組件上面發出的鼠標事件。全局過濾器有一個問題:只能用在主線程。
重寫QCoreApplication::notify()函數。這是最強大的,和全局事件過濾器一樣提供完全控制,並且不受線程的限制。但是全局範圍內只能有一個被使用(因爲QCoreApplication是單例的)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章