參考:
- (35條訊息) Qt事件回圈及QEventLoop的使用_kupeThinkPoem的博客-CSDN博客
- (35條訊息) Qt訊息機制:事件分發和事件過濾_qt 訊息過濾_SOC羅三炮的博客-CSDN博客
Qt 事件系統總結
Qt 事件
-
在 Qt 中,事件(event)是一些物件,它們都派生自抽象類
QEvent
-
事件是應用程式所關心的,程式內部發生的事或是外部行動的結果
-
當一個事件發生,Qt 會創建一個事件物件,它是一個派生自抽象類
QEvent
的類的實體,用來代表發生的事件 -
有時一個事件包含多個事件型別,比如滑鼠事件
QMouseEvent
又可以分為滑鼠按下、雙擊、滾輪滾動和移動等多種操作 -
事件由誰接收:事件可以被任何派生自
QObject
的型別的實體接收和處理- QObject 類的三大核心功能其中之一就是:事件處理,
QObject
通過event()
函式獲取和分發事件,
- QObject 類的三大核心功能其中之一就是:事件處理,
-
事件由誰產生:
- 由作業系統或應用程式內部產生
- 使用
bool QEvent::spontaneous() const
判斷事件是否來自于應用程式外部,如果事件來自于外部回傳 true,否則回傳 false
Qt 事件回圈
主事件回圈
- 每一個 Qt 程式,main 函式中一般都有唯一的 QCoreApplication/QGuiApplication/QApplication,并在末尾呼叫
exec()
,這樣就開始 Qt 的事件回圈 - 事件回圈的本質是無限回圈,使用
exec()
開啟事件回圈,如果事件回圈不結束,exec()
后面的代碼永遠不會執行, - 在執行
exec()
函式之后,程式將進入事件回圈來監聽應用程式的事件,事件多數情況下是被分發到一個佇列中(事件佇列),當佇列中有事件時就不停的將佇列中的事件發送給QObject
物件,當佇列為空時就回圈等待事件, - 當事件發生時,Qt 將創建一個事件物件,Qt 中所有事件類都繼承于
QEvent
,這也是事件不同于信號(信號與槽中的信號)的一點 —— 事件是類具有特定型別, 而信號是信號函式 QCoreApplication
中提供了一下處理事件的函式:
/// 給任何執行緒的任何物件發送任何事件都會呼叫該函式,可以重寫該函式來達到全域的事件處理與控制的功能,
[virtual] bool QCoreApplication::notify(QObject *receiver, QEvent *event)
/// 直接使用 notify() 將事件發送給事件的接收者,回傳事件處理程式回傳的值,事件被發送后并不會被自動被銷毀,因此事件物件常常可以宣告在堆疊上作為自動變數,
[static] bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
/// 添加事件到事件佇列然后立即回傳,事件必須宣告在堆上,當控制回傳到主事件回圈時,所有存盤在事件佇列中的事件都將使用 notify 函式發送出去,
/// 事件按優先級排隊,高優先級的事件先入隊,事件優先級是一個整機變數,
/// 函式是【執行緒安全】的
[static] void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority = Qt::NormalEventPriority)
/// 立即分派在事件佇列中的所有事件接收物件為 receiver 事件型別為 event_type 的事件,
/// 如果 receiver = nullptr ,所有事件型別為 event_type 都會被立即發送給接收者
/// 如果 event_type = 0, 所有發送給 receiver 的事件都會被立即發送給它
[static] void QCoreApplication::sendPostedEvents(QObject *receiver = nullptr, int event_type = 0)
/// 告訴應用以指定的回傳碼退出事件回圈,exec() 將結束并回傳該回傳碼,任何非零回傳碼意味著錯誤,
[static] void QCoreApplication::exit(int returnCode = 0)
/// 告訴應用正常退出事件回圈,相當于 exit(0),通常信號與該槽應該進行[佇列連接],因為如果在主事件回圈開始之前,信號發送導致的 quit() 回呼是無效的(事件回圈沒有開始,何談退出)
/// 使用佇列連接確保槽函式不會再事件回圈開始前執行,
[static slot] void QCoreApplication::quit()
QEventLoop 類
/// 開啟事件回圈
int exec(QEventLoop::ProcessEventsFlags flags = AllEvents)
void exit(int returnCode = 0)
/// 如果事件回圈是運行著的,回傳 true,否則回傳 false,事件回圈在 exec() 和 exit() 之間被認為是運行的
bool isRunning() const
[slot] void QEventLoop::quit()
-
事件回圈是可以嵌套的,當在子事件回圈中的時候,父事件回圈中的事件實際上處于中斷狀態,這就相當于回圈嵌套,
-
當子事件回圈結束,exec() 回傳之后才可以執行父回圈中的事件,當然,這不代表在執行子回圈的時候,類似父回圈中的界面回應會被中斷,因為往往子回圈中也會有父回圈的大部分事件,執行QMessageBox::exec(),QEventLoop::exec()的時候,雖然這些exec()打斷了main()中的QApplication::exec(),但是由于GUI界面的回應已經被包含到子回圈中了,所以GUI界面依然能夠得到回應,
-
如果某個子事件回圈仍然有效,但其父回圈被強制跳出,此時父回圈不會立即執行跳出,而是等待子事件回圈跳出后,父回圈才會跳出,
事件的轉發與處理流程
Qt 程式需要在 main()
函式創建一個 QApplication
物件,然后呼叫它的 exec()
函式,這個函式就是開始 Qt 的事件回圈,在執行 exec()
函式之后,程式將進入事件回圈來監聽應用程式的事件,


同步與異步事件
- 同步事件: 呼叫
QCoreApplication::sendEvent()
,會直接使用QCoreApplication::notify
將事件發送給事件接收方,事件會立即被執行, - 異步事件:呼叫
QCoreApplication::postEvent()
, 會將事件加入到事件佇列,等待事件回圈進行處理,
事件分發器
Qt 中每個事件型別都有一個列舉型別 QEvent::Type
的資料成員,通過該列舉型別,在程式中可以區分不同的事件型別,根據不同的事件型別進行不同的動作,如下即為 QObject::event
的原始碼:
- 事件分發器根據事件的不同,將事件發送給不同的事件處理器進行處理,因此可以通過重寫事件處理器函式,讓指定事件發生發生時,執行我們想要的事件處理動作,
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;
case QEvent::DeferredDelete:
qDeleteInEventHandler(this);
break;
case QEvent::MetaCall:
{
QAbstractMetaCallEvent *mce = static_cast<QAbstractMetaCallEvent*>(e);
if (!d_func()->connections.loadRelaxed()) {
QBasicMutexLocker locker(signalSlotLock(this));
d_func()->ensureConnectionData();
}
QObjectPrivate::Sender sender(this, const_cast<QObject*>(mce->sender()), mce->signalId());
mce->placeMetaCall(this);
break;
}
case QEvent::ThreadChange: {
Q_D(QObject);
QThreadData *threadData = https://www.cnblogs.com/Critical-Thinking/archive/2023/06/12/d->threadData.loadRelaxed();
QAbstractEventDispatcher *eventDispatcher = threadData->eventDispatcher.loadRelaxed();
if (eventDispatcher) {
QList timers = eventDispatcher->registeredTimers(this);
if (!timers.isEmpty()) {
// do not to release our timer ids back to the pool (since the timer ids are moving to a new thread).
eventDispatcher->unregisterTimers(this);
QMetaObject::invokeMethod(this,"_q_reregisterTimers", Qt::QueuedConnection,
Q_ARG(void*, (new QList<QAbstractEventDispatcher::TimerInfo>(timers))));
}
}
break;
}
default:
if (e->type() >= QEvent::User) {
customEvent(e);
break;
}
return false;
}
return true;
}
- 如果希望在事件分發之前做一些操作,就可以重寫這個
event()
函式, - 如果傳入的事件已被識別并且處理,則需要回傳 true,否則回傳 false,如果回傳值是 true,那么 Qt 會認為這個事件已經處理完畢,不會再將這個事件發送給其它物件,而是會繼續處理事件佇列中的下一事件
???在
event()
函式中,呼叫事件物件的accept()
和ignore()
函式是沒有作用的,不會影響到事件的傳播
事件過濾器
事件過濾器可以對其他組件接收到的事件進行監控
事件過濾器使用步驟如下:
-
創建一個事件過濾器
-
任意的
QObject
物件都可以作為事件過濾器使用 -
事件過濾器物件需要重寫
eventFilter()
函式eventFilter()
中可以決定是否將事件傳遞給組件物件,事件處理程式也可以提前寫在事件過濾器中,- 不讓事件繼續轉發回傳 true, 否則回傳 false
-
-
被監控物件安裝事件過濾器
void QObject::installEventFilter(QObject *filterObj)
事件過濾器的呼叫時間是目標物件(也就是 eventFilter()
引數里面的 watched 物件)接收到事件物件之前,如果事件被過濾掉(回傳 true) 那么組件物件就不會收到該事件,
??? 事件過濾器和被安裝過濾器的組件必須在同一執行緒,否則,過濾器將不起作用,另外,如果在安裝過濾器之后,這兩個組件到了不同的執行緒,那么,只有等到二者重新回到同一執行緒的時候過濾器才會有效
全域事件過濾器 在 QAppliaction::instance()
或 QCoreApplication::instance()
上安裝事件過濾器,那么任何事件在通過 notify()
函式發送給其他物件之前都要先傳給事件過濾器,
Qt 事件處理的 5 個層次
- 重寫
paintEvent()
、mousePressEvent()
等事件處理函式,最普通、最簡單的形式, - 重寫
event()
函式,event()
是任何 Qt 物件的所有事件的入口,默認是根據事件型別的不同將事件分發給不同的事件處理函式 - 在特定物件上安裝事件過濾器,該事件過濾器僅過濾該物件接收到的事件
- 使用全域事件過濾器,在
QAppliaction::instance()
或QCoreApplication::instance()
上安裝事件過濾器,事件過濾器可以安裝多個(多個事件過濾器會按安裝順序逆序激活),相比重寫notify()
更加靈活,全域過濾器有一個問題:只能用在主執行緒, - 重寫
QCoreApplication::notify()
這是最強大的,和全域事件過濾器一樣提供完全控制,并且不受執行緒的限制,但是全域范圍內只能有一個被使用(因為QCoreApplication是單例的),
事件(QEvent)與信號(SIGNAL)的區別
事件 | 信號 | |
---|---|---|
本質區別 | 事件是物件,都是派生自 QEvent 的類的實體 |
信號是QObject 或是其派生類的函式成員 |
與 QObject 的關系 | 事件由 QObject 及其派生類的實體物件接收并進行處理 |
信號由QObject 或是其派生類的實體物件發出(emit ) |
對程式影響 | 改寫事件處理函式可能導致程式行為發生改變 | 如果將信號與不同的槽函式連接,會導致不同的行為 |
- 兩者的聯系:
- 事件的轉發和處理,信號與槽實作的物件間通訊,都依靠于 QObject,都依靠 Qt 的事件回圈,
- 一些信號,是在事件處理函式中發出的,
QPushButton 事件處理分析
示例代碼:
#ifndef MYAPPLICATION_H
#define MYAPPLICATION_H
#include <QApplication>
class MyApplication : QApplication
{
Q_OBJECT
public:
MyApplication(int &argc, char **argv);
bool notify(QObject *receiver, QEvent *event) override;
void installEventFilter(QObject *filter);
int exec();
protected:
bool event(QEvent *e) override;
};
#endif // MYAPPLICATION_H
#include "myapplication.h"
#include "qdebug.h"
MyApplication::MyApplication(int &argc, char **argv)
: QApplication{argc, argv}
{
}
bool MyApplication::notify(QObject *receiver, QEvent *event)
{
if(event->type() == QEvent::MouseButtonPress){
qDebug() << "MyApplication::notify():發布滑鼠按下事件給型別為 " << receiver->metaObject()->className()
<< " 的物件;";
}
return QApplication::notify(receiver, event);
}
void MyApplication::installEventFilter(QObject *filter)
{
QApplication* a = static_cast<QApplication*>(this);
a->installEventFilter(filter);
}
int MyApplication::exec()
{
return QApplication::exec();
}
bool MyApplication::event(QEvent *e)
{
if(e->type() == QEvent::MouseButtonPress){
qDebug() << "MyApplication::event(): 分發滑鼠按下事件";
}
return QApplication::event(e);
}
#ifndef MYBUTTON_H
#define MYBUTTON_H
#include <QPushButton>
class MyButton : public QPushButton
{
Q_OBJECT
public:
MyButton(QWidget* parent = nullptr);
MyButton(QString const& text, QWidget* parent = nullptr);
protected:
void mousePressEvent(QMouseEvent *e) override;
bool event(QEvent* e) override;
};
#endif // MYBUTTON_H
#include "mybutton.h"
#include <QDebug>
#include <QEvent>
#include <QMouseEvent>
MyButton::MyButton(QWidget* parent)
: QPushButton{parent}
{
}
MyButton::MyButton(const QString &text, QWidget *parent)
: QPushButton{text, parent}
{
}
void MyButton::mousePressEvent(QMouseEvent *e)
{
qDebug() << "MyButton::mousePressEvent() 按鈕按下事件被處理";
QPushButton::mousePressEvent(e);// 默認的事件處理函式中,會發出信號: emit pressed()
qDebug() << "槽函式回呼回傳后,執行發送信號(emit)后面的代碼";
}
bool MyButton::event(QEvent *e)
{
if(e->type() == QEvent::MouseButtonPress){
qDebug() << "MyButton::event(): 按鈕點擊事件被分發";
}
return QPushButton::event(e);
}
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "mybutton.h"
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
protected:
bool event(QEvent *e) override;
bool eventFilter(QObject* watched, QEvent *event) override;
private:
MyButton *button;
private slots:
void buttonPressedSlot();
};
#endif // WIDGET_H
#include "widget.h"
#include <QEvent>
#include <QMouseEvent>
#include <QDebug>
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
button = new MyButton(">>> 按鈕 <<<",this);
button->installEventFilter(this);
connect(button, &MyButton::pressed, this, &Widget::buttonPressedSlot);
}
Widget::~Widget()
{
}
bool Widget::event(QEvent *e)
{
if(e->type() == QEvent::MouseButtonPress){
qDebug() << "Widget::event(): 按鈕按下事件被分發";
}
return QWidget::event(e);
}
bool Widget::eventFilter(QObject* watched, QEvent *event)
{
if(watched == this->button && event->type() == QEvent::MouseButtonPress){
qDebug() << "在滑鼠按下事件發送給 button 之前,事件過濾器 Widget::eventFilter() 先對事件進行處理";
}
return QWidget::eventFilter(watched, event);
}
void Widget::buttonPressedSlot(){
qDebug() << "Widget::buttonPressedSlot(): 按下按鈕的槽函式被呼叫";
}
#include "widget.h"
#include "myapplication.h"
#include <QDebug>
#include <memory.h>
class GlobalFilter : public QObject
{
public:
GlobalFilter(QObject *parent = nullptr) : QObject{parent}{}
protected:
bool eventFilter(QObject* watched, QEvent *event) override{
if(event->type() == QEvent::MouseButtonPress){
qDebug() << "在滑鼠按下事件發送給型別為 " << watched->metaObject()->className()
<< " 的物件之前,全域事件過濾器 GlobalFilter::eventFilter() 先對事件進行處理;";
}
return QObject::eventFilter(watched, event);
}
};
int main(int argc, char *argv[])
{
MyApplication a(argc, argv);
std::unique_ptr<GlobalFilter> uptr_filter(new GlobalFilter);
a.installEventFilter(uptr_filter.get());
Widget w;
w.show();
return a.exec();
}

輸出:
# 點擊按鈕輸出如下資訊:
MyApplication::notify():發布滑鼠按下事件給型別為 QWidgetWindow 的物件;
在滑鼠按下事件發送給型別為 QWidgetWindow 的物件之前,全域事件過濾器 GlobalFilter::eventFilter() 先對事件進行處理;
MyApplication::notify():發布滑鼠按下事件給型別為 MyButton 的物件;
在滑鼠按下事件發送給型別為 MyButton 的物件之前,全域事件過濾器 GlobalFilter::eventFilter() 先對事件進行處理;
在滑鼠按下事件發送給 button 之前,事件過濾器 Widget::eventFilter() 先對事件進行處理
MyButton::event(): 按鈕點擊事件被分發
MyButton::mousePressEvent() 按鈕按下事件被處理
Widget::buttonPressedSlot(): 按下按鈕的槽函式被呼叫
槽函式回呼回傳后,執行發送信號(emit)后面的代碼
示例:查看圖片的簡單應用
功能如下:
- 選擇和打開圖片:點擊按鈕,選擇打開一個圖片
- 縮放圖片:通過滑鼠滾輪能夠縮放圖片,縮放中心為圖片繪制視窗的幾何中心
- 相關事件:
wheelEvent()
,QEvent::Resize
,QEvent::Paint
- 相關事件:
- 拖動圖片:通過滑鼠左鍵按住來拖動圖片
- 相關事件:
mousePressEvent()
,mouseMoveEvent()
,QEvent::Paint
- 相關事件:

#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
protected:
/// 重寫事件過濾器,在事件過濾器中來處理子視窗的繪圖事件
/// 默認的事件過濾器默會把把父視窗下子控制元件的繪圖事件過濾掉,因此重新父視窗的 paintEvent 是無法在子控制元件上繪圖的,
/// 因此,直接在事件過濾器中處理繪圖事件
bool eventFilter(QObject *watched, QEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
private:
/// 設定圖片的縮放中心
/// 輸入縮放中心在圖窗中的坐標,來求更新 zoomCenter 和 zoomCenterPic
void setZoomCenter(const QPointF& zoomCenter);
/// 因為圖片縮放默認以圖片坐標系原點(圖片左上角頂點)為縮放中心
/// 為了以 zoomCenter 為縮放中心,需要修改圖片的繪制位置,
/// 通過縮放 + 移動使得圖片相當于以指定的縮放中心縮放
void correctImagPosition();
Ui::Widget *ui;
QPixmap pixmap;
double scaleFactor; ///< 縮放因子
QPointF zoomCenter; ///< 圖片縮放中心在圖窗坐標系下的坐標
QPointF zoomCenterPic; ///< 當縮放因子為 1 時,縮放中心相對于圖片坐標系的坐標
QPointF posit; ///< 圖片繪制位置(為圖片左上角頂點在圖窗中的坐標)
bool isImgError; ///< 讀取圖片是否錯誤的標志
QPoint lastDragPos; ///< 暫存滑鼠最新的拖動位置
private slots:
void openImg();
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include <QFileDialog>
#include <QPainter>
#include <QMouseEvent>
#include <cmath>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
, scaleFactor(1)
, isImgError(false)
{
ui->setupUi(this);
ui->widget->installEventFilter(this);// 為圖片顯示視窗安裝事件過濾器
connect(ui->pushButton, &QPushButton::clicked, this, &Widget::openImg);
}
Widget::~Widget()
{
delete ui;
}
bool Widget::eventFilter(QObject *watched, QEvent *event)
{
if(watched == ui->widget && event->type() == QEvent::Paint){
// 處理子視窗 ui->widget 的 Paint 事件
QPainter painter(ui->widget);
if(pixmap.isNull()){
painter.fillRect(rect(), Qt::black);
painter.setPen(Qt::white);
if(isImgError){
painter.drawText(rect(), Qt::AlignCenter, tr("無法打開圖片"));
return true;
}else{
painter.drawText(rect(), Qt::AlignCenter, tr("請選擇圖片"));
return true;
}
}
// 縮放圖片
QPixmap img = pixmap.scaled(pixmap.width() / scaleFactor, pixmap.height() / scaleFactor, Qt::KeepAspectRatio/*, Qt::SmoothTransformation*/);
correctImagPosition();// 修正圖片位置
painter.drawPixmap(posit, img);// 繪制圖片
}else if(watched == ui->widget && event->type() == QEvent::Resize){
// 處理子視窗 ui->widget 的 Resize 事件
QResizeEvent* re = static_cast<QResizeEvent*>(event);
setZoomCenter(QPointF(re->size().width() / 2.0, re->size().height() / 2.0));// 更新縮放中心位置
return false;
}else{
return QWidget::eventFilter(watched,event);//其它事件交給父類事件過濾器處理
}
}
void Widget::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
lastDragPos = event->pos();// 暫存滑鼠按下時的位置
}
void Widget::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
QPoint delta = event->pos() - lastDragPos;// 計算滑鼠拖拽時的相對位置變化
posit.rx() += delta.x();
posit.ry() += delta.y();
// 因為 posit 改變,縮放中心相對于圖片坐標系的位置也發生了改變
zoomCenterPic -= delta * scaleFactor;//或:setZoomCenter(QPointF(this->width() / 2.0, this->height() / 2.0));
lastDragPos = event->pos();
update();
}
}
void Widget::wheelEvent(QWheelEvent *event)
{
// 滾輪朝前推為正,朝后推為負
// 滑鼠滾輪每滾動 1°,angleDelta() 值加或減 8
const int numDegrees = event->angleDelta().y() / 8;
// 實際中,滾輪每滾動一格,角度變化為 15°
const double numSteps = numDegrees / double(15);
// 根據滾輪的移動格數修改縮放因子
double tmp = scaleFactor * pow(0.8, numSteps);
// 限制縮放因子的范圍
if(tmp < 0.1)
scaleFactor = pow(0.8, 10);
else if(tmp > 10)
scaleFactor = pow(0.8, -10);
else
scaleFactor = tmp;
update();
}
void Widget::setZoomCenter(const QPointF& zoomCenter)
{
this->zoomCenter = zoomCenter;
this->zoomCenterPic = (zoomCenter - posit) * scaleFactor;
}
void Widget::correctImagPosition()
{
// 縮放時修正圖片繪制位置
posit = zoomCenter - zoomCenterPic / scaleFactor;
}
void Widget::openImg()
{
QString fileName = QFileDialog::getOpenFileName(this, tr("選擇圖片"),
QDir::homePath(),
tr("Images (*.png *.xpm *.jpg)"));
if(!pixmap.load(fileName)){
isImgError = true;
}else{
isImgError = false;
// 每次打開圖片,設定初始縮放因子為 1
scaleFactor = 1;
// 設定圖片初始在圖窗中心顯示
posit.rx() = (this->width() - pixmap.width()) / 2.0;
posit.ry() = (this->height() - pixmap.height()) / 2.0;
// 設定圖窗中心為縮放中心
setZoomCenter(QPointF(this->width() / 2.0, this->height() / 2.0));
}
update();// 更新視窗顯示
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/554954.html
標籤:其他
上一篇:一篇文章帶你入門HBase
下一篇:返回列表