今天我們來介紹一下 Bootstrap Blazor 中 Table 組件的虛擬滾動行,什麼是虛擬滾動呢,我查到的解釋是:只渲染可視區域的列表項,非可見區域的 完全不渲染,在滾動條滾動時動態更新列表項。 然後很明顯,在實際應用中不可能實現“非可見區域的 完全不渲染”,這樣的體驗效果太差了,下拉直接空 ...
本文記錄一個 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])。