多屏多缩放环境下如何将原生DPI坐标转换为WPF逻辑DPI坐标?
多屏多缩放场景下WPF屏幕工作区DPI适配方案
问题根源
你的代码在多屏缩放不一致时出现偏移,核心原因是:MONITORINFO.rcWork返回的是全局虚拟屏幕的物理像素坐标,直接用当前屏幕的DPI转换所有坐标值(包括X/Y偏移)会破坏全局坐标系的连续性——因为虚拟屏幕的坐标原点基于主屏幕,偏移量的刻度是主屏幕的物理像素,而非当前屏幕的。
可靠适配方案
1. 确保应用启用Per-Monitor DPI感知
在项目的app.manifest中添加以下配置,让WPF支持每屏幕DPI自适应:
<application xmlns="urn:schemas-microsoft-com:asm.v3"> <windowsSettings> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware> <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness> </windowsSettings> </application>
2. 修改屏幕信息获取逻辑
调整坐标转换规则:全局偏移量用主屏幕DPI转换,屏幕自身尺寸用当前屏幕DPI转换,保证全局DPI坐标系的连续性。
更新后的完整代码:
public readonly struct ScreenInfo(Rect workArea, uint dpiX, uint dpiY, bool isPrimary) { public Rect WorkArea { get; } = workArea; public uint DpiX { get; } = dpiX; public uint DpiY { get; } = dpiY; public bool IsPrimary { get; } = isPrimary; } public static List<ScreenInfo> Screens { get; } = new(); public static void UpdateScreenInfos() { Screens.Clear(); EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, MonitorEnumProc, IntPtr.Zero); } [DllImport("user32.dll")] private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, EnumDisplayMonitorsDelegate lpfnEnum, IntPtr dwData); private delegate bool EnumDisplayMonitorsDelegate(HMONITOR hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData); private static bool MonitorEnumProc(HMONITOR hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData) { MONITORINFO monitorInfo = new() { cbSize = (uint)Marshal.SizeOf<MONITORINFO>() }; if (!PInvoke.GetMonitorInfo(hMonitor, ref monitorInfo)) return true; if (!PInvoke.GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var monitorDpiX, out var monitorDpiY).Succeeded) return true; // 获取主屏幕系统DPI var systemDpi = GetSystemDpi(); // 全局偏移量转换:主屏幕DPI -> WPF DIP var systemScaleX = 96d / systemDpi.DpiX; var systemScaleY = 96d / systemDpi.DpiY; // 屏幕尺寸转换:当前屏幕DPI -> WPF DIP var monitorScaleX = 96d / monitorDpiX; var monitorScaleY = 96d / monitorDpiY; var workArea = monitorInfo.rcWork; var dipWorkArea = new Rect( workArea.X * systemScaleX, workArea.Y * systemScaleY, workArea.Width * monitorScaleX, workArea.Height * monitorScaleY ); Screens.Add(new ScreenInfo( dipWorkArea, monitorDpiX, monitorDpiY, (monitorInfo.dwFlags & PInvoke.MONITORINFOF_PRIMARY) != 0 )); return true; } // 获取主屏幕系统DPI private static (uint DpiX, uint DpiY) GetSystemDpi() { if (PInvoke.GetDpiForSystem(out var dpiX, out var dpiY).Succeeded) return (dpiX, dpiY); // 兼容旧系统的降级方案 return (96, 96); }
3. 窗口定位示例
使用转换后的ScreenInfo.WorkArea定位OSD窗口:
// 定位到目标屏幕的左上角 var targetScreen = Screens.First(s => !s.IsPrimary); osdWindow.Left = targetScreen.WorkArea.Left; osdWindow.Top = targetScreen.WorkArea.Top; // 定位到目标屏幕的居中位置 osdWindow.Left = targetScreen.WorkArea.Left + (targetScreen.WorkArea.Width - osdWindow.Width) / 2; osdWindow.Top = targetScreen.WorkArea.Top + (targetScreen.WorkArea.Height - osdWindow.Height) / 2;
核心原理
- 全局虚拟屏幕的物理像素偏移量基于主屏幕,因此必须用主屏幕DPI转换为WPF的DIP坐标系,保证跨屏位置的连续性。
- 每个屏幕的自身尺寸需要用当前屏幕的DPI转换,确保窗口在对应屏幕上的实际显示大小符合预期。
- 启用Per-Monitor DPI感知后,WPF会自动根据窗口所在屏幕调整渲染DPI,避免模糊或缩放错误。
内容的提问来源于stack exchange,提问作者Ace_Radom
相关产品推荐
相关产品推荐

