Note

本篇讲解 Qt 的事件系统:事件与信号的区别、QEvent 家族、鼠标键盘事件的重写、event() 分发函数,以及事件过滤器的用法。适合已经会用信号槽、想搞懂”点击背后到底发生了什么”的初学者。

事件是什么

Qt 是一个事件驱动的框架。程序启动后并不按顺序执行到底,而是进入 QApplication::exec() 的事件循环:不断从队列里取出事件,交给对应的控件处理,然后等待下一个事件。用户点鼠标、敲键盘、拖动窗口、甚至系统定时器到期,都会产生事件。

事件本质上是一个 QEvent 对象,它描述了”发生了什么事”。QEvent 有一个庞大的子类家族,常见的有:

事件类型对应场景
QMouseEvent鼠标按下、释放、移动
QKeyEvent键盘按下、释放
QPaintEvent控件需要重绘
QResizeEvent / QMoveEvent尺寸、位置变化
QCloseEvent窗口被关闭
QWheelEvent滚轮滚动
QTimerEventQTimer 定时器到期

事件与信号的区别

很多初学者会把事件和信号混为一谈,它们是两个不同层次的概念:

  • 事件(Event) 是底层机制。由窗口系统或 Qt 自身产生,进入事件队列,由事件循环分发,最终由控件的特定处理函数响应。事件是”第一时间”感知输入的途径。
  • 信号(Signal) 是更高层的抽象。很多信号本身就是事件处理函数内部发出来的——比如 QPushButton 的 clicked() 信号,就是在内部处理了鼠标按下、释放等一系列事件之后才发射的。

简单说:事件是原料,信号是加工后的成品。当你能用现成的信号(如 clicked())就别去碰事件;当你需要的交互粒度信号给不了(比如”鼠标右键按下""按下时移动”),才需要重写事件处理函数。

Warning

事件处理函数是 protected 虚函数,不能像信号槽那样用 connect 连接,只能在子类里重写(override)。千万不要试图给 QPushButton 挂一个”mousePressEvent 处理器”——正确做法是写一个继承 QPushButton 的子类。

重写鼠标事件

下面实现一个能追踪鼠标的控件:按住左键拖动时画点轨迹,并在状态里记录按键。

// mywidget.h
#ifndef MYWIDGET_H
#define MYWIDGET_H
 
#include <QWidget>
#include <QMouseEvent>
 
class MyWidget : public QWidget
{
    Q_OBJECT
public:
    explicit MyWidget(QWidget *parent = nullptr);
 
protected:
    // 重写鼠标按下事件
    void mousePressEvent(QMouseEvent *event) override;
    // 重写鼠标移动事件
    void mouseMoveEvent(QMouseEvent *event) override;
    // 重写鼠标滚轮事件
    void wheelEvent(QWheelEvent *event) override;
 
private:
    QString m_status;   // 记录状态文本
};
 
#endif // MYWIDGET_H
// mywidget.cpp
#include "mywidget.h"
#include <QPainter>
 
MyWidget::MyWidget(QWidget *parent) : QWidget(parent)
{
    setWindowTitle("鼠标事件演示");
    resize(400, 300);
    // 开启鼠标移动追踪:默认只有按住按键时才收到 move 事件
    setMouseTracking(true);
}
 
void MyWidget::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        m_status = QString("左键按下:(%1, %2)")
                       .arg(event->pos().x()).arg(event->pos().y());
    } else if (event->button() == Qt::RightButton) {
        m_status = "右键按下";
    }
    update();   // 触发重绘,让新状态显示出来
}
 
void MyWidget::mouseMoveEvent(QMouseEvent *event)
{
    m_status = QString("移动中:(%1, %2)")
                   .arg(event->pos().x()).arg(event->pos().y());
    update();
}
 
void MyWidget::wheelEvent(QWheelEvent *event)
{
    // delta() 为正表示向上滚,为负表示向下滚
    m_status = event->angleDelta().y() > 0 ? "滚轮向上" : "滚轮向下";
    update();
}
// main.cpp
#include <QApplication>
#include "mywidget.h"
 
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MyWidget w;
    w.show();
    return app.exec();
}

几个关键细节:

  • event->button() 返回触发本次事件的那个按键,只在按下/释放事件里有意义;event->buttons()(复数)返回当前所有按住的按键,移动事件中常用。
  • QMouseEvent::pos() 返回相对控件的坐标(Qt6 中推荐 position().toPoint(),两者在 Qt5.15+ 都可用)。
  • 默认情况下 mouseMoveEvent 只在按住某个按键时才触发;调用 setMouseTracking(true) 后,不按键也能收到移动事件——做绘图跟随、悬停效果时必须开启。

Success

在事件处理函数里改完状态后调用 update(),不要直接在里面画图。update() 只是”申请重绘”,Qt 会在合适时机合并多次申请、统一发一次 QPaintEvent,效率更高。

重写键盘事件

void MyWidget::keyPressEvent(QKeyEvent *event)
{
    // 组合键检测:Ctrl + S
    if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_S) {
        m_status = "保存(Ctrl+S)";
    } else if (event->key() == Qt::Key_Escape) {
        m_status = "按下了 Esc";
    } else {
        // 把键值转成可读文本
        m_status = QString("按下了:%1").arg(QKeySequence(event->key()).toString());
    }
    update();
}

注意 event->text() 返回的是按键对应的字符(比如按 Shift+A 得到 “A”),而 event->key() 返回键值枚举(Qt::Key_A)。做快捷键用 key,做文本输入用 text。

event():事件的分发枢纽

每个 QObject 都有一个 event(QEvent *e) 函数,它是所有事件的总入口。事件循环把事件交给控件时,先进入 event(),它根据事件类型转发给对应的 mousePressEvent、keyPressEvent 等具体函数。默认实现里,未被处理的事件会继续向上传给父控件(这就是”事件传播”)。

重写 event() 可以做两件事:

bool MyWidget::event(QEvent *e)
{
    if (e->type() == QEvent::KeyPress) {
        // 拦截所有按键:处理后返回 true,事件到此为止
        return true;
    }
    // 其余事件交给父类按默认规则分发
    return QWidget::event(e);
}
  • 返回 true:表示”这个事件我处理完了”,事件不再传播;
  • 返回 false 或调用父类实现:事件按默认流程继续走。

一般只在需要统一拦截或监控某类事件时才重写 event(),具体事件仍交给具体函数处理。

事件过滤器:不继承也能”截胡”

有时候你想监视别的控件的事件,但又不能(或不想)为它写子类——比如给窗口里所有输入框统一加回车跳转。这时就该用事件过滤器

原理:控件 A 调用 installEventFilter(B) 后,A 的所有事件在分发之前会先送进 B 的 eventFilter(),由 B 决定放行还是拦截。

Qt 事件流转示意图:操作系统产生事件后经事件循环进入 event() 分发,或先经过事件过滤器,最终到达具体事件处理函数

// 在窗口类中安装过滤器(例如在构造函数里)
MyWidget::MyWidget(QWidget *parent) : QWidget(parent)
{
    auto *edit1 = new QLineEdit(this);
    auto *edit2 = new QLineEdit(this);
    // 让 this 监视这两个输入框的事件
    edit1->installEventFilter(this);
    edit2->installEventFilter(this);
}
 
// 重写 eventFilter:this 作为过滤器时会被回调
bool MyWidget::eventFilter(QObject *watched, QEvent *event)
{
    if (event->type() == QEvent::KeyPress) {
        auto *keyEvent = static_cast<QKeyEvent *>(event);
        if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
            // 回车时让焦点跳到下一个控件,并拦截本次按键
            focusNextChild();
            return true;
        }
    }
    // 其余事件放行,继续走正常流程
    return QWidget::eventFilter(watched, event);
}

要点:

  • eventFilter 的返回值规则与 event() 相同:true 拦截,false 放行。
  • 一个过滤器可以同时监视多个控件,通过 watched 参数区分来源。
  • 控件可以被多个过滤器监视,事件按安装的逆序依次经过各过滤器。

Warning

事件过滤器中 watched 指向的控件如果在这之前被删除,过滤器回调可能访问到悬空指针。Qt 会在对象销毁时自动移除过滤器,但如果你在 eventFilter 里手动 delete 了 watched 对象,必须立即 return true,不要再碰它。

Success

事件过滤器适合做”横切”逻辑:全局快捷键、输入校验、悬浮提示、日志埋点。如果逻辑只和某一个控件自己有关,优先考虑写子类重写事件函数,代码更内聚。

自测:三个问题检验掌握程度