Skip to content
Bruno Sonnino
Menu
  • Home
  • About
Menu

Enumerating Displays in WPF with High DPI

Posted on 10 September 2022

Sometimes, we need to get our display disposition to position windows on them in specific places. The usual way to do it in .NET is to use the Screen class, with a code like this one:

internal record Rect(int X, int Y, int Width, int Height);
internal record Display(string DeviceName, Rect Bounds, Rect WorkingArea, double ScalingFactor);

private void InitializeDisplayCanvas()
{

    var minX = 0;
    var minY = 0;
    var maxX = 0;
    var maxY = 0;
    foreach(var screen in Screen.AllScreens)
    {
        if (minX > screen.WorkingArea.X)
            minX = screen.WorkingArea.X;
        if (minY > screen.WorkingArea.Y)
            minY = screen.WorkingArea.Y;
        if (maxX < screen.WorkingArea.X+screen.WorkingArea.Width)
            maxX = screen.WorkingArea.X+screen.WorkingArea.Width;
        if (maxY < screen.WorkingArea.Y+screen.WorkingArea.Height)
            maxY = screen.WorkingArea.Y+screen.WorkingArea.Height;

        _displays.Add(new Display(screen.DeviceName, screen.Bounds, screen.WorkingArea, 1.0));
    }
    DisplayCanvas.Width = maxX - minX;
    DisplayCanvas.Height = maxY - minY;
    DisplayCanvas.RenderTransform = new TranslateTransform(-minX, -minY);
    var background = new System.Windows.Shapes.Rectangle
    {
        Width = DisplayCanvas.Width,
        Height = DisplayCanvas.Height,
        Fill = new SolidColorBrush(System.Windows.Media.Color.FromArgb(1,242,242,242)),
    };
    Canvas.SetLeft(background, minX);
    Canvas.SetTop(background, minY);
    DisplayCanvas.Children.Add(background);
    var numDisplay = 0;
    foreach (var display in _displays)
    {
        numDisplay++;
        var border = new Border
        {
            Width = display.WorkingArea.Width,
            Height = display.WorkingArea.Height,
            Background = System.Windows.Media.Brushes.DarkGray,
            CornerRadius = new CornerRadius(30)
        };
        var text = new TextBlock
        {
            Text = numDisplay.ToString(),
            FontSize = 200,
            FontWeight = FontWeights.Bold,  
            HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center,
        };
        border.Child = text;
        Canvas.SetLeft(border, display.WorkingArea.Left);
        Canvas.SetTop(border, display.WorkingArea.Top);
        DisplayCanvas.Children.Add(border);
    }

}
C#

If you run the code, you will see something like this:

The monitors aren't contiguous, as you would expect. But, the worst is that it doesn't work well. If you try to position a window in the center of the middle monitor, with a code like this:

private void Button_Click(object sender, RoutedEventArgs e)
{
    var display = _displays[0];
    var window = new NewWindow
    {
        Top = display.WorkingArea.Top + (display.WorkingArea.Height - 200) / 2,
        Left = display.WorkingArea.Left + (display.WorkingArea.Width - 200) / 2,
    };
    window.Show();
}
Charp

You will get something like this:

As you can see, the window is far from centered. And why is that? The reason for these problems are the usage of high DPI. When you set the displays in you system, you set the resolution and the scaling factor:

In my setup, I have three monitors:

  • 1920x1080 125%
  • 3840x2160 150%
  • 1920x1080 100%

This scale factor is not taken in account when you are enumerating the displays and, when I am enumerating them, I have no way of getting this value. That way, everything is positioned in the wrong place. It would work fine if all monitors had a scaling factor of 100%, but most of the time that's not true.

And this code has also another problem: it's a WPF app that is using a Winforms class: Screen is declared in System.Windows.Forms and there is no equivalent in WPF. To use, it you must add UseWindowsForms in the csproj:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net6.0-windows</TargetFramework>
        <Nullable>enable</Nullable>
        <UseWPF>true</UseWPF>
        <UseWindowsForms>true</UseWindowsForms>
    </PropertyGroup>
</Project>
XML

That is something I really don't like to do: use Winforms in a WPF project. If you want to check the project, the branch I've used is here.

So, I tried to find a way to enumerate the displays in WPF and have the right scaling factor, and I found two ways: query the registry or use Win32 API. Yes, the API that is available since the beginning of Windows, and it's still there.

We could go to http://pinvoke.net/ to get the signatures we need for our project. This is a great site and a nice resource when you want to use Win32 APIs in C#, but we'll use another resource: CsWin32, a nice source generator that generates the P/Invokes for us. I have already written an article about it, if you didn't see it, you should check it out.

For that, we should install the NuGet package Microsoft.Windows.CsWin32 (be sure to check the pre-release box). Once installed, you must create a text file and name it NativeMethods.txt. There, we will add the names of all the methods and structures we need.

The first function we need is EnumDisplayMonitors, which we add there. With that, we can use it in our method:

private unsafe void InitializeDisplayCanvas()
{
    Windows.Win32.PInvoke.EnumDisplayMonitors(null, null, enumProc, IntPtr.Zero);
}
C#

We are passing all parameters as null, except for the third one, which is the callback function. As I don't know the parameters of this function, I will let Visual Studio create it for me. Just press Ctrl+. and Generate method enumProc and Visual Studio will generate the method for us:

private unsafe BOOL enumProc(HMONITOR param0, HDC param1, RECT* param2, LPARAM param3)
{
    throw new NotImplementedException();
}
C#

We can change the names of the parameters and add the return value true to continue the enumeration. This function will be called for each monitor in the system and will pass in the first parameter the handle of the monitor. With that handle we can determine its properties with GetMonitorInfo (which we add in NativeMethods.txt) and use it to get the monitor information. This function uses a MONITORINFO struct as a parameter, and we must declare it before calling the function:

private unsafe BOOL enumProc(HMONITOR monitor, HDC hdc, RECT* clipRect, LPARAM data)
{
    var mi = new MONITORINFO
    {
        cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFO))
    };
    if (Windows.Win32.PInvoke.GetMonitorInfo(monitor, ref mi))
    {

    }
    return true;
}
C#

You have noticed that we've set the size of the structure before passing it to the function. This is common to many WinApi functions and forgetting to set this member or setting it with a wrong value is a common source of bugs.

MONITORINFO is declared in Windows.Win32.Graphics.Gdi, which is included in the usings for the file. Now, mi has the data for the monitor:

internal partial struct MONITORINFO
{
    internal uint cbSize;
    internal winmdroot.Foundation.RECT rcMonitor;
    internal winmdroot.Foundation.RECT rcWork;
    internal uint dwFlags;
}
C#

But it doesn't have the name of the monitor. This was solved by using the structure MONITORINFOEX, which has the name of the monitor. There is a catch, here: although GetMonitorInfo has an overload that uses the MONITORINFOEX structure, it's not declared in CsWin32, so we must do a trick, here:

private unsafe BOOL enumProc(HMONITOR monitor, HDC hdc, RECT* clipRect, LPARAM data)
{
    var mi = new MONITORINFOEXW();
    mi.monitorInfo.cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFOEXW));

    if (Windows.Win32.PInvoke.GetMonitorInfo(monitor, (MONITORINFO*) &mi))
    {

    }
    return true;
}
C#

MONITORINFOEX is not declared automatically, you must use the explicit wide version, MONITORINFOEXW and add the impport to NativeMethods.txt. To use it, you must create the structure, initialize it and then cast the pointer to a pointer of MONITORINFO. It's not the most beautiful code, but it works. Now we have the code to enumerate the displays:

int _minX = 0;
int _minY = 0;
int _maxX = 0;
int _maxY = 0;
private unsafe void InitializeDisplayCanvas()
{
    Windows.Win32.PInvoke.EnumDisplayMonitors(null, null, enumProc, IntPtr.Zero);
    FillDisplayData();
}

private void FillDisplayData()
{
    DisplayCanvas.Width = _maxX - _minX;
    DisplayCanvas.Height = _maxY - _minY;
    DisplayCanvas.RenderTransform = new TranslateTransform(-_minX, -_minY);
    var background = new System.Windows.Shapes.Rectangle
    {
        Width = DisplayCanvas.Width,
        Height = DisplayCanvas.Height,
        Fill = new SolidColorBrush(System.Windows.Media.Color.FromArgb(1, 242, 242, 242)),
    };
    Canvas.SetLeft(background, _minX);
    Canvas.SetTop(background, _minY);
    DisplayCanvas.Children.Add(background);
    var numDisplay = 0;
    foreach (var display in _displays)
    {
        numDisplay++;
        var border = new Border
        {
            Width = display.WorkingArea.Width,
            Height = display.WorkingArea.Height,
            Background = System.Windows.Media.Brushes.DarkGray,
            CornerRadius = new CornerRadius(30)
        };
        var text = new TextBlock
        {
            Text = numDisplay.ToString(),
            FontSize = 200,
            FontWeight = FontWeights.Bold,
            HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center,
        };
        border.Child = text;
        Canvas.SetLeft(border, display.WorkingArea.X);
        Canvas.SetTop(border, display.WorkingArea.Y);
        DisplayCanvas.Children.Add(border);
    }
}

private unsafe BOOL enumProc(HMONITOR monitor, HDC hdc, RECT* clipRect, LPARAM data)
{
    var mi = new MONITORINFOEXW();
    mi.monitorInfo.cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFOEXW));

    if (Windows.Win32.PInvoke.GetMonitorInfo(monitor, (MONITORINFO*) &mi))
    {
        if (_minX > mi.monitorInfo.rcWork.X)
            _minX = mi.monitorInfo.rcWork.X;
        if (_minY > mi.monitorInfo.rcWork.Y)
            _minY = mi.monitorInfo.rcWork.Y;
        if (_maxX < mi.monitorInfo.rcWork.X + mi.monitorInfo.rcWork.Width)
            _maxX = mi.monitorInfo.rcWork.X + mi.monitorInfo.rcWork.Width;
        if (_maxY < mi.monitorInfo.rcWork.Y + mi.monitorInfo.rcWork.Height)
            _maxY = mi.monitorInfo.rcWork.Y + mi.monitorInfo.rcWork.Height;

        _displays.Add(new Display(mi.szDevice.ToString(), mi.monitorInfo.rcMonitor, mi.monitorInfo.rcWork, 1.0));
    }

    return true;
}
C#

If you run this code, you'll get the same result we've got with the previous version, but at least we don't have to include WinForms here. But we can go a step further and get the scale for the monitors.

The source code for this article is at https://github.com/bsonnino/EnumeratingDisplays

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • May 2025
  • December 2024
  • October 2024
  • August 2024
  • July 2024
  • June 2024
  • November 2023
  • October 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • June 2022
  • April 2022
  • March 2022
  • February 2022
  • January 2022
  • July 2021
  • June 2021
  • May 2021
  • April 2021
  • March 2021
  • February 2021
  • January 2021
  • December 2020
  • October 2020
  • September 2020
  • April 2020
  • March 2020
  • January 2020
  • November 2019
  • September 2019
  • August 2019
  • July 2019
  • June 2019
  • April 2019
  • March 2019
  • February 2019
  • January 2019
  • December 2018
  • November 2018
  • October 2018
  • September 2018
  • August 2018
  • July 2018
  • June 2018
  • May 2018
  • November 2017
  • October 2017
  • September 2017
  • August 2017
  • June 2017
  • May 2017
  • March 2017
  • February 2017
  • January 2017
  • December 2016
  • November 2016
  • October 2016
  • September 2016
  • August 2016
  • July 2016
  • June 2016
  • May 2016
  • April 2016
  • March 2016
  • February 2016
  • October 2015
  • August 2013
  • May 2013
  • February 2012
  • January 2012
  • April 2011
  • March 2011
  • December 2010
  • November 2009
  • June 2009
  • April 2009
  • March 2009
  • February 2009
  • January 2009
  • December 2008
  • November 2008
  • October 2008
  • July 2008
  • March 2008
  • February 2008
  • January 2008
  • December 2007
  • November 2007
  • October 2007
  • September 2007
  • August 2007
  • July 2007
  • Development
  • English
  • Português
  • Uncategorized
  • Windows

.NET AI Algorithms asp.NET Backup C# Debugging Delphi Dependency Injection Desktop Bridge Desktop icons Entity Framework JSON Linq Mef Minimal API MVVM NTFS Open Source OpenXML OzCode PowerShell Sensors Silverlight Source Code Generators sql server Surface Dial Testing Tools TypeScript UI Unit Testing UWP Visual Studio VS Code WCF WebView2 WinAppSDK Windows Windows 10 Windows Forms Windows Phone WPF XAML Zip

  • Entries RSS
  • Comments RSS
©2025 Bruno Sonnino | Design: Newspaperly WordPress Theme
Menu
  • Home
  • About