Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Auto-Scale but still process WM_DPICHANGED

Tags:

c#

winforms

dpi

I'm having a bit of a problem with a very complicated WinForms application written in C#. I want the application to let Windows auto-scale when the DPI is changed but I still need to hook the WM_DPICHANGED event in order to scale some custom drawn text.

The dilemma is that if I leave the application DPI unaware the WM_DPICHANGED message is never intercepted in the DefWndProc and the proper DPI scale can never be retrieved but the form "auto-scales" the way I want. But if I make the application DPI Aware then the WM_DPICHANGED message is intercepted and the proper DPI can be calculated but the form will not "auto-scale".

As I said the application is very complex and uses a lot of third-party controls so I am unable to take the time to re-write the app in WPF or try and scale the application myself.

How can I get the app to intercept the WM_DPICHANGED message, calculate the proper DPI and still allow Windows to manage the form scaling?

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);
    }

UPDATE: I am using Windows 10 with multiple monitor setup. All monitor are the same model with a base resolution of 1920x1080. I set one of my monitors to be at 125% of the size using the display settings.

like image 527
David Avatar asked Oct 26 '15 16:10

David


1 Answers

Instead of capturing the WM_DPICHANGED event, what about just asking the current DPI settings whenever you need it (in Paint events or whatever)?

This is also not obvious, though. If you search StackOverflow, usually you can find the following answer:

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

However, it will return always 96, regardless of actual DPI settings, unless...

  • You use Windows XP or the compatibility mode is checked in at DPI settings. Problem: you cannot enforce it at the users.
  • DWM is turned off (you use Basic or Classic themes). Problem: same as above.
  • You call SetProcessDPIAware function before using GetDeviceCaps. Problem: This function should be called once, before all other rendering. If you have an existing DPI-unaware app, changing the awareness will ruin the whole appearance. It cannot be turned off once you called the function.
  • You call SetProcessDpiAwareness before and after using GetDeviceCaps. Problem: This function requires at least Windows 8.1

The real working solution

It seems that the GetDeviceCaps function is not fully documented at MSDN. At least I discovered that pinvoke.net mentions a few further options that can be obtained by the function. At the end I came out with the following solution:

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);
    }
}

And the required additional code in the examples above:

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);
like image 108
György Kőszeg Avatar answered Sep 18 '22 12:09

György Kőszeg