【问题标题】:Auto-Scale but still process WM_DPICHANGED自动缩放但仍处理 WM_DPICHANGED
【发布时间】:2015-11-03 23:21:48
【问题描述】:

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

困境是,如果我让应用程序 DPI 不知道 WM_DPICHANGED 消息永远不会在 DefWndProc 中被截获,并且永远无法检索到正确的 DPI 比例,但表单会以我想要的方式“自动缩放”。但是如果我让应用程序 DPI Aware 则 WM_DPICHANGED 消息被截获并且可以计算正确的 DPI 但表单不会“自动缩放”。

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

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

程序.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。所有显示器都是相同型号,基本分辨率为 1920x1080。我使用显示设置将我的一台显示器设置为 125% 的尺寸。

【问题讨论】:

  • 如果您要否决这个问题,至少要说明原因,以便改进。
  • @ToddSprang - 我已经看到了,但是这个例子是针对 WPF 的,并且 xaml 表单是使用我不熟悉和/或不知道如何转换为 WinForms 代码的方法重新绘制的。默认情况下,WPF 支持 DPI。
  • @ToddSprang - 该链接看起来很有希望,但我正在编写的应用程序中有很多控件,因此这可能会或可能不会与大量额外代码一起使用。我会试一试,但我想看看是否可以通过让 Windows 完成所有调整大小的工作来获得更简单的方法。

标签: c# winforms dpi


【解决方案1】:

与其捕获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 已关闭(您使用 Basic 或 Classic 主题)。 问题:同上。
  • 在使用 GetDeviceCaps 之前调用 SetProcessDPIAware 函数。 问题:这个函数应该在所有其他渲染之前调用一次。如果您有一个现有的不支持 DPI 的应用程序,那么更改感知会破坏整个外观。一旦调用该函数,它就无法关闭。
  • 在使用 GetDeviceCaps 前后调用 SetProcessDpiAwareness问题:此功能至少需要 Windows 8.1

真正可行的解决方案

似乎GetDeviceCaps function 没有在 MSDN 上完整记录。至少我发现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
{
    /// <summary>
    /// Logical pixels inch in X
    /// </summary>
    LOGPIXELSX = 88,

    /// <summary>
    /// Horizontal width in pixels
    /// </summary>
    HORZRES = 8,

    /// <summary>
    /// Horizontal width of entire desktop in pixels
    /// </summary>
    DESKTOPHORZRES = 118
}

/// <summary>
/// Retrieves device-specific information for the specified device.
/// </summary>
/// <param name="hdc">A handle to the DC.</param>
/// <param name="nIndex">The item to be returned.</param>
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, DeviceCaps nIndex);

【讨论】:

    【解决方案2】:

    在 taffer 的帮助下,我终于完成了这项工作。设置的方法是不要将您的应用程序设置为可识别 DPI,并计算在进行任何自定义绘图或字体大小调整时要使用的特定显示器的 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));
            }       
        }
    

    【讨论】:

    • 所以,您采纳了我的建议,即按需询问 DPI 而不是设置 DPI 感知,使用我利用一些未记录功能的研究结果,采用我建议的 GetSystemDpi 方法,稍作修改,最后将其复制粘贴到新答案中。嗯,恭喜。我并不是说我的答案应该被标记为 the 答案,但至少对我的(显然有用的)努力和研究来说,赞成票是公平的。
    • @taffer:我很抱歉。如果系统在单个监视器上运行,您的代码运行良好,而我不是。我忽略了一个事实,即在您发布答案后我对我的问题进行了编辑。我将接受的答案更改为您的答案,因为我不清楚原始问题。
    • 你能展示一下你如何以及在哪里使用它吗?我有点被你遇到的同样的问题所困扰。
    • @Gnial0id:我在答案中添加了一个典型的用法示例。简而言之,您使用 fScale 计算,您可以通过将系统 DPI 除以 96(标准)并将任何宽度、高度或大小乘以它来绘制到 OS 的正确缩放比例。已申请。
    • 这不会编译。 Win32 在你的代码中来自哪里?
    猜你喜欢
    • 2012-06-27
    • 2021-07-08
    • 2020-09-10
    • 2013-12-15
    • 1970-01-01
    • 2019-11-07
    • 1970-01-01
    • 2019-02-24
    • 2020-03-29
    相关资源
    最近更新 更多