2. 嵌入式触摸系统概述

触摸屏开发,手势识别是绕不开的核心环节。很多刚入行的朋友觉得手势不就是“点一下、划一下”嘛,真做起来才发现坑不少。我最早做工业平板项目时,就因为在手势识别上偷了懒,结果现场操作员反馈“点十次有三次没反应”——那叫一个尴尬。

这一章,咱们把Qt里的触摸手势体系彻底捋一遍。从最底层的单点触摸事件,到系统级的多点触摸,再到QGesture框架,最后手撸一个自定义手势。你跟着走一遍,以后遇到手势相关的问题心里就有底了。

2.1 单点触摸:最基础也最容易出错

单点触摸,说白了就是“一根手指”的操作。Qt里处理单点触摸,核心是重写 QWidget::touchEvent() 或者 QQuickItem::touchEvent()。

但这里有个常见的误区:很多人以为 mousePressEvent 就能搞定触摸。嗯,在电阻屏时代确实可以,但电容屏的触摸事件和鼠标事件是两条独立的路径。如果你只处理鼠标事件,在纯触摸设备上可能会漏掉一些触摸特有的属性,比如触摸压力、触摸面积。

我的习惯:在嵌入式触摸设备上,我通常同时处理 touchEvent 和 mouseEvent,但以 touchEvent 为主。因为触摸事件能拿到更精确的 QTouchEvent::TouchPoint 信息。

来看一个最简单的单点触摸示例:

// 在QWidget子类中启用触摸
setAttribute(Qt::WA_AcceptTouchEvents);

// 重写touchEvent
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();
        
        if (points.size() == 1) {
            // 单点触摸处理
            const QTouchEvent::TouchPoint &pt = points.first();
            qDebug() << "触摸位置:" << pt.pos() << "压力:" << pt.pressure();
        }
        return true;
    }
    return QWidget::event(event);
}
我曾经踩过的坑:在Linux下用tslib驱动时,如果没设置 QPA_EGLFS_INTEGRATION 或者 QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS,触摸事件可能根本不会触发。调试时先确认 QT_DEBUG_PLUGINS=1 环境变量下能看到触摸插件加载成功。

2.2 多点触摸:两根手指的世界

多点触摸,最常见的就是双指缩放、双指旋转。Qt对多点触摸的支持,底层依赖 QTouchEvent 中的 TouchPoint 列表。每个手指对应一个 TouchPoint,它们有独立的ID、位置、压力、面积。

你想想看,如果要做双指缩放,核心逻辑就是计算两个手指之间的距离变化。我见过有人用 QPinchGesture 来做,但有时候手势识别不够灵敏,不如自己算距离来得直接。

触摸点数 常见手势 Qt推荐处理方式
1 点击、拖动、长按 QTouchEvent + 自定义逻辑
2 缩放、旋转、双指滑动 QPinchGesture / 手动计算距离
3+ 三指切换、四指任务 QTouchEvent + 手指ID追踪

下面是一个手动实现双指缩放的片段:

void MyView::touchEvent(QTouchEvent *event)
{
    const auto &points = event->touchPoints();
    if (points.size() == 2) {
        QPointF p1 = points[0].pos();
        QPointF p2 = points[1].pos();
        qreal currentDist = QLineF(p1, p2).length();
        
        if (event->type() == QEvent::TouchBegin) {
            m_lastDist = currentDist;
        } else if (event->type() == QEvent::TouchUpdate) {
            qreal scale = currentDist / m_lastDist;
            // 应用缩放
            applyZoom(scale);
            m_lastDist = currentDist;
        }
    }
}
关键点:多点触摸一定要追踪手指ID。如果手指ID乱了,缩放就会“跳变”。我习惯用 QTouchEvent::TouchPoint::id() 来建立手指到逻辑点的映射。

2.3 QGesture手势识别框架

Qt提供了一套高级手势识别框架——QGesture。它把底层的触摸事件封装成了更语义化的手势:捏合、旋转、滑动、轻扫。你不需要自己算距离、算角度,框架帮你做了。

使用QGesture的步骤很简单:

  1. 在目标widget上调用 grabGesture() 注册手势类型
  2. 重写 event() 或 gestureEvent() 处理 QGestureEvent
  3. 从 QGesture 子类中提取手势参数

举个例子,注册并处理捏合缩放:

// 在构造函数中
grabGesture(Qt::PinchGesture);

// 重写event
bool MyWidget::event(QEvent *event)
{
    if (event->type() == QEvent::Gesture) {
        QGestureEvent *ge = static_cast<QGestureEvent*>(event);
        if (QGesture *pinch = ge->gesture(Qt::PinchGesture)) {
            QPinchGesture *pg = static_cast<QPinchGesture*>(pinch);
            qreal scaleFactor = pg->scaleFactor();
            qreal rotationAngle = pg->rotationAngle();
            // 应用缩放和旋转
            return true;
        }
    }
    return QWidget::event(event);
}
我建议:QGesture适合“标准手势”,比如缩放、旋转。但如果你需要自定义的“画圈”、“画勾”等手势,QGesture就不够灵活了。这时候需要自己实现手势识别引擎。

2.4 自定义手势:从零开始造轮子

为什么需要自定义手势?因为实际项目里总有奇奇怪怪的需求。我记得有个医疗设备项目,客户要求“在屏幕上画一个‘L’形来启动校准”。这种手势QGesture肯定不支持,只能自己写。

自定义手势的核心思路:

  • 采集轨迹:在 touchEvent 中记录手指移动的点序列
  • 特征提取:计算方向、速度、曲率、拐点
  • 模板匹配:与预定义的手势模板做相似度比较
  • 触发动作:匹配成功则执行对应操作

下面是一个简单的“画圈”手势识别器骨架:

class CircleGestureRecognizer {
public:
    bool recognize(const QVector<QPointF> &points) {
        if (points.size() < 20) return false; // 点数太少
        
        // 1. 计算中心点
        QPointF center;
        for (auto &p : points) center += p;
        center /= points.size();
        
        // 2. 计算每个点到中心的距离方差
        qreal avgDist = 0;
        for (auto &p : points) {
            avgDist += QLineF(p, center).length();
        }
        avgDist /= points.size();
        
        qreal variance = 0;
        for (auto &p : points) {
            qreal d = QLineF(p, center).length();
            variance += (d - avgDist) * (d - avgDist);
        }
        variance /= points.size();
        
        // 3. 如果方差很小,说明轨迹接近圆形
        return variance < 50.0; // 阈值需根据实际调整
    }
};
我曾经犯过的错:自定义手势的阈值不能写死。不同屏幕尺寸、不同触摸采样率下,同样的手势画出来的轨迹差异很大。我后来把阈值做成可配置的,在设备初始化时根据屏幕DPI自动计算。

2.5 手势识别架构总览

为了让你对整个体系有个直观认识,我画了一张结构图。它展示了从底层触摸事件到上层手势识别的完整链路。

Qt触摸手势识别架构 硬件层 触摸屏驱动 / tslib / evdev / 多点触控协议 Qt事件层 QTouchEvent / QMouseEvent / QEvent::TouchBegin/Update/End QGesture框架 QPinchGesture / QPanGesture / QSwipeGesture grabGesture() + gestureEvent() 自定义手势引擎 轨迹采集 → 特征提取 → 模板匹配 QTouchEvent + 自定义算法 应用层 QWidget / QML Item / 手势响应逻辑 / UI反馈 说明:手势识别支持QGesture框架和自定义引擎两种路径,可根据项目需求灵活选择

2.6 手势识别的性能调优

手势识别做不好,最直接的表现就是“卡顿”或“误触”。我总结了几条实战经验:

  • 减少事件处理中的计算量:不要在 touchEvent 里做复杂的数学运算,尤其是浮点运算。把特征提取放到单独的线程或延迟处理。
  • 合理设置采样率:不是采样率越高越好。60fps的触摸采样率已经足够,过高的采样率只会增加CPU负担。
  • 使用QGesture的缓存机制:QGesture内部有状态缓存,不要重复创建QGesture对象。
  • 避免在手势处理中触发重绘:手势识别过程中尽量减少 update() 调用,可以用定时器合并重绘请求。
我的经验值:在Cortex-A7级别的嵌入式平台上,一个完整的手势识别周期(从触摸到响应)应该控制在16ms以内,也就是60fps。如果超过这个时间,用户就会感觉到延迟。

2.7 调试技巧:让手势问题无处遁形

手势调试比普通UI调试更麻烦,因为它是“动态的”。我常用的调试手段:

  • 可视化轨迹:在屏幕上实时绘制触摸点的移动轨迹,颜色随压力变化。这样一眼就能看出触摸是否准确。
  • 日志打点:在 TouchBegin、TouchUpdate、TouchEnd 三个关键点打印时间戳,计算事件间隔是否均匀。
  • 使用Qt的触摸模拟器:在PC上开发时,可以用 QTouchDevice 模拟触摸事件,不用每次都烧写到板子上。
// 可视化轨迹的简单实现
void MyWidget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    
    // 绘制触摸轨迹
    painter.setPen(QPen(Qt::blue, 3));
    for (int i = 1; i < m_touchPoints.size(); ++i) {
        painter.drawLine(m_touchPoints[i-1], m_touchPoints[i]);
    }
    
    // 绘制触摸点
    for (auto &pt : m_touchPoints) {
        qreal radius = pt.pressure() * 20; // 压力越大,点越大
        painter.setBrush(QColor(255, 0, 0, 128));
        painter.drawEllipse(pt.pos(), radius, radius);
    }
}

嗯,手势识别这块内容就这些。从单点触摸到自定义手势,每个层次都有它的适用场景。你在实际项目中,可以根据硬件能力和交互复杂度,选择最合适的方案。记住一点:手势识别不是越复杂越好,稳定、可预测才是第一位的。