如何让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中正确的设备/逻辑坐标转换应该用TransformToDevice和TransformFromDevice:
// 获取当前窗口的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




