Qt 事件系统总结
  uP1dzibcqnBU 2023年11月02日 45 0
C++

参考:

Qt 事件系统总结

Qt 事件

  • 在 Qt 中,事件(event)是一些对象,它们都派生自抽象类 QEvent

  • 事件是应用程序所关心的,程序内部发生的事或是外部行动的结果

  • 当一个事件发生,Qt 会创建一个事件对象,它是一个派生自抽象类 QEvent 的类的实例,用来代表发生的事件

  • 有时一个事件包含多个事件类型,比如鼠标事件 QMouseEvent 又可以分为鼠标按下双击滚轮滚动移动等多种操作

  • 事件由谁接收:事件可以被任何派生自 QObject 的类型的实例接收和处理

    • QObject 类的三大核心功能其中之一就是:事件处理。QObject通过 event() 函数获取和分发事件。
  • 事件由谁产生:

    • 由操作系统或应用程序内部产生
    • 使用 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() 函数之后,程序将进入事件循环来监听应用程序的事件

image-20230608165523292
事件循环与事件队列
image-20230608203644315
事件的转发与处理流程

同步与异步事件

  • 同步事件: 调用 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 = d->threadData.loadRelaxed();
        QAbstractEventDispatcher *eventDispatcher = threadData->eventDispatcher.loadRelaxed();
        if (eventDispatcher) {
            QList<QAbstractEventDispatcher::TimerInfo> 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() 函数是没有作用的,不会影响到事件的传播

事件过滤器

事件过滤器可以对其他组件接收到的事件进行监控

事件过滤器使用步骤如下:

  1. 创建一个事件过滤器

    • 任意的 QObject 对象都可以作为事件过滤器使用

    • 事件过滤器对象需要重写 eventFilter() 函数

      • eventFilter() 中可以决定是否将事件传递给组件对象,事件处理程序也可以提前写在事件过滤器中。
      • 不让事件继续转发返回 true, 否则返回 false
  2. 被监控对象安装事件过滤器

    • void QObject::installEventFilter(QObject *filterObj)

img

事件过滤器的调用时间是目标对象(也就是 eventFilter() 参数里面的 watched 对象)接收到事件对象之前。如果事件被过滤掉(返回 true) 那么组件对象就不会收到该事件。

❗❗❗ 事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效

全局事件过滤器QAppliaction::instance()QCoreApplication::instance() 上安装事件过滤器,那么任何事件在通过 notify() 函数发送给其他对象之前都要先传给事件过滤器。

Qt 事件处理的 5 个层次

  1. 重写 paintEvent()mousePressEvent() 等事件处理函数。最普通、最简单的形式。
  2. 重写 event() 函数。event() 是任何 Qt 对象的所有事件的入口,默认是根据事件类型的不同将事件分发给不同的事件处理函数
  3. 在特定对象上安装事件过滤器,该事件过滤器仅过滤该对象接收到的事件
  4. 使用全局事件过滤器,在 QAppliaction::instance()QCoreApplication::instance() 上安装事件过滤器。事件过滤器可以安装多个(多个事件过滤器会按安装顺序逆序激活),相比重写 notify() 更加灵活,全局过滤器有一个问题:只能用在主线程。
  5. 重写 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();
}

image-20230609162238213

输出:

# 点击按钮输出如下信息:
MyApplication::notify():发布鼠标按下事件给类型为  QWidgetWindow  的对象;
在鼠标按下事件发送给类型为  QWidgetWindow  的对象之前,全局事件过滤器 GlobalFilter::eventFilter() 先对事件进行处理;
MyApplication::notify():发布鼠标按下事件给类型为  MyButton  的对象;
在鼠标按下事件发送给类型为  MyButton  的对象之前,全局事件过滤器 GlobalFilter::eventFilter() 先对事件进行处理;
在鼠标按下事件发送给 button 之前,事件过滤器 Widget::eventFilter() 先对事件进行处理
MyButton::event(): 按钮点击事件被分发
MyButton::mousePressEvent() 按钮按下事件被处理
Widget::buttonPressedSlot(): 按下按钮的槽函数被调用
槽函数回调返回后,执行发送信号(emit)后面的代码

示例:查看图片的简单应用

功能如下:

  • 选择和打开图片:点击按钮,选择打开一个图片
  • 缩放图片:通过鼠标滚轮能够缩放图片,缩放中心为图片绘制窗口的几何中心
    • 相关事件:wheelEvent()QEvent::ResizeQEvent::Paint
  • 拖动图片:通过鼠标左键按住来拖动图片
    • 相关事件:mousePressEvent()mouseMoveEvent()QEvent::Paint
image-20230612105958478
#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();// 更新窗口显示
}
image-20230612105833568
【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  8Tw5Riv1mGFK   2024年05月01日   80   0   0 C++
  BYaHC1OPAeY4   2024年05月08日   56   0   0 C++
  yZdUbUDB8h5t   2024年05月05日   43   0   0 C++
  oXKBKZoQY2lx   2024年05月17日   57   0   0 C++
uP1dzibcqnBU