3. 事件传递与分发:事件传递路径、事件过滤器、事件重写
好,咱们今天聊聊Qt里事件是怎么“跑”起来的。说白了,就是用户点了一下鼠标,或者敲了一下键盘,这个“消息”是怎么从系统一路传到你的控件里,最后被你处理掉的。
我刚开始学Qt的时候,总觉得事件处理很玄乎。后来踩了几个坑才明白,其实就三条路:事件传递路径、事件过滤器、事件重写。掌握了这三板斧,大部分事件问题都能搞定。
3.1 事件传递路径:消息是怎么“流”到你手里的?
先看一张图,我画了个简单的流程,帮你理解事件从产生到被处理的完整路径。
你看,事件从系统产生后,先经过 QApplication::notify(),然后到事件过滤器,再到 event() 函数,最后才到具体的处理函数(比如 mousePressEvent)。如果某一层没处理,事件就会沿着父控件链往回传。
嗯,这里有个关键点:事件传递是“自顶向下”再“自底向上”的。先发给最外层的窗口,然后一层层往下找,直到找到能处理它的控件。如果这个控件也没处理,事件又会沿着父链往回传。
核心原则:事件传递遵循“先父后子,再子后父”的路径。任何一层都可以“吃掉”事件,阻止它继续传递。
3.2 事件过滤器:在事件到达前“截胡”
事件过滤器,说白了就是“半路拦截”。你可以在一个对象上安装过滤器,这样所有发给这个对象的事件,都会先经过过滤器“审一遍”。
我个人习惯用事件过滤器来处理一些“跨控件”的逻辑。比如,我想让一个对话框里的所有输入框都只能输入数字,不用每个输入框都去重写 keyPressEvent,装一个过滤器就搞定了。
// 安装事件过滤器
ui->lineEdit->installEventFilter(this);
// 重写 eventFilter 函数
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (obj == ui->lineEdit) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
// 只允许数字键
if (keyEvent->key() < Qt::Key_0 || keyEvent->key() > Qt::Key_9) {
return true; // 吃掉事件,不让它继续传递
}
}
}
return QMainWindow::eventFilter(obj, event); // 其他事件正常处理
}
小技巧:事件过滤器返回 true 表示“事件已被处理,不用再往下传了”。返回 false 则让事件继续走正常路径。我曾经因为忘记调用父类的 eventFilter,导致整个窗口的事件都失效了,排查了半天……
3.3 事件重写:直接接管控件的“生杀大权”
事件重写是最直接的方式。你继承一个控件,然后重写它的 event() 函数或者具体的处理函数(比如 paintEvent、mousePressEvent)。
你想想看,什么时候用重写?当你需要彻底改变一个控件的行为时。比如,你想做一个“点击按钮后按钮变红”的效果,直接重写 QPushButton 的 mousePressEvent 和 mouseReleaseEvent 就行了。
class MyButton : public QPushButton
{
protected:
void mousePressEvent(QMouseEvent *event) override
{
// 先做自己的逻辑
setStyleSheet("background-color: red;");
// 再调用父类处理(如果不调用,按钮的默认点击效果就没了)
QPushButton::mousePressEvent(event);
}
void mouseReleaseEvent(QMouseEvent *event) override
{
setStyleSheet("background-color: green;");
QPushButton::mouseReleaseEvent(event);
}
};
注意:重写事件处理函数时,一定要考虑是否调用父类的同名函数。不调用的话,控件的默认行为(比如按钮的按下弹起动画)就全没了。我刚开始做的时候经常忘,结果按钮点下去一点反馈都没有,还以为代码写错了。
3.4 三种方式的对比与选择
这三种方式各有各的适用场景。我整理了一个表格,方便你对比:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 事件过滤器 | 跨控件、全局性的事件拦截 | 不需要继承控件,灵活 | 性能略差(所有事件都要过一遍) |
| 重写 event() | 需要拦截多种事件类型 | 集中管理,逻辑清晰 | 代码量稍大 |
| 重写具体事件函数 | 只关心某一种事件 | 简单直接,代码量少 | 无法拦截其他类型事件 |
说白了,选哪种取决于你的需求。如果你只是想“偷听”一下某个控件的事件,用过滤器。如果你想彻底改造一个控件的行为,那就重写。如果你需要同时处理鼠标、键盘、触摸等多种事件,重写 event() 会更方便。
3.5 避坑指南:我曾经踩过的坑
最后分享几个我实际项目中遇到的坑,希望能帮你少走弯路:
- 事件过滤器忘记返回父类调用: 有一次我写了一个全局的事件过滤器,用来统计用户操作。结果返回
true的地方太多,导致很多按钮点不动了。后来才发现,不是所有事件都需要被“吃掉”的。 - 重写事件函数时没调用父类: 这个前面提过,但值得再说一遍。尤其是
paintEvent,如果你不调用父类的,控件就变成一片空白了。 - 事件传递路径搞反了: 我刚开始以为事件是从子控件往父控件传的,结果调试了半天才发现,其实是先父后子。这个顺序搞错了,很多逻辑都会出问题。
总结一下:事件传递路径是基础,事件过滤器是“半路拦截”,事件重写是“直接接管”。三者配合使用,基本能搞定Qt里所有的事件处理需求。嗯,多写多试,慢慢就有感觉了。