15. QT事件处理:事件系统、鼠标事件、键盘事件、事件过滤器、重写事件处理器

事件处理,说白了就是让程序知道用户干了什么,然后做出反应。你点了一下鼠标,程序得知道「哦,用户点了这里」;你按了个键盘,程序得知道「哦,用户按了A键」。QT里这套机制,我用了十几年,说实话,设计得挺优雅的。

咱们先看一张整体结构图,心里有个谱:

QT事件处理体系 事件源(鼠标/键盘/定时器等) 事件队列 事件分发(QApplication) 重写事件处理器 事件过滤器 默认处理(忽略) 事件从产生到被处理的完整流程

15.1 事件系统基础

QT里的事件,本质上就是一个QEvent对象。每种事件都有对应的类型,比如鼠标按下是QEvent::MouseButtonPress,键盘按下是QEvent::KeyPress。系统会把事件塞进事件队列,然后由QApplication的主循环一个个取出来分发。

我个人习惯把事件理解成「消息」。你想想看,Windows底层也是消息机制,QT只是封装了一层而已。事件来了,先发给目标控件,控件如果没处理,就往上抛给父控件,一直抛到顶层窗口。这个叫「事件传播」。

核心要点:事件传播是自底向上的。子控件先处理,处理不了交给父控件。如果你在子控件里调用了event->accept(),事件就停了;调了event->ignore(),事件继续往上冒。

15.2 鼠标事件

鼠标事件是开发中最常用的。QT提供了QMouseEvent类,里面包含了鼠标位置、按键状态、修饰键等信息。

常用的鼠标事件处理器有这几个:

事件处理器 触发时机 典型应用
mousePressEvent 鼠标按下 点击按钮、选中对象
mouseReleaseEvent 鼠标释放 拖拽结束、确认操作
mouseMoveEvent 鼠标移动 绘图、拖拽、悬停效果
mouseDoubleClickEvent 鼠标双击 打开文件、编辑文本
wheelEvent 滚轮滚动 缩放、滚动列表

写个例子你就明白了。假设我们要做一个自定义画板,鼠标按下时记录起点,移动时画线:

void MyWidget::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        lastPoint = event->pos();
        isDrawing = true;
    }
}

void MyWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (isDrawing && (event->buttons() & Qt::LeftButton)) {
        QPainter painter(this);
        painter.drawLine(lastPoint, event->pos());
        lastPoint = event->pos();
    }
}

void MyWidget::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        isDrawing = false;
    }
}

小技巧:注意mouseMoveEvent里我用了event->buttons()(复数),而不是event->button()(单数)。前者返回当前所有按下的按键状态,后者只返回触发事件的按键。移动事件里要用复数,不然你按着左键移动时检测不到。

我曾经在做一个绘图软件时,就因为这个细节踩了坑。用户按着左键拖拽,我死活拿不到按键状态,查了半天才发现是单复数用错了。嗯,这种坑踩一次就记住了。

15.3 键盘事件

键盘事件由QKeyEvent表示。常用的处理器是keyPressEventkeyReleaseEvent

键盘事件里最重要的几个信息:

  • key():返回按键的枚举值,比如Qt::Key_AQt::Key_Return
  • text():返回按键产生的文本字符,比如按A返回"a"或"A"
  • modifiers():返回修饰键状态,比如Qt::ControlModifier

举个例子,实现一个快捷键功能:

void MyWindow::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_S && 
        event->modifiers() == Qt::ControlModifier) {
        // Ctrl+S 保存
        saveFile();
        event->accept();
        return;
    }
    
    if (event->key() == Qt::Key_Escape) {
        // ESC 取消
        cancelOperation();
        event->accept();
        return;
    }
    
    // 没处理的事件交给父类
    QWidget::keyPressEvent(event);
}

注意:如果你想让控件能接收键盘事件,记得设置setFocusPolicy()。默认很多控件是不接受焦点输入的,你按键盘它根本没反应。我刚开始学QT时就纳闷,为什么我重写了keyPressEvent却死活不触发,后来才发现忘了设焦点策略。

15.4 事件过滤器

事件过滤器是QT里一个很巧妙的设计。说白了,就是让一个对象去监听另一个对象的事件。你不需要继承那个控件,也不需要重写它的处理器,只要装个「监听器」就行。

实现步骤很简单:

  1. 调用targetWidget->installEventFilter(this)安装过滤器
  2. 在监听者里重写eventFilter方法

看个实际例子。假设我们有个对话框,里面一堆输入框,想统一处理回车键跳转到下一个输入框:

bool MyDialog::eventFilter(QObject *obj, QEvent *event)
{
    if (event->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
        if (keyEvent->key() == Qt::Key_Return || 
            keyEvent->key() == Qt::Key_Enter) {
            // 跳到下一个输入框
            QWidget *next = findNextWidget(obj);
            if (next) {
                next->setFocus();
                return true; // 事件已处理,不再传播
            }
        }
    }
    // 其他事件交给默认处理
    return QDialog::eventFilter(obj, event);
}

我在项目中遇到过一种情况:一个表格里嵌了自定义控件,表格本身要处理鼠标事件,控件也要处理。如果直接重写事件处理器,两者会冲突。用事件过滤器就完美解决了——表格作为过滤器,先拦截事件,判断是否需要自己处理,不需要再放给子控件。

事件过滤器的优势:

  • 不需要继承控件类
  • 一个过滤器可以监听多个控件
  • 可以在事件到达目标前拦截
  • 适合做全局性的行为控制

15.5 重写事件处理器

这是最直接的方式。继承一个控件类,重写它的mousePressEventpaintEvent等方法。适合做自定义控件时使用。

但这里有个坑,我必须要说。你重写事件处理器时,一定要考虑要不要调用父类的实现。比如:

void MyButton::mousePressEvent(QMouseEvent *event)
{
    // 完全自定义行为,不调用父类
    // 这样按钮就不会有默认的按下效果
    doSomething();
}

void MyButton::mousePressEvent(QMouseEvent *event)
{
    // 先做自己的事,再让父类处理
    doSomething();
    QPushButton::mousePressEvent(event); // 保留默认效果
}

我曾经接手过一个项目,里面有个自定义按钮,重写了mousePressEvent但没调父类。结果按钮按下去没任何视觉反馈,用户以为按钮坏了。排查了半天才发现是这个问题。

所以我的建议是:除非你确定不需要父类的行为,否则最好调一下。或者至少把事件传给父类,让父类决定要不要处理。

15.6 事件处理的优先级

当一个事件发生时,QT会按这个顺序查找处理者:

  1. 先问事件过滤器(如果有安装的话)
  2. 再问控件本身的event()方法
  3. 然后调用具体的事件处理器(如mousePressEvent
  4. 如果都没处理,事件往上传播给父控件

你想想看,这个顺序其实很合理。过滤器优先级最高,因为它可以在事件到达控件前就拦截。然后是event()方法,它是个总入口,可以在这里做统一处理。最后才是具体的事件处理器。

实用建议:

  • 简单的控件行为,直接重写事件处理器就够了
  • 需要监听多个控件,或者不想改控件代码,用事件过滤器
  • 需要全局拦截事件(比如禁用所有鼠标操作),重写QApplication::notify()

好了,QT事件处理这块,说白了就是三招:重写、过滤、通知。大部分场景下,前两招就够用了。记住一点:事件处理不是越复杂越好,够用就行。我见过有人为了一个简单的点击事件,又是过滤器又是重写event(),绕了一大圈,其实直接重写mousePressEvent三行代码就搞定了。

嗯,今天就聊到这儿。这些内容你多写几个例子,自然就熟练了。


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