QT入门第七天:事件处理机制详解 | 零基础学QT

QT入门第七天:事件处理机制详解 | 零基础学QT

前言

前六天我们学习了环境搭建、信号与槽、常用控件、布局管理器、对话框、菜单栏工具栏状态栏。今天我们来学习QT的核心机制之一------事件处理

什么是事件?简单说,就是"发生了什么事"。比如:

  • 鼠标点了一下 → 鼠标点击事件
  • 键盘按了一个键 → 键盘按下事件
  • 窗口变大变小了 → 尺寸变化事件
  • 窗口显示出来了 → 显示事件
  • 定时器到时间了 → 定时器事件

QT是事件驱动的程序,程序的大部分时间都在"等待事件",事件来了就去处理。理解事件机制,你就能更灵活地控制程序的行为。

今天我们学习:

  • 什么是事件,事件驱动的概念
  • 常见的事件类型
  • 怎么重写事件处理函数
  • 事件的传递和忽略
  • 事件过滤器
  • 定时器事件

一、事件的基本概念

1.1 什么是事件驱动

传统的程序是顺序执行的:从第一行跑到最后一行,跑完就结束了。

而GUI程序是事件驱动的:程序启动后,就进入一个"事件循环",一直在等事件。用户点一下鼠标、按一下键盘,就产生一个事件,程序收到事件后去执行对应的处理函数,处理完又继续等下一个事件。

💡 生活类比:就像餐厅服务员,客人不来的时候就等着,客人来了(事件发生)就去招待(处理事件),招待完又继续等下一位客人。

1.2 QT的事件机制

在QT中,所有事件都继承自QEvent类。当一个事件发生时,QT会创建一个对应的QEvent对象,然后把它发给对应的控件,控件的event()函数会收到这个事件,再分发给具体的事件处理函数。

事件的分发流程大概是这样的:

复制代码
事件发生 → 创建QEvent对象 → 发给目标控件 → event()函数接收 
→ 分发给具体的处理函数(如mousePressEvent)→ 处理完返回

1.3 处理事件的三种方式

QT提供了三种处理事件的方式:

  1. 重写事件处理函数(最常用)

    • 比如重写mousePressEvent()来处理鼠标点击
    • 简单直接,适合自定义控件
  2. 安装事件过滤器

    • 在一个对象上监听另一个对象的事件
    • 适合在不修改控件的情况下,拦截它的事件
  3. 重写event()函数

    • 在事件分发之前拦截
    • 最底层,最灵活,但也最复杂

💡 初学者重点掌握第一种:重写事件处理函数。事件过滤器了解一下就行,后面进阶再深入。

二、常见的事件类型

QT里事件类型很多,下面是最常用的一些:

事件类型 处理函数 触发时机
鼠标按下 mousePressEvent() 鼠标按钮按下时
鼠标释放 mouseReleaseEvent() 鼠标按钮松开时
鼠标移动 mouseMoveEvent() 鼠标移动时
鼠标双击 mouseDoubleClickEvent() 鼠标双击时
滚轮 wheelEvent() 鼠标滚轮滚动时
键盘按下 keyPressEvent() 键盘按键按下时
键盘释放 keyReleaseEvent() 键盘按键松开时
尺寸变化 resizeEvent() 控件大小改变时
显示 showEvent() 控件显示时
隐藏 hideEvent() 控件隐藏时
绘制 paintEvent() 控件需要重绘时
定时器 timerEvent() 定时器到时间时
进入 enterEvent() 鼠标进入控件时
离开 leaveEvent() 鼠标离开控件时
关闭 closeEvent() 窗口关闭时
焦点进入 focusInEvent() 控件获得焦点时
焦点离开 focusOutEvent() 控件失去焦点时

三、鼠标事件

3.1 鼠标按下事件

cpp 复制代码
#include <QMouseEvent>

void MyWidget::mousePressEvent(QMouseEvent *event)
{
    // 判断是哪个按钮按下了
    if (event->button() == Qt::LeftButton) {
        qDebug() << "左键按下,位置:" << event->pos();
    } else if (event->button() == Qt::RightButton) {
        qDebug() << "右键按下,位置:" << event->pos();
    } else if (event->button() == Qt::MiddleButton) {
        qDebug() << "中键按下,位置:" << event->pos();
    }
}

3.2 QMouseEvent的常用方法

cpp 复制代码
event->pos();          // 相对于控件的位置(QPoint)
event->x();            // x坐标
event->y();            // y坐标
event->globalPos();    // 相对于屏幕的全局位置
event->button();       // 触发事件的按钮(左键/右键/中键)
event->buttons();      // 当前所有按下的按钮(按下多个时用&判断)
event->modifiers();    // 修饰键(Ctrl、Shift、Alt等)

3.3 鼠标移动事件

cpp 复制代码
void MyWidget::mouseMoveEvent(QMouseEvent *event)
{
    // 实时显示鼠标位置
    qDebug() << "鼠标移动到:" << event->pos();
    
    // 判断是否同时按着左键移动
    if (event->buttons() & Qt::LeftButton) {
        qDebug() << "按着左键拖动";
    }
}

⚠️ 注意:默认情况下,只有按着鼠标按钮移动时,才会触发mouseMoveEvent。如果想让鼠标移动时一直触发,需要设置:

cpp 复制代码
setMouseTracking(true);  // 开启鼠标追踪

3.4 鼠标释放事件

cpp 复制代码
void MyWidget::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        qDebug() << "左键释放";
    }
}

3.5 鼠标双击事件

cpp 复制代码
void MyWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        qDebug() << "左键双击";
    }
}

3.6 滚轮事件

cpp 复制代码
#include <QWheelEvent>

void MyWidget::wheelEvent(QWheelEvent *event)
{
    // angleDelta()返回滚轮滚动的角度,单位是1/8度
    // 通常鼠标滚轮一格是15度,也就是120
    int delta = event->angleDelta().y();
    
    if (delta > 0) {
        qDebug() << "滚轮向上滚动";
    } else {
        qDebug() << "滚轮向下滚动";
    }
}

四、键盘事件

4.1 键盘按下事件

cpp 复制代码
#include <QKeyEvent>

void MyWidget::keyPressEvent(QKeyEvent *event)
{
    // 判断按了哪个键
    switch (event->key()) {
    case Qt::Key_Escape:
        qDebug() << "按了ESC键";
        break;
    case Qt::Key_Return:
    case Qt::Key_Enter:
        qDebug() << "按了回车键";
        break;
    case Qt::Key_Space:
        qDebug() << "按了空格键";
        break;
    default:
        // 普通字符键
        qDebug() << "按了:" << event->text();
        break;
    }
    
    // 判断修饰键
    if (event->modifiers() & Qt::ControlModifier) {
        qDebug() << "按着Ctrl";
    }
    if (event->modifiers() & Qt::ShiftModifier) {
        qDebug() << "按着Shift";
    }
    if (event->modifiers() & Qt::AltModifier) {
        qDebug() << "按着Alt";
    }
    
    // 组合键:Ctrl + S
    if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_S) {
        qDebug() << "Ctrl + S 保存";
    }
}

4.2 常用按键枚举

QT里的按键都有对应的枚举值,常用的有:

按键 枚举值
回车/确认 Qt::Key_Return, Qt::Key_Enter
退出 Qt::Key_Escape
空格 Qt::Key_Space
退格 Qt::Key_Backspace
删除 Qt::Key_Delete
方向键上 Qt::Key_Up
方向键下 Qt::Key_Down
方向键左 Qt::Key_Left
方向键右 Qt::Key_Right
Home Qt::Key_Home
End Qt::Key_End
PageUp Qt::Key_PageUp
PageDown Qt::Key_PageDown
F1~F12 Qt::Key_F1 ~ Qt::Key_F12
字母A~Z Qt::Key_A ~ Qt::Key_Z
数字0~9 Qt::Key_0 ~ Qt::Key_9

4.3 键盘释放事件

cpp 复制代码
void MyWidget::keyReleaseEvent(QKeyEvent *event)
{
    qDebug() << "松开了键:" << event->key();
}

⚠️ 注意:要让控件能收到键盘事件,它需要获得焦点。可以调用setFocusPolicy(Qt::StrongFocus)来设置焦点策略。

五、其他常用事件

5.1 尺寸变化事件

窗口或控件大小改变时触发:

cpp 复制代码
#include <QResizeEvent>

void MyWidget::resizeEvent(QResizeEvent *event)
{
    qDebug() << "尺寸变化:" << event->oldSize() << " → " << event->size();
    qDebug() << "新宽度:" << width() << " 新高度:" << height();
}

5.2 显示/隐藏事件

cpp 复制代码
#include <QShowEvent>
#include <QHideEvent>

void MyWidget::showEvent(QShowEvent *event)
{
    qDebug() << "控件显示了";
}

void MyWidget::hideEvent(QHideEvent *event)
{
    qDebug() << "控件隐藏了";
}

5.3 进入/离开事件

鼠标进入或离开控件时触发:

cpp 复制代码
#include <QEnterEvent>

void MyWidget::enterEvent(QEnterEvent *event)
{
    qDebug() << "鼠标进入控件";
}

void MyWidget::leaveEvent(QEvent *event)
{
    qDebug() << "鼠标离开控件";
}

5.4 关闭事件

窗口关闭时触发,可以用来做保存提示:

cpp 复制代码
#include <QCloseEvent>
#include <QMessageBox>

void MainWindow::closeEvent(QCloseEvent *event)
{
    // 如果有未保存的内容,询问用户
    if (isWindowModified()) {
        QMessageBox::StandardButton btn = QMessageBox::question(
            this, "提示", "文件未保存,确定要退出吗?",
            QMessageBox::Yes | QMessageBox::No
        );
        
        if (btn == QMessageBox::Yes) {
            event->accept();  // 接受关闭事件,窗口关闭
        } else {
            event->ignore();  // 忽略关闭事件,窗口不关闭
        }
    } else {
        event->accept();
    }
}

六、事件的传递和忽略

6.1 accept() 和 ignore()

每个事件都可以调用accept()ignore()

  • accept():表示"这个事件我处理了,不用再传给别人了"
  • ignore():表示"这个事件我不处理,继续传给父控件吧"

默认情况下,事件处理函数是accept的。

6.2 事件冒泡

如果子控件不处理某个事件(调用了ignore()),这个事件会继续传给它的父控件,父控件再不处理就继续往上传,直到有人处理或者传到最顶层。

💡 生活类比:就像工作汇报,员工解决不了的问题,上报给主管,主管解决不了再上报给经理,直到有人能解决。

6.3 示例:点击按钮的事件传递

cpp 复制代码
// 自定义按钮
class MyButton : public QPushButton
{
protected:
    void mousePressEvent(QMouseEvent *event) override {
        qDebug() << "MyButton收到鼠标按下事件";
        // 不调用父类的mousePressEvent,按钮就不会有按下的效果
        // QPushButton::mousePressEvent(event);  // 注释掉这行试试
        
        event->ignore();  // 忽略事件,继续传给父控件
    }
};

// 父窗口
class MyWidget : public QWidget
{
protected:
    void mousePressEvent(QMouseEvent *event) override {
        qDebug() << "MyWidget收到鼠标按下事件";
    }
};

如果按钮ignore了事件,父窗口也会收到这个鼠标按下事件。

七、定时器事件

7.1 什么是定时器事件

定时器就是"每隔一段时间触发一次"的事件。比如:

  • 时钟显示,每秒更新一次
  • 动画,每16毫秒刷新一帧
  • 倒计时,每秒减1

7.2 基本用法

cpp 复制代码
// 启动定时器,返回定时器ID
// 参数是毫秒数,1000毫秒 = 1秒
int timerId = startTimer(1000);

// 重写timerEvent处理定时器事件
void MyWidget::timerEvent(QTimerEvent *event)
{
    if (event->timerId() == timerId) {
        qDebug() << "定时器到时间了";
    }
}

// 停止定时器
killTimer(timerId);

7.3 示例:倒计时

cpp 复制代码
class CountdownWidget : public QWidget
{
    Q_OBJECT
public:
    CountdownWidget(QWidget *parent = nullptr) : QWidget(parent) {
        m_count = 10;
        m_timerId = startTimer(1000);  // 每秒触发一次
    }

protected:
    void timerEvent(QTimerEvent *event) override {
        if (event->timerId() == m_timerId) {
            m_count--;
            qDebug() << "倒计时:" << m_count;
            
            if (m_count <= 0) {
                killTimer(m_timerId);  // 停止定时器
                qDebug() << "时间到!";
            }
        }
    }

private:
    int m_count;
    int m_timerId;
};

7.4 QTimer类(更方便)

除了用startTimer,QT还提供了QTimer类,用信号槽的方式更方便:

cpp 复制代码
#include <QTimer>

QTimer *timer = new QTimer(this);
timer->setInterval(1000);  // 间隔1秒

connect(timer, &QTimer::timeout, this, [](){
    qDebug() << "定时器触发了";
});

timer->start();  // 启动
// timer->stop();  // 停止

💡 建议:一般情况下用QTimer更方便,代码更清晰。只有需要多个定时器或者性能要求很高时,才用startTimer。

八、事件过滤器

8.1 什么是事件过滤器

事件过滤器可以让一个对象监听另一个对象的所有事件,在事件到达目标对象之前拦截它。

比如:你想给一个输入框加个"按回车就清空"的功能,但又不想继承QLineEdit重写keyPressEvent,那就可以给它安装一个事件过滤器。

8.2 使用方法

  1. 在过滤器对象上重写eventFilter()函数
  2. 给目标对象调用installEventFilter()安装过滤器
cpp 复制代码
// 给输入框安装事件过滤器
ui->lineEdit->installEventFilter(this);

// 重写eventFilter
bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == ui->lineEdit) {
        if (event->type() == QEvent::KeyPress) {
            QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
            if (keyEvent->key() == Qt::Key_Return) {
                ui->lineEdit->clear();
                return true;  // 返回true表示这个事件已经处理了,不再往下传
            }
        }
    }
    
    // 其他情况交给父类处理
    return QMainWindow::eventFilter(watched, event);
}

8.3 返回值的含义

  • 返回true:表示这个事件我处理了,不要再发给目标对象了
  • 返回false:表示不处理,继续发给目标对象

⚠️ 注意:事件过滤器很强大,但也容易出问题。如果返回true把事件吞掉了,目标对象就收不到这个事件了,可能会导致功能异常。

九、综合实战:简易绘图板

我们来做一个简单的绘图板,用鼠标可以画线。

9.1 完整代码

cpp 复制代码
#include <QWidget>
#include <QMouseEvent>
#include <QPainter>
#include <QPoint>
#include <QVector>

class PaintWidget : public QWidget
{
    Q_OBJECT
public:
    PaintWidget(QWidget *parent = nullptr) : QWidget(parent) {
        setMouseTracking(true);  // 开启鼠标追踪
        setWindowTitle("简易绘图板");
        resize(600, 400);
        
        m_drawing = false;
    }

protected:
    // 鼠标按下:开始画线
    void mousePressEvent(QMouseEvent *event) override {
        if (event->button() == Qt::LeftButton) {
            m_drawing = true;
            m_lastPoint = event->pos();
            
            // 开始一条新线
            m_lines.append(QVector<QPoint>());
            m_lines.last().append(m_lastPoint);
        }
    }
    
    // 鼠标移动:继续画线
    void mouseMoveEvent(QMouseEvent *event) override {
        if (m_drawing && (event->buttons() & Qt::LeftButton)) {
            m_lastPoint = event->pos();
            m_lines.last().append(m_lastPoint);
            update();  // 触发重绘
        }
    }
    
    // 鼠标释放:结束画线
    void mouseReleaseEvent(QMouseEvent *event) override {
        if (event->button() == Qt::LeftButton) {
            m_drawing = false;
        }
    }
    
    // 绘制事件:把所有线画出来
    void paintEvent(QPaintEvent *event) override {
        Q_UNUSED(event);
        QPainter painter(this);
        
        // 设置画笔
        painter.setPen(QPen(Qt::blue, 3, Qt::SolidLine, Qt::RoundCap));
        
        // 画所有的线
        for (const auto &line : m_lines) {
            if (line.size() < 2) continue;
            for (int i = 1; i < line.size(); i++) {
                painter.drawLine(line[i-1], line[i]);
            }
        }
    }
    
    // 双击清空
    void mouseDoubleClickEvent(QMouseEvent *event) override {
        if (event->button() == Qt::LeftButton) {
            m_lines.clear();
            update();
        }
    }

private:
    bool m_drawing;               // 是否正在画图
    QPoint m_lastPoint;           // 上一个点
    QVector<QVector<QPoint>> m_lines;  // 所有的线
};

9.2 运行效果

运行后:

  • 按住左键拖动 → 可以画蓝色的线
  • 双击左键 → 清空画布
  • 鼠标移动时实时显示位置

是不是很简单?只用了几个事件处理函数,就做出了一个简易绘图板!

十、今日总结

今天我们学习了QT的事件处理机制,这是QT的核心概念之一。

知识点汇总

事件类型 处理函数 用途
鼠标按下 mousePressEvent 处理鼠标点击
鼠标移动 mouseMoveEvent 处理鼠标拖动、追踪
鼠标释放 mouseReleaseEvent 处理鼠标松开
键盘按下 keyPressEvent 处理按键
尺寸变化 resizeEvent 窗口大小改变时调整布局
关闭 closeEvent 关闭前做保存提示
定时器 timerEvent 定时执行任务
绘制 paintEvent 自定义绘制

重要概念

  • 事件驱动:程序大部分时间在等事件,事件来了就处理
  • 重写事件处理函数:最常用的事件处理方式
  • accept() / ignore():接受或忽略事件,控制是否继续传递
  • 事件冒泡:子控件不处理的事件会传给父控件
  • 事件过滤器:在不修改控件的情况下拦截它的事件
  • 定时器:每隔一段时间触发一次

经验分享

  1. 重写事件处理函数时,记得调用父类的实现:除非你确定要完全覆盖默认行为,否则一般都要调用父类的同名函数
  2. 鼠标移动默认不追踪 :需要setMouseTracking(true)才会在不按按钮时也触发
  3. 键盘事件需要焦点:控件要获得焦点才能收到键盘事件
  4. 定时器不要太频繁:过于频繁的定时器会占用大量CPU
  5. 事件过滤器慎用:很强大但也容易出bug,用之前想清楚

十一、明日预告

明天我们将学习QT的绘图机制(QPainter)

绘图是GUI编程的重要部分,很多炫酷的效果都是画出来的。我们会学习:

  • QPainter的基本用法
  • 画直线、矩形、圆、椭圆
  • 画笔(QPen)和画刷(QBrush)
  • 画文字
  • 画图片
  • 坐标变换(平移、旋转、缩放)
  • 渐变和抗锯齿

学会绘图,你就能做出各种自定义控件和炫酷效果了!


📝 学习建议:事件是QT的核心机制,一定要理解透。

练习建议:

  • 把今天的绘图板代码敲一遍运行看看
  • 试试给绘图板加橡皮擦功能(右键擦除)
  • 试试加颜色选择功能
  • 试试做一个打地鼠的小游戏(用定时器)

事件处理掌握了,QT的核心机制你就懂了一大半!明天见,继续加油!💪