一个Qt鼠标透传场景与事件过滤器的用法
最近工作中遇到一个开发场景,将一个QWidget控件(称为控件A)放入QScrollArea,该控件A重写了QWidget::wheelEvent,根据鼠标滚轮事件缩放内部的绘制视图。当控件过大时,QScrollArea的滚动条出现,此时鼠标滚动也会触发页面滚动,用一个Demo演示:
问题
对于这个体验问题,提出了一个方案,当按住Ctrl并滚动滚轮,执行控件A的触发缩放逻辑;当没有按Ctrl时,仅滚动页面。但是,由于该控件A是第三方库创建的,无法重写事件处理,所以只能借助Qt事件过滤器或者其他方案。
分析
出现该问题的原因是,控件A重写QWidget::wheelEvent处理缩放,没有将QWheelEvent设置为accepted,即标记为已处理,而Qt对于输入事件(鼠标、键盘等),如果目标控件没有处理,则会将事件投递给其父控件。
因此需要通过事件过滤器,处理控件A的逻辑,不继续透传事件,或者直接透传事件。
基本事件分发流程
Qt分发事件的总入口在QApplication::notify,事件在这里被分发给目标控件。事件分发后,对于Qt的鼠标等事件,会根据事件是否被处理(即QEvent::isAccepted),将未处理的事件透传给当前控件的父控件,或者称之为冒泡。直到其中一个父级控件处理,事件停止透传。
事件分发给目标控件前,会查找目标控件安装的事件过滤器(通过QObject::installEventFilter注册)对象,并按顺序调用其QObject::eventFilter。该方法返回bool值,true则直接返回到QApplication::notify,后续的事件过滤器对象也就不再被调用。
如果所有的事件过滤器对象(QObject::eventFilter)都返回false,则会调用目标控件的QObject::event。对于QWidget,此时会根据事件类型,调用对应的事件处理函数(如 QWidget::wheelEvent)。不同的Qt控件,可能也会重写QWidget::event,在调用其事件处理函数前,执行一些逻辑。
解决方法
首先控件A安装事件过滤器到任意QObject对象,重写该对象的eventFilter方法:
bool Widget::eventFilter(QObject *watched, QEvent *event)
{
if(event->type() == QEvent::Wheel && watched == target)
{
if(static_cast<QWheelEvent*>(event)->modifiers() & Qt::CTRL)
{
watched->event(event); // 1
event->accept(); // 2
return true; // 3
}
else
{
event->ignore(); // 4
return true; // 5
}
}
return false; // 原则上应该调用父类的eventFilter。如果父类没有被安装过滤器,直接false即可
}
代码解释:
- 1,直接调用QObject::event接口。因为QWidget::event重写时改成protected
- 2、3,标记事件已被处理,并返回true。按上面说的事件分发流程,回到QApplication::notify,由于事件被处理,鼠标滚轮事件不再继续透传给父控件,分发中断。
- 4、5,标记事件未处理,返回true,同样回到QApplication::notify,事件透传给父控件。因此跳过了当前控件的处理逻辑。