如何追踪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
相关产品推荐
相关产品推荐

