15. QT事件处理:事件系统、鼠标事件、键盘事件、事件过滤器、重写事件处理器
事件处理,说白了就是让程序知道用户干了什么,然后做出反应。你点了一下鼠标,程序得知道「哦,用户点了这里」;你按了个键盘,程序得知道「哦,用户按了A键」。QT里这套机制,我用了十几年,说实话,设计得挺优雅的。
咱们先看一张整体结构图,心里有个谱:
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表示。常用的处理器是keyPressEvent和keyReleaseEvent。
键盘事件里最重要的几个信息:
key():返回按键的枚举值,比如Qt::Key_A、Qt::Key_Returntext():返回按键产生的文本字符,比如按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里一个很巧妙的设计。说白了,就是让一个对象去监听另一个对象的事件。你不需要继承那个控件,也不需要重写它的处理器,只要装个「监听器」就行。
实现步骤很简单:
- 调用
targetWidget->installEventFilter(this)安装过滤器 - 在监听者里重写
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 重写事件处理器
这是最直接的方式。继承一个控件类,重写它的mousePressEvent、paintEvent等方法。适合做自定义控件时使用。
但这里有个坑,我必须要说。你重写事件处理器时,一定要考虑要不要调用父类的实现。比如:
void MyButton::mousePressEvent(QMouseEvent *event)
{
// 完全自定义行为,不调用父类
// 这样按钮就不会有默认的按下效果
doSomething();
}
void MyButton::mousePressEvent(QMouseEvent *event)
{
// 先做自己的事,再让父类处理
doSomething();
QPushButton::mousePressEvent(event); // 保留默认效果
}
我曾经接手过一个项目,里面有个自定义按钮,重写了mousePressEvent但没调父类。结果按钮按下去没任何视觉反馈,用户以为按钮坏了。排查了半天才发现是这个问题。
所以我的建议是:除非你确定不需要父类的行为,否则最好调一下。或者至少把事件传给父类,让父类决定要不要处理。
15.6 事件处理的优先级
当一个事件发生时,QT会按这个顺序查找处理者:
- 先问事件过滤器(如果有安装的话)
- 再问控件本身的
event()方法 - 然后调用具体的事件处理器(如
mousePressEvent) - 如果都没处理,事件往上传播给父控件
你想想看,这个顺序其实很合理。过滤器优先级最高,因为它可以在事件到达控件前就拦截。然后是event()方法,它是个总入口,可以在这里做统一处理。最后才是具体的事件处理器。
实用建议:
- 简单的控件行为,直接重写事件处理器就够了
- 需要监听多个控件,或者不想改控件代码,用事件过滤器
- 需要全局拦截事件(比如禁用所有鼠标操作),重写
QApplication::notify()
好了,QT事件处理这块,说白了就是三招:重写、过滤、通知。大部分场景下,前两招就够用了。记住一点:事件处理不是越复杂越好,够用就行。我见过有人为了一个简单的点击事件,又是过滤器又是重写event(),绕了一大圈,其实直接重写mousePressEvent三行代码就搞定了。
嗯,今天就聊到这儿。这些内容你多写几个例子,自然就熟练了。