C# Getting DPI Scaling for each Monitor in Windows

17.3k views Asked by At

I'm working with code in a WPF application that needs to figure out the DPI scaling size for each monitor in Windows. I'm able to figure out the DPI of the primary screen but for some reason I cannot figure out how to get the scale for other monitors - the others all return the same DPI as the main monitor.

There's a bit of code to do this so bear with me. The first set of code deals with getting the DPI based on an HWND. The code gets the active monitor and then retrieves the DPI settings and compares figures out a ratio to the 96 DPI (typically 100%).

public static decimal GetDpiRatio(Window window)
{
    var dpi = WindowUtilities.GetDpi(window, DpiType.Effective);
    decimal ratio = 1;
    if (dpi > 96)
        ratio = (decimal)dpi / 96M;

    return ratio;
}
public static decimal GetDpiRatio(IntPtr hwnd)
{            
    var dpi = GetDpi(hwnd, DpiType.Effective);            
    decimal ratio = 1;
    if (dpi > 96)
        ratio = (decimal)dpi / 96M;

    //Debug.WriteLine($"Scale: {factor} {ratio}");
    return ratio;
}

public static uint GetDpi(IntPtr hwnd, DpiType dpiType)
{            
    var screen = Screen.FromHandle(hwnd);            
    var pnt = new Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
    var mon = MonitorFromPoint(pnt, 2 /*MONITOR_DEFAULTTONEAREST*/);

    Debug.WriteLine("monitor handle: " + mon);
    try
    {
        uint dpiX, dpiY;
        GetDpiForMonitor(mon, dpiType, out dpiX, out dpiY);
        return dpiX;
    }
    catch
    {
        // fallback for Windows 7 and older - not 100% reliable
        Graphics graphics = Graphics.FromHwnd(hwnd);
        float dpiXX = graphics.DpiX;                
        return Convert.ToUInt32(dpiXX);
    }
}


public static uint GetDpi(Window window, DpiType dpiType)
{
    var hwnd = new WindowInteropHelper(window).Handle;
    return GetDpi(hwnd, dpiType);
}     

[DllImport("User32.dll")]
private static extern IntPtr MonitorFromPoint([In]System.Drawing.Point pt, [In]uint dwFlags);

[DllImport("Shcore.dll")]
private static extern IntPtr GetDpiForMonitor([In]IntPtr hmonitor, [In]DpiType dpiType, [Out]out uint dpiX, [Out]out uint dpiY);        


public enum DpiType
{
    Effective = 0,
    Angular = 1,
    Raw = 2,
}

This code is used as part of a screen capture solution where there's supposed to be an overlay over the window the user's mouse is over. I capture the mouse position and based on that I get a pixel location and I then create the WPF window there. Here I have to apply the DPI ratio in order to get the Window to render in the right place and size.

This all works fine on the primary monitor or on multiple monitors as long as the DPI is the same.

The problem is that the call to GetDpiForMonitor() always returns the primary monitor DPI even though the HMONITOR value passed to it is different.

DPI Awareness

This is a WPF application so the app is DPI aware, but WPF runs in System DPI Awareness, rather than Per Monitor DPI Aware. To that effect I hooked up static App() code on startup to explicitly set to per monitor DPI:

    try
    {
        // for this to work make sure [assembly:dpiawareness
        PROCESS_DPI_AWARENESS awareness;
        GetProcessDpiAwareness(Process.GetCurrentProcess().Handle, out awareness);
        var result = SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware);
        GetProcessDpiAwareness(Process.GetCurrentProcess().Handle, out awareness);
}

[DllImport("SHCore.dll", SetLastError = true)]
public static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness);

[DllImport("SHCore.dll", SetLastError = true)]
public static extern void GetProcessDpiAwareness(IntPtr hprocess, out PROCESS_DPI_AWARENESS awareness);

public enum PROCESS_DPI_AWARENESS
{
    Process_DPI_Unaware = 0,
    Process_System_DPI_Aware = 1,
    Process_Per_Monitor_DPI_Aware = 2
}

// and in assemblyinfo
[assembly: DisableDpiAwareness]

I see that the DPI setting changes to Process_Per_Monitor_DPI_Aware but that also seems to have no effect on the behavior. I still see the DPI results returned as the same as the main monitor.

There's a test in a largish solution that allows playing with this here: https://github.com/RickStrahl/MarkdownMonster/blob/master/Tests/ScreenCaptureAddin.Test/DpiDetectionTests.cs in case anyone is interested in checking this out.

Any ideas how I can reliably get the DPI Scaling level for all monitors on the system (and why the heck is there no system API or even a WMI setting for this)?

2

There are 2 answers

1
mm8 On BEST ANSWER

WPF has per-monitor DPI support since .NET Framework 4.6.2. There is more information and an example available at GitHub: http://github.com/Microsoft/WPF-Samples/tree/master/PerMonitorDPI.

You may also want to check out the VisualTreeHelper.GetDpi method.

0
PhysicalEd On

I have been wrestling with similar problems (secondary monitor Screen bounds seemed to be scaled by the same scale factor as set on the primary display), and I found some documentation that seems to at least explain that this is expected behavior:

DPI Awareness Mode - System
Windows Version Introduced - Vista
Application's view of DPI - All displays have the same DPI (the DPI of the primary display at the time the Windows session was started)
Behavior on DPI change - Bitmap-stretching (blurry)

This is extracted from the first table in High DPI desktop application development on Windows

That's the first documentation I found that at least explicitly spells out that the code will report that all windows share the same scaling when the application is under System DPI Awareness.