You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多屏多缩放环境下如何将原生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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 03:37:03