自定义触摸屏驱动引发QScrollArea移动抖动问题排查
数年前为一款运行在Scientific Linux 6.4(基于CentOS 6)的应用编写了自定义触摸屏驱动,该系统无原生触摸支持,但内核支持触摸事件,因此直接从/dev/input/event*读取触摸屏原始数据,通过Qt生成鼠标事件模拟多点触摸控制应用。近期迁移至RedHat 8.4,因原生Qt触摸事件无法提供同等控制度,禁用了原生触摸驱动。
测试中发现,使用触摸屏操作应用内的图像显示区域(内置QImage、隐藏滚动条的QScrollArea)时,移动出现严重抖动。该区域通过mouseMoveEvent根据鼠标事件的delta值调整滚动条实现拖动,右键拖拽时功能正常。
相关代码如下:
void PanArea::mouseMoveEvent(QMouseEvent* e) { SafeStr str; if (m_pw) { if (m_disable_panning == false && e->buttons().testFlag(Qt::RightButton)) { QPoint delta = e->pos() - m_last_pos; horizontalScrollBar()->setValue(horizontalScrollBar()->value() - delta.x()); verticalScrollBar()->setValue(verticalScrollBar()->value() - delta.y()); m_last_pos = e->pos(); emit signalScrollPositionChanged(getScrollPosition()); // This force update has been added because // when fast panning cause black to appear within the image // because some pan widget updates were being skipped. // Performance seems acceptable with this here. m_pw->update(); } else if (...) { // irrelevant code removed to save space } } }
void InputHandler::panStart(TouchPoint* tp, QWidget* widget) { QPoint pos(tp->cx(), tp->cy()); QWidget* target = widget; if (target == NULL) target = QApplication::widgetAt(pos); if (target != NULL) { QPoint local = target->mapFromGlobal(pos); QMouseEvent* press = new QMouseEvent(QEvent::MouseButtonPress, local, pos, Qt::RightButton, Qt::RightButton, Qt::NoModifier); QApplication::postEvent(widget, press); } } void InputHandler::panMove(TouchPoint* tp, QWidget* widget) { QPoint pos(tp->cx(), tp->cy()); QWidget* target = widget; if (target == NULL) target = QApplication::widgetAt(pos); if (target != NULL) { QPoint local = target->mapFromGlobal(pos); QMouseEvent* move = new QMouseEvent(QEvent::MouseMove, local, pos, Qt::NoButton, Qt::RightButton, Qt::NoModifier); QApplication::postEvent(widget, move); } }
可能的原因及修复方案
1. 动态控件查找导致坐标映射错误
在panStart和panMove中,使用QApplication::widgetAt(pos)动态查找目标控件,当滚动区域移动后,widgetAt返回的控件可能不是预期的PanArea,导致局部坐标计算偏差,最终引发delta值跳变。
修复:直接使用传入的widget(即目标PanArea)进行坐标映射,避免动态查找:
void InputHandler::panStart(TouchPoint* tp, QWidget* widget) { if (widget == NULL) return; QPoint pos(tp->cx(), tp->cy()); QPoint local = widget->mapFromGlobal(pos); QMouseEvent* press = new QMouseEvent(QEvent::MouseButtonPress, local, pos, Qt::RightButton, Qt::RightButton, Qt::NoModifier); QApplication::postEvent(widget, press); } void InputHandler::panMove(TouchPoint* tp, QWidget* widget) { if (widget == NULL) return; QPoint pos(tp->cx(), tp->cy()); QPoint local = widget->mapFromGlobal(pos); QMouseEvent* move = new QMouseEvent(QEvent::MouseMove, local, pos, Qt::NoButton, Qt::RightButton, Qt::NoModifier); QApplication::postEvent(widget, move); }
2. Qt5事件队列特性导致的事件延迟/乱序
Qt5与旧版Qt(如Scientific Linux 6.4默认的Qt4)的事件队列处理逻辑存在差异,postEvent将事件放入队列异步处理,可能导致事件延迟或顺序错乱,进而使坐标计算基于过时的控件状态。
修复:改用sendEvent同步发送事件,确保事件立即处理:
// panStart中替换postEvent QApplication::sendEvent(widget, press); // panMove中替换postEvent QApplication::sendEvent(widget, move);
注意:sendEvent不需要手动删除事件对象,Qt会自动回收。
3. 局部坐标系随滚动变化导致delta计算错误
在mouseMoveEvent中,使用控件局部坐标计算delta,但调整滚动条后,控件的局部坐标系会发生变化,m_last_pos记录的是滚动前的坐标,下一次delta计算会出现偏差。
修复:改用全局坐标计算delta,全局坐标不受控件滚动影响:
// 在PanArea类中添加成员变量QPoint m_last_global_pos; void PanArea::mousePressEvent(QMouseEvent* e) { if (e->button() == Qt::RightButton) { m_last_global_pos = e->globalPos(); } // 保留原有逻辑 } void PanArea::mouseMoveEvent(QMouseEvent* e) { SafeStr str; if (m_pw) { if (m_disable_panning == false && e->buttons().testFlag(Qt::RightButton)) { QPoint delta = e->globalPos() - m_last_global_pos; horizontalScrollBar()->setValue(horizontalScrollBar()->value() - delta.x()); verticalScrollBar()->setValue(verticalScrollBar()->value() - delta.y()); m_last_global_pos = e->globalPos(); emit signalScrollPositionChanged(getScrollPosition()); m_pw->update(); } else if (...) { // 无关代码省略 } } }
4. 触摸屏原始数据噪声
RedHat 8.4下的触摸屏驱动可能输出更多原始数据噪声,导致生成的鼠标事件坐标跳变。
修复:在读取/dev/input/event*数据时添加滤波处理,比如:
- 丢弃与上一次坐标偏差超过阈值的点;
- 使用滑动平均算法平滑连续的坐标点;
- 对原始触摸数据做去抖处理。
内容的提问来源于stack exchange,提问作者Darin Beaudreau

