How to get DPI scale for all screens?
Asked Answered
A

1

18

I need to get the DPI scale, as set from Control Panel > Display, for each of the screens connected to the computer, even those that do not have a WPF window open. I have seen a number of ways to get DPI (for example, http://dzimchuk.net/post/Best-way-to-get-DPI-value-in-WPF) but these seem to be dependent on either Graphics.FromHwnd(IntPtr.Zero) or PresentationSource.FromVisual(visual).CompositionTarget.TransformToDevice.

Is there a way to get the DPI settings for each individual screen?

Background - I am creating a layout configuration editor so that the user can set up their configuration prior to launch. For this, I draw each of the screens relative to each other. For one configuration we are using a 4K display that has a larger than default DPI scale set. It is drawing much smaller than it physically appears in relation to the other screens because it reports as the same resolution as the other screens.

Altimetry answered 3/4, 2015 at 19:25 Comment(5)
Can you just create a dummy window on each screen and grab the info this way?Israelite
You could go with previous comment, otherwise you will have to enumerate display devices and get DPIs that way.Patrick
Windows now supports per-screen DPI, starting with 8.1. You are likely to encounter it in a setup where you have one expensive "retina" display and another plain one, usually a projector. Backgrounder for WPF is here.Fornix
@GabrielNegut - Yes, I believe that would work also. However, I was looking for a more programmatic solution so I could build a data bound converter rather than create windows and wait for them to load before I started drawing.Altimetry
@HansPassant - Yes, that is problem I am trying to fix here. As in my example, the screens having different DPIs is causing them to draw incorrectly in relation to each other because the Screen.Bounds represents the DPI adjusted pixel size.Altimetry
N
32

I found a way to get the dpi’s with the WinAPI. As first needs references to System.Drawing and System.Windows.Forms. It is possible to get the monitor handle with the WinAPI from a point on the display area - the Screen class can give us this points. Then the GetDpiForMonitor function returns the dpi of the specified monitor.

public static class ScreenExtensions
{
    public static void GetDpi(this System.Windows.Forms.Screen screen, DpiType dpiType, out uint dpiX, out uint dpiY)
    {
        var pnt = new System.Drawing.Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
        var mon = MonitorFromPoint(pnt, 2/*MONITOR_DEFAULTTONEAREST*/);
        GetDpiForMonitor(mon, dpiType, out dpiX, out dpiY);
    }

    //https://msdn.microsoft.com/en-us/library/windows/desktop/dd145062(v=vs.85).aspx
    [DllImport("User32.dll")]
    private static extern IntPtr MonitorFromPoint([In]System.Drawing.Point pt, [In]uint dwFlags);

    //https://msdn.microsoft.com/en-us/library/windows/desktop/dn280510(v=vs.85).aspx
    [DllImport("Shcore.dll")]
    private static extern IntPtr GetDpiForMonitor([In]IntPtr hmonitor, [In]DpiType dpiType, [Out]out uint dpiX, [Out]out uint dpiY);
}

//https://msdn.microsoft.com/en-us/library/windows/desktop/dn280511(v=vs.85).aspx
public enum DpiType
{
    Effective = 0,
    Angular = 1,
    Raw = 2,
}

There are three types of scaling, you can find a description in the MSDN.

I tested it quickly with a new WPF application:

private void Window_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
    var sb = new StringBuilder();
    sb.Append("Angular\n");
    sb.Append(string.Join("\n", Display(DpiType.Angular)));
    sb.Append("\nEffective\n");
    sb.Append(string.Join("\n", Display(DpiType.Effective)));
    sb.Append("\nRaw\n");
    sb.Append(string.Join("\n", Display(DpiType.Raw)));

    this.Content = new TextBox() { Text = sb.ToString() };
}

private IEnumerable<string> Display(DpiType type)
{
    foreach (var screen in System.Windows.Forms.Screen.AllScreens)
    {
        uint x, y;
        screen.GetDpi(type, out x, out y);
        yield return screen.DeviceName + " - dpiX=" + x + ", dpiY=" + y;
    }
}

I hope it helps!

Nominative answered 5/4, 2015 at 23:58 Comment(5)
I had to change MonitorFromPoint(Point, short) to MonitorFromPoint(Point, uint), but other than that it works great. Thank you!Altimetry
A quick note, this only works on Windows 8 and above.Unguiculate
I found that on Windows 10 using multiple monitors with differing DPI scales, Angular was the correct DpiType to get the proper scale for screen elements. Regular DPI is 96, whereas angular on my 175% screen was 55. 96 / 1.75 = 54.857, rounded to 55.Gastric
In a Visual Studio 2019 process that is doing strange DPI things, the heuristic to retrieve the monitor from Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1) didn't work. I had to use https://mcmap.net/q/615646/-screen-allscreen-is-not-giving-the-correct-monitor-count EnumDisplayMonitors to get all monitors handles IntPtr + I had to use DpiType.Effective to get the right DPI value (96 = 100% 144 = 150% 192 = 200% and so on...)Ignazio
GetDpiForMonitor will show wrong DPI for monitor when you change primary monitor. Only restart the app can help.Archetype

© 2022 - 2024 — McMap. All rights reserved.