1. 程式人生 > >Qt消息機制和事件

Qt消息機制和事件

參數 question 建立 idg 鍵盤 exe mes ber 也不能

Qt消息機制和事件

1 事件

事件(event)是由系統或者 Qt 本身在不同的時刻發出的。當用戶按下鼠標、敲下鍵盤,或者是窗口需要重新繪制的時候,都會發出一個相應的事件。一些事件在對用戶操作做出響應時發出,如鍵盤事件等;另一些事件則是由系統自動發出,如計時器事件。

在前面我們也曾經簡單提到,Qt 程序需要在main()函數創建一個QApplication對象,然後調用它的exec()函數。這個函數就是開始 Qt 的事件循環。在執行exec()函數之後,程序將進入事件循環來監聽應用程序的事件。當事件發生時,Qt 將創建一個事件對象。Qt 中所有事件類都繼承於QEvent。在事件對象創建完畢後,Qt 將這個事件對象傳遞給QObject的event()函數。event()函數並不直接處理事件,而是按照事件對象的類型分派給特定的事件處理函數

(event handler),關於這一點,會在後邊詳細說明。

在所有組件的父類QWidget中,定義了很多事件處理的回調函數,如

keyPressEvent()

keyReleaseEvent()

mouseDoubleClickEvent()

mouseMoveEvent()

mousePressEvent()

mouseReleaseEvent() 等。

這些函數都是 protected virtual 的,也就是說,我們可以在子類中重新實現這些函數。下面來看一個例子:

class EventLabel : public QLabel

{

protected:

void mouseMoveEvent(QMouseEvent *event);

void mousePressEvent(QMouseEvent *event);

void mouseReleaseEvent(QMouseEvent *event);

};

void EventLabel::mouseMoveEvent(QMouseEvent *event)

{

this->setText(QString("<center><h1>Move: (%1, %2)

</h1></center>").arg(QString::number(event->x()),

QString::number(event->y())));

}

void EventLabel::mousePressEvent(QMouseEvent *event)

{

this->setText(QString("<center><h1>Press:(%1, %2)

</h1></center>").arg(QString::number(event->x()),

QString::number(event->y())));

}

void EventLabel::mouseReleaseEvent(QMouseEvent *event)

{

QString msg;

msg.sprintf("<center><h1>Release: (%d, %d)</h1></center>",

event->x(), event->y());

this->setText(msg);

}

int main(int argc, char *argv[])

{

QApplication a(argc, argv);

EventLabel *label = new EventLabel;

label->setWindowTitle("MouseEvent Demo");

label->resize(300, 200);

label->show();

return a.exec();

}

EventLabel繼承了QLabel,覆蓋了mousePressEvent()、mouseMoveEvent()和MouseReleaseEvent()三個函數。我們並沒有添加什麽功能,只是在鼠標按下(press)、鼠標移動(move)和鼠標釋放(release)的時候,把當前鼠標的坐標值顯示在這個Label上面。由於QLabel是支持 HTML 代碼的,因此我們直接使用了 HTML 代碼來格式化文字。

QString的arg()函數可以自動替換掉QString中出現的占位符。其占位符以 % 開始,後面是占位符的位置,例如 %1,%2 這種。

QString("[%1, %2]").arg(x).arg(y);

語句將會使用x替換 %1,y替換 %2,因此,生成的QString為[x, y]。

在mouseReleaseEvent()函數中,我們使用了另外一種QString的構造方法。我們使用類似 C 風格的格式化函數sprintf()來構造QString。

運行上面的代碼,當我們點擊了一下鼠標之後,label 上將顯示鼠標當前坐標值。

技術分享

為什麽要點擊鼠標之後才能在mouseMoveEvent()函數中顯示鼠標坐標值?

這是因為QWidget中有一個mouseTracking屬性,該屬性用於設置是否追蹤鼠標。只有鼠標被追蹤時,mouseMoveEvent()才會發出。如果mouseTracking是 false(默認即是),組件在至少一次鼠標點擊之後,才能夠被追蹤,也就是能夠發出mouseMoveEvent()事件。如果mouseTracking為 true,則mouseMoveEvent()直接可以被發出。

知道了這一點,我們就可以在main()函數中添加如下代碼:

label->setMouseTracking(true);

在運行程序就沒有這個問題了。

2 事件的接受與忽略

前邊我們介紹了有關事件的相關內容。我們曾經提到,事件可以依情況接受和忽略。現在,我們就來了解下有關事件的更多的知識。

首先來看一段代碼:

//!!! Qt5

// ---------- custombutton.h ---------- //

class CustomButton : public QPushButton

{

Q_OBJECT

public:

CustomButton(QWidget *parent = 0);

private:

void onButtonCliecked();

};

// ---------- custombutton.cpp ---------- //

CustomButton::CustomButton(QWidget *parent) :

QPushButton(parent)

{

connect(this, &CustomButton::clicked,

this, &CustomButton::onButtonCliecked);

}

void CustomButton::onButtonCliecked()

{

qDebug() << "You clicked this!";

}

// ---------- main.cpp ---------- //

int main(int argc, char *argv[])

{

QApplication a(argc, argv);

CustomButton btn;

btn.setText("This is a Button!");

btn.show();

return a.exec();

}

這是一段簡單的代碼,經過我們前面一段時間的學習,我們已經能夠知道這段代碼的運行結果:點擊按鈕,會在控制臺打印出“You clicked this!”字符串。這是我們前面介紹過的內容。下面,我們向CustomButton類添加一個事件函數:

// CustomButton

...

protected:

void mousePressEvent(QMouseEvent *event);

...

// ---------- custombutton.cpp ---------- //

...

void CustomButton::mousePressEvent(QMouseEvent *event)

{

if (event->button() == Qt::LeftButton)

{

qDebug() << "left";

}

else

{

QPushButton::mousePressEvent(event);

}

}

我們重寫了CustomButton的mousePressEvent()函數,也就是鼠標按下。在這個函數中,我們判斷如果鼠標按下的是左鍵,則打印出來“left”字符串,否則,調用父類的同名函數。編譯運行這段代碼,當我們點擊按鈕時,“You clicked this!”字符串不再出現,只有一個“left”。也就是說,我們把父類的實現覆蓋掉了。由此可以看出,父類QPushButton的mousePressEvent()函數中肯定發出了clicked()信號,否則的話,我們的槽函數怎麽會不執行了呢?這暗示我們一個非常重要的細節:當重寫事件回調函數時,時刻註意是否需要通過調用父類的同名函數來確保原有實現仍能進行!比如我們的CustomButton了,如果像我們這麽覆蓋函數,clicked()信號永遠不會發生,你連接到這個信號的槽函數也就永遠不會被執行。這個錯誤非常隱蔽,很可能會浪費你很多時間才能找到。因為這個錯誤不會有任何提示。這一定程度上說,我們的組件“忽略”了父類的事件,但這更多的是一種違心之舉,一種錯誤。

通過調用父類的同名函數,我們可以把 Qt 的事件傳遞看成鏈狀:如果子類沒有處理這個事件,就會繼續向其父類傳遞。Qt 的事件對象有兩個函數:accept()和ignore()。正如它們的名字一樣,

accept()用來告訴 Qt,這個類的事件處理函數想要處理這個事件;

如果一個事件處理函數調用了一個事件對象的accept()函數,這個事件就不會被繼續傳播給其父組件

ignore()則告訴 Qt,這個類的事件處理函數不想要處理這個事件。

如果調用了事件的ignore()函數,Qt 會從其父組件中尋找另外的接受者

在事件處理函數中,可以使用isAccepted()來查詢這個事件是不是已經被接收了。

事實上,我們很少會使用accept()和ignore()函數,而是像上面的示例一樣,如果希望忽略事件(所謂忽略,是指自己不想要這個事件),只要調用父類的響應函數即可。為了避免自己去調用accept()和ignore()函數,而是盡量調用父類實現,Qt 做了特殊的設計:事件對象默認是 accept 的,而作為所有組件的父類QWidget的默認實現則是調用ignore()。這麽一來,如果你自己實現事件處理函數,不調用QWidget的默認實現,你就等於是接受了事件;如果你要忽略事件,只需調用QWidget的默認實現。

事件的傳播是在組件層次上面的,而不是依靠類繼承機制。

在一個特殊的情形下,我們必須使用accept()和ignore()函數,那就是窗口關閉的事件。對於窗口關閉QCloseEvent事件,調用accept()意味著 Qt 會停止事件的傳播,窗口關閉;調用ignore()則意味著事件繼續傳播,即阻止窗口關閉。我們再closeEvent函數中做如下處理:

void MainWindow::closeEvent(QCloseEvent *event)

{

bool exit = QMessageBox::question(this,

tr("Quit"),

tr("Are you sure to quit this application?"),

QMessageBox::Yes | QMessageBox::No,

QMessageBox::No) == QMessageBox::Yes);

if (exit)

{

event->accept();

}

else

{

event->ignore();

}

}

在這個函數中,關閉窗口會彈出詢問框,問一下是否要退出。如果用戶點擊了“Yes”,則接受關閉事件,這個事件所在的操作就是關閉窗口。因此,一旦接受事件,窗口就會被關閉;否則窗口繼續保留。

3 event()

事件對象創建完畢後,Qt 將這個事件對象傳遞給QObject的event()函數。event()函數並不直接處理事件,而是將這些事件對象按照它們不同的類型,分發給不同的事件處理器(event handler)。

如上所述,event()函數主要用於事件的分發。所以,如果你希望在事件分發之前做一些操作,就可以重寫這個event()函數了。例如,我們希望在一個QWidget組件中監聽 tab 鍵的按下,那麽就可以繼承QWidget,並重寫它的event()函數,來達到這個目的:

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);

}

CustomWidget是一個普通的QWidget子類。我們重寫了它的event()函數,這個函數有一個QEvent對象作為參數,也就是需要轉發的事件對象。函數返回值是 bool 類型。

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

在event()函數中,調用事件對象的accept()和ignore()函數是沒有作用的,不會影響到事件的傳播

我們可以通過使用QEvent::type()函數可以檢查事件的實際類型,其返回值是QEvent::Type類型的枚舉。我們處理過自己感興趣的事件之後,可以直接返回 true,表示我們已經對此事件進行了處理;對於其它我們不關心的事件,則需要調用父類的event()函數繼續轉發,否則這個組件就只能處理我們定義的事件了。為了測試這一種情況,我們可以嘗試下面的代碼:

bool CustomTextEdit::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 false;

}

CustomTextEdit是QTextEdit的一個子類。我們重寫了其event()函數,卻沒有調用父類的同名函數。這樣,我們的組件就只能處理 Tab 鍵,再也無法輸入任何文本,也不能響應其它事件,比如鼠標點擊之後也不會有光標出現。這是因為我們只處理的KeyPress類型的事件,並且如果不是KeyPress事件,則直接返回 false,鼠標事件根本不會被轉發,也就沒有了鼠標事件。

通過查看QObject::event()的實現,我們可以理解,event()函數同前面的章節中我們所說的事件處理器有什麽聯系:

//!!! Qt5

bool QObject::event(QEvent *e)

{

switch (e->type()) {

case QEvent::Timer:

timerEvent((QTimerEvent*)e);

break;

case QEvent::ChildAdded:

case QEvent::ChildPolished:

case QEvent::ChildRemoved:

childEvent((QChildEvent*)e);

break;

// ...

default:

if (e->type() >= QEvent::User) {

customEvent(e);

break;

}

return false;

}

return true;

}

這是 Qt 5 中QObject::event()函數的源代碼(Qt 4 的版本也是類似的)。我們可以看到,同前面我們所說的一樣,Qt 也是使用QEvent::type()判斷事件類型,然後調用了特定的事件處理器。比如,如果event->type()返回值是QEvent::Timer,則調用timerEvent()函數。可以想象,QWidget::event()中一定會有如下的代碼:

switch (event->type()) {

case QEvent::MouseMove:

mouseMoveEvent((QMouseEvent*)event);

break;

// ...

}

事實也的確如此。timerEvent()和mouseMoveEvent()這樣的函數,就是我們前面章節所說的事件處理器 event handler。也就是說,event()函數中實際是通過事件處理器來響應一個具體的事件。這相當於event()函數將具體事件的處理“委托”給具體的事件處理器。而這些事件處理器是 protected virtual 的,因此,我們重寫了某一個事件處理器,即可讓 Qt 調用我們自己實現的版本。

由此可以見,event()是一個集中處理不同類型的事件的地方。如果你不想重寫一大堆事件處理器,就可以重寫這個event()函數,通過QEvent::type()判斷不同的事件。鑒於重寫event()函數需要十分小心註意父類的同名函數的調用,一不留神就可能出現問題,所以一般還是建議只重寫事件處理器(當然,也必須記得是不是應該調用父類的同名處理器)。這其實暗示了event()函數的另外一個作用:屏蔽掉某些不需要的事件處理器。正如我們前面的CustomTextEdit例子看到的那樣,我們創建了一個只能響應 tab 鍵的組件。這種作用是重寫事件處理器所不能實現的。

4 事件過濾器

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

通過前面的章節,我們已經知道,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添加事件過濾器。這種全局的事件過濾器將會在所有其它特性對象的事件過濾器之前調用。盡管很強大,但這種行為會嚴重降低整個應用程序的事件分發效率。因此,除非是不得不使用的情況,否則的話我們不應該這麽做。

註意,

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

5 總結

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是單例的)。

Qt消息機制和事件