通过解读 WPF 触摸源码,分析 WPF 插拔设备触摸失效的问题(问题篇)
在 .NET Framework 4.7 以前,WPF 程序的触摸处理是基于操作系统组件但又自成一套的,这其实也为其各种各样的触摸失效问题埋下了伏笔。再加上它出现得比较早,触摸失效问题也变得更加难以解决。即便是 .NET Framework 4.7 以后也需要开发者手动开启 Pointer
消息,并且存在兼容性问题。
本文将通过解读 WPF 触摸部分的源码,分析 WPF 插拔设备触摸失效的问题。随后,会给微软报这个 Bug。
本文使用多种语言编写,请选择适合你阅读的语言:
- 通过解读 WPF 触摸源码,分析 WPF 插拔设备触摸失效的问题(问题篇)
- WPF Applications Stop Responding to Touches after Adding or Removing Tablet Devices
所谓“触摸失效”,指的是无论你如何使用手指或触摸笔在触摸屏上书写、交互,程序都没有任何反应。而使用鼠标操作则能正常使用。
本文所述的“触摸失效问题”我在 WPF 程序无法触摸操作 一文中有所提及,但本文偏向于分析其内部发生的原因。
本文与 林德熙 的 WPF 插拔触摸设备触摸失效 所述的是同一个问题。那篇文章会更多的偏向于源码解读,而本文更多地偏向于分析触摸失效的过程。
WPF 程序插拔设备导致触摸失效问题
无论你写的 WPF 程序多么简单,哪怕只有一个最简单的窗口带着一个可以交互的按钮,本文所述的触摸失效问题你都可能遇到。
具体需要的条件为:
- 运行 任意的 WPF 程序
- 插拔带有触摸的 HID 设备(可以是物理插拔,也可以是驱动或软件层面的插拔)
以上虽说是必要条件,但如果要提高触摸失效的复现概率,需要制造一个较高的 CPU 占用:
- 当前系统中有 较高的 CPU 占用率
可能还有一些尚不确定的条件:
- 是否对 .NET Framework 的版本有要求?
- 是否对 Windows 操作系统的版本有要求?
将以上所有条件组合起来,对于触摸失效的问题描述为:
- 当运行任意的 WPF 程序时,如果此时操作系统有较高的 CPU 占用,并且此时存在带有触摸的 HID 设备插拔,那么此 WPF 程序可能出现“触摸失效”问题,即此后此程序再也无法触摸操作了。
- 如果此时系统中同时运行了多个 WPF 程序,多个 WPF 程序可能都会在此时出现触摸失效问题。
触摸失效原因初步分析
WPF 从收集设备触摸到大多数开发者所熟知的 Stylus
和 Mouse
事件需要两个不同的线程完成。
- 主线程,负责进行 Windows 消息循环
- StylusInput 线程,负责从 WPF 非托管代码和 COM 组件中获得触摸信息
主线程中的 Windows 消息循环处理这些消息:
- LBUTTONDOWN, LBUTTONUP
- DEVICECHANGE, TABLETADDED, TABLETREMOVED
Stylus Input 线程主要由 PenThreadWorker
类创建,在线程循环中使用 GetPenEvent
和 GetPenEventMultiple
这两个函数来获取整个触摸设备中的触摸事件,并将触摸的原始信息向 WPF 的其他触摸处理模块传递。传递的其中一个模块是 WorkerOperationGetTabletsInfo
类,其的 OnDoWork
方法中会通过 COM 组件获取触摸设备个数。
而导致触摸失效的错误代码就发生在以上 Stylus Input 线程的处理中。
PenThreadWorker
的GetPenEventMultiple
方法传入的_handles
为空数组,这会导致进行无限的等待。WorkerOperationGetTabletsInfo
的OnDoWork
因为 COM 组件错误出现COMException
或因为线程安全问题出现ArgumentException
;此时方法内部会catch
然后返回空数组,这使得即时存在触摸设备也会因此而识别为不存在。
为了方便理解以上的两个 Bug,可以看看我简化后的 .NET Framework 源码:
// PenThreadWorker.ThreadProc
while(这里是两层循环,简化成一个以便理解)
{
// 以下的 break 都只退出一层循环而已。
if (this._handles.Length == 1)
{
if (!GetPenEvent(this._handles[0], 其他参数))
{
break;
}
}
else if (!GetPenEventMultiple(this._handles, 其他参数))
{
break;
}
// 后续逻辑。
}
// WorkerOperationGetTabletsInfo.OnDoWork
try
{
_tabletDeviceInfo = PenThreadWorker.GetTabletInfoHelper(pimcTablet);
}
catch(COMException)
{
_tabletDevicesInfo = new TabletDeviceInfo[0];
}
catch(ArgumentException)
{
_tabletDevicesInfo = new TabletDeviceInfo[0];
}
// 其他异常。
以上的问题分析中,ArgumentException
异常几乎可以肯定是线程安全问题所致;COMException
不能确定;而 GetPenEventMultiple
中的参数 handles
实际上是用来进行非托管和托管代码线程同步用的 ResetEvent
集合,所以实际上也是线程同步问题导致的死锁。
同时联系以上必要复现步骤中,如果当前存在高 CPU 占用则可以大大提高复现概率;我们几乎可以推断,此问题是 WPF 对触摸的处理存在线程安全的隐患所致。
此触摸失效问题的解决方法
在推断出初步原因后,根本的解决方法其实只剩下两个了:
- 修复 WPF 的 Bug
- 由于我们无法编译 .NET Framework 的源码,所以几乎只能由微软来修复这个 Bug,即需要新版本的 WPF 来解决这个线程安全隐患
- 当然,此问题的修复可以跟随 .NET Framework 更新,也可以跟随即将推出的 .NET Core 3 进行更新。
- 更新 Windows(传说中的补丁)
- 新的 Windows 提供给 WPF 的 COM 组件可能也需要修复线程安全或其他与触摸硬件相关的问题
比较彻底的方案是以上两者都需要修复,但都 只能由微软来完成。
那我们非微软开发者可以做些什么呢?
- 降低 CPU 占用率
- 虽然这不由我们控制,不过我们如果能降低一些意料之外的高 CPU 占用,则可以大幅降低 WPF 触摸失效问题出现的概率。
然而作为用户又可以做些什么呢?
- 重新插拔触摸设备(如果你的触摸框是通过 USB 连接可以手工插拔的话)
触摸失效问题的分析过程
以上结论的得出,离不开对 .NET Framework 源码的解读和调试。
由于 WPF 的触摸原理涉及到较多类型和源码,需要大量篇幅描述,所以不在本文中说明。阅读以下文章可以更加深入地了解这个触摸失效的问题:
本文所有的 .NET Framework 源码均由 dnSpy 反编译得出,分析过程也基本是借助 dnSpy 的无 pdb 调试特性进行。关于 dnSpy 的更多使用,可以阅读:
通过解读 WPF 触摸源码,分析 WPF 插拔设备触摸失效的问题(问题篇)的更多相关文章
- 鸿蒙内核源码分析(根文件系统) | 先挂到`/`上的文件系统 | 百篇博客分析OpenHarmony源码 | v66.01
百篇博客系列篇.本篇为: v66.xx 鸿蒙内核源码分析(根文件系统) | 先挂到/上的文件系统 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...
- v76.01 鸿蒙内核源码分析(共享内存) | 进程间最快通讯方式 | 百篇博客分析OpenHarmony源码
百篇博客分析|本篇为:(共享内存篇) | 进程间最快通讯方式 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁) | 同样 ...
- 鸿蒙内核源码分析(内存规则篇) | 内存管理到底在管什么 | 百篇博客分析OpenHarmony源码 | v16.02
百篇博客系列篇.本篇为: v16.xx 鸿蒙内核源码分析(内存规则篇) | 内存管理到底在管什么 | 51.c.h .o 内存管理相关篇为: v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有哪 ...
- 安卓MonkeyRunner源码分析之与Android设备通讯方式
如前文<谁动了我的截图?--Monkeyrunner takeSnapshot方法源码跟踪分析>所述,本文主要会尝试描述android的自动化测试框架MonkeyRunner究竟是如何和目 ...
- 二维码zxing源码分析(四)wifi部分
前三个部分的地址是:ZXING源码分析(一)CAMERA部分 . zxing源码分析(二)decode部分.zxing源码分析(三)result.history部分 前面三篇文章基本上已经把zxin ...
- 安卓MonkeyRunner源码分析之工作原理架构图及系列集合
花了点时间整理了下MonkeyRunner的工作原理图,请配合本人博客里面MonkeyRunner其他源码分析文章进行阅读.下面整理成相应系列列表方便大家阅读: MonkeyRunner源码分析之-谁 ...
- Monkey源码分析之事件源
上一篇文章<Monkey源码分析之运行流程>给出了monkey运行的整个流程,让我们有一个概貌,那么往后的文章我们会尝试进一步的阐述相关的一些知识点. 这里先把整个monkey类的结构图给 ...
- 安卓Monkey源码分析之运行流程
在<MonkeyRunner源码分析之与Android设备通讯方式>中,我们谈及到MonkeyRunner控制目标android设备有多种方法,其中之一就是在目标机器启动一个monkey服 ...
- 【spring源码分析】IOC容器初始化(总结)
前言:在经过前面十二篇文章的分析,对bean的加载流程大致梳理清楚了.因为内容过多,因此需要进行一个小总结. 经过前面十二篇文章的漫长分析,终于将xml配置文件中的bean,转换成我们实际所需要的真正 ...
随机推荐
- OSI7层网络模型协议精析
OSI7层网络模型协议精析 一.总结 一句话总结:在7层模型中,每一层都提供一个特殊的网络功能.从网络功能的角度观察:下面4层(物理层.数据链路层.网络层和传输层)主要提供数据传输和交换功能,即以节点 ...
- 学习 nginx (持续更新)
什么是代理与反向代理,有什么应用场景? 平常经常听别人说代理与反向代理,那么这二者到底有什么区别呢? 代理 场景:我需要访问一个服务器C,但是由于某些原因我无法访问到它,(典型的就是你FQ,然后fai ...
- 迷宫实现递归版本C++
迷宫实现递归版本C++ 问题描述: //////////////////////////////////////////////////////////////题目:迷宫求解问题. 大致思路: //1 ...
- 2018.2.2IDEA 项目层级问题
这个问题也不是太大,个人爱好,老是觉着影响美观.网上找了一下:[Hide Empty Middle Packages]说是把这个打勾的去掉就可以了. 但是:我的这个好像没有. 这样点击小齿轮把第二个C ...
- JavaScript--跨域
跨域 什么是跨域? 跨域请求就是不同域的网站之间的文件数据之间的传送 ,由于浏览器的同源策略机制(基于安全,同源策略阻止从一个源加载的文档或脚本获取或设置另一个源加载的文档的属性)Ajax直接请求普通 ...
- vue.js的安装部署+cnpm install 安装过程卡住不动----亲测可用
1.到Node.js的官网下载node node.js的下载地址,下载完成后,我在d盘新建一个文件夹“node”, 安装到node目录下(安装之后环境变量自动配置了,自己无需再配),比如我的安装路径是 ...
- PCA--主成份分析
主成份分析(Principle Component Analysis)主要用来对数据进行降维.对于高维数据,处理起来比较麻烦,而且高维数据可能含有相关的维度,数据存在冗余,PCA通过把高维数据向低维映 ...
- SpringInAction--自动化装配Bean(显示装配之xml配置)
Spring在配置时候有三种方案可选 1.在xml中进行显示配置 2.在java中进行显示配置 3.隐式的Bean发现机制和自动装配 今天学习的 第一种—— 在xml中进行显示配置 老规矩 先创建 C ...
- New Concept English three(11)
Customs Officers are quite tolerant these days, but they can still stop you when you are going throu ...
- Linux系统在启动过程中mbr主引导程序被破坏的解决方案
首先,mbr主引导程序被破坏是指系统在启动过程中,磁头找不到/boot分区(windows的启动分区在c盘). 1)下面我们模拟主引导分区被破坏的情况:(在启动分区划分446M的存储大小) 2)重启( ...