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);
}
}
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();
}
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>
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);
}
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();
}
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;
}
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;
}
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;
}
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;
}
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