Note
本篇讲解 Qt 的事件系统:事件与信号的区别、QEvent 家族、鼠标键盘事件的重写、event() 分发函数,以及事件过滤器的用法。适合已经会用信号槽、想搞懂”点击背后到底发生了什么”的初学者。
事件是什么
Qt 是一个事件驱动的框架。程序启动后并不按顺序执行到底,而是进入 QApplication::exec() 的事件循环:不断从队列里取出事件,交给对应的控件处理,然后等待下一个事件。用户点鼠标、敲键盘、拖动窗口、甚至系统定时器到期,都会产生事件。
事件本质上是一个 QEvent 对象,它描述了”发生了什么事”。QEvent 有一个庞大的子类家族,常见的有:
| 事件类型 | 对应场景 |
|---|---|
| QMouseEvent | 鼠标按下、释放、移动 |
| QKeyEvent | 键盘按下、释放 |
| QPaintEvent | 控件需要重绘 |
| QResizeEvent / QMoveEvent | 尺寸、位置变化 |
| QCloseEvent | 窗口被关闭 |
| QWheelEvent | 滚轮滚动 |
| QTimerEvent | QTimer 定时器到期 |
事件与信号的区别
很多初学者会把事件和信号混为一谈,它们是两个不同层次的概念:
- 事件(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 决定放行还是拦截。
// 在窗口类中安装过滤器(例如在构造函数里)
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
事件过滤器适合做”横切”逻辑:全局快捷键、输入校验、悬浮提示、日志埋点。如果逻辑只和某一个控件自己有关,优先考虑写子类重写事件函数,代码更内聚。
自测:三个问题检验掌握程度
自测:三个问题检验掌握程度
- QPushButton 的 clicked() 信号和 QMouseEvent 有什么关系?什么时候必须放弃信号、改用事件?
- mouseMoveEvent 默认什么时候才触发?想做”鼠标悬停跟随”需要先调用哪个函数?
- eventFilter 返回 true 和返回 false 分别意味着什么?一个控件被多个过滤器监视时,事件按什么顺序经过过滤器?
(答案都在上文)