筆者用Qt算是用了挺長時間了,當初入C++的坑就是因為需要用Qt設計上位機軟件?,F在打算總結一下一些當初覺得有點深度的知識點,其中我覺得Qt最需要花事件理解的就是Qt的事件機制了。
1.簡述
個人認為,事件機制是Qt最難以理解且最為精妙的一部分。事件主要分為兩種:
- 在與用戶交互時發生。比如按下鼠標(mousePressEvent),敲擊鍵盤(keyPressEvent)等。
- 系統自動發生,比如計時器事件(timerEvent)等。
在發生事件時(比如說上面說的按下鼠標),就會產生一個QEvent對象(這里是QMouseEvent,為QEvent的子類),這個QEvent對象會傳給當前組件的event函數。如果當前組件沒有安裝事件過濾器(這個下文會提到),則會被event函數發放到相應的xxxEvent函數中(這里是mousePressEvent函數)。
Qt中所有的事件類都繼承于QEvent類
這個QEvent對象會有各種各樣的屬性,這是由用戶與界面交互時產生的。xxxEvent函數可以對其進行不同的處理(比如說是鼠標左鍵按下還是右鍵?)。查看幫助文檔,可以看到QMouseEvent類有以下枚舉。
那么就可以在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;
}
在這個例子中,因為沒有調用父類QTextEdit的event函數,所以只能處理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的事件機制時,應該按照以下思路進行
- 重寫paintEvent、mousePressEvent等事件處理函數。這是最普通、最簡單的形式,同時功能也最簡單。
- 重寫event函數。event函數是所有對象的事件入口,QObject和QWidget中的實現,默認是把事件傳遞給特定的事件處理函數。
- 在特定對象上面安裝事件過濾器。該過濾器僅過濾該對象接收到的事件。
- 在QCoreApplication::instance()上面安裝事件過濾器。該過濾器將過濾所有對象的所有事件,但會有多線程問題。
- 重寫QCoreApplication::notify()函數。這是最強大的,和全局事件過濾器一樣提供完全控制,并且不受線程的限制。
Reference:
傳智播客 Qt教程
Qt學習之路27--事件傳遞過程和事件過濾器