Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Getting DPI Scaling for each Monitor in Windows

Tags:

windows

dpi

wpf

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

like image 801
Rick Strahl Avatar asked Jan 08 '17 09:01

Rick Strahl


1 Answers

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.

like image 137
mm8 Avatar answered Oct 10 '22 22:10

mm8