Flatly-stacked
Flatly-stacked

Reputation: 649

Auto-Scale but still process WM_DPICHANGED

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.

Upvotes: 7

Views: 3723

Answers (2)

Flatly-stacked
Flatly-stacked

Reputation: 649

I finally got this working with a bit of help from taffer. The way to set this up is to NOT set your application to be DPI aware and calculate the specific mointor's DPI setting to be used when doing any custom drawing or font sizing. There is no need to capture the WM_DPICHANGED event which won't fire anyway.

The hwnd parameter is the handle of the program window. The Win32 class is used to hold all of the PInvoke stuff. Here is the solution:

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

Sample:

Typical usage for this method would be performing custom drawing in an application that is not DPI-aware and allows the OS to auto-scale it. For example if I were drawing a font on the screen from my main form this is how I would use the code:

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

Upvotes: 3

György Kőszeg
György Kőszeg

Reputation: 18033

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

Upvotes: 6

Related Questions