You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何让WPF窗口尺寸适配所有DPI缩放场景的屏幕?

解决WPF多DPI场景下窗口适配的通用方案

咱们先把核心问题理清楚:你遇到的DPI获取错误、窗口适配失效,本质是DPI感知模式设置不到位,加上Win7/Win10的API差异,以及WPF窗口初始化时的屏幕关联时机问题。下面是覆盖所有场景的完整解决方案:


1. 先搞定DPI感知模式(最关键的一步)

WPF默认是系统级DPI感知,不会自动适配单屏的缩放变化。你之前调用SetPerMonitorDPIAware失败,大概率是调用时机不对——必须在WPF窗口创建之前设置,用应用清单配置是最可靠的方式:

用应用清单配置(兼容Win7/Win10)

在项目里添加app.manifest文件,替换默认的DPI感知配置:

<application xmlns="urn:schemas-microsoft-com:asm.v3">
  <windowsSettings>
    <!-- Win10及以上:启用每屏DPI感知v2 -->
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true/PM</dpiAware>
    <!-- Win8.1:启用每屏DPI感知 -->
    <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
    <!-- Win7:启用系统DPI感知 -->
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
  </windowsSettings>
</application>

这个配置会自动根据系统版本选合适的感知模式,完全避开代码调用的时机坑。

代码方式(备用,仅当无法用清单时)

如果必须用代码设置,一定要在App.OnStartup之前执行,比如在Main方法里:

[STAThread]
static void Main()
{
    // 先设置DPI感知
    SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
    // 再启动WPF应用
    var app = new App();
    app.Run();
}

// Win10 API声明
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetProcessDpiAwarenessContext(IntPtr dpiFlag);
private static readonly IntPtr DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = new IntPtr(-4);

2. 正确获取当前窗口所在屏幕的DPI(兼容Win7/Win10)

你的GetDpi扩展方法有两个问题:一是Win7没有GetDpiForMonitor,二是窗口初始化时Handle可能还没关联到正确屏幕。下面是兼容版实现:

public static class ScreenDpiHelper
{
    // 获取指定窗口所在屏幕的DPI缩放因子(缩放因子 = DPI/96)
    public static (double ScaleX, double ScaleY) GetWindowScreenScale(IntPtr windowHandle)
    {
        var screen = System.Windows.Forms.Screen.FromHandle(windowHandle);
        return GetScreenScale(screen);
    }

    // 获取指定屏幕的DPI缩放因子
    public static (double ScaleX, double ScaleY) GetScreenScale(System.Windows.Forms.Screen screen)
    {
        if (Environment.OSVersion.Version.Major >= 10)
        {
            // Win10+用GetDpiForMonitor
            var point = new System.Drawing.Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
            var monitor = MonitorFromPoint(point, 2); // MONITOR_DEFAULTTONEAREST
            GetDpiForMonitor(monitor, DpiType.Effective, out uint dpiX, out uint dpiY);
            return (dpiX / 96.0, dpiY / 96.0);
        }
        else
        {
            // Win7用GetDeviceCaps
            var dc = GetDC(IntPtr.Zero);
            var dpiX = GetDeviceCaps(dc, 88); // LOGPIXELSX
            var dpiY = GetDeviceCaps(dc, 90); // LOGPIXELSY
            ReleaseDC(IntPtr.Zero, dc);
            return (dpiX / 96.0, dpiY / 96.0);
        }
    }

    // API声明
    private enum DpiType
    {
        Effective = 0,
        Angular = 1,
        Raw = 2
    }

    [DllImport("User32.dll")]
    private static extern IntPtr MonitorFromPoint(System.Drawing.Point pt, uint dwFlags);

    [DllImport("Shcore.dll")]
    private static extern int GetDpiForMonitor(IntPtr hmonitor, DpiType dpiType, out uint dpiX, out uint dpiY);

    [DllImport("gdi32.dll")]
    private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);

    [DllImport("user32.dll")]
    private static extern IntPtr GetDC(IntPtr hwnd);

    [DllImport("user32.dll")]
    private static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);
}

3. 正确设置窗口尺寸(避开初始化时机坑)

你的setMainWindowDimensions在窗口初始化时调用,此时窗口可能还没定位到目标屏幕,导致获取错误的DPI。建议在窗口Loaded事件窗口位置变化事件中都调用:

private void setMainWindowDimensions()
{
    var windowInteropHelper = new WindowInteropHelper(nativeWindow);
    var scale = ScreenDpiHelper.GetWindowScreenScale(windowInteropHelper.Handle);
    
    // 获取屏幕工作区的实际可用尺寸(已转换为WPF逻辑单位)
    var screen = System.Windows.Forms.Screen.FromHandle(windowInteropHelper.Handle);
    var workingAreaWidth = screen.WorkingArea.Width / scale.ScaleX;
    var workingAreaHeight = screen.WorkingArea.Height / scale.ScaleY;

    // 设置窗口最大不超过1000x800,同时不超出屏幕工作区
    nativeWindow.Width = Math.Min(workingAreaWidth, 1000);
    nativeWindow.Height = Math.Min(workingAreaHeight, 800);

    // 可选:将窗口居中到当前屏幕
    nativeWindow.Left = (workingAreaWidth - nativeWindow.Width) / 2 + screen.WorkingArea.Left / scale.ScaleX;
    nativeWindow.Top = (workingAreaHeight - nativeWindow.Height) / 2 + screen.WorkingArea.Top / scale.ScaleY;
}

// 在窗口构造函数中绑定事件
public MainWindow()
{
    InitializeComponent();
    Loaded += OnWindowLoaded;
    LocationChanged += OnWindowLocationChanged;
}

private void OnWindowLoaded(object sender, RoutedEventArgs e)
{
    setMainWindowDimensions();
}

private void OnWindowLocationChanged(object sender, EventArgs e)
{
    setMainWindowDimensions();
}

4. 绘图坐标的正确转换(适配DPI)

你的density属性逻辑有问题,WPF中正确的设备/逻辑坐标转换应该用TransformToDeviceTransformFromDevice

// 获取当前窗口的DPI缩放因子
private (double ScaleX, double ScaleY) GetWindowScale()
{
    var presentationSource = PresentationSource.FromVisual(this);
    if (presentationSource == null) return (1, 1);
    
    var transform = presentationSource.CompositionTarget.TransformToDevice;
    return (transform.M11, transform.M22);
}

// 逻辑坐标转设备坐标(比如鼠标位置转绘图控件的实际像素)
private Point LogicalToDevice(Point logicalPoint)
{
    var scale = GetWindowScale();
    return new Point(logicalPoint.X * scale.ScaleX, logicalPoint.Y * scale.ScaleY);
}

// 设备坐标转逻辑坐标(比如控件尺寸转逻辑单位)
private Point DeviceToLogical(Point devicePoint)
{
    var scale = GetWindowScale();
    return new Point(devicePoint.X / scale.ScaleX, devicePoint.Y / scale.ScaleY);
}

5. 解决老板单屏笔记本的问题

如果老板的单屏笔记本仍有问题,大概率是这几个原因:

  • 应用清单没生效,导致DPI感知模式不对
  • 窗口初始化时屏幕关联延迟,第一次获取的DPI错误(所以要在Loaded事件中重新设置)
  • 显卡驱动的缩放设置覆盖了系统DPI(比如NVIDIA的"缩放"设置),让老板检查显卡控制面板,设置为"由操作系统缩放"

关键注意事项

  • 确保项目目标框架是**.NET Framework 4.6及以上**,Per-Monitor DPI v2是在4.6引入的
  • 混用WinForms和WPF的屏幕API时,注意坐标单位转换(WinForms是像素,WPF是与设备无关的逻辑单位)
  • 测试时要覆盖不同场景:单屏不同缩放、多屏不同缩放、Win7/Win10系统

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

火山引擎 最新活动