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

如何追踪WM_DPICHANGED发送的最后DPI并实现WPF自缩放?

WPF自定义DPI缩放方案(禁用系统缩放,适配Windows11)

核心问题

  • 启动阶段未收到WM_DPICHANGED消息,无法获取初始DPI完成渲染缩放
  • 需要全程追踪系统发送的最后DPI值,替代系统缩放实现自定义界面适配

纯C#实现步骤(适配Windows11)

1. 配置进程DPI感知模式,禁用系统默认缩放

在应用启动时设置进程的DPI感知上下文为DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,这是Windows10+(含Windows11)推荐的最高级感知模式,确保系统不会自动缩放窗口,同时能接收跨显示器的DPI变化通知。

在App.xaml.cs的OnStartup方法中添加以下代码:

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;

public partial class App : Application
{
    // Win32 API导入
    [DllImport("user32.dll", SetLastError = true)]
    private static extern IntPtr SetProcessDpiAwarenessContext(IntPtr dpiContext);

    private const int DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4;

    private int _currentDpi = 96; // 默认DPI(100%缩放)

    protected override void OnStartup(StartupEventArgs e)
    {
        // 设置DPI感知模式,禁用系统自动缩放
        SetProcessDpiAwarenessContext((IntPtr)DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
        
        base.OnStartup(e);

        // 创建主窗口并初始化缩放
        var mainWindow = new MainWindow();
        mainWindow.Loaded += (s, args) =>
        {
            var hwndSource = PresentationSource.FromVisual(mainWindow) as HwndSource;
            if (hwndSource != null)
            {
                // 获取初始DPI
                _currentDpi = GetDpiForWindow(hwndSource.Handle);
                // 应用初始缩放
                ApplyCustomScaling(mainWindow, _currentDpi);

                // 注册窗口消息钩子,拦截WM_DPICHANGED
                hwndSource.AddHook(WndProc);
            }
        };
        mainWindow.Show();
    }

    // 获取窗口当前DPI的Win32 API
    [DllImport("user32.dll")]
    private static extern int GetDpiForWindow(IntPtr hwnd);

    // 窗口消息处理函数
    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        const int WM_DPICHANGED = 0x02E0;

        if (msg == WM_DPICHANGED)
        {
            // 提取新的DPI值(wParam的高16位是X方向DPI,低16位是Y方向,通常两者相等)
            int newDpi = (int)wParam >> 16;
            _currentDpi = newDpi;

            // 获取当前窗口实例
            var window = HwndSource.FromHwnd(hwnd)?.RootVisual as Window;
            if (window != null)
            {
                // 应用新的缩放
                ApplyCustomScaling(window, newDpi);
            }

            // 返回0,阻止系统默认的缩放处理
            handled = true;
            return IntPtr.Zero;
        }

        return IntPtr.Zero;
    }

    // 自定义缩放逻辑
    private void ApplyCustomScaling(Window window, int dpi)
    {
        // 计算缩放比例:当前DPI / 默认DPI(96)
        double scaleFactor = dpi / 96.0;

        // 为窗口根元素设置缩放变换
        if (window.Content is FrameworkElement rootElement)
        {
            rootElement.LayoutTransform = new ScaleTransform(scaleFactor, scaleFactor);
            // 调整窗口大小,避免缩放后内容溢出
            window.Width /= scaleFactor;
            window.Height /= scaleFactor;
        }

        // 调整全局字体大小,确保文字清晰
        var baseFontSize = SystemFonts.MessageFontSize;
        window.FontSize = baseFontSize * scaleFactor;
    }
}

2. 追踪最后DPI值

  • 用全局变量_currentDpi存储最新的DPI值,启动时通过GetDpiForWindow初始化,每次收到WM_DPICHANGED消息时更新
  • 任何需要使用当前缩放因子的地方,直接读取_currentDpi计算即可(scaleFactor = _currentDpi / 96.0)

3. 优化自定义缩放细节

  • 布局适配:优先使用LayoutTransform而非RenderTransform,确保布局逻辑会考虑缩放后的尺寸
  • 窗口大小调整:缩放后反向调整窗口原始宽高,避免窗口被拉伸过大或过小
  • 字体处理:通过全局设置FontSize乘以缩放因子,保证文字缩放后清晰可读
  • 控件适配:自定义控件可重写MeasureOverride和ArrangeOverride方法,基于当前缩放因子调整布局

关键说明

  • DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2模式下,系统不会自动缩放窗口,所有缩放逻辑由应用自行处理
  • GetDpiForWindow能直接获取窗口所在显示器的当前DPI,解决启动阶段无WM_DPICHANGED消息的问题
  • 拦截WM_DPICHANGED并返回0,彻底禁用系统的默认缩放行为,确保自定义缩放完全生效

内容的提问来源于stack exchange,提问作者MERUN KUMAR MAITY

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 21:55:01