自动缩放但仍处理WM_DPICHANGED

我用C#编写的非常复杂的WinForms应用程序遇到了一些问题。 我希望应用程序在更改DPI时让Windows自动缩放,但我仍然需要挂钩WM_DPICHANGED事件以缩放某些自定义绘制文本。

困境是,如果我离开应用程序DPI没有意识到WM_DPICHANGED消息永远不会在DefWndProc中被截获,并且永远无法检索到正确的DPI标度,而是以我想要的方式“自动缩放”。 但是,如果我使应用程序DPI Aware,则截获WM_DPICHANGED消息并且可以计算正确的DPI,但表单不会“自动缩放”。

正如我所说,应用程序非常复杂并且使用了大量第三方控件,因此我无法花时间在WPF中重写应用程序或尝试自行扩展应用程序。

如何让应用程序拦截WM_DPICHANGED消息,计算正确的DPI并仍然允许Windows管理表单缩放?

Program.cs中:

static class Program { [STAThread] static void Main() { if (Environment.OSVersion.Version.Major >= 6) { // If the line below is commented out then the app is no longer DPI aware and the // WM_DPICHANGED event will never fire in the DefWndProc of the form int retValue = SetProcessDpiAwareness(ProcessDPIAwareness.ProcessPerMonitorDPIAware); } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } private enum ProcessDPIAwareness { ProcessDPIUnaware = 0, ProcessSystemDPIAware = 1, ProcessPerMonitorDPIAware = 2 } [DllImport("shcore.dll")] private static extern int SetProcessDpiAwareness(ProcessDPIAwareness value); } 

Form1.cs中:

 protected override void DefWndProc(ref Message m) { switch (m.Msg) { case 0x02E0: //WM_DPICHANGED { int newDpi = m.WParam.ToInt32() & 0xFFFF; float scaleFactor = (float)newDpi / (float)96; } break; } base.DefWndProc(ref m); } 

更新:我使用Windows 10与多显示器设置。 所有显示器都是相同的型号,基本分辨率为1920×1080。 我使用显示设置将我的一个显示器设置为125%的大小。

而不是捕获WM_DPICHANGED事件,只需在需要时询问当前的DPI设置(在Paint事件或其他什么)?

但这也不是很明显。 如果您搜索StackOverflow,通常可以找到以下答案:

 using (Graphics screen = Graphics.FromHwnd(IntPtr.Zero)) { IntPtr hdc = screen.GetHdc(); int dpiX = GetDeviceCaps(hdc, DeviceCaps.LOGPIXELSX); screen.ReleaseHdc(hdc); } 

但是,无论实际DPI设置如何,它都将始终返回96,除非……

  • 您使用Windows XP或在DPI设置中签入兼容模式。 问题 :您无法在用户处执行此操作。
  • DWM已关闭(您使用基本或经典主题)。 问题 :与上述相同。
  • 在使用GetDeviceCaps之前调用SetProcessDPIAware函数。 问题 :在所有其他渲染之前,应该调用此函数一次。 如果你有一个现有的DPI-unaware应用程序,改变意识将破坏整个外观。 调用该function后无法关闭它。
  • 在使用GetDeviceCaps之前和之后调用SetProcessDpiAwareness 。 问题 :此function至少需要Windows 8.1

真正的工作解决方案

似乎在MSDN上没有完整记录GetDeviceCaps函数 。 至少我发现pinvoke.net提到了一些可以通过该函数获得的其他选项。 最后,我提出了以下解决方案:

 public static int GetSystemDpi() { using (Graphics screen = Graphics.FromHwnd(IntPtr.Zero)) { IntPtr hdc = screen.GetHdc(); int virtualWidth = GetDeviceCaps(hdc, DeviceCaps.HORZRES); int physicalWidth = GetDeviceCaps(hdc, DeviceCaps.DESKTOPHORZRES); screen.ReleaseHdc(hdc); return (int)(96f * physicalWidth / virtualWidth); } } 

以上示例中所需的附加代码:

 private enum DeviceCaps { ///  /// Logical pixels inch in X ///  LOGPIXELSX = 88, ///  /// Horizontal width in pixels ///  HORZRES = 8, ///  /// Horizontal width of entire desktop in pixels ///  DESKTOPHORZRES = 118 } ///  /// Retrieves device-specific information for the specified device. ///  /// A handle to the DC. /// The item to be returned. [DllImport("gdi32.dll")] private static extern int GetDeviceCaps(IntPtr hdc, DeviceCaps nIndex); 

我终于在taffer的帮助下得到了这个工作。 设置此方法的方法是不将应用程序设置为DPI识别,并计算在进行任何自定义绘图或字体大小调整时要使用的特定mointor的DPI设置。 无需捕获无论如何都不会触发的WM_DPICHANGED事件。

hwnd参数是程序窗口的句柄。 Win32类用于保存所有PInvoke内容。 这是解决方案:

 public static int GetSystemDpi(IntPtr hwnd) { Screen screen = Screen.FromHandle(hwnd); IntPtr screenHdc = Win32.CreateDC(null, screen.DeviceName, null, IntPtr.Zero); int virtualWidth = Win32.GetDeviceCaps(screenHdc, (int)Win32.DeviceCap.HORZRES); int physicalWidth = Win32.GetDeviceCaps(screenHdc, (int)Win32.DeviceCap.DESKTOPHORZRES); Win32.DeleteDC(screenHdc); return (int)(96f * physicalWidth / virtualWidth); } 

样品:

此方法的典型用法是在不支持DPI的应用程序中执行自定义绘图,并允许操作系统自动缩放它。 例如,如果我从我的主窗体在屏幕上绘制字体,这就是我将如何使用代码:

 private void DrawText() { // Figure out any screen scaling needed float fScale = ((float)GetSystemDpi(this.Handle) / 96f); Font myfont = new Font("whatever", 10f * fScale, FontStyle.Regular); using (Graphics g = this.CreateGraphics()) { g.DrawString("My Custom Text", myfont, Brushes.Black, new PointF(0, 0)); } }