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.

Sarah
  • 328
  • 1
  • 6
  • 15
  • Can you just create a dummy window on each screen and grab the info this way? – Gabriel Negut Apr 03 '15 at 19:40
  • You could go with previous comment, otherwise you will have to enumerate display devices and get DPIs that way. – Kcvin Apr 03 '15 at 21:48
  • 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](https://msdn.microsoft.com/en-us/library/windows/desktop/ee308410%28v=vs.85%29.aspx). – Hans Passant Apr 06 '15 at 19:09
  • @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. – Sarah Apr 08 '15 at 14:01
  • @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. – Sarah Apr 08 '15 at 14:19

1 Answers1

31

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!

Koopakiller
  • 2,838
  • 3
  • 32
  • 47
  • I had to change `MonitorFromPoint(Point, short)` to `MonitorFromPoint(Point, uint)`, but other than that it works great. Thank you! – Sarah Apr 06 '15 at 15:05
  • 2
    A quick note, this only works on Windows 8 and above. – xandermonkey Mar 31 '17 at 15:35
  • 2
    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. – Ryan Lundy Jun 06 '17 at 12:49
  • 2
    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://stackoverflow.com/a/5020623/27194 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...) – Patrick from NDepend team May 31 '19 at 09:16
  • GetDpiForMonitor will show wrong DPI for monitor when you change primary monitor. Only restart the app can help. – steam3d Nov 20 '21 at 10:08