本文記錄一個 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 觸摸相關
更多關于我博客請參閱 博客導航
博客園博客只做備份,博客發布就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可,歡迎轉載、使用、重新發布,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布,如有任何疑問,請與我[聯系](mailto:[email protected]),
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/508719.html
標籤:.NET技术
