本文记录一个 WPF 在 dotnet 6 的一个已知问题,且此问题我已修复提交给官方仓库。这是一个只有在 dotnet 6 框架下,非 dotnet 5 也非 .NET Core 3.1 也非 .NET Framework 的问题,要求开启 DPI 感觉等级为 PerMonitorV2 的特性,在带触摸屏上的应用,应用运行过程中,切换屏幕的 DPI 之后,触摸过程有概率触发在触摸线程访问 UI 的依赖属性,在触摸线程抛出异常炸掉应用

条件

必须同时满足以下条件:

  • dotnet 6: dotnet 6.0.1 及以上版本

    • dotnet 5 和 .NET Core 3.1 和 .NET Framework 没有此问题,这是新改出来的,细节请参阅原理部分
  • 应用开启 PerMonitorV2 的特性
  • 应用开启 StylusPlugIn 的支持
  • 在触摸设备上运行,进行触摸交互
  • 应用运行过程存在切换系统的 DPI 的值
    • 需要先运行应用,对应用进行触摸交互,再切换,再触摸
    • 可以选择多个屏幕不同的 DPI 让 WPF 在多个屏幕来回移动和触摸
    • 可以选择一个屏幕,在运行应用过程切换 DPI 的值

这也算是一个好消息,要求很严格,而且在用户端,很多都是只有一个屏幕。再加上切换 DPI 系统会提示要重启电脑,重启电脑就不会存在此问题。也就是说这个问题影响其实是比较小的

最后也是最重要的是,这个 Bug 不是必复现的,也许你需要很多次测试才可以遇到,详细请参阅下面步骤

步骤

如以上条件,在 Win10 的 1703 以上版本运行,通过 支持 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 应用开发 - walterlv 博客的方法给应用开启 PM v2 的功能

根据以上条件,给应用附加上 StylusPlugIn 的支持,方法请参阅 附加 StylusPlugIn 的例子

准备完成之后,执行以下步骤

  1. 启动应用,进行触摸

  2. 接着打开设置,点击屏幕选项卡,修改缩放和布局的 更改文本、应用等项目的大小,修改百分比

  3. 切换回应用,继续触摸应用

这是一个非必定复现的坑,需要多次循环以上步骤,也许才能遇到此坑。行为是在触摸线程 Stylus Input 线程将会因为调用的 GetAndCacheTransformToDeviceMatrix 方法碰了 UI 线程的属性,抛出如下异常

Application: Application.exe
CoreCLR Version: 6.0.121.56705
.NET Version: 6.0.1
Description: The process was terminated due to an unhandled exception.
Exception Info: System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it.
at System.Windows.Threading.Dispatcher.ThrowVerifyAccess()
at System.Windows.Threading.Dispatcher.VerifyAccess()
at System.Windows.Threading.DispatcherObject.VerifyAccess()
at System.Windows.Media.CompositionTarget.VerifyAPIReadOnly()
at System.Windows.Interop.HwndTarget.get_TransformToDevice()
at System.Windows.Input.StylusLogic.GetAndCacheTransformToDeviceMatrix(PresentationSource source)
at System.Windows.Input.StylusWisp.WispLogic.GetTabletToViewTransform(PresentationSource source, TabletDevice tabletDevice)
at System.Windows.Input.PenContexts.InvokeStylusPluginCollection(RawStylusInputReport inputReport)
at System.Windows.Input.StylusWisp.WispLogic.InvokeStylusPluginCollection(RawStylusInputReport inputReport)
at System.Windows.Input.StylusWisp.WispLogic.ProcessInputReport(RawStylusInputReport inputReport)
at System.Windows.Input.StylusWisp.WispLogic.ProcessInput(RawStylusActions actions, PenContext penContext, Int32 tabletDeviceId, Int32 stylusDeviceId, Int32[] data, Int32 timestamp, PresentationSource inputSource)
at System.Windows.Input.PenContexts.ProcessInput(RawStylusActions actions, PenContext penContext, Int32 tabletDeviceId, Int32 stylusPointerId, Int32[] data, Int32 timestamp)
at System.Windows.Input.PenContexts.OnPenDown(PenContext penContext, Int32 tabletDeviceId, Int32 stylusPointerId, Int32[] data, Int32 timestamp)
at System.Windows.Input.PenContext.FirePenDown(Int32 stylusPointerId, Int32[] data, Int32 timestamp)
at System.Windows.Input.PenThreadWorker.FireEvent(PenContext penContext, Int32 evt, Int32 stylusPointerId, Int32 cPackets, Int32 cbPacket, IntPtr pPackets)
at System.Windows.Input.PenThreadWorker.ThreadProc()
at System.Threading.Thread.StartHelper.Callback(Object state)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.Thread.StartCallback()

如果自己试了几次也没有复现,可以试试用我的版本,保证按照上面步骤,一定挂。我的版本由以下三个 NuGet 包组成

相信想用定制版本的 WPF 的开发者都知道可以使用吧

为什么使用 6.0.4-alpha05-FixTouch01 版本是能一定复现,还请看下面的原理部分

原理

为什么使用 6.0.4-alpha05-FixTouch01 版本是能一定复现,那是因为我改了触摸模块,我修复了触摸偏移问题导致了此问题暴露。为什么有触摸问题?这是因为 Rob LaDuca 大佬在 Fix raw stylus data to support per-monitor DPI by rladuca · Pull Request #2891 · dotnet/wpf 修复了 PM 的触摸问题,然而他的修复引入新的问题。我问他,你有触摸屏测试没,他说没有,不过 WPF 内部有个自动化测试,自动化测试通过就可以了。然而他的更改已合入主干,导致了使用 StylusPlugIn 的触摸存在偏移

我在 Try fix the first point in StylusPlugin in high DPI by lindexi · Pull Request #6428 · dotnet/wpf 修复了以上的触摸偏移问题,但是由于此修复引入了新的问题。修复之前,如 WPF 高速书写 StylusPlugIn 原理 描述,将会在 UI 线程收到触摸之前,先在触摸线程收到。在触摸线程收到时,还没有找到命中的元素,这就导致了拿到的空值,无法处理当前命中到的元素所在的窗口,从而无法了解当前触摸点的 DPI 的参数。于是触摸就因为拿不到 DPI 参数进行计算而偏移

我修复了触摸偏移问题是通过拿触摸输入源的窗口句柄进行获取 DPI 计算。获取触摸的输入源窗口,不需要等待 UI 线程命中测试,于是修复了触摸偏移的问题

然而以上输入引入了新的问题,那就是在开启 PM v2 特性,在 DPI 变更之后,触摸比 UI 线程更快进入 GetAndCacheTransformToDeviceMatrix 方法。 此方法的作用是获取或计算 DPI 换算 Matrix 参数。如果是在 UI 线程先进来,那自然能更新为一个符合预期的值。然而如果是触摸线程先进来,将会由于触摸线程没有从 _transformToDeviceMatrices 字典获取到对应的 DPI 的参数,从而需要获取 TransformToDevice 属性。在获取 TransformToDevice 属性的时候,由于 TransformToDevice 属性默认是限制只有 UI 线程可以访问,于是就抛出了异常

以下是 GetAndCacheTransformToDeviceMatrix 代码,我添加了足够的注释,方便大家了解

 protected Matrix GetAndCacheTransformToDeviceMatrix(PresentationSource source)
{
// 在当前 dotnet 主干分支上,由于 Rob LaDuca 大佬修复 per-monitor DPI 时,没有考虑到 StylusPlugIn 比 UI 线程更快进入此函数,在首次触摸时,让 PresentationSource 参数为空,从而无法获取到正确的值进行计算,从而计算触摸点由于缺少参数,在 DPI 非 96 情况下偏移 DPI 比例 var hwndSource = source as HwndSource;
Matrix toDevice = Matrix.Identity; if (hwndSource?.CompositionTarget != null)
{
// 如果更改了 DPI 且开启特性,那么在触摸线程比 UI 线程更快进入此函数时,将会在 _transformToDeviceMatrices 字典里面获取不到参数,需要 触摸线程 计算
// If we have not yet seen this DPI, store the matrix for it.
if (!_transformToDeviceMatrices.ContainsKey(hwndSource.CompositionTarget.CurrentDpiScale))
{
// 触摸线程获取 TransformToDevice 参数,将会因为 TransformToDevice 参数默认限制只有 UI 线程可以访问从而炸掉
_transformToDeviceMatrices[hwndSource.CompositionTarget.CurrentDpiScale] = hwndSource.CompositionTarget.TransformToDevice;
Debug.Assert(_transformToDeviceMatrices[hwndSource.CompositionTarget.CurrentDpiScale].HasInverse);
} toDevice = _transformToDeviceMatrices[hwndSource.CompositionTarget.CurrentDpiScale];
} return toDevice;
}

问题已反馈给 WPF 官方: WPF tocuh in Window with StylusPlugIn may throw InvalidOperationException · Issue #6829 · dotnet/wpf

少珺 小伙伴的帮助下,我修复了此问题,请看 Fix get TransformToDevice in Stylus Input thread will throw the InvalidOperationException by lindexi · Pull Request #6840 · dotnet/wpf

核心修复的方法是在触摸线程计算,而不是获取 TransformToDevice 属性,这是因为 TransformToDevice 属性的获取方法里面也是一个简单的计算。从性能角度和安全角度都是自己计算会更好

 public override Matrix TransformToDevice
{
get
{
VerifyAPIReadOnly();
Matrix m = Matrix.Identity;
m.Scale(CurrentDpiScale.DpiScaleX, CurrentDpiScale.DpiScaleY);
return m;
}
}

性能上以上的计算可能比从字典获取的性能更好,不过这部分我没有测试

修复方法

最佳修复方法,等待 WPF 的大佬们合入我的修复,分发新的 dotnet 版本,更新版本即可

我所在的团队也分发了私有的 WPF 版本,包含此修复,如果大家也遇到此问题,且等不及我的修复合入主干,可以试试我所在的团队分发的版本,请看 https://www.nuget.org/packages/dotnetCampus.WPF/6.0.4-alpha06-test02

更多文档

更多 DPI 相关请参阅

更多触摸请参阅 WPF 触摸相关

更多关于我博客请参阅 博客导航

WPF dotnet 6 开启 PM v2 的 DPI 感知 导致触摸线程访问 UI 属性抛异常的更多相关文章

  1. WPF / Win Form:多线程去修改或访问UI线程数据的方法( winform 跨线程访问UI控件 )

    WPF:谈谈各种多线程去修改或访问UI线程数据的方法http://www.cnblogs.com/mgen/archive/2012/03/10/2389509.html 子线程非法访问UI线程的数据 ...

  2. WPF 非UI线程更新UI界面的各种方法小结

    转载:https://www.cnblogs.com/bdbw2012/articles/3777594.html 我们知道只有UI线程才能更新UI界面,其他线程访问UI控件被认为是非法的.但是我们在 ...

  3. ModernUI教程:独立显示器DPI感知

             独立显示器DPI感知,是在Windows 8.1中新增的特性,这个特性针对拥有多个显示器同时各个显示器的DPI设定又不同的人.对这个新特性做了优化支持的软件能够在一个高DPI的显示器 ...

  4. ASP.NET Boilerplate 学习 AspNet Core2 浏览器缓存使用 c#基础,单线程,跨线程访问和线程带参数 wpf 禁用启用webbroswer右键菜单 EF Core 2.0使用MsSql/MySql实现DB First和Code First ASP.NET Core部署到Windows IIS QRCode.js:使用 JavaScript 生成

    ASP.NET Boilerplate 学习   1.在http://www.aspnetboilerplate.com/Templates 网站下载ABP模版 2.解压后打开解决方案,解决方案目录: ...

  5. C#用副线程改主线程(UI线程)的控件属性的方法(包括Winform和WPF)

    C#用副线程去试图修改主线程的UI控件会报出异常,解决方案是使用副线程注册事件通知主线程自己去修改UI控件 在winform中,方法如下 private void button1_Click(obje ...

  6. wpf(怎么跨线程访问wpf控件)

    在编写代码时,我们经常会碰到一些子线程中处理完的信息,需要通知另一个线程(我这边处理完了,该你了). 但是当我们通知WPF的UI线程时需要用到Dispatcher. 首先我们需要想好在UI控件上需要显 ...

  7. WPF 中那些可跨线程访问的 DispatcherObject(WPF Free Threaded Dispatcher Object)

    原文 WPF 中那些可跨线程访问的 DispatcherObject(WPF Free Threaded Dispatcher Object) 众所周知的,WPF 中多数对象都继承自 Dispatch ...

  8. WPF Dispatcher.BeginInvoke子线程更新UI

    在开发WPF应用时出现:”调用线程无法访问此对象,因为另一个线程拥有该对象.“ 是因为UI线程是WPF应用的主线程,若尝试子线程更新UI线程应使用Dispatcher.BeginInvoke()或者I ...

  9. phpfpm开启pm.status_path配置,查看fpm状态参数

    php-fpm配置 pm.status_path = /phpfpm_status nginx配置 server {    root /data/www;    listen 80;    serve ...

  10. 关于windows系统DPI增大导致字体变大的原因分析

    最近再学习WPF开发,其中提到一个特性“分辨率无关性”,主要功能就是实现开发的桌面程序在不同分辨率的电脑上显示时,会根据系统的DPI自动进行UI的缩放,从而不会导致应用程序的失真. 这个里面就提到了个 ...

随机推荐

  1. Springboot访问html页面

    项目结构如图 1.html页面创建 在原有的项目resouces目录下创建static包,并在static下创建pages,然后在pages包下index.html. index.html内容 < ...

  2. 索引与查询使用的 collate 不一致导致无法使用索引

    索引与表的collate 不一致的情况下,会导致表上的索引不可用,这时要想使用索引,必须在SQL 语句指定建索引所用的collate. 数据库默认collate : test=# \l List of ...

  3. SQL优化篇之-如何减少耗时查询的调用次数

    函数调用次数与性能 在查询语句中,如果 Select 子句调用了较为耗时的函数或子查询,需要特别考虑函数调用次数对于SQL整体执行时间的影响. 一.数据准备,SQL 语句 模拟较耗时的用户函数 确保执 ...

  4. 【Learning eBPF-1】什么是 eBPF?为什么它很吊?

    本书中, eBPF 被称为一种 革命性的 内核技术,被广泛应用于网络.观测 和 安全工具中. 这种技术允许你在不重新编译内核的情况下,使能你的自定义工具,与内核数据进行交互.听起来很厉害. 1.1 追 ...

  5. Scala 不可变Map

    1 package chapter07 2 3 object Test08_ImmutableMap { 4 def main(args: Array[String]): Unit = { 5 // ...

  6. #交互#CF1375F Integer Game

    题目 有三堆石子初始石子数分别为\(a,b,c\),可以选择先手还是后手操作, 每次操作形如先手选择一个正整数 \(k\) ,后手自由选择一堆石子加上 \(k\) , 但是不能和上一次操作选择的石堆相 ...

  7. 开发板如何适配OpenHarmony 3.2

      简介 OpenAtom OpenHarmony(以下简称"OpenHarmony") 3.2 Beta5版本在OpenHarmony 3.1 Release版本的基础上,有以下 ...

  8. OpenHarmony集成OCR三方库实现文字提取

    1. 简介 Tesseract(Apache 2.0 License)是一个可以进行图像OCR识别的C++库,可以跨平台运行 .本样例基于Tesseract库进行适配,使其可以运行在OpenAtom ...

  9. Java 继承与多态:代码重用与灵活性的巧妙结合

    Java 继承(子类和超类) 在 Java 中,可以从一个类继承属性和方法到另一个类.我们将"继承概念"分为两类: 子类(child): 从另一个类继承的类 超类(parent): ...

  10. 深入解析 Java 面向对象编程与类属性应用

    Java 面向对象编程 面向对象编程 (OOP) 是一种编程范式,它将程序组织成对象.对象包含数据和操作数据的方法. OOP 的优势: 更快.更易于执行 提供清晰的结构 代码更易于维护.修改和调试 提 ...