27、Qt事件处理:鼠标事件、键盘事件、事件过滤器、重写事件函数
事件处理,说白了就是让程序知道用户干了什么。
鼠标点了一下,键盘敲了一下,窗口被拖动了——这些都是事件。Qt 把这些底层操作封装成了一个个事件对象,然后分发到对应的控件上。我们只需要在合适的地方接住它,处理它,就行了。
核心要点:Qt 的事件处理有四种常见方式——重写事件函数、安装事件过滤器、使用信号槽、以及重写 event() 函数。本章我们重点讲前三种。
27.1 鼠标事件:点、拖、移、滚
鼠标事件是 GUI 程序里最常用的事件。Qt 提供了 QMouseEvent 类,里面包含了鼠标按键、坐标位置、修饰键等信息。
我个人习惯把鼠标事件分成三类:点击、移动、滚轮。点击又分按下和释放。
来看一个例子。我想让一个 QLabel 在鼠标点击时改变颜色:
// 自定义 QLabel,重写鼠标按下事件
class ClickableLabel : public QLabel {
protected:
void mousePressEvent(QMouseEvent *event) override {
if (event->button() == Qt::LeftButton) {
setStyleSheet("background-color: lightblue;");
qDebug() << "点击位置:" << event->pos();
}
QLabel::mousePressEvent(event); // 调用基类,保持默认行为
}
};
小技巧:记得调用基类的 mousePressEvent()。如果不调,某些默认行为(比如双击选中文字)可能会失效。我在项目中踩过这个坑。
鼠标移动事件需要注意一点:默认情况下,只有按下鼠标按钮时才会触发 mouseMoveEvent。如果你希望不按按钮也能跟踪鼠标移动,需要调用 setMouseTracking(true)。
// 开启鼠标跟踪
label->setMouseTracking(true);
// 然后在 mouseMoveEvent 中获取坐标
void YourWidget::mouseMoveEvent(QMouseEvent *event) {
statusBar()->showMessage(
QString("鼠标坐标: (%1, %2)")
.arg(event->x()).arg(event->y())
);
}
滚轮事件用 QWheelEvent,可以获取滚动的角度和方向。我做过一个图片缩放功能,就是用滚轮事件实现的:
void ImageViewer::wheelEvent(QWheelEvent *event) {
double scaleFactor = 1.15;
if (event->angleDelta().y() > 0) {
// 向上滚,放大
scale(scaleFactor);
} else {
// 向下滚,缩小
scale(1.0 / scaleFactor);
}
event->accept();
}
27.2 键盘事件:按键响应与快捷键
键盘事件由 QKeyEvent 表示。常用的有 keyPressEvent 和 keyReleaseEvent。
我曾经做一个文本编辑器,需要支持 Ctrl+S 保存。用键盘事件来处理:
void EditorWindow::keyPressEvent(QKeyEvent *event) {
if (event->modifiers() & Qt::ControlModifier) {
if (event->key() == Qt::Key_S) {
saveFile();
return; // 处理完直接返回,不再传递
}
}
// 其他按键交给基类处理
QMainWindow::keyPressEvent(event);
}
注意:判断修饰键要用位与操作符 &,不要用 ==。因为用户可能同时按了 Ctrl+Shift,modifiers() 返回的是组合值。
键盘事件里还有一个常用场景:方向键控制移动。比如做一个贪吃蛇小游戏:
void GameWidget::keyPressEvent(QKeyEvent *event) {
switch (event->key()) {
case Qt::Key_Up:
if (direction != Down) direction = Up;
break;
case Qt::Key_Down:
if (direction != Up) direction = Down;
break;
case Qt::Key_Left:
if (direction != Right) direction = Left;
break;
case Qt::Key_Right:
if (direction != Left) direction = Right;
break;
default:
QWidget::keyPressEvent(event);
}
}
这里有个细节:防止蛇直接掉头。如果当前正在向上走,按向下键应该被忽略。否则蛇会直接撞到自己身上。
27.3 事件过滤器:拦截与监控
事件过滤器是 Qt 里一个很巧妙的设计。它允许一个对象监听另一个对象的所有事件。
说白了,就是给某个控件装一个「监控摄像头」。所有进出这个控件的事件,都会被过滤器先看到。
使用步骤很简单:
- 调用
target->installEventFilter(this)安装过滤器 - 重写
eventFilter(QObject *obj, QEvent *event)方法
举个例子。我想监控一个 QLineEdit,当它获得焦点时背景变黄:
bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
if (obj == ui->lineEdit) {
if (event->type() == QEvent::FocusIn) {
ui->lineEdit->setStyleSheet("background-color: yellow;");
return false; // 不拦截,继续传递
}
if (event->type() == QEvent::FocusOut) {
ui->lineEdit->setStyleSheet(""); // 恢复默认
return false;
}
}
return QMainWindow::eventFilter(obj, event);
}
关键点:eventFilter 返回 true 表示事件被拦截,不再往下传递。返回 false 表示放行。我建议除非你明确要拦截,否则一律返回 false。
事件过滤器特别适合处理「我不想修改控件源码,但又想干预它的行为」的场景。比如给第三方控件加一些额外校验。
27.4 重写事件函数:最直接的方式
如果你拥有控件的源码,重写事件函数是最直接、最清晰的方式。
Qt 里每个控件都有对应的事件虚函数。比如:
| 事件类型 | 虚函数 | 说明 |
|---|---|---|
| 鼠标按下 | mousePressEvent | 鼠标按键被按下 |
| 鼠标释放 | mouseReleaseEvent | 鼠标按键被释放 |
| 鼠标双击 | mouseDoubleClickEvent | 鼠标双击 |
| 鼠标移动 | mouseMoveEvent | 鼠标移动 |
| 键盘按下 | keyPressEvent | 键盘按键被按下 |
| 键盘释放 | keyReleaseEvent | 键盘按键被释放 |
| 绘制 | paintEvent | 控件需要重绘 |
| 大小变化 | resizeEvent | 控件大小改变 |
| 显示/隐藏 | showEvent / hideEvent | 控件显示或隐藏 |
重写事件函数时,有一个原则我一直记着:如果你处理完了,就调用基类版本;如果你完全接管了,可以不调。
举个例子。我想做一个只能输入数字的 QLineEdit:
class NumberLineEdit : public QLineEdit {
protected:
void keyPressEvent(QKeyEvent *event) override {
// 只允许数字键、退格键、删除键、方向键
if (event->key() >= Qt::Key_0 && event->key() <= Qt::Key_9) {
QLineEdit::keyPressEvent(event); // 放行
} else if (event->key() == Qt::Key_Backspace ||
event->key() == Qt::Key_Delete ||
event->key() == Qt::Key_Left ||
event->key() == Qt::Key_Right) {
QLineEdit::keyPressEvent(event); // 放行
} else {
// 其他按键忽略
event->ignore();
}
}
};
个人经验:重写事件函数时,尽量保持「要么全处理,要么全放行」的风格。不要在一个函数里既处理又放行,容易出逻辑混乱。
27.5 三种方式的对比与选择
你可能会问:这三种方式到底该用哪个?
我一般这样选:
- 重写事件函数:你拥有控件的继承权,且只需要处理特定控件。最常用。
- 事件过滤器:你不能修改控件源码,或者需要同时监控多个控件。灵活但稍慢。
- 信号槽:Qt 已经帮你封装好了的事件(比如 clicked()),直接用信号槽最方便。
举个例子。如果你只是想响应按钮点击,用 connect(button, &QPushButton::clicked, ...) 就够了。完全不需要碰事件系统。
但如果你需要拦截鼠标按下并判断按下的位置,那就得用事件函数或过滤器了。
避坑指南:我曾经在一个项目里,给整个窗口安装了事件过滤器,然后在 eventFilter 里判断每个子控件的事件。结果性能很差,因为事件太多了。后来改成只给需要的子控件单独安装过滤器,问题解决。
事件处理是 Qt 编程里绕不开的一环。掌握了鼠标事件、键盘事件、事件过滤器和重写事件函数,你就能让程序真正「活」起来。嗯,动手试试吧,写一个能拖动的无边框窗口,或者一个支持快捷键的文本输入框——这些练习会让你对事件的理解更深。
公众号:蓝海资料掘金营,微信deep3321