参考:
- (35条消息) Qt事件循环及QEventLoop的使用_kupeThinkPoem的博客-CSDN博客
 - (35条消息) Qt消息机制:事件分发和事件过滤_qt 消息过滤_SOC罗三炮的博客-CSDN博客
 
在 Qt 中,事件(event)是一些对象,它们都派生自抽象类 QEvent
事件是应用程序所关心的,程序内部发生的事或是外部行动的结果
(资料图片)
当一个事件发生,Qt 会创建一个事件对象,它是一个派生自抽象类 QEvent的类的实例,用来代表发生的事件
有时一个事件包含多个事件类型,比如鼠标事件 QMouseEvent又可以分为鼠标按下、双击、滚轮滚动和移动等多种操作
事件由谁接收:事件可以被任何派生自 QObject的类型的实例接收和处理
QObject通过 event()函数获取和分发事件。事件由谁产生:
bool QEvent::spontaneous() const判断事件是否来自于应用程序外部,如果事件来自于外部返回 true,否则返回 falseexec()。这样就开始 Qt 的事件循环exec()开启事件循环,如果事件循环不结束,exec()后面的代码永远不会执行。exec()函数之后,程序将进入事件循环来监听应用程序的事件。事件多数情况下是被分发到一个队列中(事件队列),当队列中有事件时就不停的将队列中的事件发送给 QObject对象,当队列为空时就循环等待事件。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()    /// 开启事件循环 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(e);            if (!d_func()->connections.loadRelaxed()) {                QBasicMutexLocker locker(signalSlotLock(this));                d_func()->ensureConnectionData();            }            QObjectPrivate::Sender sender(this, const_cast(mce->sender()), mce->signalId());            mce->placeMetaCall(this);            break;        }    case QEvent::ThreadChange: {        Q_D(QObject);        QThreadData *threadData = 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(timers))));            }        }        break;    }    default:        if (e->type() >= QEvent::User) {            customEvent(e);            break;        }        return false;    }    return true;}    event()函数。❗❗❗在
event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播
事件过滤器可以对其他组件接收到的事件进行监控
事件过滤器使用步骤如下:
创建一个事件过滤器
任意的 QObject对象都可以作为事件过滤器使用
事件过滤器对象需要重写 eventFilter()函数
eventFilter()中可以决定是否将事件传递给组件对象,事件处理程序也可以提前写在事件过滤器中。被监控对象安装事件过滤器
void QObject::installEventFilter(QObject *filterObj)事件过滤器的调用时间是目标对象(也就是 eventFilter()参数里面的 watched 对象)接收到事件对象之前。如果事件被过滤掉(返回 true) 那么组件对象就不会收到该事件。
❗❗❗ 事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效
全局事件过滤器在 QAppliaction::instance()或 QCoreApplication::instance()上安装事件过滤器,那么任何事件在通过 notify()函数发送给其他对象之前都要先传给事件过滤器。
paintEvent()、mousePressEvent()等事件处理函数。最普通、最简单的形式。event()函数。event()是任何 Qt 对象的所有事件的入口,默认是根据事件类型的不同将事件分发给不同的事件处理函数QAppliaction::instance()或 QCoreApplication::instance()上安装事件过滤器。事件过滤器可以安装多个(多个事件过滤器会按安装顺序逆序激活),相比重写 notify()更加灵活,全局过滤器有一个问题:只能用在主线程。QCoreApplication::notify()这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)。| 事件 | 信号 | |
|---|---|---|
| 本质区别 | 事件是对象,都是派生自 QEvent的类的实例 | 信号是QObject或是其派生类的函数成员 | 
| 与 QObject 的关系 | 事件由 QObject及其派生类的实例对象接收并进行处理 | 信号由QObject或是其派生类的实例对象发出(emit) | 
| 对程序影响 | 改写事件处理函数可能导致程序行为发生改变 | 如果将信号与不同的槽函数连接,会导致不同的行为 | 
示例代码:
#ifndef MYAPPLICATION_H#define MYAPPLICATION_H#include class MyApplication : QApplication{    Q_OBJECTpublic:    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(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 class MyButton : public QPushButton{    Q_OBJECTpublic:    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 #include #include 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 #include "mybutton.h"class Widget : public QWidget{    Q_OBJECTpublic:    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 #include #include #include 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 #include 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 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::PaintmousePressEvent(),mouseMoveEvent(),QEvent::Paint#ifndef WIDGET_H#define WIDGET_H#include QT_BEGIN_NAMESPACEnamespace Ui { class Widget; }QT_END_NAMESPACEclass Widget : public QWidget{    Q_OBJECTpublic:    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 #include #include #include 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(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();// 更新窗口显示}     						
						
					  标签:
仓储物流“成渝圈”如何乘势而上? 12月3日,连接昆明和万象的中老铁路全线开通运营,被惠及的显...
两件西周青铜簋时隔三千年成功配对 考古工作者介绍,这个铜簋的盖、身分别时隔40余年出土,纹饰...
“医保砍价”不是一个人在战斗 晁星 “我眼泪都快掉下来了”“每一个小群体都不该被放弃”…...
“购物成瘾”真的是一种病 刘艳 牛雅娟 本周日即将迎来“双十二”促销季,很多人又开始摩拳...
因迷恋山间风景,一男子在甘孜州稻城县海拔4000多米的无人区迷失方向,随后与同伴失联。12月的稻城...
嫌疑人DNA信息比中后,成都市公安局刑侦支队技术处DNA实验室民警白小刚一下坐在凳子上,恍惚迟疑间...
一批反映南京大屠杀历史的新书发布 新华社南京12月7日电(记者邱冰清、蒋芳)“以史为鉴,开创未来...
我在现场·照片背后的故事|电影《亲爱的》里面没有的结局,在我眼前“上映” 12月6日,在深圳市...
冥想?泡脚?不如听听助眠音乐 晚上睡不着,白天睡不醒,成为最贴合都市人群的“睡眠画像”。随...
养老话题 老年教育面临缺口 “终身教育”潜力无限 【现实挑战】“新老年”群体愿意在培养兴...
孙海洋被拐14年儿子如何找到的? 警方侦办另一宗拐骗儿童案时发现线索,通过人像比对、DNA确认找...
北京天文馆、圆明园将对未成年人免费开放 12月6日,北京天文馆发布通知称,12月8日起试行对未成...
今年全国粮食总产量再创新高 连续7年保持在1 3万亿斤以上 根据对全国31个省(区、市)的抽样调...
斑块软的很危险 硬的就无碍? 血管里的“垃圾”分类 赶快学起来! 一项最新研究显示:中国...
诺西那生钠注射液大幅降价 聚焦医保谈判背后脊髓性肌萎缩症家庭 医保目录公布那天 好多家长都...
抖音“窗花剪剪”遭抄袭 被判获赔20万元 法院认为“窗花剪剪”的这种表达方式理应受到《著作权...
公安机关近日侦破3起拐卖儿童案件 失散十几年 3组家庭终于团圆了 北京青年报记者12月6日从公...
2021年度十大网络用语发布 本报讯(记者 路艳霞)作为年度“汉语盘点”活动最具网络特色的组成部...
北京天文馆向未成年人免费开放 本报讯(记者 牛伟坤)北京天文馆对票价免费及优惠政策作出调整:1...
2021北京百个网红打卡地发布 本报讯(记者 李洋)2021北京网红打卡地推荐榜单昨晚正式发布。自然...