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

WPF无边界窗口多显示器场景下异常行为问询

解决WPF多显示器无边界窗口的位置与尺寸异常问题

我之前碰到过几乎一模一样的多显示器无边界窗口问题,你的情况核心是WPF对WindowStyle.None窗口的DPI缩放行为和带边框窗口存在本质差异——尤其是在多显示器不同缩放比例(比如主屏250%、副屏100%)的场景下,系统不会自动为无边界窗口做跨显示器的DPI适配,直接导致逻辑坐标和实际显示坐标错位。

问题拆解

先帮你理清楚为什么会出现这些奇怪的现象:

  • 带边框窗口(WindowStyle.SingleBorderWindow):WPF会自动处理DPI感知,窗口的逻辑尺寸和位置会根据显示器缩放比例自动转换为物理像素,所以你的代码能正常工作。
  • 无边界窗口:系统默认不会为其启用自动DPI缩放,加上你当前的坐标计算逻辑没有考虑无边界窗口的特殊处理,就会出现:
    1. 主屏启动时副屏窗口“消失”——实际是位置计算错误,窗口被渲染到了不可见区域;
    2. 副屏启动时主屏窗口尺寸错误——逻辑尺寸没有按主屏的250%缩放,导致物理像素和副屏一致。
  • 布局检查器能看到正确位置:因为它是基于WPF的逻辑坐标系(不受实际DPI缩放影响),但实际窗口渲染受系统DPI和窗口样式的限制,所以出现“逻辑正确、显示错误”的矛盾。

解决方案

1. 先开启应用程序级DPI感知

这是基础前提,没有正确的DPI感知,后续的坐标计算都是空谈。在项目的app.manifest中添加以下配置:

<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>

如果没有app.manifest,右键项目→添加→新建项→选择“应用程序清单文件”即可。

2. 修正无边界窗口的位置与尺寸计算

这里给你两种可靠的方案,选一种适合你的即可:

方案A:用WPF原生DPI API适配

修改你的ShowWindows方法,针对无边界窗口手动适配当前显示器的DPI:

private void ShowWindows(bool decorated)
{
    foreach (Monitor monitor in MonitorUtility.GetMonitors())
    {
        Point point = monitor.WpfCoordinateRelativeToTopLeft(10, 10);
        Window tempWindow = new Window
        {
            Left = point.X,
            Top = point.Y,
            Width = 400,
            Height = 400,
            Background = Brushes.Red,
            Owner = this,
            WindowStyle = decorated ? WindowStyle.SingleBorderWindow : WindowStyle.None,
            ResizeMode = decorated ? ResizeMode.CanResize : ResizeMode.NoResize
        };

        // 无边界窗口特殊处理:手动适配DPI
        if (!decorated)
        {
            tempWindow.Loaded += (s, e) =>
            {
                var window = s as Window;
                var source = PresentationSource.FromVisual(window);
                if (source?.CompositionTarget != null)
                {
                    // 获取当前显示器的DPI缩放比例
                    var dpiScale = source.CompositionTarget.TransformToDevice.M11;
                    // 调整窗口尺寸为逻辑尺寸对应的物理像素
                    window.Width = 400 * dpiScale;
                    window.Height = 400 * dpiScale;
                    // 修正位置(确保逻辑坐标转物理坐标后正确)
                    window.Left = point.X;
                    window.Top = point.Y;
                }
            };
        }

        tempWindow.Show();
        identifierWindows.Add(tempWindow);
    }
}

方案B:用Win32 API直接设置物理位置(更稳定)

如果WPF的DPI API还是有问题,直接绕开WPF的自动缩放,用SetWindowPos设置窗口的物理像素位置和尺寸:

// 先添加Win32 API声明
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

private const uint SWP_SHOWWINDOW = 0x0040;

private void ShowWindows(bool decorated)
{
    foreach (Monitor monitor in MonitorUtility.GetMonitors())
    {
        // 直接用物理像素计算位置和尺寸
        int physicalX = monitor.MonitorInfo.Monitor.Left + 10;
        int physicalY = monitor.MonitorInfo.Monitor.Top + 10;
        int physicalWidth = 400;
        int physicalHeight = 400;

        Window tempWindow = new Window
        {
            Background = Brushes.Red,
            Owner = this,
            WindowStyle = decorated ? WindowStyle.SingleBorderWindow : WindowStyle.None,
            ResizeMode = decorated ? ResizeMode.CanResize : ResizeMode.NoResize,
            ShowInTaskbar = false
        };

        tempWindow.Show();
        identifierWindows.Add(tempWindow);

        if (!decorated)
        {
            // 获取窗口句柄,直接设置物理位置
            var hwnd = new WindowInteropHelper(tempWindow).Handle;
            SetWindowPos(hwnd, IntPtr.Zero, physicalX, physicalY, physicalWidth, physicalHeight, SWP_SHOWWINDOW);
        }
        else
        {
            // 带边框窗口用原来的逻辑坐标逻辑
            Point point = monitor.WpfCoordinateRelativeToTopLeft(10, 10);
            tempWindow.Left = point.X;
            tempWindow.Top = point.Y;
            tempWindow.Width = 400;
            tempWindow.Height = 400;
        }
    }
}

注意:这里需要修改你的Monitor类,让它能直接访问MonitorInformationStruct实例(比如添加一个MonitorInfo属性)。

3. 额外优化:避免无边界窗口被意外调整

无边界窗口容易被系统意外拉伸,你可以在窗口初始化时禁用最大化/最小化样式:

// 添加对应的Win32 API声明
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);

[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

private const int GWL_STYLE = -16;
private const int WS_MAXIMIZEBOX = 0x10000;
private const int WS_MINIMIZEBOX = 0x20000;

// 在创建窗口时添加
tempWindow.SourceInitialized += (s, e) =>
{
    var hwnd = new WindowInteropHelper(tempWindow).Handle;
    var style = GetWindowLong(hwnd, GWL_STYLE);
    SetWindowLong(hwnd, GWL_STYLE, style & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
};

验证效果

修改后,无论在哪个显示器启动程序,无边界窗口都应该能正确显示在目标位置,且尺寸符合逻辑坐标系的设定——主屏幕250%缩放时,窗口物理尺寸会是1000x1000像素,视觉上和副屏幕的400x400像素窗口大小一致。

内容的提问来源于stack exchange,提问作者Dan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:07:17