27、Qt事件处理:鼠标事件、键盘事件、事件过滤器、重写事件函数

事件处理,说白了就是让程序知道用户干了什么。

鼠标点了一下,键盘敲了一下,窗口被拖动了——这些都是事件。Qt 把这些底层操作封装成了一个个事件对象,然后分发到对应的控件上。我们只需要在合适的地方接住它,处理它,就行了。

核心要点:Qt 的事件处理有四种常见方式——重写事件函数、安装事件过滤器、使用信号槽、以及重写 event() 函数。本章我们重点讲前三种。

Qt 事件处理体系 鼠标事件 键盘事件 事件过滤器 mousePressEvent keyPressEvent eventFilter() mouseMoveEvent keyReleaseEvent 事件拦截与转发

27.1 鼠标事件:点、拖、移、滚

鼠标事件是 GUI 程序里最常用的事件。Qt 提供了 QMouseEvent 类,里面包含了鼠标按键、坐标位置、修饰键等信息。

我个人习惯把鼠标事件分成三类:点击、移动、滚轮。点击又分按下和释放。

来看一个例子。我想让一个 QLabel 在鼠标点击时改变颜色:

// 自定义 QLabel,重写鼠标按下事件
class ClickableLabel : public QLabel {
protected:
    void mousePressEvent(QMouseEvent *event) override {
        if (event->button() == Qt::LeftButton) {
            setStyleSheet("background-color: lightblue;");
            qDebug() << "点击位置:" << event->pos();
        }
        QLabel::mousePressEvent(event); // 调用基类,保持默认行为
    }
};

小技巧:记得调用基类的 mousePressEvent()。如果不调,某些默认行为(比如双击选中文字)可能会失效。我在项目中踩过这个坑。

鼠标移动事件需要注意一点:默认情况下,只有按下鼠标按钮时才会触发 mouseMoveEvent。如果你希望不按按钮也能跟踪鼠标移动,需要调用 setMouseTracking(true)

// 开启鼠标跟踪
label->setMouseTracking(true);

// 然后在 mouseMoveEvent 中获取坐标
void YourWidget::mouseMoveEvent(QMouseEvent *event) {
    statusBar()->showMessage(
        QString("鼠标坐标: (%1, %2)")
        .arg(event->x()).arg(event->y())
    );
}

滚轮事件用 QWheelEvent,可以获取滚动的角度和方向。我做过一个图片缩放功能,就是用滚轮事件实现的:

void ImageViewer::wheelEvent(QWheelEvent *event) {
    double scaleFactor = 1.15;
    if (event->angleDelta().y() > 0) {
        // 向上滚,放大
        scale(scaleFactor);
    } else {
        // 向下滚,缩小
        scale(1.0 / scaleFactor);
    }
    event->accept();
}

27.2 键盘事件:按键响应与快捷键

键盘事件由 QKeyEvent 表示。常用的有 keyPressEvent 和 keyReleaseEvent。

我曾经做一个文本编辑器,需要支持 Ctrl+S 保存。用键盘事件来处理:

void EditorWindow::keyPressEvent(QKeyEvent *event) {
    if (event->modifiers() & Qt::ControlModifier) {
        if (event->key() == Qt::Key_S) {
            saveFile();
            return; // 处理完直接返回,不再传递
        }
    }
    // 其他按键交给基类处理
    QMainWindow::keyPressEvent(event);
}

注意:判断修饰键要用位与操作符 &,不要用 ==。因为用户可能同时按了 Ctrl+Shift,modifiers() 返回的是组合值。

键盘事件里还有一个常用场景:方向键控制移动。比如做一个贪吃蛇小游戏:

void GameWidget::keyPressEvent(QKeyEvent *event) {
    switch (event->key()) {
    case Qt::Key_Up:
        if (direction != Down) direction = Up;
        break;
    case Qt::Key_Down:
        if (direction != Up) direction = Down;
        break;
    case Qt::Key_Left:
        if (direction != Right) direction = Left;
        break;
    case Qt::Key_Right:
        if (direction != Left) direction = Right;
        break;
    default:
        QWidget::keyPressEvent(event);
    }
}

这里有个细节:防止蛇直接掉头。如果当前正在向上走,按向下键应该被忽略。否则蛇会直接撞到自己身上。

27.3 事件过滤器:拦截与监控

事件过滤器是 Qt 里一个很巧妙的设计。它允许一个对象监听另一个对象的所有事件。

说白了,就是给某个控件装一个「监控摄像头」。所有进出这个控件的事件,都会被过滤器先看到。

使用步骤很简单:

  1. 调用 target->installEventFilter(this) 安装过滤器
  2. 重写 eventFilter(QObject *obj, QEvent *event) 方法

举个例子。我想监控一个 QLineEdit,当它获得焦点时背景变黄:

bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
    if (obj == ui->lineEdit) {
        if (event->type() == QEvent::FocusIn) {
            ui->lineEdit->setStyleSheet("background-color: yellow;");
            return false; // 不拦截,继续传递
        }
        if (event->type() == QEvent::FocusOut) {
            ui->lineEdit->setStyleSheet(""); // 恢复默认
            return false;
        }
    }
    return QMainWindow::eventFilter(obj, event);
}

关键点:eventFilter 返回 true 表示事件被拦截,不再往下传递。返回 false 表示放行。我建议除非你明确要拦截,否则一律返回 false。

事件过滤器特别适合处理「我不想修改控件源码,但又想干预它的行为」的场景。比如给第三方控件加一些额外校验。

27.4 重写事件函数:最直接的方式

如果你拥有控件的源码,重写事件函数是最直接、最清晰的方式。

Qt 里每个控件都有对应的事件虚函数。比如:

事件类型 虚函数 说明
鼠标按下 mousePressEvent 鼠标按键被按下
鼠标释放 mouseReleaseEvent 鼠标按键被释放
鼠标双击 mouseDoubleClickEvent 鼠标双击
鼠标移动 mouseMoveEvent 鼠标移动
键盘按下 keyPressEvent 键盘按键被按下
键盘释放 keyReleaseEvent 键盘按键被释放
绘制 paintEvent 控件需要重绘
大小变化 resizeEvent 控件大小改变
显示/隐藏 showEvent / hideEvent 控件显示或隐藏

重写事件函数时,有一个原则我一直记着:如果你处理完了,就调用基类版本;如果你完全接管了,可以不调。

举个例子。我想做一个只能输入数字的 QLineEdit:

class NumberLineEdit : public QLineEdit {
protected:
    void keyPressEvent(QKeyEvent *event) override {
        // 只允许数字键、退格键、删除键、方向键
        if (event->key() >= Qt::Key_0 && event->key() <= Qt::Key_9) {
            QLineEdit::keyPressEvent(event); // 放行
        } else if (event->key() == Qt::Key_Backspace ||
                   event->key() == Qt::Key_Delete ||
                   event->key() == Qt::Key_Left ||
                   event->key() == Qt::Key_Right) {
            QLineEdit::keyPressEvent(event); // 放行
        } else {
            // 其他按键忽略
            event->ignore();
        }
    }
};

个人经验:重写事件函数时,尽量保持「要么全处理,要么全放行」的风格。不要在一个函数里既处理又放行,容易出逻辑混乱。

27.5 三种方式的对比与选择

你可能会问:这三种方式到底该用哪个?

我一般这样选:

  • 重写事件函数:你拥有控件的继承权,且只需要处理特定控件。最常用。
  • 事件过滤器:你不能修改控件源码,或者需要同时监控多个控件。灵活但稍慢。
  • 信号槽:Qt 已经帮你封装好了的事件(比如 clicked()),直接用信号槽最方便。

举个例子。如果你只是想响应按钮点击,用 connect(button, &QPushButton::clicked, ...) 就够了。完全不需要碰事件系统。

但如果你需要拦截鼠标按下并判断按下的位置,那就得用事件函数或过滤器了。

避坑指南:我曾经在一个项目里,给整个窗口安装了事件过滤器,然后在 eventFilter 里判断每个子控件的事件。结果性能很差,因为事件太多了。后来改成只给需要的子控件单独安装过滤器,问题解决。

事件处理是 Qt 编程里绕不开的一环。掌握了鼠标事件、键盘事件、事件过滤器和重写事件函数,你就能让程序真正「活」起来。嗯,动手试试吧,写一个能拖动的无边框窗口,或者一个支持快捷键的文本输入框——这些练习会让你对事件的理解更深。


公众号:蓝海资料掘金营,微信deep3321