QT入门第七天:事件处理机制详解 | 零基础学QT
前言
前六天我们学习了环境搭建、信号与槽、常用控件、布局管理器、对话框、菜单栏工具栏状态栏。今天我们来学习QT的核心机制之一------事件处理。
什么是事件?简单说,就是"发生了什么事"。比如:
- 鼠标点了一下 → 鼠标点击事件
- 键盘按了一个键 → 键盘按下事件
- 窗口变大变小了 → 尺寸变化事件
- 窗口显示出来了 → 显示事件
- 定时器到时间了 → 定时器事件
QT是事件驱动的程序,程序的大部分时间都在"等待事件",事件来了就去处理。理解事件机制,你就能更灵活地控制程序的行为。
今天我们学习:
- 什么是事件,事件驱动的概念
- 常见的事件类型
- 怎么重写事件处理函数
- 事件的传递和忽略
- 事件过滤器
- 定时器事件
一、事件的基本概念
1.1 什么是事件驱动
传统的程序是顺序执行的:从第一行跑到最后一行,跑完就结束了。
而GUI程序是事件驱动的:程序启动后,就进入一个"事件循环",一直在等事件。用户点一下鼠标、按一下键盘,就产生一个事件,程序收到事件后去执行对应的处理函数,处理完又继续等下一个事件。
💡 生活类比:就像餐厅服务员,客人不来的时候就等着,客人来了(事件发生)就去招待(处理事件),招待完又继续等下一位客人。
1.2 QT的事件机制
在QT中,所有事件都继承自QEvent类。当一个事件发生时,QT会创建一个对应的QEvent对象,然后把它发给对应的控件,控件的event()函数会收到这个事件,再分发给具体的事件处理函数。
事件的分发流程大概是这样的:
事件发生 → 创建QEvent对象 → 发给目标控件 → event()函数接收
→ 分发给具体的处理函数(如mousePressEvent)→ 处理完返回
1.3 处理事件的三种方式
QT提供了三种处理事件的方式:
-
重写事件处理函数(最常用)
- 比如重写
mousePressEvent()来处理鼠标点击 - 简单直接,适合自定义控件
- 比如重写
-
安装事件过滤器
- 在一个对象上监听另一个对象的事件
- 适合在不修改控件的情况下,拦截它的事件
-
重写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。如果想让鼠标移动时一直触发,需要设置:
cppsetMouseTracking(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 使用方法
- 在过滤器对象上重写
eventFilter()函数 - 给目标对象调用
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():接受或忽略事件,控制是否继续传递
- ✅ 事件冒泡:子控件不处理的事件会传给父控件
- ✅ 事件过滤器:在不修改控件的情况下拦截它的事件
- ✅ 定时器:每隔一段时间触发一次
经验分享
- 重写事件处理函数时,记得调用父类的实现:除非你确定要完全覆盖默认行为,否则一般都要调用父类的同名函数
- 鼠标移动默认不追踪 :需要
setMouseTracking(true)才会在不按按钮时也触发 - 键盘事件需要焦点:控件要获得焦点才能收到键盘事件
- 定时器不要太频繁:过于频繁的定时器会占用大量CPU
- 事件过滤器慎用:很强大但也容易出bug,用之前想清楚
十一、明日预告
明天我们将学习QT的绘图机制(QPainter)。
绘图是GUI编程的重要部分,很多炫酷的效果都是画出来的。我们会学习:
- QPainter的基本用法
- 画直线、矩形、圆、椭圆
- 画笔(QPen)和画刷(QBrush)
- 画文字
- 画图片
- 坐标变换(平移、旋转、缩放)
- 渐变和抗锯齿
学会绘图,你就能做出各种自定义控件和炫酷效果了!
📝 学习建议:事件是QT的核心机制,一定要理解透。
练习建议:
- 把今天的绘图板代码敲一遍运行看看
- 试试给绘图板加橡皮擦功能(右键擦除)
- 试试加颜色选择功能
- 试试做一个打地鼠的小游戏(用定时器)
事件处理掌握了,QT的核心机制你就懂了一大半!明天见,继续加油!💪