qt 事件機制

什麼是自發事件?哪些類型的事件可以被propagated 或compressed? posting and sending 事件之間有何不同?什麼時候應該調用 accept() 或是ignore() ? 如果這些問題你還不是很瞭解,那麼繼續看下去。 
事件起源:

基於事件如何被產生與分發,可以把事件分爲三類:
* Spontaneous 事件,由窗口系統產生,它們被放到系統隊列中,通過事件循環逐個處理。
* Posted 事件,由Qt或是應用程序產生,它們被Qt組成隊列,再通過事件循環處理。
* Sent  事件,由Qt或是應用程序產生,但它們被直接發送到目標對象。
當我們在main()函數的末尾調用QApplication::exec()時,程序進入了Qt的事件循環,大概來講,事件循環如下面所示:
while (!exit_was_called)
{
  while(!posted_event_queue_is_empty)
       {
         process_next_posted_event();
       }
  while(!spontaneous_event_queue_is_empty)
      {
         process_next_spontaneous_event();
      }
  while(!posted_event_queue_is_empty)
      {
        process_next_posted_event();
      }
}
首 先,事件循環處理所有的posted事件,直到隊列空。然後再處理所有的spontaneous事件,最後它處理所有的因爲處理spontaneous事 件而產生的posted事件。send 事件並不在事件循環內處理,它們都直接被髮送到了目標對象。現在看一下實踐中的paint 事件是如何工作的。當一個widget第一次可見,或是被遮擋後再次變爲可見,
窗口系統產生一個(spontaneous) paint事件,要求程序重畫widget,事件循環最終從事件隊列中撿選這個事件並把它分發到那個需要重畫的widget。
並不是所有的paint事件都是由窗口系統產生的。當你調用QWidget::update()去強行重畫widget,這個widget會post 一個paint 事件給自己。這個paint事件被放入隊列,最終被事件循環分發之。
假 如你很不耐煩,等不及事件循環去重畫一個widget, 理論上,你應該直接調用paintEvent()強制進行立即的重畫。但實際上這不總是可行的,因爲paintEvent()函數是protected的 (很可能訪問不了)。它也繞開了任何存在的事件過濾器。因爲這些原因,Qt提供了一個機制,直接sending事件而不是posting 。
QWidget::repaint()就使用了這個機制來強制進行立即重畫。
posting 相對於sending的一個優勢是,它給了Qt一個壓縮(compress)事件的機會。假如你在一個widget上連續地調用update() 十次,因update()而產生的這十個事件,將會自動地被合併爲一個單獨的事件,但是QPaintEvents事件附帶的區域信息也合併了。可壓縮的事 件類型包括:paint,move,resize,layout hint,language change。
最後要注意,你可以在任何時候調用QApplication::sendPostedEvent(),強制Qt產生一個對象的posted事件。

人工合成的事件

QT應用程序可以產生他們自己的事件,或是預定義類型,或是自定義類型。 這可以通過創建QEvent類或它的
子類的實例,並且調用QApplication:postEvent()或QApplication::sendEvent()來實現。
這兩個函數需要一個 QObject* 與一個QEvent * 作爲參數,假如你調用postEvent(),你必須用 new 操作符來創建事件對象,Qt會它被處理後幫你刪除它。假如你用sendEvent(), 你應該在棧上來創建事件。下面舉兩個例子:
一是posting 事件:
QApplication::postEvent(mainWin, new QKeyEvent(QEvent::KeyPress,Key_X,'X',0));
二是sending 事件:
    QKeyEvent event(QEvent::KeyPress, Key_X, 'X', 0);
    QApplication::sendEvent(mainWin, &event);
Qt應用程序很少直接調用postEvent()或是sendEvnet(),因爲大多數事件會在必要時被Qt或是窗口系統自動產生
。在大多數的情況下,當你想發送一個事件時,Qt已經爲了準備好了一個更高級的函數來爲你服務。(例如
update()與repaint())。

定製事件類型

qt允許你創建自己的事件類型,這在多線程的程序中尤其有用。在單線程的程序也相當有用,它可以作爲
對象間的一種通訊機制。爲什麼你應該用事件而不是其他的標準函數調用,或信號、槽的主要原因是:事件既可用於同步也可用於異步(依賴於你是調用sendEvent()或是postEvents()),函數調用或是槽調用總是同步的。事件的另外一個好處是它可以被過濾。
演示如何post一個定製事件的代碼片段:
const QEvent::Type MyEvent = (QEvent::Type)1234;
  ...
QApplication::postEvent(obj, new QCustomEvent(MyEvent));
事件必須是QCustomEvent類型(或子類)的。構造函數的參數是事件的類型,1024以下被Qt保留。其他可被程序使用。爲處理定製事件類型,要重新實現customEvent()函數:
void MyLineEdit::customEvent(QCustomEvent *event)
    {
        if (event->type() == MyEvent) {
            myEvent();
        } else {
            QLineEdit::customEvent(event);
        }
    }
QcustomEvent類有一個void *的成員,可用於特定的目的。你也可以子類化QCustomEvent,加上別的成員,但是你也需要在customEvent()中轉換QCustomeEvent到你特有的類型。

事件處理與過濾

Qt中的事件可以在五個不同的層次上被處理
1,重新實現一個特定的事件handler
 QObject與QWidget提供了許多特定的事件handlers,分別對應於不同的事件類型。(如paintEvent()對應paint事件)
2,重新實現QObject::event()
 event()函數是所有對象事件的入口,QObject和QWidget中缺省的實現是簡單地把事件推入特定的事件handlers。
3,在QObject安裝上事件過濾器
  事件過濾器是一個對象,它接收別的對象的事件,在這些事件到達指定目標之間。
4,在aApp上安裝一個事件過濾器,它會監視程序中發送到所有對象的所有事件
5,重新實現QApplication:notify(),Qt的事件循環與sendEvent()調用這個函數來分發事件,通過重寫它,你可以在別人之前看到事件。

一些事件類型可以被傳遞。這意味着假如目標對象不處理一個事件,Qt會試着尋找另外的事件接收者。用新的目標來調用 QApplication::notify()。舉例來講,key事件是傳遞的,假如擁有焦點的Widget不處理特定鍵,Qt會分發相同的事件給父 widget,然後是父親的父親,直到最頂層widget。

接受或是忽略?

可被傳遞的事件有一個accept()函數和一個ignore()函數,你可以用它們來告訴Qt,你“接收”或是
“忽略”這個事件。假如事件handler調用accept(),這個事件將不會再被傳遞。假如事件handler調用
ignore(),Qt會試着查找另外的事件接收者。
像大多數的開發者一樣,你可能不會被調用accept()或是ignore()所煩惱。缺省情況下是“接收”,在
QWidget中的缺省實現是調用ignore(),假如你希望接收事件,你需要做的是重新實現事件handler,避免
調用QWidget的實現。假如你想“忽略”事件,只需簡單地傳遞它到QWidget的實現。下面的代碼演示了這一點:
void MyFancyWidget::keyPressEvent(QKeyEvent *event)
    {
        if (event->key() == Key_Escape) {
            doEscape();
        } else {
            QWidget::keyPressEvent(event);
        }
    }
在上面的例子裏,假如用戶按了"ESC"鍵,我們會調用doEscape()並且事件被“接收”了(這是缺省的情況),
事件不會被傳遞到父widget,假如用戶按了別的鍵,則調用QWidget的缺省實現。
void QWidget::keyPressEvent(QKeyEvent *event)
    {
        event->ignore();
    }
應該感謝ignore(),事件會被傳遞到父widget中去。
討論到目前爲至,我們都假設基類是QWidget,然而,同樣的規則也可以應用到別的層次中,只要用QWidget
代替基類即可。舉例來說:
 void MyFancyLineEdit::keyPressEvent(QKeyEvent *event)
    {
        if (event->key() == Key_SysReq) {
            doSystemRequest();
        } else {
            QLineEdit::keyPressEvent(event);
        }
    }
由 於某些原因,你會在event()中處理事件,而不是在特定的handler中,如keyPressEvent(),這個過程會有些不同。event() 會返回一個布爾值,來告訴調用者是否事件被accept或ignore,(true表示accept),從event()中調用accept()或是 ignore()是沒有意義的。“Accept”標記是event()與特定事件handler之間的一種通訊機制。而從event()返回的布爾值卻是 用來與QApplication:notify()通訊的。在QWidgetk中缺省的event()實現是轉換“Accept”標記爲一個布爾值,如下 所示:
bool QWidget::event(QEvent *event)
    {
        switch (e->type()) {
        case QEvent::KeyPress:
            keyPressEvent((QKeyEvent *)event);
            if (!((QKeyEvent *)event)->isAccepted())
                return false;
            break;
        case QEvent::KeyRelease:
            keyReleaseEvent((QKeyEvent *)event);
            if (!((QKeyEvent *)event)->isAccepted())
                return false;
            break;
            ...
        }
        return true;
    }

到現在爲至,我們所說的內容不僅僅適用於key事件,也適用於mouse,wheel,tablet,context menu等事件
Close事件有點不同,調用QCloseEvent:ignore()取消了關閉操作,而accept()告訴Qt繼續執行正常的關閉操作。爲了避免混亂,最好是在closeEvent()的新實現中明確地進行accept()與ignore()的調用:
 void MainWindow::closeEvent(QCloseEvent *event)
    {
        if (userReallyWantsToQuit()) {
            event->accept();
        } else {
            event->ignore();
        }
    }


----------------------------------------------

Qt程序是事件驅動的程序的每個動作都是由幕後某個事件所觸發. Qt事件的類型很多常見的qt的事件如下:

<!--[if !supportLists]-->§           <!--[endif]-->鍵盤事件按鍵按下和鬆開.

<!--[if !supportLists]-->§           <!--[endif]-->鼠標事件鼠標移動,鼠標按鍵的按下和鬆開.

<!--[if !supportLists]-->§           <!--[endif]-->拖放事件用鼠標進行拖放.

<!--[if !supportLists]-->§           <!--[endif]-->滾輪事件鼠標滾輪滾動.

<!--[if !supportLists]-->§           <!--[endif]-->繪屏事件重繪屏幕的某些部分.

<!--[if !supportLists]-->§           <!--[endif]-->定時事件定時器到時.

<!--[if !supportLists]-->§           <!--[endif]-->焦點事件鍵盤焦點移動.

<!--[if !supportLists]-->§           <!--[endif]-->進入和離開事件鼠標移入widget之內,或是移出.

<!--[if !supportLists]-->§           <!--[endif]-->移動事件: widget的位置改變.

<!--[if !supportLists]-->§           <!--[endif]-->大小改變事件: widget的大小改變.

<!--[if !supportLists]-->§           <!--[endif]-->顯示和隱藏事件: widget顯示和隱藏.

<!--[if !supportLists]-->§           <!--[endif]-->窗口事件窗口是否爲當前窗口.

 

還有一些非常見的qt事件,比如socket事件,剪貼板事件,字體改變,佈局改變等等.

Qt的事件和Qt中的signal不一樣後者通常用來"使用"widget, 而前者用來"實現" widget. 比如一個按鈕我們使用這個按鈕的時候我們只關心他clicked()signal, 至於這個按鈕如何接收處理鼠標事件,再發射這個信號,我們是不用關心的但是如果我們要重載一個按鈕的時候,我們就要面對event比如我們可以改變它的行爲,在鼠標按鍵按下的時候(mouse press event) 就觸發clicked()signal而不是通常在釋放的( mouse release event)時候.

 

2. 事件產生和處理流程

 

2.1 事件的產生

事件的兩種來源:

       一種是系統產生的;通常是window system把從系統得到的消息,比如鼠標按鍵,鍵盤按鍵等放入系統的消息隊列中. Qt事件循環的時候讀取這些事件,轉化爲QEvent,再依次處理.

       一種是由Qt應用程序程序自身產生的.程序產生事件有兩種方式一種是調用QApplication::postEvent(). 例如QWidget::update()函數,當需要重新繪製屏幕時,程序調用update()函數,new出來一個paintEvent,調用QApplication::postEvent(),將其放入Qt的消息隊列中,等待依次被處理另一種方式是調用sendEvent()函數這時候事件不會放入隊列而是直接被派發和處理, QWidget::repaint()函數用的就是這種方式.

 

       // 自定義事件的時候講述需要注意的時這兩個函數的使用方法不大一樣一個是new, 一個是....

 

2.2 事件的調度

兩種調度方式,一種是同步的一種是異步.

Qt的事件循環是異步的,當調用QApplication::exec(),就進入了事件循環該循環可以簡化的描述爲如下的代碼:

while ( !app_exit_loop ) {

       while( !postedEvents ) {             processPostedEvents()       }

       while( !qwsEvnts ){            qwsProcessEvents();   }

       while( !postedEvents ) {             processPostedEvents()       }

}

先處理Qt事件隊列中的事件直至爲空再處理系統消息隊列中的消息直至爲空在處理系統消息的時候會產生新的Qt事件需要對其再次進行處理.

調用QApplication::sendEvent的時候消息會立即被處理,是同步的實際上QApplication::sendEvent()是通過調用QApplication::notify(), 直接進入了事件的派發和處理環節.

2.3 事件的派發和處理

首先說明Qt中事件過濾器的概念事件過濾器是Qt中一個獨特的事件處理機制功能強大而且使用起來靈活方便通過它可以讓一個對象偵聽攔截另外一個對象的事件事件過濾器是這樣實現的在所有Qt對象的基類: QObject中有一個類型爲QObjectList的成員變量,名字爲eventFilters,當某個QObjec (qobjA)給另一個QObject (qobjB)安裝了事件過濾器之後, qobjB會把qobjA的指針保存在eventFiltersqobjB處理事件之前,會先去檢查eventFilters列表如果非空就先調用列表中對象的eventFilter()函數一個對象可以給多個對象安裝過濾器同樣一個對象能同時被安裝多個過濾器在事件到達之後這些過濾器以安裝次序的反序被調用事件過濾器函數( eventFilter() ) 返回值是bool如果返回true, 則表示該事件已經被處理完畢, Qt將直接返回進行下一事件的處理如果返回false, 事件將接着被送往剩下的事件過濾器或是目標對象進行處理.

Qt,事件的派發是從QApplication::notify() 開始的因爲QAppliction也是繼承自QObject, 所以先檢查QAppliation對象如果有事件過濾器安裝在qApp先調用這些事件過濾器接下來QApplication::notify() 會過濾或合併一些事件(比如失效widget的鼠標事件會被過濾掉而同一區域重複的繪圖事件會被合併). 之後,事件被送到reciver::event() 處理.

同樣reciver::event()先檢查有無事件過濾器安裝在reciever若有則調用之接下來,根據QEvent的類型調用相應的特定事件處理函數一些常見的事件都有特定事件處理函數比如:mousePressEvent(), focusOutEvent(),  resizeEvent(), paintEvent(), resizeEvent()等等在實際應用中經常需要重載這些特定事件處理函數在處理事件但對於那些不常見的事件是沒有相對應的特定事件處理函數的如果要處理這些事件就需要使用別的辦法比如重載event() 函數或是安裝事件過濾器.

事件派發和處理的流程圖如下:

 2.4 事件的轉發

       對於某些類別的事件如果在整個事件的派發過程結束後還沒有被處理那麼這個事件將會向上轉發給它的父widget, 直到最頂層窗口如圖所示事件最先發送給QCheckBox, 如果QCheckBox沒有處理那麼由QGroupBox接着處理如果QGroupBox沒有處理再送到QDialog, 因爲QDialog已經是最頂層widget, 所以如果QDialog不處理, QEvent將停止轉發.       如何判斷一個事件是否被處理了呢? Qt中和事件相關的函數通過兩種方式相互通信. QApplication::notify(), QObject::eventFilter(), QObject::event() 通過返回bool值來表示是否已處理. “表示已經處理, “表示事件需要繼續傳遞另一種是調用QEvent::ignore()  QEvent::accept() 對事件進行標識這種方式只用於event() 函數和特定事件處理函數之間的溝通而且只有用在某些類別事件上是有意義的,這些事件就是上面提到的那些會被轉發的事件包括鼠標滾輪按鍵等事件.

3. 實際運用

 

根據對Qt事件機制的分析我們可以得到5種級別的事件過濾,處理辦法以功能從弱到強排列如下:

3.1 重載特定事件處理函數.

最常見的事件處理辦法就是重載象mousePressEvent(), keyPressEvent(), paintEvent() 這樣的特定事件處理函數以按鍵事件爲例一個典型的處理函數如下:

void imageView::keyPressEvent(QKeyEvent * event)

{

switch (event->key()) {

case Key_Plus:

zoomIn();

break;

case Key_Minus:

zoomOut();

break;

case Key_Left:

// …

default:

QWidget::keyPressEvent(event);

}

}

      

3.2重載event()函數.

通過重載event()函數,我們可以在事件被特定的事件處理函數處理之前(keyPressEvent())處理它比如當我們想改變tab鍵的默認動作時,一般要重載這個函數在處理一些不常見的事件(比如:LayoutDirectionChange),evnet()也很有用,因爲這些函數沒有相應的特定事件處理函數當我們重載event()函數時需要調用父類的event()函數來處理我們不需要處理或是不清楚如何處理的事件.

下面這個例子演示瞭如何重載event()函數改變Tab鍵的默認動作: (默認的是鍵盤焦點移動到下一個控件上. )

bool CodeEditor::event(QEvent * event)

{

if (event->type() == QEvent::KeyPress) {

QKeyEvent *keyEvent = (QKeyEvent *) event;

if (keyEvent->key() == Key_Tab) {

insertAtCurrentPosition('\t');

return true;

}

}

return QWidget::event(event);

}

3.3 Qt對象上安裝事件過濾器.

安裝事件過濾器有兩個步驟: (假設要用A來監視過濾B的事件)

首先調用BinstallEventFilter( const QOject *obj ), A的指針作爲參數這樣所有發往B的事件都將先由AeventFilter()處理.

 然後, A要重載QObject::eventFilter()函數eventFilter() 中書寫對事件進行處理的代碼.

用這種方法改寫上面的例子: (假設我們將CodeEditor 放在MainWidget)

MainWidget::MainWidget()

{

       // …

CodeEditor * ce = new CodeEditor( this, “code editor”);

ce->installEventFilter( this );

// …

}

bool MainWidget::eventFilter( QOject * target , QEvent * event )

{

       if( target == ce ){

              if( event->type() == QEvent::KeyPress ) {

                     QKeyEvent *ke = (QKeyEvent *) event;

                     if( ke->key() == Key_Tab ){

ce->insertAtCurrentPosition('\t');

return true;

                     }

              }

       }

       return false;

}

3.4 QAppliction對象安裝事件過濾器.

一旦我們給qApp(每個程序中唯一的QApplication對象)裝上過濾器,那麼所有的事件在發往任何其他的過濾器時,都要先經過當前這個eventFilter(). debug的時候,這個辦法就非常有用也常常被用來處理失效了的widget的鼠標事件,通常這些事件會被QApplication::notify()丟掉. ( QApplication::notify() 是先調用qApp的過濾器再對事件進行分析以決定是否合併或丟棄)

4.5 繼承QApplication,並重載notify()函數.

Qt是用QApplication::notify()函數來分發事件的.想要在任何事件過濾器查看任何事件之前先得到這些事件,重載這個函數是唯一的辦法通常來說事件過濾器更好用一些因爲不需要去繼承QApplication而且可以給QApplication對象安裝任意個數的事件。

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