排查自模拟QWheelEvent消息不能向上传递
1、事情起因
因为我们的项目需要做一次大升级。从Qt 5.5升级到Qt 5.15.2.0,这个大版本的升级真是把我折腾的够呛。
在我们的项目中,自己模拟了一个鼠标滚轮消息的。利用QApplication::postEvent()
函数,向消息队列中发送一个QWheelEvent
消息。这样就可以控件就可以自动滚动了。
但是项目升级到Qt 5.15.2.0之后发现不行了。
擦了擦额头的汗,这个bug怎么修啊?无从下手的感觉。
去请教别人?万一,别人觉得我很low怎么办。关键是大佬时间也很宝贵。
没办法,只有自己慢慢的探索了。需要准备好Qt 5.15.2.0 pdb文件和源码。
2、解决方案
我先说原因:
因为升级到高版本之后,Qt的对QWheelEvent事件处理发生了变化。在Qt 5.5中会自动将QEvent::spont
属性设置true
而到了高版本之后,则去掉了这样的处理。导致事件不会继续向上处理,所以父控件没有收到消息。
在其文档也进行了说明。
只有系统触发的event,才会设置为true,否则就是false.
Returns true if the event originated outside the application (a system event); otherwise returns false.
接下来就是方案,有两种:
- 重写
QApplication::notify(QObject* pObject, QEvent* pEvent)
函数中拦截QEvent::Wheel
事件,强制设置pEvent->spont
属性为true
case QEvent::Wheel:
{
// 检查Qt的版本号
if (QT_VERSION == QT_VERSION_CHECK(5,15,2))
{
//TODO:
// 此处hack pEvent的数据成员,如果要升级项目此处一定要注意,可能QEvent内存布局发生变化
// 因为项目升级到5.15.5.0,源码发生很大的变化。
// 在Qt 5.5中会自动将pEvent->spont 设置为true。(此成员未提供public方法修改)
// 在Qt 5.15.2.0 只会转发pEvent数据,不做任何的修改。
// 所以找到pEvent地址,找到成员变量的偏移地址,再修改成员变量的值
// address = (char *)pEvent + sizeof(vtable ptr) + sizeof(QEventPrivate *d) + sizeof(ushort)
// 从源码中可以找到QEvent的成员变量的分布
if (!pEvent->spontaneous())
{
char* flag = ((char*)pEvent + sizeof(char *)*2 + sizeof(ushort));
// 强制设置flag的第二个字节位为1
*flag |= 0b10;
}
}
}
- 重新编译Qt的源码,将设置该属性的函数设置为public。
但是在项目中我没有选择这种方式,因为时间成本问题。
3、排查过程
如果上面的方案解决不了你的问题。你可能需要看下我排查的过程了。
万事开头难,刚开始时,我是一点头绪都没有。
所以我采取的方法就是把项目相关的源码通读一遍,遇到看不懂的地方就跳过去,不重要的也跳过去。
1.确定是自己的bug还是Qt的bug?
因为这个父控件是我们的自定义控件,而且为了适应我们的应用场景,内部逻辑十分复杂。
所以我只能将环境一点点的简化,就是排除法去验证是谁的问题。
-
当我发现我用鼠标直接去滚轮,而不是模拟鼠标滚动,发现居然是可以的。那我基本排除控件应该没有问题的。但是我不是很肯定,需要再进一步的验证。
-
我又在父控件中对拦截QWheelEvent消息处下了断点,发现居然没有拦截下来。这几乎可以判断父控件没有问题,就是没有收到QWheelEvent消息,但是问题是为什么收到应有的消息?
当我知道这个原因之后就立马去google下,因为我觉得肯定有人遇到了,但是一无所获。
我觉得这个应该是Qt的bug,所以又去的Qt report bug网站上关键词搜了下,也没有结果。
2.源码和pdb准备
求人不如求己吧。准备跟踪下调用堆栈,看看有没有区别。
我刚开始时根据堆栈不断的去溯源,我就是想看看这个QWheelEvent是从哪里发出来的。它的源头从哪里来的。
这个花了很多的时间,最后我发现就是windows的底层消息,经过Qt的包装一层,不断的往上传递。
其实我也是看的云里雾里的,一知半解。但是大概过程我是了解的。
这种鬼见愁的堆栈,一看就头大。因为消息的发送和处理,完全是异步的过程。
这是自己模拟滚轮消息处理堆栈。
在这个过程中,我就在思考这要是Qt的bug,我TM的怎么修复啊。对整个框架的不清楚,就修这个bug,大概率又衍生出其他的 bug。
我开始对比我们自己发送的QWheelEvent消息和系统发送的消息到底有没有什么不同。
我心生一计,我就在QAapplication::notify里面对QWheel拦截,看看这自己发送和系统生成两个消息到底有什么不同。
经过对比之后,我的确发现这两个消息有些地方是不一样的。但是我没法确切的知道到底是哪两个不一样造成的。
同时,因为不知道到底是哪个不一样。我还去对比了他们的父类数据。这不是重点我就不展开说了。
那就用最笨也是最有用的方法,一个个的试。
我就按照系统发送的消息值,一个个的去设置自模拟的值。经过我艰苦卓绝的实验,发现就只有一个值设置和系统的一样就成功了。
那就是QWheelEvent中的spont属性设置为true。
嘿嘿……高兴极了,于是我就开始查文档,这个属性值调用函数设置下就OK了。
一查文档就傻眼了,这个属性是父类QEvent中的,Qt就没有提供public接口给我设置。这就说明这个属性就是内部使用。
经过短暂的懵逼状态,我想直接把你hack了吧。第二种方案要不和领导申请下编译下这个版本的源码吧,但是这个时间成本较高,直接 被我放弃了。
事情到了本该差不多结束了。
3.再跟源码,到底是哪里不一样。
我虽然修复了这个bug,但是我还是不知道具体的原因。到底是怎么造成的,没有找到更具体的原因,我始终不甘心。
更何况这种修复方案,肯定要和领导说明的。要是没有说服力的证据,很难让领导相信这种方案更好。说不定稳妥起见,可能会编译源 码。
先看Qt 5.5源码堆栈:
在Qt 5.5中最迷惑的就是,在第一次的时候你会发现QEvent::spont属性也是0.只有第二次才是我们想要的。
从堆栈中我看到一个非常重要的函数QCoreApplication::sendSpontaneousEvent
,定位到源码看一看。
嚯,一切了然了。原来在这里帮我设置了这个属性值为1(true)了。
inline bool QCoreApplication::sendSpontaneousEvent(QObject *receiver, QEvent *event)
{ if (event) event->spont = true; return self ? self->notifyInternal(receiver, event) : false; }
在看Qt 5.15.2.0的。为了看的更清楚,我把自定义的消息堆栈和系统消息的堆栈放在一起对比下。
从这张图就能明显的看出来,系统发送的消息和我们自定义的消息多调了一个函数QCoreApplication::sendSpontaneousEvent
至此,我才觉得心中出了一口气,原来系统调用了一个函数来设置spont值。我猜测之所以这样做,可能就是为了区别这个消息是不是别人模拟的假消息,而在早期的版本并没有这样的区别。
4.设置spont属性的原理
对于上述为什么需要这样的设置,我解释下。但是你如果熟悉c++对象的内存模型,就应该很好理解。如果不理解,可以先看看我的《C++幕后故事》系类文章。
我画一张内存图方便理解。
这些信息都是从源码获取的。
最后,我虽然修复了这个问题。但是我始终心有不安,这样做到底有没有其他的问题。我进行了仔细的分析。
- 如果你迁移了项目,这个成员变量发生了变化,那么后果不堪设想。
所以我加上了对版本的判断,如果是其他的版本需要再验证下成员变量分布是否有变化。
- 指针的大小和ushort大小,我们知道在不同的平台,这些可能不一样大的。
所以在计算偏移量的时候,我都不是用固定值,而是采取动态计算的方法。
5.反思与总结
排查这个问题,花了几天的时间。
我觉得如何找到切入点是个非常重要的事情。我期间花了很多的时间走了弯路,比如:试图搞清楚整个消息的来龙去脉,结果搞得我灰头土脸,还是太高估自己了。
一定要有全局观,适当的放弃一些细节问题。只有纵览全局,才能不被一些细枝末节缠住。