简介:本资源为嵌入式Linux平台下解决Qt应用程序触摸屏失灵的实战技术文档,面向从事工控设备开发、嵌入式Qt应用移植的工程师与进阶学习者。内容围绕触摸屏断开后Qt程序无法自动恢复响应这一常见缺陷展开,剖析Qt5.4.1中QTsLibMouseHandler的初始化流程与doSelect在select出错时禁用notifier和signal-slot的机制,并给出两条改造思路:一是在tslib中新增ts_status状态查询函数,逐层为dejitter、input-raw、linear、pthres、variance等插件补充stat接口;二是在Qt侧加入守护定时器,持续读取设备状态并尝试重新打开与注册触摸屏事件管理器。资源共1个PDF文件,压缩包约72KB,内容紧凑,含关键源码片段与修改位置说明,便于对照工程快速定位改动点。目前已有3904人学习,适合需要排查同类触摸屏热插拔恢复问题的开发者参考借鉴。
1. 触摸没反应时先别改驱动:QT 应用这条事件链断在哪一环
一块 i.MX6 或者全志的板子,手指按在屏上毫无反应,多数人的第一反应是去翻触摸驱动源码。但现场排查下来,十次里有六七次驱动是好的:evtest /dev/input/event1按下去照样刷出ABS_MT_POSITION_X,问题出在 QT 这一侧压根没打开那个设备节点。嵌入式 Linux 上的 QT 读触摸不走 X11 那套 Input 扩展,而是由 QPA 平台插件(linuxfb、eglfs)直接读 evdev,设备路径、坐标轴方向、校准文件任意一项没配对上,表现出来的都是「点了没反应」或者「点偏半个屏」。
这条链上有三个容易断的环节。内核 input 子系统有没有把触摸屏枚举成 evdev 节点;QPA 平台插件有没有被加载、有没有绑定到这个节点;QT 应用程序自己有没有接住触摸事件。三层的手段完全不同,混在一起查最费时间,很多人把应用层没设WA_AcceptTouchEvents误判成驱动没有上报。
下面按「链路拆解 → 插件配置 → 校准与适配 → 分层验证」推进,每一环给出该看的日志、该改的环境变量、该调的参数,目标是让你在板子上复现出一次被 Qt 正确识别的触摸。
2. 从 /dev/input/eventX 到 QTouchEvent:嵌入式 Linux 触摸事件链路拆解
整条链可以简化成四跳:触摸 IC 通过 I2C/SPI 上报给内核驱动,驱动按 input 子系统规范注册成 evdev 节点,QPA 插件从节点 read 出struct input_event,最后 Qt 把它翻译成QTouchEvent派发给窗口。任何一跳参数不对,应用侧看到的现象都是「没反应」,所以定位必须从底层往上走,而不是一上来就改 Qt 代码。
2.1 内核 input 子系统给出的三个证据
先确认触摸屏在系统里到底以什么形式存在。/proc/bus/input/devices是第一个要看的地方,它同时给出了设备名、事件节点和这个设备支持哪些事件类型。
# 列出系统内所有 input 设备及其事件节点 cat /proc/bus/input/devices # 典型输出片段 # I: Bus=0018 Vendor=0000 Product=0000 Version=0000 # N: Name="Goodix Capacitive TouchScreen" # P: Phys=i2c-1-005d/input0 # S: Sysfs=/devices/platform/soc/2100000.i2c/i2c-1/1-005d/input/input2 # U: Uniq= # H: Handlers=event2 # B: PROP=2 # B: EV=b # B: KEY=400 0 0 0 0 0 0 0 0 0 0 # B: ABS=2608000 1000003逻辑与参数说明:H: Handlers=event2直接告诉你这个触摸屏对应的节点是/dev/input/event2,后面所有配置都要用这个路径,写错一个数字就是「完全没反应」。B: EV=b换算成二进制是1011,表示支持EV_SYN、EV_KEY、EV_ABS三类事件;触摸屏必须有EV_ABS。B: PROP=2对应INPUT_PROP_DIRECT,说明这是直接坐标设备(触摸屏),而不是触摸板这类间接设备,没有这一位 Qt 就未必按触摸屏处理。B: ABS=...这一行末尾能看到ABS_MT_SLOT、ABS_MT_POSITION_X/Y、ABS_MT_TRACKING_ID是否齐全,齐全才说明驱动实现了 MT protocol B。
第二个证据是事件本身能不能读出来。这一步不涉及 Qt,纯粹验证驱动和硬件:
# 直接读取事件流,手指按屏时应持续输出 ABS_MT_* 事件 evtest /dev/input/event2 # 只看事件类型和编码,不打印数值,便于快速肉眼扫 evtest --grab /dev/input/event2 | grep -E "EV_ABS|EV_KEY"逻辑与参数说明:evtest不带参数会列出所有设备让你选,带设备路径就直接进入抓包模式。能刷出ABS_MT_POSITION_X、ABS_MT_POSITION_Y和ABS_MT_TRACKING_ID,说明第一跳是通的;抬起手指时ABS_MT_TRACKING_ID变成-1,这是正常松手。如果按屏时毫无输出,别往下看了,先修驱动和硬件。--grab会独占设备,防止其它进程抢事件,调试时可以开,但调试完记得退出,否则 Qt 应用会拿不到事件。
2.2 evdev、tslib、libinput 三条读取路径
Qt 在嵌入式 Linux 上可以用三种方式拿触摸数据,选错组合是很多「配置看起来都对却没反应」的根因。
| 读取路径 | 典型使用者 | 优点 | 限制 |
|---|---|---|---|
| evdevtouch QPA 插件 | Qt5/Qt6 + eglfs/linuxfb | 无额外依赖,直接读/dev/input/eventX | 电阻屏校准能力弱,旋转靠参数拼 |
| tslib | 老式电阻屏、工业 HMI | 有ts_calibrate校准、滤波、去抖 | 需要额外编译 tslib,环境变量多 |
| libinput | X11 / Wayland 桌面 | 自动识别设备、多点触控完善 | 嵌入式上依赖 udev、seat 管理,偏重 |
选择原则很直接:电容屏 + eglfs/linuxfb,优先用 evdevtouch;电阻屏且需要现场校准,用 tslib;跑 X11 或 Weston,交给 libinput 或 compositor 自己处理,别再去手动指定 evdevtouch,否则会出现「两套输入同时抢设备」的怪现象。
2.3 用日志把断点卡在某一跳
Qt 从 5.8 起支持按类别打开日志,输入相关的日志能把「插件有没有加载、设备有没有打开、事件有没有被派发」全部打出来。
# 打开 QPA 输入层全部日志,前台运行应用 export QT_LOGGING_RULES="qt.qpa.input*=true" export QT_QPA_PLATFORM=eglfs ./myapp 2>&1 | tee /tmp/qt-touch.log # 只关心设备探测和事件分发两类 export QT_LOGGING_RULES="qt.qpa.input.devices=true;qt.qpa.input.events=true"逻辑与参数说明:qt.qpa.input.devices打的是设备枚举结果,正常情况下能看到evdevtouch: Using device /dev/input/event2这类行,看不到就说明插件没加载或路径不对。qt.qpa.input.events打的是事件的接收与分发,手指按下去有QTouchEvent相关输出,说明链路通到了应用;这里空但 devices 有输出,问题就在窗口或控件没接受触摸事件。QT_LOGGING_RULES支持用分号分隔多条、用*通配,调试完记得 unset,日志量在电容屏高频上报下相当可观。
3. 让 QT 找到触摸设备:QPA 平台插件的配置实战
确认驱动侧没问题之后,重点转向 Qt 怎么找到并打开设备。这一层的所有配置几乎都落在环境变量上,不重新编译 Qt 也能改,是现场调试最常动的地方。
3.1 evdevtouch 插件在 eglfs/linuxfb 下的加载条件
Qt5 把 evdevtouch 做成了 generic 插件,需要显式声明;Qt6 里它随平台插件一起分发,配置方式略有不同。先确认插件文件真的存在于目标板上,交叉编译时漏装 plugins 目录是常见事故。
# 常见插件目录,随发行版和 Qt 版本变化 ls /usr/lib/qt/plugins/generic/ | grep evdev ls /usr/lib/arm-linux-gnueabihf/qt5/plugins/generic/ | grep evdev # 期望看到 libqevdevtouchplugin.so 或 libqevdevkeyboardplugin.so # 显式声明加载 evdevtouch 并绑定设备 export QT_QPA_PLATFORM=eglfs export QT_QPA_GENERIC_PLUGINS=evdevtouch:/dev/input/event2 ./myapp逻辑与参数说明:QT_QPA_GENERIC_PLUGINS的格式是插件名:参数,多个插件用逗号分隔,比如evdevtouch:/dev/input/event2,evdevkeyboard:/dev/input/event0。冒号后面的参数会原样传给插件解析,可以继续追加rotate、invertx之类的关键字,用冒号连接。如果ls根本没找到.so,说明 Qt 编译时没开-evdev,需要重新配置 Qt 或者补装插件包,此时改任何环境变量都是白费。
提示:
QT_QPA_GENERIC_PLUGINS里的设备路径如果写了一个不存在的节点,eglfs 启动时只会打一行 warning 然后继续跑,应用界面正常显示但完全不能触控,这个 warning 很容易被启动脚本的日志刷掉,务必单独确认。
3.2 QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS 参数逐项说明
除了 generic plugins,Qt 还提供了专门的触摸屏参数变量,用来描述设备节点和坐标变换。它和上面的 generic plugins 是两种写法,选一种用就行,重复配置可能互相覆盖。
| 参数 | 取值示例 | 作用 | 用错的现象 |
|---|---|---|---|
| 设备路径 | /dev/input/event2 | 指定触摸事件节点 | 完全无触控 |
rotate | rotate=90 | 按角度旋转坐标 | 触控点与手指差 90 度 |
invertx | invertx | X 轴反向 | 左右镜像 |
inverty | inverty | Y 轴反向 | 上下镜像 |
swapxy | swapxy | 交换 X/Y | 横竖屏错位 |
hw_range | minx=0:maxx=4095 | 覆盖硬件坐标范围 | 点准但边缘够不着 |
# 一次完整的配置:设备路径 + 旋转 + 轴交换 export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS="/dev/input/event2:rotate=90:invertx" export QT_QPA_PLATFORM=eglfs ./myapp逻辑与参数说明:参数串以设备路径开头,各参数用冒号分隔,顺序不敏感,但布尔型参数(invertx、inverty、swapxy)只写名字不写值。rotate的取值一般是 90、180、270,写rotate=0等价于不旋转。当屏是竖装而系统按横屏渲染时,通常先加rotate=90,再用invertx/inverty修正镜像;如果旋转后出现 X/Y 互换的效果,就补一个swapxy。这三个参数的调试顺序建议固定成「先 rotate,再 swapxy,最后 invert」,因为三者组合有十几种可能,乱试很难收敛。
3.3 走 tslib 路线时的环境变量与配置
电阻屏或者需要现场重新校准的场景,用 tslib 更省事。Qt 侧只要打开开关,剩下的交给 tslib 自己的配置文件。
# tslib 侧 export TSLIB_TSDEVICE=/dev/input/event2 export TSLIB_CONFFILE=/etc/ts.conf export TSLIB_PLUGINDIR=/usr/lib/ts export TSLIB_CALIBFILE=/etc/pointercal export TSLIB_CONSOLEDEVICE=none # Qt 侧启用 tslib export QT_QPA_FB_TSLIB=1 # linuxfb 平台 export QT_QPA_EGLFS_TSLIB=1 # eglfs 平台 export QT_QPA_GENERIC_PLUGINS=tslib逻辑与参数说明:TSLIB_TSDEVICE是原始 evdev 节点,tslib 从这里读数据再做变换;TSLIB_CALIBFILE指向校准结果,ts_calibrate生成的 9 个参数就存在这个文件里,文件缺失时 tslib 会退化成恒等映射,表现为「能点但偏得离谱」。TSLIB_CONFFILE里的模块顺序决定滤波和线性化流程,缺了pthres、dejitter这类模块时,电容屏可能出现抖动或连点。Qt 侧的开关变量名随平台不同:linuxfb 用QT_QPA_FB_TSLIB,eglfs 用QT_QPA_EGLFS_TSLIB,写错那个前缀会被静默忽略。
3.4 udev 权限与 X11、Wayland 下的差异
仿真器里跑得好好的,一上板子就没反应,八成是权限。/dev/input/event*默认属 root:input,普通用户进程 read 会直接失败,而且这个失败在 Qt 日志里往往只表现为「打不开设备」一行。
# 快速验证是不是权限问题 sudo -u root ./myapp # 若 root 下正常,基本可以确定是权限 # 永久方案 cat > /etc/udev/rules.d/99-touchscreen.rules <<'EOF' SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="Goodix*", MODE="0666", GROUP="input" EOF udevadm control --reload-rules && udevadm trigger逻辑与参数说明:ATTRS{name}用2.1节里/proc/bus/input/devices看到的N: Name=值来匹配,支持通配符,比按 event 编号硬绑更稳。MODE="0666"在产线上方便调试,量产建议改成MODE="0660"加把运行用户加进input组。跑 X11 时触摸由 X server 的 libinput 驱动接管,用xinput list和xinput test-<id>验证,此时设QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS无效;跑 Weston/Wayland 则由 compositor 接管,要确认 compositor 的weston.ini里没有把触摸设备排除。
4. 坐标不对、点不准、单点可用多点失灵:参数校准与 QT 应用层适配
到这一步假设事件已经进到 Qt 了,剩下的是「点位不对」和「事件被吞」两类问题。前者属于坐标变换和校准,后者属于应用层事件接收,处理方式完全不同。
4.1 tslib 校准:ts_calibrate 与 ts.conf 必改项
电阻屏必须做的动作是校准,ts_calibrate会依次在屏幕四角显示十字,点完生成pointercal。
# 前台跑校准,注意要能看到画面 export TSLIB_TSDEVICE=/dev/input/event2 export TSLIB_CALIBFILE=/etc/pointercal ts_calibrate # 校验校准效果 ts_test # 画线测试,线条应紧跟手指 ts_print # 打印归一化坐标,范围约 0~屏幕宽高逻辑与参数说明:ts_calibrate依赖 framebuffer 直接输出,在 eglfs 下运行可能什么都看不到,常见做法是先在 linuxfb 或者独立小工具里校准,把生成的pointercal拷到目标文件系统。ts_test提供画线和打印两种模式,能直观看出是否线性、边缘是否压缩。ts.conf里保留module_raw input和module linear两条基本就够,中间的滤波模块按屏的类型增减,电容屏加太多滤波反而导致快速滑动丢点。
4.2 rotate/invertx/swapxy 的试错顺序与自查表
旋转和镜像组合是排错里最磨人的部分,建议按固定顺序逐个锁定,每步只改一个变量并记录现象。
| 现象 | 优先尝试 | 说明 |
|---|---|---|
| 手指在左下,光标跑到右下 | inverty | Y 轴反向 |
| 手指在左下,光标跑到左上 | invertx | X 轴反向 |
| 手指沿横向移动,光标竖向走 | swapxy | X/Y 交换 |
| 整体偏移 90 度的倍数 | rotate=90/180/270 | 与 swapxy 配合使用 |
| 中心准、边缘偏差大 | 重跑校准或设hw_range | 硬件坐标范围与实际不符 |
# 一个可复用的调试脚本,改一行参数重启一次应用 #!/bin/sh DEV=/dev/input/event2 export QT_QPA_PLATFORM=eglfs export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS="${DEV}:rotate=90:swapxy:inverty" export QT_LOGGING_RULES="qt.qpa.input.devices=true" exec ./myapp "$@"逻辑与参数说明:把参数集中在一个脚本里,每次只改rotate/swapxy/invert中的一个,配合qt.qpa.input.devices日志确认插件确实吃到了这串参数。注意swapxy和rotate=90同时加时,效果等价于旋转后再镜像一次,容易让人误判,最好先把swapxy去掉单独验证rotate。如果屏幕是标准横屏但应用强制竖屏渲染,优先在应用层用QScreen或QT_QPA_EGLFS_WIDTH/HEIGHT处理分辨率,而不是硬靠rotate凑。
4.3 QT 应用侧事件接收:WA_AcceptTouchEvents 与事件分发
设备层全对但应用还是没反应,问题往往在控件本身不收触摸事件。QWidget 默认只处理鼠标事件,触摸事件需要显式声明接受。
// main.cpp:全局开启触摸事件,或在具体控件上单独设置 #include <QApplication> #include <QTouchEvent> int main(int argc, char *argv[]) { QApplication a(argc, argv); // 方式一:全局属性,影响所有顶层窗口 a.setAttribute(Qt::AA_AcceptTouchEvents); MainWindow w; // 方式二:只让这个控件接受触摸 w.setAttribute(Qt::WA_AcceptTouchEvents, true); w.show(); return a.exec(); } // 在控件里重写事件处理,确认事件真的到了 bool MainWindow::event(QEvent *e) { if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd) { QTouchEvent *te = static_cast<QTouchEvent *>(e); qDebug() << "touch points:" << te->touchPoints().size(); return true; // 表示已处理,不再往下传 } return QWidget::event(e); }逻辑与参数说明:Qt::AA_AcceptTouchEvents是应用级属性,必须在创建窗口之前设置,晚了不生效;Qt::WA_AcceptTouchEvents是控件级属性,适合只有某个绘图区需要触摸的场景。event()里返回true表示事件已被消费,如果返回false,事件会继续冒泡给父控件,调试阶段建议先返回true确认能收到,再根据业务逻辑决定是否透传。切记 Qt 会把触摸事件合成为鼠标事件,控件在处理完TouchBegin后如果不 accept,还可能再收到一个MouseButtonPress,导致一次点击被触发两遍,这在按钮类控件上是高频坑。
4.4 多点触控在 QWidget 与 QML 下的差别
单点能用、双指缩放无反应,通常不是驱动问题,而是控件的触摸属性或 QML 元素配置不对。QWidget 走QTouchEvent的touchPoints(),要自己算手势;QML 的MultiPointTouchArea和PinchArea直接给出现成信号,配置成本低很多。
// QML 下接收多点触控 import QtQuick 2.12 Rectangle { width: 800; height: 480 MultiPointTouchArea { anchors.fill: parent minimumTouchPoints: 1 maximumTouchPoints: 5 onPressed: (points) => console.log("points:", points.length) onUpdated: (points) => { /* 遍历 points 拿每个点的 x/y */ } } }逻辑与参数说明:minimumTouchPoints和maximumTouchPoints决定这个区域响应几个手指,设成相同值可以只吃双指手势,避免单指操作被拦截。onPressed/onUpdated回调里的points是touchPoints数组,顺序不保证和手指编号一致,业务代码应通过point.id追踪同一根手指。如果 QML 下也收不到,先回头用QT_LOGGING_RULES="qt.qpa.input.events=true"看事件有没有进 QPA 层,确定是上游还是 QML 元素层级遮挡的问题。
5. 进阶排错:把触摸问题拆成内核、QPA、应用三层的最小验证法
前面几章都是按顺序推的,但现场往往是「不知道坏在哪一层」。有效的做法是构造三个最小验证点,用最简单的程序逐层确认,每层跑通再往上走,避免同时改好几个变量。
| 层级 | 验证工具 | 通过标准 | 不通过时的动作 |
|---|---|---|---|
| 内核/驱动 | evtest /dev/input/event2 | 按屏有ABS_MT_*输出 | 查 I2C 通信、中断脚、驱动 probe 日志 |
| Qt 输入层 | QT_LOGGING_RULES="qt.qpa.input*=true" | 出现设备打开与事件分发日志 | 查插件文件、设备路径、权限 |
| Qt 应用层 | 重写event()打印触摸类型 | 打印出TouchBegin | 查WA_AcceptTouchEvents与控件遮挡 |
先把最底层的一次性排除掉,命令就是evtest,三秒钟就能判定。驱动没问题再往上,只保留QT_QPA_PLATFORM和触摸参数两个变量,其它全部 unset,用一个空白窗口程序测试,别拿业务程序当靶子,业务里可能有全屏遮罩层把触摸吃掉了。
第二个技巧是用strace直接看进程有没有真的 read 那个设备节点,这能区分「插件加载了但没打开设备」和「打开了但没数据」两种完全不同的故障。
# 跟踪应用对 /dev/input 下节点的 open/read 调用 strace -f -e trace=openat,read,ioctl ./myapp 2>&1 | grep -E "event[0-9]|EACCES|ENOENT" # 期望看到:openat(AT_FDCWD, "/dev/input/event2", O_RDONLY|O_NONBLOCK) = 3 # 权限问题:openat(...) = -1 EACCES (Permission denied) # 路径写错:openat(...) = -1 ENOENT (No such file or directory) # 打开了但一直没 read:说明主线程被阻塞或插件没进事件循环逻辑与参数说明:-f跟进子线程和子进程,Qt 的输入读取通常在独立线程里,不加这个参数看不到关键的 read。-e trace=只过滤关心的系统调用,不然输出量太大没法读。返回值为 3 这样的正数就是文件描述符,说明打开成功;EACCES回落权限;ENOENT说明路径或节点不存在。如果 open 成功但完全没有 read 的调用,问题就不在设备而在 Qt 事件循环,检查是不是有耗时任务阻塞了主线程,或者用了QCoreApplication而没创建 GUI 事件循环。
第三个技巧是针对旋转屏和多屏场景:把rotate与硬件坐标范围分开验证。先保证evtest里读到的手指坐标范围是 0 到硬件最大值(比如 4095),再决定是靠hw_range缩放还是靠 Qt 应用层的QScreen逻辑做映射。两者混用会出现「校准过了但还是偏」的假象,很多产线返工都栽在这。最后留一份可复现的环境变量清单贴在设备里,比如/etc/profile.d/qt-touch.sh,把设备路径、rotate、tslib 开关固定下来,避免开机脚本顺序变化导致变量丢失——这类「昨天还好今天不行」的问题,绝大多数只是某个启动脚本提前 export 了QT_QPA_PLATFORM。
本文还有配套的精品资源,点击获取