System Displays disposition and VirtualScreen
In a Windows System, the Primary Screen (programming perspective) is the Display device which has its upper left corner position set at Point(0,0)
.
This implies that the Displays positioned on the left of the Primary Screen, will have negative X
coordinates (the Y
coordinate could be negative if the Display is in Portrait layout).
The Displays on the right will have positive X
coordinates (the Y
coordinate could be negative if the Display is in Portrait layout).
Displays on the Left of the Primary Screen:
In other words, Displays that have a negative Point.X
origin.
The Point.X
origin is the sum of all of the preceding Screens[].Width
, subtracted from the Point.X
origin coordinate of the Primary Screen.
Displays on the Right of the Primary Screen:
In other words, Displays that have a positive Point.X
origin.
The Point.X
origin is the sum of all of the preceding Screens[].Width
, Primary included, added to the origin Point.X
coordinate of the Primary Screen.
Important note about Dpi Awareness:
If the application is not DPI Aware, all these measures can be compromised by the virtualization and automatic DPI Scaling performed by the System. All measures will be uniformed to a default 96 Dpi: the application will receive scaled values. This also includes the values retrieved from non-Dpi ware Win32 API functions. See:
High DPI Desktop Application Development on Windows
Enable support for all targeted Systems in the app.manifest
file, uncommenting the required sections.
.NET Framework prior to 4.7.2:
Add/Uncomment the DpiAware and DpiAwareness sections in the app.manifest
file.
.NET Framework prior to 4.7.2+:
The PerMonitorV2 Dpi Awareness mode can be set in the app.config
file (available from Windows 10 Creators Edition).
.NET6+:
Add or edit the <ApplicationHighDpiMode>
property in the Project's file (.csproj
), e.g.:
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
For all possible settings, see MSBuild reference for .NET Desktop SDK projects
In this scenario, app.manifest
is only used to specify which System versions your app supports. app.config
is not used.
See also:
DPI and Device-Independent Pixels
Mixed-Mode DPI Scaling and DPI-aware APIs
Example:
Consider a System with 3 Monitors:
PrimaryScreen (\\.\DISPLAY1): Width: (1920 x 1080)
Secondary Display (Right) (\\.\DISPLAY2): Width: (1360 x 768)
Secondary Display (Left) (\\.\DISPLAY3): Width: (1680 x 1050)
PrimaryScreen:
Bounds: (0, 0, 1920, 1080) Left: 0 Right: 1920 Top: 0 Bottom: 1080
Secondary Display (Right):
Bounds: (1360, 0, 1360, 768) Left: 1360 Right: 2720 Top: 0 Bottom: 768
Secondary Display (Left):
Bounds: (-1680, 0, 1680, 1050) Left: -1680 Right: 0 Top: 0 Bottom: 1050
If we change, using the System applet, the Primary Screen reference, setting it to \\.\DISPLAY3
, the coordinates will be modified accordingly:
Virtual Screen
The Virtual Screen is a virtual display, which dimensions are represented by:
Origin: the origin coordinate of the left-most Screen
Width: the sum of all the Screens
Widths.
Height: the Height of the highest Screen
.
These measure are reported by SystemInformation.VirtualScreen
The Primary Screen Size
is reported by SystemInformation.PrimaryMonitorSize
All the Screens current measures and position can also be retrieved using Screen.AllScreens and inspecting each \\.\DISPLAY[N]
properties.
Using the preceding example as reference, in the first disposition, the VirtualScreen
bounds are:
Bounds: (-1680, 0, 3280, 1080) Left: -1680 Right: 3280 Top: 0 Bottom: 1080
In the second disposition, the VirtualScreen
bounds are:
Bounds: (0, 0, 4960, 1080) Left: 0 Right: 4960 Top: 0 Bottom: 1080
Window Position inside a Display area:
The Screen class offers multiple methods that can be used to determine in which screen a specific window is currently displayed:
Screen.FromControl([Control reference])
Returns the Screen
object that contains the largest section of the specified Control
reference.
Screen.FromHandle([Window Handle])
Returns the Screen
object that contains the largest section of the Window\Control referenced by an Handle
Screen.FromPoint([Point])
Returns the Screen
object that contains a specific Point
Screen.FromRectangle([Rectangle])
Returns the Screen
object that contains the largest section of the specified Rectangle
Screen.GetBounds()
(overloaded)
Returns a Rectangle
structure that references the Screen Bounds that contain:
- a specific
Point
- largest section of the specified
Rectangle
- A
Control
reference
To determine the \\.\DISPLAY[N]
in which the current Form is shown, call (for example):
Screen.FromHandle(this);
To determine in which Screen a secondary Form is shown:
(Using the Displays layout shown in the sample images)
var f2 = new Form2();
f2.Location = new Point(-1400, 100);
f2.Show();
Rectangle screenSize = Screen.GetBounds(f2);
Screen screen = Screen.FromHandle(f2.Handle);
screenSize
will be equal to the \\.\DISPLAY3
Bounds.
screen
will be the Screen
object representing the \\.\DISPLAY3
properties.
screen
object will also report the \\.\DISPLAY[N]
name of the Screen
in which form2
is shown.
Obtain the hMonitor
Handle of a Screen object:
The .NET Reference Source shows that the hMonitor
is returned calling [Screen].GetHashCode();
IntPtr monitorHwnd = new IntPtr([Screen].GetHashCode());
Or using the same native Win32 functions:
MonitorFromWindow, MonitorFromPoint and MonitorFromRect
[Flags]
internal enum MONITOR_DEFAULTTO
{
NULL = 0x00000000,
PRIMARY = 0x00000001,
NEAREST = 0x00000002,
}
[DllImport("User32.dll", SetLastError = true)]
internal static extern IntPtr MonitorFromWindow(IntPtr hwnd, MONITOR_DEFAULTTO dwFlags);
[DllImport("User32.dll", SetLastError = true)]
internal static extern IntPtr MonitorFromPoint([In] POINT pt, MONITOR_DEFAULTTO dwFlags);
[DllImport("User32.dll", SetLastError = true)]
internal static extern IntPtr MonitorFromRect([In] ref RECT lprc, MONITOR_DEFAULTTO dwFlags);
- To detect Window movements between Monitors, you can handle
WM_WINDOWPOSCHANGED
messages, call MonitoFromWindow
, then GetScaleFactorForMonitor to determine whether there's a DPI change and react to a new setting, eventually.
Obtain a Handle of the device context of a Screen:
A generic method to retrieve the hDC of any Display available.
The Screen coordinates or Screen Device can determined using one of the methods previously described when only a specific Screen reference is required.
The Screen.DeviceName property, which is retrieved calling GetMonitorInfo(), passing a MONITORINFOEX struct (see the declaration at the bottom) can be used as the lpszDriver
parameter of GDI's CreateDC function. It will return the hDC of the display that Graphics.FromHdc can use to create a valid Graphics object, which will allow to paint on a specific screen.
Here, assuming at least two Displays are available:
[DllImport("gdi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
internal static extern IntPtr CreateDC(string lpszDriver, string lpszDevice, string lpszOutput, IntPtr lpInitData);
[DllImport("gdi32.dll", SetLastError = true, EntryPoint = "DeleteDC")]
internal static extern bool DeleteDC([In] IntPtr hdc);
public static IntPtr CreateDCFromDeviceName(string deviceName)
{
return CreateDC(deviceName, null, null, IntPtr.Zero);
}
Screen[] screens = Screen.AllScreens;
IntPtr screenDC1 = CreateDCFromDeviceName(screens[0].DeviceName);
IntPtr screenDC2 = CreateDCFromDeviceName(screens[1].DeviceName);
using (Graphics g1 = Graphics.FromHdc(screenDC1))
using (Graphics g2 = Graphics.FromHdc(screenDC2))
using (Pen pen = new Pen(Color.Red, 10))
{
g1.DrawRectangle(pen, new Rectangle(new Point(100, 100), new Size(200, 200)));
g2.DrawRectangle(pen, new Rectangle(new Point(100, 100), new Size(200, 200)));
}
DeleteDC(screenDC1);
DeleteDC(screenDC2);
Declare MONITORINFOREX
like this:
(dwFlags
is set to 0x00000001
if it's the primary Monitor)
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public unsafe struct MONITORINFOEXW
{
public uint cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
public fixed char szDevice[32];
}