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:
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:
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:
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:
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:
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:
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:
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:
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:
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