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 自带的识别器经常误判。

我个人更倾向于自己写轻量级手势引擎。核心思路就三步:

  1. 状态机:记录每个手指的 ID、起始位置、当前位置、时间戳
  2. 特征计算:计算位移、速度、角度、两指间距变化
  3. 规则匹配:根据特征阈值判断手势类型

举个例子,最简单的两指缩放判断:

// 假设 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 应用,中间经过好几层。我画了张图帮你理清这个链路:

多点触控协议链路 多点触控协议链路:从硬件到 Qt 应用 触摸屏硬件 内核驱动层 输入子系统 I2C/SPI input_event libinput / evdev Qt 事件系统 QTouchEvent 通过 /dev/input/event* QPA 插件 事件分发 关键协议说明: • 内核层:使用 MT Protocol A(无状态)或 MT Protocol B(有状态)上报触摸数据 • Protocol A:每次上报所有触摸点,适合简单设备 • Protocol B:只上报变化点,通过 TRACKING_ID 追踪手指,适合电容屏 • Qt 通过 evdev 读取,解析后封装为 QTouchEvent,每个触摸点对应一个 TouchPoint

这张图里,最值得关注的是内核层的 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 排查驱动问题