浅谈Qt事件子系统——以可拖动的通用Widget为例子

浅谈Qt事件子系统------以可拖动的通用Widget为例子

这一篇文章是一个通过实现可拖动的通用Widget为引子简单介绍一下我们的事件对象子系统的事情
代码和所有的文档

1:Qt侧的API介绍和说明

​ 这个是每一个小项目的惯例,我会介绍大部分Qt程序中使用到的细节,比如说,本项目当中就是eventFilter和事件处理队列的Qt编程技术。这个也是我们编程Qt的一个重点。

​ 本项目打算介绍的是------Qt的事件处理机制,以及对象事件监听机制。如果您很熟悉了,可以考虑直接跳过本篇。

所以,Qt的事件处理机制

​ 我喜欢写一个东西的时候,直接说明我要写什么。很简单。

  • Qt是如何实现事件处理的?技术的要点有哪些?
  • 我们作为开发人员,重点关心的接口有哪些?
  • 如何监听,甚至是拦截其他对象的事件处理呢【这个是本项目的实现要点】

Qt是如何实现事件处理的

​ 毫无疑问,事件驱动处理是GUI的一个命根子,我们的GUI接受事件,展示对应的变化;同时我们的用户跟GUI交互,将用户的意图传递给我们的后台。这就是GUI的一个最大的要点。

所以,我们关心事件驱动的对象有哪些呢?

​ 我们的事件队列的处理主要依赖一个重要的概念,叫"事件循环"(Event Loop)。事件循环是一个持续运行的循环,它不断检测、分发并处理各种事件,包括用户输入(如键盘、鼠标事件)、系统消息以及自定义事件。主要过程大致如下:

  1. 事件产生:当用户操作或系统状态变化时,Qt会生成一个对应的事件对象(QEvent的子类实例)。
  2. 事件队列:事件对象被放入事件队列中等待处理。
  3. 事件分发:事件循环依次从队列中取出事件,分发给相应的对象处理。
  4. 事件响应:目标对象在其事件处理函数中对事件作出响应,更新界面或执行其他逻辑。

​ 我们分析事件,也是主要抓手这四个部分进行学习。

​ 我们事件队列处理的开始,在QApplication::exec上,调用这个,我们的全应用程序的事件队列就开始工作了。下面,我们来看看一些API函数:

​ 对于框架层次,你需要知道这员工的一些函数:

QCoreApplication::notify

Qt的事件分发机制主要依赖于QCoreApplication类中的notify()方法。每当一个事件需要传递给某个QObject对象时,都会经过该方法。其主要职责是:

  1. 统一调度:集中管理所有事件的发送和转发。
  2. 异常处理:对事件处理过程中可能出现的异常进行捕获和处理,保证整个事件循环的稳定性。
  3. 事件过滤:在正式分发事件前,提供预处理的机会(见下文的事件过滤机制)。

​ 我们一般不会跑去重写notify(至少笔者没见过特殊到要重写notify的)

事件队列与异步处理

Qt支持将事件异步投递到目标对象中,通过QCoreApplication::postEvent()方法将事件放入事件队列,等待事件循环处理。这种方式使得事件发送和处理解耦,避免在调用过程中产生阻塞,提升了系统响应能力。

与之对应的同步事件发送方式为QCoreApplication::sendEvent(),该方法直接调用目标对象的事件处理函数,在调用者线程中立刻执行。这种方式适用于对时序和结果有严格要求的情况,但需注意同步调用可能会引发递归调用或死锁问题。

事件循环(Event Loop)

每个Qt应用程序通常都有一个主事件循环,通过调用QCoreApplication::exec()启动。事件循环在不断地检测、分发和处理事件的同时,也会处理定时器、信号等异步任务。

  • 阻塞与非阻塞:事件循环既能阻塞等待事件,也能在无事件时进入休眠状态,保证资源利用率。
  • 嵌套事件循环:在某些对话框或模态窗口中,Qt会启动嵌套事件循环,保证界面依然响应用户操作。需要注意的是,嵌套循环可能会带来事件处理顺序和状态管理方面的复杂性。

讨论事件的类型

​ 事件事件,啥事件呢?这就是事件的类型。Qt中的所有事件都以QEvent为基类,其派生类涵盖了丰富的事件类型,如:

  • 用户输入事件:QMouseEvent、QKeyEvent、QWheelEvent等。
  • 窗口系统事件:QResizeEvent、QCloseEvent等。
  • 自定义事件:开发人员可以继承QEvent,定义属于自己的事件类型,实现特定业务逻辑的事件传递。

​ 这些事件呢,就在我们后面的开发接口上埋下了伏笔,所以,让我们马上进入第二个部分

开发人员关心的关键接口

​ 我们的一个大头中的大头,是QObject的一个重要的函数,或者说,QT元对象系统的一个重要的特化于事件处理的核心,就是我们的一个虚函数event(QEvent *event),这是所有事件最终处理的入口函数。每个QObject子类都可以重写这个函数,根据事件类型作出不同的响应。

​ 我们需要注意的是------event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,**分发给不同的事件处理器(event handler)。**重写一个event事件,我们往往可能是要特化一部分操作。当然,往往我们的功能是------需要在原先拥有事件处理的基础上,进一步扩展通用事件处理的能力,比如说要做薄记,比如说统一的处理,这个时候重写event就是一个很明智的选择了!

​ 例如,在自定义控件中,可以重写event()函数,对特定事件(如鼠标点击、键盘输入)进行处理,从而实现自定义行为。当然!这只是一个例子,实际上没人这样写!我们会有专门的函数来处理,这是我们下面会提到的议题!

cpp 复制代码
bool MyWidget::event(QEvent *event) {
    if (event->type() == QEvent::MouseButtonPress) {
        my_process_of_mouseEvent(event);
        return true;
    }
    // 调用基类的事件处理,保证其他事件正常分发
    return QWidget::event(event);
}

​ 你需要注意的是------请看,这里函数返回的是一个Bool值,这个bool值的含义是什么呢?答案是------当你返回了true的时候,就说明你的事件已经处理结束,Qt 将会检查这个函数的返回值,如果是true,说明这个事件已经被处理完成,会转而取事件队列的下一个进行预取,如果返回的是false,那么会继续把这个事件传递给其他的组件让他们接着处理

专用事件处理函数

为了简化事件处理,Qt为常见的事件提供了专用的虚函数,举个例子看看:

  • mousePressEvent(QMouseEvent *event):处理鼠标按下事件。
  • keyPressEvent(QKeyEvent *event):处理键盘按下事件。
  • resizeEvent(QResizeEvent *event):处理窗口尺寸变化事件。

这些函数通常在对应的控件类中重写,目的是对特定事件进行精细控制。需要注意的是,如果同时重写了event()函数和专用事件函数,则通常应保证事件在其中一个函数中得到完整处理,避免重复调用。这些在源码中的表先就是:判断事件的Type,然后依据事件的类型转发给对应的回调函数,就是这样简单!

事件发送接口

QCoreApplication::sendEvent()

同步事件发送接口sendEvent()直接调用目标对象的事件处理函数,并返回处理结果。这种方式适用于需要立即获得事件处理结果的情况。但由于它是在当前线程中执行的,因此要注意防止在事件处理过程中产生阻塞或递归调用。

QCoreApplication::postEvent()

异步事件投递接口postEvent()将事件放入目标对象所在线程的事件队列中,由事件循环在合适的时机进行分发。常见的应用场景包括跨线程通信、延迟处理等。由于postEvent()并不会立即调用事件处理函数,开发人员在设计逻辑时应考虑事件延时带来的影响。

自定义事件

在许多场景中,内置的事件类型无法满足特定需求,开发者可以通过继承QEvent来定义自定义事件。常见步骤如下:

  1. 定义新的事件类型(通常选用Qt::User 类型及之后的值)。
  2. 创建自定义事件类,包含特定数据和处理逻辑。
  3. 通过postEvent()或sendEvent()将自定义事件投递到目标对象中。
  4. 在目标对象的event()函数中进行识别和处理。

这种方式提供了极大的扩展性,使得复杂的应用逻辑可以通过事件机制进行模块化解耦。

事件过滤器

Qt还提供了事件过滤器机制,使得开发人员可以在事件传递前拦截、监控或修改事件。关键接口是QObject的installEventFilter(QObject *filterObj)eventFilter(QObject *watched, QEvent *event)函数。通过在某个对象上安装事件过滤器,过滤器对象可以提前捕获并处理目标对象的事件。

例如,在全局日志记录、调试或临时修改事件响应逻辑时,事件过滤器是一种非常有效的手段。下列代码展示了如何为一个窗口安装事件过滤器:

cpp 复制代码
// 在构造函数中安装过滤器
myWidget->installEventFilter(this);

// 重写eventFilter函数
bool MyClass::eventFilter(QObject *watched, QEvent *event) {
    if (watched == myWidget && event->type() == QEvent::KeyPress) {
        // 对键盘事件进行特殊处理
        qDebug() << "捕获到键盘事件";
        return true;  // 返回true表示事件已经被处理,不再传递
    }
    // 调用基类实现,确保其他事件可以正常传递
    return QObject::eventFilter(watched, event);
}

通过上述接口,开发者可以在不改动原有对象代码的前提下,实现对事件的监听和拦截。


如何监听和拦截其他对象的事件(本次文档的重点)

在实际开发中,经常需要对已有控件或对象的事件进行监听、修改甚至拦截。Qt提供了非常方便的事件过滤机制,使得这一需求得以高效实现。

​ 事件过滤器的核心在于:每个QObject对象都有一个内部列表,用于存储安装到该对象上的过滤器。当事件到达目标对象前,系统会先依次调用每个过滤器对象的eventFilter()方法。

  • 如果某个过滤器返回true,表示该事件已经被处理,后续的过滤器和目标对象本身将不再接收到此事件。
  • 如果所有过滤器都返回false,事件则继续传递给目标对象进行正常处理。

这种机制使得开发人员可以在不侵入原对象逻辑的情况下,对事件进行预处理,甚至阻断事件传递。

安装和使用事件过滤器

要实现对其他对象事件的监听和拦截,主要步骤如下:

  1. 编写过滤器类
    通常通过继承QObject并重写eventFilter()方法,编写自定义过滤器类。在该方法中,根据watched参数判断当前捕获的事件属于哪个对象,并根据事件类型进行处理。
  2. 安装过滤器
    在需要监控的对象上调用installEventFilter()方法,将自定义过滤器对象注册到该对象上。一个对象可以安装多个过滤器,调用顺序与安装顺序有关。
  3. 事件拦截与传递控制
    在eventFilter()中,当检测到感兴趣的事件后,可以选择返回true(表示事件已处理,不继续传递),也可以返回false(让目标对象继续处理)。

例如,假设我们需要拦截某个QLineEdit控件中的鼠标事件,可以这样实现:

cpp 复制代码
class MyEventFilter : public QObject {
    Q_OBJECT
protected:
    bool eventFilter(QObject *watched, QEvent *event) override {
        if (watched->inherits("QLineEdit")) {
            if (event->type() == QEvent::MouseButtonDblClick) {
                // 拦截双击事件
                qDebug() << "QLineEdit双击事件被拦截";
                return true;  // 阻止事件继续传递
            }
        }
        // 其他情况继续传递事件
        return QObject::eventFilter(watched, event);
    }
};

在程序初始化时,为目标对象安装过滤器:

cpp 复制代码
QLineEdit *edit = new QLineEdit(this);
MyEventFilter *filter = new MyEventFilter();
edit->installEventFilter(filter);

这样,当用户对该QLineEdit进行双击操作时,MyEventFilter将捕获并拦截该事件,而QLineEdit本身不会收到双击事件。

动态监听与跨对象事件监控

有时,我们不仅需要拦截单个对象的事件,还需要在全局范围内对多个对象进行统一监控。例如,在大型应用中调试或记录日志时,可以为整个应用安装一个全局事件过滤器。通常的做法是将过滤器安装在QCoreApplication对象上,这样所有事件都会先经过该过滤器的检测。

cpp 复制代码
class GlobalEventFilter : public QObject {
    Q_OBJECT
protected:
    bool eventFilter(QObject *watched, QEvent *event) override {
        // 可以对所有对象和事件进行日志记录或特定处理
        qDebug() << "全局过滤器捕获到事件:" << event->type() << "来自对象:" << watched;
        // 根据需求选择是否拦截或继续传递
        return QObject::eventFilter(watched, event);
    }
};

// 在main()函数中安装全局过滤器
int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    GlobalEventFilter *globalFilter = new GlobalEventFilter();
    app.installEventFilter(globalFilter);
    // 后续创建的所有对象的事件均会经过globalFilter的检测
    // ...
    return app.exec();
}

这种全局过滤器的使用,尤其适用于调试阶段,对复杂交互过程中的事件进行全面记录和分析,或在某些特殊情况下统一拦截某类事件。

注意事项与最佳实践

​ 当然这里说一些重点的事情。在使用事件过滤器时,还需要注意以下几点:

  • 性能问题:全局事件过滤器会处理所有事件,因此在实现中要避免执行过于耗时的操作,防止影响界面响应。
  • 返回值控制 :返回true表示事件被完全拦截,可能导致目标对象无法得到响应;返回false则允许事件继续传递。开发人员需要仔细判断实际需求。
  • 层次关系:如果一个对象安装了多个事件过滤器,事件会按照安装顺序依次经过各过滤器,过滤器之间可能存在相互影响,因此在设计时要考虑好先后次序。
  • 安全释放 :当过滤器对象不再需要时,必须及时调用removeEventFilter()方法,或者在对象销毁时自动移除,避免悬挂指针问题。

本项目的实现的重要文档思路

​ 注意,这个文档可能不会跟我们的源码有一定保证的同步,只是提供一种参考!

如何让Widgets跟随鼠标移动呢

​ 一种办法,是让我们创建一个SubWidget,这个SubWidget负责一对一的维护一个目标控件。比如说一个按钮,或者是任何一个其他的控件,当我们的的目标事件传递到这个控件的时候,会优先的投射到我们的这个widgets上来。通过调用控件的 installEventFilter() 方法,将当前对象(this)作为过滤器安装到 holding_widget 上。安装事件过滤器后,该控件产生的所有事件都会首先传递到当前对象的 eventFilter() 方法中进行预处理。如果在 eventFilter() 中返回了 true,那么该事件就不会继续传递到控件自身的事件处理函数中;如果返回 false,则事件会继续传递。

​ 这样,我们就可以写自己的一个eventFilter来控制目标widget的行为。而不需要重载我们的对象添加一个Movable或者是其他任何的属性,这样看就会非常的方便。

​ 下面我们要做的就是准备处理我们的move行为

c 复制代码
bool CCMovableWidget::eventFilter(QObject *watched, QEvent *event) {
    if (!holding_widget || watched != holding_widget) {
        return false;
    }

    QMouseEvent *mouseEvent = dynamic_cast<QMouseEvent *>(event);
    if (!mouseEvent) {
        return false;
    }

    // here we handle the mouse events
    // this will promise the future extensions
    switch (event->type()) {
        case QEvent::MouseButtonPress:
            handling_mousePressEvent(mouseEvent);
            break;
        case QEvent::MouseButtonRelease:
            handling_mouseReleaseEvent(mouseEvent);
            break;
        case QEvent::MouseMove:
            handling_mouseMoveEvent(mouseEvent);
            break;
        default:
            break;
    }

    // back the default behavior
    return QObject::eventFilter(watched, event);
}

​ 这是笔者的处理方式,依次对这个事件的MouseButtonPress,MouseButtonRelease和MouseMove进行了传递。这也就意味着这里它的事件就传递进来了进行了处理,当然处理结束后,我们还希望让它做进一步的处理,所以我们让他进一步维护其默认的实现。不要更改控件原来的行为。

剩下的内容

​ 剩下的内容就没什么新鲜的了,这里就让AI帮我代劳吧!

c 复制代码
// widget is pressed by the mouse, so this means we shell start our moving
void CCMovableWidget::handling_mousePressEvent(QMouseEvent *event) {
    qDebug() << "Mouse pressed";
    if (!holding_widget)
        return; // no widget to hold, reject process
    if (accept_buttons.size() > 0 && !accept_buttons.contains(event->button()))
        return;                            // the button is not acceptable, reject process
    widget_state.lastPoint = event->pos(); // memorize the last point
    widget_state.pressed   = true;
}

handling_mousePressEvent(QMouseEvent *event) 是用户按下鼠标时触发的事件处理函数,是整个拖动行为的起点。当鼠标点击到控件上时,首先通过日志输出来表明事件已经被捕获。接着,程序判断 holding_widget 是否存在,如果为空,则说明当前没有设置任何需要被移动的目标控件,因此直接返回,放弃此次操作。随后,如果开发者为这个类设定了一个特定可接受的鼠标按钮列表 accept_buttons,而当前触发事件的按钮不在该列表中,也同样视为无效事件,拒绝处理。只有当这些条件都满足后,事件才被视为有效操作。此时程序记录当前鼠标点击的位置,保存在 widget_state.lastPoint 中,用于后续计算移动偏移量,并将 widget_state.pressed 标志设为 true,表明控件已被点击按住,准备进行拖动。

复制代码
void CCMovableWidget::handling_mouseReleaseEvent(QMouseEvent *event) {
    qDebug() << "Mouse released";
    if (!holding_widget)
        return; // no widget to hold, reject process
    widget_state.pressed = false;
}

handling_mouseReleaseEvent(QMouseEvent *event) 则是用户释放鼠标按钮时调用的函数,它的作用相对简单。同样以日志开始,表示捕获了释放事件。随后依旧先检查是否存在 holding_widget,如果当前并未绑定任何控件,则此次释放事件无需处理。若控件存在,则将 widget_state.pressed 设为 false,这一行为本质上是标记当前已结束拖动操作,后续的鼠标移动将不再引起控件的位置变化。

c 复制代码
void CCMovableWidget::handling_mouseMoveEvent(QMouseEvent *event) {
    qDebug() << "Mouse moved";
    if (!holding_widget)
        return; // no widget to hold, reject process
    if (!widget_state.pressed)
        return; // the widget is not pressed, reject process

    // calculate the offset
    int offsetX = event->pos().x() - widget_state.lastPoint.x();
    int offsetY = event->pos().y() - widget_state.lastPoint.y();

    // calculate the new position
    int x = holding_widget->x() + offsetX;
    int y = holding_widget->y() + offsetY;

    // check if the widget should be in the parent
    if (widget_state.inParent) {
        QWidget *w = dynamic_cast<QWidget *>(holding_widget->parent());
        if (w && (sizeIsOutlier(QPoint(x, y), w) || positionIsOutlier(QPoint(x, y)))) {
            return;
        }

        // move the widget
        holding_widget->move(x, y);
    }
}

handling_mouseMoveEvent(QMouseEvent *event) 是核心函数,它在用户拖动鼠标时不断被调用,从而持续地更新控件位置,完成"随鼠标移动"的视觉效果。函数首先打印出"鼠标移动"的日志,确认事件的发生。紧接着,它做出两个防御性检查。第一,是否存在 holding_widget,否则自然不该响应移动。第二,判断是否存在 widget_state.pressed 为真的状态,这是防止控件在未被按住的情况下跟随鼠标移动,确保只有在"鼠标按下后并且未释放"的情形下才进入后续逻辑。接下来,程序通过当前位置与上次记录的鼠标按下点 lastPoint 计算出一个偏移量 offsetXoffsetY,这是拖动过程中控件应该移动的距离。然后,根据当前控件的原始位置加上偏移量,计算出控件新的坐标 xy

但并非所有位置更新都是合理的,因此函数中还加入了一道逻辑判断,即如果当前设置了 widget_state.inParent 为真(意味着控件应保持在其父组件内),就需要判断新位置是否越界。这里调用了 sizeIsOutlier(QPoint(x, y), w)positionIsOutlier(QPoint(x, y)) 两个函数,前者大概是判断控件在给定位置上是否尺寸越界,后者则可能是判断位置是否超出允许的边界。这一检查使得控件不能被拖出其父容器或显示区域之外。如果这两个函数判定位置无效,则不执行移动操作,函数直接返回。

最后,如果所有条件都满足,程序调用 holding_widget->move(x, y) 将控件平滑地移动到新位置上。这一行为便是"拖动"体验的实现者,控件就随着鼠标游走而流畅移动。

相关推荐
猪猪虾的业余生活2 小时前
QT实现WPS功能
开发语言·qt·wps
hunandede2 小时前
QT三 自定义控件
开发语言·qt
勇敢滴勇2 小时前
Qt信号与槽高级特性与项目实战:原理剖析与工程化应用指南
网络·数据库·c++·qt·qt5·qt6.3
fengbingchun2 小时前
Qt的内存管理机制
qt
island13145 小时前
【QT】一文学会 QT 多线程(QThread )
服务器·数据库·qt
mrbone116 小时前
Qt-Q_ENUM宏和QMetaEnum类
开发语言·qt·枚举·元对象系统·q_enum·qmetaenum
系统工程实验室8 小时前
系统架构书单推荐(二):分析与建模
系统架构
系统工程实验室17 小时前
系统架构书单推荐(一)领域驱动设计与面向对象
设计模式·系统架构
韩曙亮18 小时前
【系统架构设计师】操作系统 - 特殊操作系统 ③ ( 微内核操作系统 | 单体内核 操作系统 | 内核态 | 用户态 | 单体内核 与 微内核 对比 )
系统架构·操作系统·软考·内核态·用户态·微内核·微内核操作系统
Antonio91518 小时前
【Q&A】QT编译过程和C++编译过程的区别?
开发语言·c++·qt