Qt5 事件(event)機制詳解

筆者用Qt算是用了挺長時間了,當初入C++的坑就是因為需要用Qt設計上位機軟件?,F在打算總結一下一些當初覺得有點深度的知識點,其中我覺得Qt最需要花事件理解的就是Qt的事件機制了。

1.簡述

個人認為,事件機制是Qt最難以理解且最為精妙的一部分。事件主要分為兩種:

  1. 在與用戶交互時發生。比如按下鼠標(mousePressEvent),敲擊鍵盤(keyPressEvent)等。
  2. 系統自動發生,比如計時器事件(timerEvent)等。

在發生事件時(比如說上面說的按下鼠標),就會產生一個QEvent對象(這里是QMouseEvent,為QEvent的子類),這個QEvent對象會傳給當前組件的event函數。如果當前組件沒有安裝事件過濾器(這個下文會提到),則會被event函數發放到相應的xxxEvent函數中(這里是mousePressEvent函數)。

Qt中所有的事件類都繼承于QEvent類


這個QEvent對象會有各種各樣的屬性,這是由用戶與界面交互時產生的。xxxEvent函數可以對其進行不同的處理(比如說是鼠標左鍵按下還是右鍵?)。查看幫助文檔,可以看到QMouseEvent類有以下枚舉。

在QtCreator中查看幫助文檔

那么就可以在mousePressEvent中根據這個QEvent對象的這些枚舉值來進行不同的處理,比如

class myLabel : public QLabel
{
protected:
    void mousePressEvent(QMouseEvent *event);
};

void myLabel::mousePressEvent(QMouseEvent *event)
{
    if(event->Buttons == LeftButton)
    {
        //do sth
    }
    else if(event->Buttons == RightButton)
    {
        //do sth
    }
}

可以看到,我們首先需要先創建一個自己的QLabel類,并繼承于Qt的QLabel類,然后并重寫相應的xxxEvent函數(這些事件處理函數都是虛函數)。

Qt程序的main函數中需要創建一個QApplication對象,然后調用exec函數。這將令程序進入一個死循環,并不斷監聽應用程序的事件,發生事件時就生成一個QEvent對象。這又稱為事件循環。

#include <QApplication>
#include "mainwindow.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MainWindow window;
    window.show();

    return app.exec();
}

2.事件的分發:event函數

上面提到的xxxEvent函數,稱為事件處理器(event handler)。而event函數的作用就在于事件的分發。如果想在事件的分發之前就進行一些操作,比如監聽某個按鍵的按下。

bool myWidget::event(QEvent *e)
{
    if (e->type() == QEvent::KeyPress) 
    {
        //將QEvent對象轉換為真正的QKeyEvent對象
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent->key() == Qt::Key_Tab) 
        {
            qDebug() << "You press tab.";
            return true;
        }
    }
    //按照原來的流程來進行事件的分發
    return QWidget::event(e);
}

在上面的程序中,myWidget是QWidget的子類。同樣的,它的event函數是一個虛函數,帶有一個QEvent類型的參數。當系統產生QEvent對象時,就會傳入這個函數并調用。函數的返回值是bool類型,返回值不同有不同的意義。

如果傳入的事件已被識別并且處理,則需要返回 true,否則返回 false。如果返回值是 true,那么 Qt 會認為這個事件已經處理完畢,不會再將這個事件發送給其它對象,而是會繼續處理事件隊列中的下一事件。

Qt系統在處理事件時,有一種機制叫事件傳播機制。也就是說,在子組件(比如說一個QButton)中發生的事件,調用了子組件的event函數之后,還會調用父組件(比如說QWidget)的event函數。event函數的返回值就用于控制這樣的一個過程。

https://blog.csdn.net/tqs_1220/article/details/82563070

需要注意的是,重寫event函數之后最好返回父類的event函數來處理其他的事件分發,不然就只能處理自己定義的事件。

bool myTextEdit::event(QEvent *e)
{
    if (e->type() == QEvent::KeyPress) 
    {
        //將QEvent對象轉換為真正的QKeyEvent對象
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent->key() == Qt::Key_Tab) 
        {
            qDebug() << "You press tab.";
            return true;
        }
    }
    //直接返回false
    return false;
}

在這個例子中,因為沒有調用父類QTextEditevent函數,所以只能處理Tab的情況,你再按其他按鍵就啥反應都沒有了。同樣,事件也不能進行傳播。

事件過濾器(Even Filter)

某些應用場景下,需要攔截某個組件發生的事件,讓這個事件不再向其他組件進行傳播,這時候可以為這個組件或其父組件安裝一個事件過濾器(evenFilter)。

QObject有一個虛函數,原型如下

virtual bool QObject::eventFilter ( QObject * watched, QEvent * event );

可以看到,函數有兩個參數,一個為具體發生事件的組件,一個為發生的事件(產生的QEvent對象)。當事件是我們感興趣的類型,可以就地進行處理,并令其不再轉發給其他組件。函數的返回值也是bool類型,作用跟even函數類似,返回true為不再轉發,false則讓其繼續被處理。

實際使用中,我們需要對QObject組件調用installEvenFilter函數,即為組件安裝過濾器,才能使用事件過濾器這個機制。這樣,該組件及其子組件的事件就會被監聽。這個機制的好處在于不用像重寫QEvent和xxxEvent函數一樣需要繼承Qt的內置類。

void QObject::installEventFilter ( QObject * filterObj );

下面舉一個例子。MainWindow中有一個QTextEdit控件,我們攔截它的鍵盤按下的事件。這樣處理之后,會在輸出窗口打印出按下的鍵位,但不會在控件上顯示。這表明事件已被攔截,不會去調用even函數。

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() << "you press" << keyEvent->key();
            //事件不再進行傳播,攔截
            return true;
        } 
        else
        {
            return false;//繼續傳播
        }
    } 
    else 
    {
        //當不確定是否繼續傳播時,按照父類的方法來處理
        //即調用父類的evenFilter函數
        return QMainWindow::eventFilter(obj, event);
    }
}

同樣的,even函數能干的事情,evenFilter也能干。比如說上面的處理鍵盤按下Tab鍵。

bool myObject::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;
}

我們可以對QApplication或者QCoreApplication對象添加事件過濾器。這種全局的事件過濾器將會在所有其它特性對象的事件過濾器之前調用。這種行為會嚴重降低整個應用程序的事件分發效率,要看具體情況使用。

事件過濾器和被安裝過濾器的組件必須在同一線程,否則,過濾器將不起作用。另外,如果在安裝過濾器之后,這兩個組件到了不同的線程,那么,只有等到二者重新回到同一線程的時候過濾器才會有效。

總結

Qt中使用事件機制,每一種事件對應一個事件處理器,比如:

  • mouseEvent()
  • keyPressEvent()
  • etc....

發生事件時會生成一個QEvent對象,則需要even函數進行分發,來調用相應的事件處理器

switch (event->type()) 
{
    case QEvent::MouseMove:
        mouseMoveEvent((QMouseEvent*)event);
        break;
    // ...
}

事件過濾器(evenFilter)可以令事件進行攔截,阻止其傳播,從而實現某些功能。

另外,有一種一般很少使用的方法,即去重寫這么一個函數

virtual bool QCoreApplication::notify ( QObject * receiver, QEvent * event );

該函數原實現相當于讓組件調用even函數,即receiver->event(event)。這相當于全局的事件過濾器,且不會受到多線程的限制。

那么,在使用Qt的事件機制時,應該按照以下思路進行

  • 重寫paintEventmousePressEvent等事件處理函數。這是最普通、最簡單的形式,同時功能也最簡單。
  • 重寫event函數。event函數是所有對象的事件入口,QObject和QWidget中的實現,默認是把事件傳遞給特定的事件處理函數。
  • 在特定對象上面安裝事件過濾器。該過濾器僅過濾該對象接收到的事件。
  • QCoreApplication::instance()上面安裝事件過濾器。該過濾器將過濾所有對象的所有事件,但會有多線程問題。
  • 重寫QCoreApplication::notify()函數。這是最強大的,和全局事件過濾器一樣提供完全控制,并且不受線程的限制。

Reference:
傳智播客 Qt教程
Qt學習之路27--事件傳遞過程和事件過濾器

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容

  • ??JavaScript 與 HTML 之間的交互是通過事件實現的。 ??事件,就是文檔或瀏覽器窗口中發生的一些特...
    霜天曉閱讀 3,517評論 1 11
  • 《Qt 學習之路 2》原文地址 Qt跨平臺策略 GUI 模擬:任何平臺都提供了圖形繪制函數,例如畫點、畫線、畫面等...
    CharlesZhangCh閱讀 2,033評論 0 5
  • vue概述 在官方文檔中,有一句話對Vue的定位說的很明確:Vue.js 的核心是一個允許采用簡潔的模板語法來聲明...
    li4065閱讀 7,261評論 0 25
  • 為什么在頭文件中有的是使用前置聲明,而有的是包含頭文件? 如下代碼: 前置聲明(forward declarati...
    Joe_HUST閱讀 1,310評論 0 6
  • 前言 這幾天不顧死活只關注自己,硬生生的推了幾篇科技小軟文,于是乎有朋友紛紛后臺留言問關于San Diego的旅行...
    隔壁陽臺上的貓閱讀 391評論 0 0