4、QTouchEvent详解:触摸点数据结构、手势识别基础、多点触控协议
触摸屏开发,说白了就是跟 QTouchEvent 打交道。很多新手一上来就接 mousePressEvent,结果在电容屏上各种诡异——点不准、乱跳、手势识别失败。嗯,这章我们就把它彻底讲透。
4.1 触摸事件的核心:QTouchEvent 与 QTouchDevice
先看事件源头。Qt 里触摸事件不是凭空产生的,它背后有个 QTouchDevice 对象。每个触摸屏设备,在 Qt 里都对应一个 QTouchDevice 实例。
关键概念:QTouchEvent 是事件载体,QTouchDevice 是设备描述。两者通过 device() 方法关联。
我习惯在构造函数里先打印一下设备信息:
// 获取当前窗口关联的触摸设备
const QList<const QTouchDevice *> devices = QTouchDevice::devices();
for (const QTouchDevice *dev : devices) {
qDebug() << "设备名称:" << dev->name();
qDebug() << "类型:" << dev->type(); // TouchScreen / TouchPad
qDebug() << "最大触摸点数:" << dev->maximumTouchPoints();
qDebug() << "支持手势:" << dev->capabilities();
}
你想想看,如果 maximumTouchPoints() 返回 1,那这屏就不支持多点触控。我在一个工控项目里就踩过这个坑——硬件明明支持 5 点,驱动没配好,Qt 只认到 1 点,折腾了两天。
4.2 触摸点数据结构:QTouchEvent::TouchPoint
每个手指或触控笔,对应一个 QTouchEvent::TouchPoint 对象。它包含的信息远比鼠标事件丰富。
| 属性 | 类型 | 说明 |
|---|---|---|
id() |
int | 触摸点唯一 ID,手指抬起前不变 |
state() |
Qt::TouchPointState | Pressed / Moved / Stationary / Released |
pos() |
QPointF | 当前坐标(相对于接收事件的 Widget) |
scenePos() |
QPointF | 场景坐标(QGraphicsScene 中使用) |
screenPos() |
QPointF | 屏幕全局坐标 |
pressure() |
qreal | 压力值,0.0 ~ 1.0 |
ellipse() |
QRectF | 触摸接触面的椭圆区域 |
velocity() |
QVector2D | 当前速度向量(部分设备支持) |
我的习惯:永远用 id() 来追踪手指,不要依赖 pos() 或列表索引。手指顺序可能乱,但 ID 不会变。
来看一个实际的事件处理代码:
bool MyWidget::event(QEvent *event) {
if (event->type() == QEvent::TouchBegin ||
event->type() == QEvent::TouchUpdate ||
event->type() == QEvent::TouchEnd) {
QTouchEvent *touchEvent = static_cast<QTouchEvent *>(event);
const QList<QTouchEvent::TouchPoint> &points = touchEvent->touchPoints();
for (const QTouchEvent::TouchPoint &tp : points) {
int id = tp.id();
QPointF pos = tp.pos();
Qt::TouchPointState state = tp.state();
// 根据 state 做不同处理
if (state == Qt::TouchPointPressed) {
// 记录手指起始位置
m_fingerStart[id] = pos;
} else if (state == Qt::TouchPointMoved) {
// 计算位移
QPointF delta = pos - m_fingerStart[id];
// 做拖拽逻辑...
} else if (state == Qt::TouchPointReleased) {
// 清理记录
m_fingerStart.remove(id);
}
}
return true;
}
return QWidget::event(event);
}
4.3 手势识别基础:从原始触摸到语义手势
原始触摸点只是一堆坐标。手势识别的本质,就是从这些坐标序列里提取出语义——比如「这是两指缩放」还是「单指滑动」。
Qt 内置了 QGestureRecognizer 框架,但说实话,我很少直接用。为什么?因为它的识别策略偏保守,而且跟平台相关。我在做医疗设备 UI 时,手势必须精确到像素级,Qt 自带的识别器经常误判。
我个人更倾向于自己写轻量级手势引擎。核心思路就三步:
- 状态机:记录每个手指的 ID、起始位置、当前位置、时间戳
- 特征计算:计算位移、速度、角度、两指间距变化
- 规则匹配:根据特征阈值判断手势类型
举个例子,最简单的两指缩放判断:
// 假设 points 包含两个有效触摸点
QPointF p1 = points[0].pos();
QPointF p2 = points[1].pos();
qreal currentDist = QLineF(p1, p2).length();
// 与上一帧的距离比较
qreal scaleFactor = currentDist / m_lastDistance;
if (qAbs(scaleFactor - 1.0) > 0.05) {
// 执行缩放
emit zoomGesture(scaleFactor);
}
m_lastDistance = currentDist;
注意:手势识别一定要做防抖处理。我曾经在原型机上没加阈值,结果手指轻微抖动就被识别成缩放,用户反馈「界面在抽风」。
4.4 多点触控协议:内核到应用层的完整链路
触摸数据从硬件到 Qt 应用,中间经过好几层。我画了张图帮你理清这个链路:
这张图里,最值得关注的是内核层的 MT Protocol A 和 B 的区别。我遇到过一个问题:某款触摸屏在 Protocol A 模式下,Qt 总是丢失触摸点。后来查内核日志,发现驱动上报的时序有问题——两个触摸点的时间戳差了几毫秒,Qt 把它们当成两次独立事件处理了。
排查技巧:用 evtest 工具直接看内核上报的原始事件。如果看到 ABS_MT_TRACKING_ID 频繁变化,说明驱动有问题。
4.5 实战:触摸事件与鼠标事件的共存策略
很多 Qt 应用既要支持触摸,又要支持鼠标。默认情况下,Qt 会把触摸事件自动转换为鼠标事件。但这会带来问题——你分不清用户是用手指还是鼠标操作的。
我的做法是:
// 在构造函数中设置
setAttribute(Qt::WA_AcceptTouchEvents);
setAttribute(Qt::WA_AcceptTouchEvents, true);
// 然后重写 event() 拦截触摸事件
bool MyWidget::event(QEvent *e) {
if (e->type() == QEvent::TouchBegin) {
// 标记为触摸操作
m_isTouch = true;
// 阻止转换为鼠标事件
e->accept();
return true;
}
// 其他事件正常处理
return QWidget::event(e);
}
我曾经踩过的坑:在 QGraphicsView 里同时启用触摸和鼠标,结果缩放时手势和滚轮事件打架。解决方案是在 QGraphicsView::viewport() 上设置 setAttribute(Qt::WA_AcceptTouchEvents),而不是在 View 本身上设。
4.6 本章小结
QTouchEvent 的核心就三件事:
- 数据结构:TouchPoint 的 ID、状态、坐标、压力——用 ID 追踪手指
- 手势识别:状态机 + 特征计算 + 阈值判断,别依赖 Qt 内置识别器
- 协议链路:从内核 MT Protocol 到 Qt 事件,用 evtest 排查驱动问题