包含ItemsControl的Border无法正确渲染为RenderTargetBitmap的高DPI适配问题
嘿,我最近碰到个挺头疼的WPF开发坑,想跟大家唠唠:我现在要实现的功能其实很简单——把一个嵌套了ItemsControl的Border,通过RenderTargetBitmap渲染成位图。这个Border是放在Canvas里的,Canvas又套在ScrollViewer里面。本来在96DPI的普通屏幕下,渲染结果完全没问题,但切换到192DPI的高分辨率屏幕时,就出岔子了:实际UI里每个矩形都是2x2英寸的尺寸,但渲染后的位图和实际UI明显对不上,元素的位置、尺寸都有偏差。
折腾了好一阵,我终于摸清楚了问题根源,也找到了几个靠谱的解决办法,分享给同样踩坑的朋友:
先确保应用是高DPI感知的
这是最基础的一步,要是应用没开启高DPI感知,WPF会自动帮你缩放UI,但RenderTargetBitmap默认是用96DPI初始化的,这就会导致渲染尺寸和实际UI的物理尺寸不匹配。你可以在App.xaml.cs的OnStartup方法里加上DPI感知设置,比如代码里这么写:using System; using System.Windows; using System.Windows.Media; protected override void OnStartup(StartupEventArgs e) { // 开启PerMonitorV2级别的DPI感知,适配不同屏幕的DPI if (Environment.OSVersion.Version.Major >= 6) { SetProcessDpiAwarenessContext((IntPtr)3); } base.OnStartup(e); } // 引入user32.dll的方法声明 [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern bool SetProcessDpiAwarenessContext(IntPtr dpiFlag);用目标元素的实际DPI初始化RenderTargetBitmap
别再用默认的96DPI创建RenderTargetBitmap了!你可以通过VisualTreeHelper.GetDpi方法,拿到要渲染的Border的实际DPI参数,用这个来初始化位图,这样渲染分辨率就和UI的实际分辨率对齐了:// targetBorder是你要渲染的目标Border var dpiInfo = VisualTreeHelper.GetDpi(targetBorder); var renderBitmap = new RenderTargetBitmap( // 计算实际要渲染的像素宽高 (int)Math.Ceiling(targetBorder.ActualWidth * dpiInfo.DpiScaleX), (int)Math.Ceiling(targetBorder.ActualHeight * dpiInfo.DpiScaleY), dpiInfo.PixelsPerInchX, dpiInfo.PixelsPerInchY, PixelFormats.Pbgra32);渲染前强制更新布局和测量
因为Border里包含的是ItemsControl,它的元素是动态生成的,很可能在你调用渲染的时候,ItemsControl还没完成布局测量,导致渲染出来的元素不全或者位置不对。所以在渲染前,一定要手动触发布局更新:// 先让Border完成测量和排列 targetBorder.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); targetBorder.Arrange(new Rect(targetBorder.DesiredSize)); targetBorder.UpdateLayout(); // 再进行渲染 renderBitmap.Render(targetBorder);别在非UI线程里搞渲染
WPF的可视化元素都是绑定到UI线程的,要是你在后台线程里调用RenderTargetBitmap.Render,很可能会出现渲染异常或者尺寸偏差的问题,一定要确保渲染操作是在UI线程执行的。
我按照这些步骤调整之后,192DPI下的渲染结果就和实际UI完全一致了,那些2x2英寸的矩形终于能正确渲染出来啦!
内容来源于stack exchange

