2. 触摸交互基础

在 Qt Quick 中,触摸交互并不是“鼠标事件的另一种写法”,而是一套独立的事件体系。鼠标事件通常只关心一个光标位置,而触摸事件天然面向多点、多指、多设备。理解触摸事件类型、QTouchEvent、触摸点(TouchPoint)与触摸设备(Device)之间的关系,是后续实现捏合缩放、旋转、多指拖拽和手势识别的基础。

一、触摸事件类型:从按下到释放的完整生命周期

Qt 将触摸交互抽象为三类核心事件:QEvent::TouchBegin、QEvent::TouchUpdate 和 QEvent::TouchEnd。它们描述的是“一组触摸点”的整体状态变化,而不是单个手指的独立事件。一次完整的触摸序列通常如下:

  • TouchBegin:第一个手指按下时触发,表示触摸序列开始。
  • TouchUpdate:手指移动、增加或减少时触发,是频率最高的事件。
  • TouchEnd:最后一个手指离开屏幕时触发,表示序列结束。

需要特别注意的是,当第二根手指按下时,并不会产生新的 TouchBegin,而是在下一次 TouchUpdate 中通过新增的触摸点体现。因此,处理多点触摸时,必须遍历事件中的所有触摸点,而不是只读取第一个点。

在 Qt Quick 中,QML 层通常使用 MultiPointTouchArea、PinchArea 或 GestureArea 处理触摸;而在 C++ 层,则通过重写 QQuickItem::touchEvent() 或安装事件过滤器来接收 QTouchEvent。

二、QTouchEvent:触摸数据的载体

QTouchEvent 继承自 QInputEvent,是 Qt 中承载触摸信息的核心类。它并不直接保存“一个坐标”,而是保存一组 QTouchEvent::TouchPoint。常用接口包括:

  • type():返回 TouchBegin、TouchUpdate 或 TouchEnd。
  • touchPoints():返回当前事件中所有触摸点的列表。
  • device():返回产生该事件的触摸设备。
  • target():返回事件接收对象。

在 C++ 中处理触摸事件的典型代码如下:

bool MyItem::touchEvent(QTouchEvent *event)
{
    switch (event->type()) {
    case QEvent::TouchBegin:
        qDebug() << "touch begin, points:" << event->touchPoints().size();
        break;
    case QEvent::TouchUpdate:
        for (const QTouchEvent::TouchPoint &p : event->touchPoints()) {
            qDebug() << "id:" << p.id()
                     << "pos:" << p.position()
                     << "state:" << p.state();
        }
        break;
    case QEvent::TouchEnd:
        qDebug() << "touch end";
        break;
    default:
        break;
    }
    event->accept();
    return true;
}

这段代码揭示了两个关键点:第一,触摸事件是“批量”的;第二,每个触摸点都带有自己的状态和坐标。只有正确遍历所有点,才能实现真正的多点交互。

三、触摸点 TouchPoint:每个手指的身份与状态

QTouchEvent::TouchPoint 描述单个触摸点。它最重要的字段包括:

字段/方法 含义 典型用途
id() 触摸点唯一标识 跨事件跟踪同一根手指
position() 相对接收对象的坐标 计算位移、缩放中心
scenePosition() 场景坐标 跨 Item 坐标转换
screenPosition() 屏幕坐标 多屏或全局定位
state() Pressed / Updated / Stationary / Released 判断手指动作阶段
pressure() 压力值(若设备支持) 压感绘画、力度交互

其中 id() 是区分手指的关键。例如,两根手指同时移动时,不能依赖列表顺序,而应通过 id 建立映射,记录每根手指上一帧的位置,从而计算各自的位移向量。

在 QML 的 MultiPointTouchArea 中,touchPoints 属性同样提供 pointId、position、state 等信息,其设计思想与 C++ 的 TouchPoint 一致,便于在两种层级间迁移理解。

四、触摸设备 Device:事件来源与能力描述

触摸事件不仅包含“点”,还包含“设备”。QTouchEvent::device() 返回一个 QTouchDevice,用于描述产生触摸的硬件。常见属性包括:

  • type():TouchScreen、TouchPad 等。
  • capabilities():是否支持位置、压力、速度等。
  • maximumTouchPoints():最大同时触摸点数。
  • name():设备名称,便于调试与日志区分。

设备信息在多设备场景中非常重要。例如,同一应用可能同时接收触摸屏和触摸板的事件,二者在精度、压力支持和手势语义上存在差异。通过设备类型判断,可以决定是否启用压感绘制,或是否将触摸板事件转换为鼠标式交互。

Qt Quick 触摸事件体系 设备 → 事件 → 触摸点 → 处理层 QTouchDevice type: TouchScreen maxTouchPoints: 10 capabilities: Position QTouchEvent TouchBegin TouchUpdate TouchEnd TouchPoint id / position state / pressure scenePosition 处理层 QML: MultiPointTouchArea / PinchArea / GestureArea C++: QQuickItem::touchEvent() / eventFilter() 关键:一次事件包含多个 TouchPoint,必须遍历处理 id 用于跨事件跟踪同一根手指

五、案例:双指捏合缩放中的触摸点跟踪

假设我们要实现一个图片查看器,用双指捏合来缩放。核心逻辑是:在 TouchBegin 时记录两个触摸点的初始距离;在 TouchUpdate 时计算当前距离,缩放因子为当前距离与初始距离之比。伪代码如下:

// TouchBegin
if (points.size() == 2) {
    initialDistance = distance(points[0].position(), points[1].position());
}

// TouchUpdate
if (points.size() == 2 && initialDistance > 0) {
    qreal current = distance(points[0].position(), points[1].position());
    qreal scale = current / initialDistance;
    item->setScale(scale);
}

这里必须使用 id() 来确认两个触摸点是否与初始时一致。如果用户中途抬起一根手指再按下,触摸点集合会变化,此时应重新初始化,否则会出现缩放跳变。

常见误区:直接使用 touchPoints().first() 处理所有触摸。这样在多点场景下会丢失其他手指的信息,导致捏合、旋转等手势失效。正确做法是遍历全部触摸点,并根据 id 和 state 进行筛选。

六、误区与复盘:触摸事件处理中的典型问题

  • 混淆鼠标与触摸:鼠标事件只有一个位置,触摸事件有多个点。用鼠标思维写触摸逻辑,必然无法支持多点。
  • 忽略事件接受状态:如果父 Item 先接受了触摸事件,子 Item 可能收不到。需要合理设置 accepted 或使用 setFiltersChildMouseEvents。
  • 未处理 TouchEnd:手指全部离开后,必须清理状态,否则下一次 TouchBegin 会沿用旧数据。
  • 设备能力假设:并非所有触摸设备都支持压力或速度,使用前应检查 capabilities()。

复盘时可以用一个简单清单检查:是否遍历了所有触摸点?是否用 id 跟踪手指?是否在 TouchEnd 中重置状态?是否根据设备类型做了兼容处理?这四点覆盖了大多数触摸交互的稳定性问题。

掌握触摸事件类型、QTouchEvent、触摸点与设备四者之间的关系后,后续的 MultiPointTouchArea、手势识别和自定义交互组件都会变得有据可依。触摸交互的本质,是对“一组带身份的点”在时间轴上的状态管理。