WPF dotnet 6 开启 PM v2 的 DPI 感知 导致触摸线程访问 UI 属性抛异常
本文记录一个 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 的特性
- 支持此特性最低系统版本是 Windows 10 的 1703 版本,低于此版本,包括 Win7 系统,将不能开启
- 默认的应用是没有开启的,需要自己通过清单等方式开启,开启方法稍微复杂,请参阅 支持 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 应用开发 - walterlv
- 应用开启 StylusPlugIn 的支持
- 在触摸设备上运行,进行触摸交互
- 应用运行过程存在切换系统的 DPI 的值
- 需要先运行应用,对应用进行触摸交互,再切换,再触摸
- 可以选择多个屏幕不同的 DPI 让 WPF 在多个屏幕来回移动和触摸
- 可以选择一个屏幕,在运行应用过程切换 DPI 的值
这也算是一个好消息,要求很严格,而且在用户端,很多都是只有一个屏幕。再加上切换 DPI 系统会提示要重启电脑,重启电脑就不会存在此问题。也就是说这个问题影响其实是比较小的
最后也是最重要的是,这个 Bug 不是必复现的,也许你需要很多次测试才可以遇到,详细请参阅下面步骤
步骤
如以上条件,在 Win10 的 1703 以上版本运行,通过 支持 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 应用开发 - walterlv 博客的方法给应用开启 PM v2 的功能
根据以上条件,给应用附加上 StylusPlugIn 的支持,方法请参阅 附加 StylusPlugIn 的例子
准备完成之后,执行以下步骤
启动应用,进行触摸
接着打开设置,点击屏幕选项卡,修改缩放和布局的 更改文本、应用等项目的大小,修改百分比
切换回应用,继续触摸应用
这是一个非必定复现的坑,需要多次循环以上步骤,也许才能遇到此坑。行为是在触摸线程 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 包组成
- https://www.nuget.org/packages/dotnetCampus.WPF/6.0.4-alpha05-FixTouch01
- https://www.nuget.org/packages/dotnetCampus.WPF.Resource/6.0.4-alpha05-FixTouch01
- https://www.nuget.org/packages/dotnetCampus.WPF.Dependencies/6.0.4-alpha05-FixTouch01
相信想用定制版本的 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 相关请参阅
- 支持 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 应用开发 - walterlv
- Windows 下的高 DPI 应用开发(UWP / WPF / Windows Forms / Win32) - walterlv
- Windows DPI Awareness for WPF - walterlv
更多触摸请参阅 WPF 触摸相关
更多关于我博客请参阅 博客导航
WPF dotnet 6 开启 PM v2 的 DPI 感知 导致触摸线程访问 UI 属性抛异常的更多相关文章
- WPF / Win Form:多线程去修改或访问UI线程数据的方法( winform 跨线程访问UI控件 )
WPF:谈谈各种多线程去修改或访问UI线程数据的方法http://www.cnblogs.com/mgen/archive/2012/03/10/2389509.html 子线程非法访问UI线程的数据 ...
- WPF 非UI线程更新UI界面的各种方法小结
转载:https://www.cnblogs.com/bdbw2012/articles/3777594.html 我们知道只有UI线程才能更新UI界面,其他线程访问UI控件被认为是非法的.但是我们在 ...
- ModernUI教程:独立显示器DPI感知
独立显示器DPI感知,是在Windows 8.1中新增的特性,这个特性针对拥有多个显示器同时各个显示器的DPI设定又不同的人.对这个新特性做了优化支持的软件能够在一个高DPI的显示器 ...
- 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.解压后打开解决方案,解决方案目录: ...
- C#用副线程改主线程(UI线程)的控件属性的方法(包括Winform和WPF)
C#用副线程去试图修改主线程的UI控件会报出异常,解决方案是使用副线程注册事件通知主线程自己去修改UI控件 在winform中,方法如下 private void button1_Click(obje ...
- wpf(怎么跨线程访问wpf控件)
在编写代码时,我们经常会碰到一些子线程中处理完的信息,需要通知另一个线程(我这边处理完了,该你了). 但是当我们通知WPF的UI线程时需要用到Dispatcher. 首先我们需要想好在UI控件上需要显 ...
- WPF 中那些可跨线程访问的 DispatcherObject(WPF Free Threaded Dispatcher Object)
原文 WPF 中那些可跨线程访问的 DispatcherObject(WPF Free Threaded Dispatcher Object) 众所周知的,WPF 中多数对象都继承自 Dispatch ...
- WPF Dispatcher.BeginInvoke子线程更新UI
在开发WPF应用时出现:”调用线程无法访问此对象,因为另一个线程拥有该对象.“ 是因为UI线程是WPF应用的主线程,若尝试子线程更新UI线程应使用Dispatcher.BeginInvoke()或者I ...
- phpfpm开启pm.status_path配置,查看fpm状态参数
php-fpm配置 pm.status_path = /phpfpm_status nginx配置 server { root /data/www; listen 80; serve ...
- 关于windows系统DPI增大导致字体变大的原因分析
最近再学习WPF开发,其中提到一个特性“分辨率无关性”,主要功能就是实现开发的桌面程序在不同分辨率的电脑上显示时,会根据系统的DPI自动进行UI的缩放,从而不会导致应用程序的失真. 这个里面就提到了个 ...
随机推荐
- Salesforce LWC学习(四十九) RefreshView API实现标准页面更新,自定义组件自动捕捉更新
本篇参考: https://developer.salesforce.com/docs/platform/lwc/guide/data-refreshview.html https://develop ...
- 记录--vue中使用vue-video-player实现直播推流播放m3u8
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 1.安装 vue-video-player npm install vue-video-player --save npm install ...
- 运行xxl-job,整合xxl-job至jeecg-boot项目
1.前言:xxl-job是一个分布式任务调度平台,其核心设计目标是开发迅速.学习简单.轻量级.易扩展.现已开放源代码并接入多家公司线上产品线,开箱即用. 源码仓库地址:https://gitee.co ...
- KingbaseESV8R6手工vacuum带有全局分区索引的分区表的影响
背景 客户现场有这样一个案例,有张500个分区的大表,每个分区有20万条记录.有update 非常频繁,经常会触发autovacuum.由于表很大,autovacuum 耗时很长.据现场同事反馈,手工 ...
- jsonb操作符
json类型以文本方式存储json对象,把输入的数据原封不动的存放到数据库中,会保留多余的空格,保留重复的Key,保留Key的顺序. jsonb类型转换文本格式json对象为二进制格式,不保留多余的空 ...
- Scala 中断循环
一.采用 Scala 自带的函数,退出循环 1 package com.atguigu.break 2 3 object TestBreak { 4 import scala.util.control ...
- #威佐夫博弈#洛谷 2252 [SHOI2002]取石子游戏
题目 有两堆石子,数量任意,可以不同.游戏开始由两个人轮流取石子. 游戏规定,每次有两种不同的取法,一是可以在任意的一堆中取走任意多的石子: 二是可以在两堆中同时取走相同数量的石子.最后把石子全部取完 ...
- C# Lock的用法
当我们使用线程的时候,效率最高的方式当然是异步,即各个线程同时运行,其间不相互依赖和等待.但当不同的线程都需要访问某个资源的时候,就需要同步机制了,也就是说当对同一个资源进行读写的时候,我们要使该资源 ...
- 深入解析C++的auto自动类型推导
关键字auto在C++98中的语义是定义一个自动生命周期的变量,但因为定义的变量默认就是自动变量,因此这个关键字几乎没有人使用.于是C++标准委员会在C++11标准中改变了auto关键字的语义,使它变 ...
- 通过UI自动化方式获取文章、视频信息
出于学习研究,对某账号的文章.视频分析一翻,尝试使用自动化方式看能否获取相应信息. 获取某号的文章有多重方法: 第一种是通过搜狗浏览器搜索账号(这种方式每天只能获取一篇文章,基本上没啥用.): 第二种 ...