当前位置: 首页 > news >正文

通过解读 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 程序多么简单,哪怕只有一个最简单的窗口带着一个可以交互的按钮,本文所述的触摸失效问题你都可能遇到。

具体需要的条件为:

  1. 运行 任意的 WPF 程序
  2. 插拔带有触摸的 HID 设备(可以是物理插拔,也可以是驱动或软件层面的插拔)

以上虽说是必要条件,但如果要提高触摸失效的复现概率,需要制造一个较高的 CPU 占用:

  • 当前系统中有 较高的 CPU 占用率

可能还有一些尚不确定的条件:

  • 是否对 .NET Framework 的版本有要求?
  • 是否对 Windows 操作系统的版本有要求?

将以上所有条件组合起来,对于触摸失效的问题描述为:

  • 当运行任意的 WPF 程序时,如果此时操作系统有较高的 CPU 占用,并且此时存在带有触摸的 HID 设备插拔,那么此 WPF 程序可能出现“触摸失效”问题,即此后此程序再也无法触摸操作了。
  • 如果此时系统中同时运行了多个 WPF 程序,多个 WPF 程序可能都会在此时出现触摸失效问题。

触摸失效原因初步分析

WPF 从收集设备触摸到大多数开发者所熟知的 StylusMouse 事件需要两个不同的线程完成。

  1. 主线程,负责进行 Windows 消息循环
  2. StylusInput 线程,负责从 WPF 非托管代码和 COM 组件中获得触摸信息

主线程中的 Windows 消息循环处理这些消息:

  • LBUTTONDOWN, LBUTTONUP
  • DEVICECHANGE, TABLETADDED, TABLETREMOVED

Stylus Input 线程主要由 PenThreadWorker 类创建,在线程循环中使用 GetPenEventGetPenEventMultiple 这两个函数来获取整个触摸设备中的触摸事件,并将触摸的原始信息向 WPF 的其他触摸处理模块传递。传递的其中一个模块是 WorkerOperationGetTabletsInfo 类,其的 OnDoWork 方法中会通过 COM 组件获取触摸设备个数。

而导致触摸失效的错误代码就发生在以上 Stylus Input 线程的处理中。

  1. PenThreadWorkerGetPenEventMultiple 方法传入的 _handles 为空数组,这会导致进行无限的等待。
  2. WorkerOperationGetTabletsInfoOnDoWork 因为 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 对触摸的处理存在线程安全的隐患所致。

此触摸失效问题的解决方法

在推断出初步原因后,根本的解决方法其实只剩下两个了:

  1. 修复 WPF 的 Bug
    • 由于我们无法编译 .NET Framework 的源码,所以几乎只能由微软来修复这个 Bug,即需要新版本的 WPF 来解决这个线程安全隐患
    • 当然,此问题的修复可以跟随 .NET Framework 更新,也可以跟随即将推出的 .NET Core 3 进行更新。
  2. 更新 Windows(传说中的补丁)
    • 新的 Windows 提供给 WPF 的 COM 组件可能也需要修复线程安全或其他与触摸硬件相关的问题

比较彻底的方案是以上两者都需要修复,但都 只能由微软来完成

那我们非微软开发者可以做些什么呢?

  1. 降低 CPU 占用率
    • 虽然这不由我们控制,不过我们如果能降低一些意料之外的高 CPU 占用,则可以大幅降低 WPF 触摸失效问题出现的概率。

然而作为用户又可以做些什么呢?

  1. 重新插拔触摸设备(如果你的触摸框是通过 USB 连接可以手工插拔的话)

触摸失效问题的分析过程

以上结论的得出,离不开对 .NET Framework 源码的解读和调试。

由于 WPF 的触摸原理涉及到较多类型和源码,需要大量篇幅描述,所以不在本文中说明。阅读以下文章可以更加深入地了解这个触摸失效的问题:

  • WPF 插拔触摸设备触摸失效 - lindexi
  • 通过解读 WPF 触摸源码,分析 WPF 插拔设备触摸失效的问题(分析篇) - walterlv

本文所有的 .NET Framework 源码均由 dnSpy 反编译得出,分析过程也基本是借助 dnSpy 的无 pdb 调试特性进行。关于 dnSpy 的更多使用,可以阅读:

  • 断点调试 Windows 源代码 - lindexi
  • 神器如 dnSpy,无需源码也能修改 .NET 程序 - walterlv

相关文章:

  • .NET 中各种混淆(Obfuscation)的含义、原理、实际效果和不同级别的差异(使用 SmartAssembly)
  • .NET 中 GetProcess 相关方法的性能
  • 常用输入法快速输入自定义格式的时间和日期(搜狗/QQ/手心/微软拼音)
  • 好的框架需要好的 API 设计 —— API 设计的六个原则
  • .NET/C# 使用反射注册事件
  • .NET/C# 判断某个类是否是泛型类型或泛型接口的子类型
  • .NET/C# 使用反射调用含 ref 或 out 参数的方法
  • WPF 多线程 UI:设计一个异步加载 UI 的容器
  • .NET 命令行参数包含应用程序路径吗?
  • 分析现有 WPF / Windows Forms 程序能否顺利迁移到 .NET Core 3.0(使用 .NET Core 3.0 Desktop API Analyzer )
  • C# 空合并操作符(??)不可重载?其实有黑科技可以间接重载!
  • UWP 轻量级样式定义(Lightweight Styling)
  • 预编译框架,开发高性能应用 - 课程 - 微软技术暨生态大会 2018
  • 将 UWP 中 CommandBar 的展开方向改为向下展开
  • .NET 中创建支持集合初始化器的类型
  • 【vuex入门系列02】mutation接收单个参数和多个参数
  • iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码
  • JavaScript HTML DOM
  • leetcode98. Validate Binary Search Tree
  • Python_网络编程
  • Quartz初级教程
  • React-Native - 收藏集 - 掘金
  • Vue实战(四)登录/注册页的实现
  • yii2中session跨域名的问题
  • 那些年我们用过的显示性能指标
  • 入门到放弃node系列之Hello Word篇
  • 使用 5W1H 写出高可读的 Git Commit Message
  • 小程序01:wepy框架整合iview webapp UI
  • 一个完整Java Web项目背后的密码
  • 进程与线程(三)——进程/线程间通信
  • ​创新驱动,边缘计算领袖:亚马逊云科技海外服务器服务再进化
  • # Panda3d 碰撞检测系统介绍
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (三)docker:Dockerfile构建容器运行jar包
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • .bashrc在哪里,alias妙用
  • .dwp和.webpart的区别
  • .NET 5种线程安全集合
  • .net core使用RPC方式进行高效的HTTP服务访问
  • .NET Project Open Day(2011.11.13)
  • .Net的C#语言取月份数值对应的MonthName值
  • .net反混淆脱壳工具de4dot的使用
  • .NET中统一的存储过程调用方法(收藏)
  • /使用匿名内部类来复写Handler当中的handlerMessage()方法
  • @Autowired多个相同类型bean装配问题
  • [BZOJ1178][Apio2009]CONVENTION会议中心
  • [C#]获取指定文件夹下的所有文件名(递归)
  • [CERC2017]Cumulative Code
  • [codevs 1288] 埃及分数 [IDdfs 迭代加深搜索 ]
  • [COI2007] Sabor
  • [C和指针].(美)Kenneth.A.Reek(ED2000.COM)pdf
  • [G-CS-MR.PS02] 機巧之形2: Ruler Circle
  • [javaSE] GUI(事件监听机制)
  • [javaSE] 数据结构(二叉查找树-插入节点)
  • [Jquery] 实现鼠标移到某个对象,在旁边显示层。