How to get scaling factor for each monitor, e.g. 1, 1.25, 1.5

Viewed 4141

I know this has been asked before, but I've tried all the answers I've found and none of them seem to work for me. Answers seem to work on a single monitor, or require a window handle, or to be in a WPF application. I've a C# class library with no UI that is called from a different language all together.

I've been asked to determine the scaling factor, e.g. 1, 1.25, 1.5, etc. for each monitor attached to the current PC in a C# class library.

I also need to provide the resolution and colour depth for each monitor. The registry does hold the DpiValue, whatever that is, in Windows 10 under

Computer\HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings

However I have no idea how to map those to a Screen in order to get the matching resolution returned in

System.Windows.Forms.Screen.AllScreens

So does anyone have a way of getting this information?

3 Answers

I believe I have finally (after a long time of searching) found an answer that works, it even works on my high DPI Surface Book 2 screen. I have tested it as much as I can, and so far it's always returned the correct value.

Here's how I did it, thanks to whoever posted the code fragments in the past where I gathered this from.

First you need a structure to call EnumDisplaySettings in user32.dll

    [StructLayout(LayoutKind.Sequential)]
    public struct DEVMODE
    {
        private const int CCHDEVICENAME = 0x20;
        private const int CCHFORMNAME = 0x20;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
        public string dmDeviceName;
        public short dmSpecVersion;
        public short dmDriverVersion;
        public short dmSize;
        public short dmDriverExtra;
        public int dmFields;
        public int dmPositionX;
        public int dmPositionY;
        public ScreenOrientation dmDisplayOrientation;
        public int dmDisplayFixedOutput;
        public short dmColor;
        public short dmDuplex;
        public short dmYResolution;
        public short dmTTOption;
        public short dmCollate;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
        public string dmFormName;
        public short dmLogPixels;
        public int dmBitsPerPel;
        public int dmPelsWidth;
        public int dmPelsHeight;
        public int dmDisplayFlags;
        public int dmDisplayFrequency;
        public int dmICMMethod;
        public int dmICMIntent;
        public int dmMediaType;
        public int dmDitherType;
        public int dmReserved1;
        public int dmReserved2;
        public int dmPanningWidth;
        public int dmPanningHeight;
    }

Then you need to declare the external function call

[DllImport("user32.dll")]
public static extern bool EnumDisplaySettings(string lpszDeviceName, int iModeNum, ref DEVMODE lpDevMode);

Then you need to use it to calculate the screen scaling

            Screen[] screenList = Screen.AllScreens;

            foreach (Screen screen in screenList)
            {
                DEVMODE dm = new DEVMODE();
                dm.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE));
                EnumDisplaySettings(screen.DeviceName, -1, ref dm);

                var scalingFactor = Math.Round(Decimal.Divide(dm.dmPelsWidth, screen.Bounds.Width), 2);
            }

Hope others find this useful.

I think you can get scaling factor for each monitor like this.

public void GetScalingFactor()
{
    List<double> physicalWidths = new List<double>();

    //Get physical width for each monitor
    ManagementObjectSearcher searcher = new ManagementObjectSearcher("\\root\\wmi", "SELECT * FROM WmiMonitorBasicDisplayParams");

    foreach (ManagementObject monitor in searcher.Get())
    {
        //Get the physical width (inch)
        double width = (byte)monitor["MaxHorizontalImageSize"] / 2.54;
        physicalWidths.Add(width);
    }

    //Get screen info for each monitor
    Screen[] screenList = Screen.AllScreens;
    int i = 0;

    foreach (Screen screen in screenList)
    {
        //Get the physical width (pixel)
        double physicalWidth;
        if (i < physicalWidths.Count)
        {
            //Get the DPI
            uint x, y;
            GetDpi(screen, DpiType.Effective, out x, out y);

            //Convert inch to pixel
            physicalWidth = physicalWidths[i] * x;
        }
        else
        {
            physicalWidth = SystemParameters.PrimaryScreenWidth;
        }
        i++;

        //Calculate the scaling
        double scaling = 100 * (physicalWidth / screen.Bounds.Width);
        double scalingFactor = physicalWidth / screen.Bounds.Width;

        //Output the result
        Console.WriteLine(scalingFactor);
    }
}

And you need also add these codes to use to get the monitor DPI (these code is from https://stackoverflow.com/a/29463627/12949439, thanks @Koopakiller):

public void GetDpi(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,
}

Unfortunately, the answer of user3225503 seems not to work (anymore?)

My scenario: WIN10 20H2, WPF-App with dpi-awareness "PerMonitor", Framework 4.7.2, 2 Monitors with different resolutions and different screen scalings ("Horror scenario"):

the dm.dmPelsWidth member of the DEVMODE structure has always the physical resolution of my monitors, so the scaling is always 1.0.

All what we want is to restore our program and its windows like we left it in the last session right? This seems to be incredibly hard, thanks to MS!

But a different approach seems to work:

  1. Switch on per-monitor dpi-awareness in the manifest file of your application:

    <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <!-- The combination of below two tags have the following effect : 
      1) Per-Monitor for >= Windows 10 Anniversary Update
      2) System < Windows 10 Anniversary Update -->
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"> PerMonitor</dpiAwareness>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    </windowsSettings>
    
  2. Always use GetPlacement and SetPlacement win32-api calls for storing/restoring window placements

  3. SetPlacement will set the wrong dialog width/height if the dialog is on a secondary display and each display has different scalings. So we need a new factor depending on scaling factors of each display to correct this in the Loading-event of the window:

event code:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
  if (string.IsNullOrWhiteSpace(Properties.Settings.Default.Placement))
    return;
  ScreenExtensions.WINDOWPLACEMENT placement = new ScreenExtensions.WINDOWPLACEMENT();
  placement.ReadFromBase64String(Properties.Settings.Default.Placement);
  System.Windows.Interop.HwndSource shwnd = System.Windows.Interop.HwndSource.FromVisual(this) as System.Windows.Interop.HwndSource;

  double PrimaryMonitorScaling = ScreenExtensions.GetScalingForPoint(new System.Drawing.Point(1, 1));
  double CurrentMonitorScaling = ScreenExtensions.GetScalingForPoint(new System.Drawing.Point(placement.rcNormalPosition.left, placement.rcNormalPosition.top));
  double RescaleFactor = CurrentMonitorScaling / PrimaryMonitorScaling;
  double width = placement.rcNormalPosition.right - placement.rcNormalPosition.left;
  double height = placement.rcNormalPosition.bottom - placement.rcNormalPosition.top;
  placement.rcNormalPosition.right = placement.rcNormalPosition.left + (int)(width / RescaleFactor + 0.5);
  placement.rcNormalPosition.bottom = placement.rcNormalPosition.top + (int)(height / RescaleFactor + 0.5);
  ScreenExtensions.SetPlacement(shwnd.Handle, placement);
}
  1. There are some more goodies in the code example, e.g. serialization of the WINDOWPLACEMENT structure. don't forget to create a member "Placement" in your application settings! Tell me if this works for you:

Example code:

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Windows;

namespace DpiApp
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();
    }

    private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
      System.Windows.Interop.HwndSource shwnd = System.Windows.Interop.HwndSource.FromVisual(this) as System.Windows.Interop.HwndSource;
      var plc = ScreenExtensions.GetPlacement(shwnd.Handle);
      Properties.Settings.Default.Placement = plc.ToString();
      Properties.Settings.Default.Save();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
      if (string.IsNullOrWhiteSpace(Properties.Settings.Default.Placement))
        return;
      ScreenExtensions.WINDOWPLACEMENT placement = new ScreenExtensions.WINDOWPLACEMENT();
      placement.ReadFromBase64String(Properties.Settings.Default.Placement);
      System.Windows.Interop.HwndSource shwnd = System.Windows.Interop.HwndSource.FromVisual(this) as System.Windows.Interop.HwndSource;

      double PrimaryMonitorScaling = ScreenExtensions.GetScalingForPoint(new System.Drawing.Point(1, 1));
      double CurrentMonitorScaling = ScreenExtensions.GetScalingForPoint(new System.Drawing.Point(placement.rcNormalPosition.left, placement.rcNormalPosition.top));
      double RescaleFactor = CurrentMonitorScaling / PrimaryMonitorScaling;
      double width = placement.rcNormalPosition.right - placement.rcNormalPosition.left;
      double height = placement.rcNormalPosition.bottom - placement.rcNormalPosition.top;
      placement.rcNormalPosition.right = placement.rcNormalPosition.left + (int)(width / RescaleFactor + 0.5);
      placement.rcNormalPosition.bottom = placement.rcNormalPosition.top + (int)(height / RescaleFactor + 0.5);
      ScreenExtensions.SetPlacement(shwnd.Handle, placement);
    }
  }

  public static class ScreenExtensions
  {
    public const string User32 = "user32.dll";
    public const string shcore = "Shcore.dll";
    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);
    }

    public static double GetScalingForPoint(System.Drawing.Point aPoint)
    {
      var mon = MonitorFromPoint(aPoint, 2/*MONITOR_DEFAULTTONEAREST*/);
      uint dpiX, dpiY;
      GetDpiForMonitor(mon, DpiType.Effective, out dpiX, out dpiY);
      return (double)dpiX / 96.0;
    }

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

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

    [DllImport(User32, CharSet = CharSet.Auto)]
    [ResourceExposure(ResourceScope.None)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl);

    [DllImport(User32, CharSet = CharSet.Auto, SetLastError = true)]
    [ResourceExposure(ResourceScope.None)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WINDOWPLACEMENT lpwndpl);

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

    public static WINDOWPLACEMENT GetPlacement(IntPtr hWnd)
    {
      WINDOWPLACEMENT placement = new WINDOWPLACEMENT();
      placement.length = Marshal.SizeOf(placement);
      GetWindowPlacement(hWnd, ref placement);
      return placement;
    }

    public static bool SetPlacement(IntPtr hWnd, WINDOWPLACEMENT aPlacement)
    {
      bool erg = SetWindowPlacement(hWnd, ref aPlacement);
      return erg;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct POINTSTRUCT
    {
      public int x;
      public int y;
      public POINTSTRUCT(int x, int y)
      {
        this.x = x;
        this.y = y;
      }
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
      public int left;
      public int top;
      public int right;
      public int bottom;

      public RECT(int left, int top, int right, int bottom)
      {
        this.left = left;
        this.top = top;
        this.right = right;
        this.bottom = bottom;
      }

      public RECT(Rect r)
      {
        this.left = (int)r.Left;
        this.top = (int)r.Top;
        this.right = (int)r.Right;
        this.bottom = (int)r.Bottom;
      }

      public static RECT FromXYWH(int x, int y, int width, int height)
      {
        return new RECT(x, y, x + width, y + height);
      }

      public Size Size
      {
        get { return new Size(this.right - this.left, this.bottom - this.top); }
      }
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct WINDOWPLACEMENT
    {
      public int length;
      public uint flags;
      public uint showCmd;
      public POINTSTRUCT ptMinPosition;
      public POINTSTRUCT ptMaxPosition;
      public RECT rcNormalPosition;

      public override string ToString()
      {
        byte[] StructBytes = RawSerialize(this);
        return System.Convert.ToBase64String(StructBytes);
      }

      public void ReadFromBase64String(string aB64)
      {
        byte[] b64 = System.Convert.FromBase64String(aB64);
        var NewWP = ReadStruct<WINDOWPLACEMENT>(b64, 0);
        length = NewWP.length;
        flags = NewWP.flags;
        showCmd = NewWP.showCmd;
        ptMinPosition.x = NewWP.ptMinPosition.x;
        ptMinPosition.y = NewWP.ptMinPosition.y;
        ptMaxPosition.x = NewWP.ptMaxPosition.x;
        ptMaxPosition.y = NewWP.ptMaxPosition.y;
        rcNormalPosition.left = NewWP.rcNormalPosition.left;
        rcNormalPosition.top = NewWP.rcNormalPosition.top;
        rcNormalPosition.right = NewWP.rcNormalPosition.right;
        rcNormalPosition.bottom = NewWP.rcNormalPosition.bottom;
      }

      static public T ReadStruct<T>(byte[] aSrcBuffer, int aOffset)
      {
        byte[] buffer = new byte[Marshal.SizeOf(typeof(T))];
        Buffer.BlockCopy(aSrcBuffer, aOffset, buffer, 0, Marshal.SizeOf(typeof(T)));
        GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        T temp = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
        handle.Free();
        return temp;
      }

      static public T ReadStruct<T>(Stream fs)
      {
        byte[] buffer = new byte[Marshal.SizeOf(typeof(T))];
        fs.Read(buffer, 0, Marshal.SizeOf(typeof(T)));
        GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        T temp = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
        handle.Free();
        return temp;
      }

      public static byte[] RawSerialize(object anything)
      {
        int rawsize = Marshal.SizeOf(anything);
        byte[] rawdata = new byte[rawsize];
        GCHandle handle = GCHandle.Alloc(rawdata, GCHandleType.Pinned);
        Marshal.StructureToPtr(anything, handle.AddrOfPinnedObject(), false);
        handle.Free();
        return rawdata;
      }
    }
  }
}
Related