系统显示配置和虚拟屏幕
在Windows系统中,主屏幕(从编程角度考虑)是具有左上角位置设置为Point(0,0)
的显示设备。
这意味着位于主屏幕左侧的显示器将具有负的X
坐标(如果显示器以纵向布局,则Y
坐标可能为负)。
位于右侧的显示器将具有正的X
坐标(如果显示器以纵向布局,则Y
坐标可能为负)。
主屏幕左侧的显示器:
换句话说,这些显示器具有负的Point.X
坐标原点。
Point.X
坐标原点是主屏幕的Point.X
坐标原点减去之前所有Screens[].Width
的总和。
主屏幕右侧的显示器:
换句话说,这些显示器具有正的Point.X
坐标原点。
Point.X
坐标原点是主屏幕的Point.X
坐标原点加上之前所有Screens[].Width
的总和(包括主屏幕本身)。
关于 DPI 感知的重要注意事项:
如果应用程序没有 DPI 感知,所有这些措施可能会受到系统执行的虚拟化和自动 DPI 缩放的影响。所有措施将统一为默认的 96 Dpi:应用程序将接收到经过缩放的数值。这也包括从非 DPI 感知的 Win32 API 函数检索到的数值。参见:
Windows 上的高 DPI 桌面应用程序开发
在app.manifest
文件中启用对所有目标系统的支持,取消注释所需的部分。
.NET Framework 4.7.2 之前的版本:
在app.manifest
文件中添加/取消注释DpiAware 和 DpiAwareness 部分。
.NET Framework 4.7.2+之前:
可以在app.config
文件中设置PerMonitorV2 Dpi Awareness模式(从Windows 10创作者更新版开始可用)。
.NET6+:
在项目的文件(.csproj
)中添加或编辑<ApplicationHighDpiMode>
属性,例如:
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
有关所有可能的设置,请参阅.NET桌面SDK项目的MSBuild参考
在这种情况下,app.manifest
仅用于指定您的应用程序支持哪些系统版本。不使用app.config
。
另请参阅:
DPI和设备无关像素
混合模式DPI缩放和DPI感知API
考虑一个有3个显示器的系统:
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
![Multi Display Disposition 1](https://istack.dev59.com/yy3dC.webp)
如果我们通过系统小程序更改主屏幕引用,将其设置为
\\.\DISPLAY3
,那么坐标将相应地被修改:
![Multi Display Disposition 1](https://istack.dev59.com/w7Ctm.webp)
虚拟屏幕
虚拟屏幕是一个虚拟显示器,其尺寸由以下参数表示:
起点:最左边屏幕的原点坐标
宽度:所有屏幕宽度之和
高度:最高屏幕的高度
这些尺寸可以通过
SystemInformation.VirtualScreen来获取。
主屏幕的尺寸可以通过
SystemInformation.PrimaryMonitorSize来获取。
所有屏幕的尺寸和位置也可以通过
Screen.AllScreens和每个\\.\DISPLAY[N]属性来检索。
以前面的例子为参考,在第一种排列方式下,虚拟屏幕的边界如下:
Bounds: (-1680, 0, 3280, 1080) Left: -1680 Right: 3280 Top: 0 Bottom: 1080
在第二个布局中,"VirtualScreen"的边界是:
Bounds: (0, 0, 4960, 1080) Left: 0 Right: 4960 Top: 0 Bottom: 1080
显示区域内的窗口位置:
屏幕类提供了多种方法,用于确定特定窗口当前显示在哪个屏幕上:
Screen.FromControl([控件引用])
返回包含指定控件
引用最大部分的屏幕
对象。
Screen.FromHandle([窗口句柄])
返回包含由句柄
引用的窗口/控件的最大部分的屏幕
对象。
Screen.FromPoint([点])
返回包含特定点
的屏幕
对象。
Screen.FromRectangle([Rectangle])
返回包含指定Rectangle
最大部分的Screen
对象。
Screen.GetBounds()
(重载)
返回引用包含屏幕边界的Rectangle
结构:
- 特定
Point
- 指定
Rectangle
的最大部分
Control
引用
要确定当前窗体显示在哪个\\.\DISPLAY[N]
上,调用以下方法(例如):
Screen.FromHandle(this);
确定在哪个屏幕上显示次要窗体:
(使用示例图像中显示的布局)
var f2 = new Form2();
f2.Location = new Point(-1400, 100);
f2.Show();
Rectangle screenSize = Screen.GetBounds(f2);
Screen screen = Screen.FromHandle(f2.Handle);
screenSize
将等于\\.\DISPLAY3
的边界。
screen
将是代表\\.\DISPLAY3
属性的Screen
对象。
screen
对象还将报告Screen
在其中显示form2
的\\.\DISPLAY[N]
名称。
获取屏幕对象的句柄:
.NET参考源代码显示调用
[Screen].GetHashCode();
返回
hMonitor
。
IntPtr monitorHwnd = new IntPtr([Screen].GetHashCode());
或者使用相同的本地 Win32 函数:
MonitorFromWindow, MonitorFromPoint 和 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);
要检测窗口在多个显示器之间的移动,您可以处理
WM_WINDOWPOSCHANGED
消息,调用
MonitoFromWindow
,然后使用
GetScaleFactorForMonitor来确定是否有DPI更改,并对新设置做出反应。
获取屏幕设备上下文的句柄:
一种通用方法,可用于检索任何可用显示器的hDC。
当只需要特定屏幕引用时,可以使用先前描述的方法之一来确定屏幕坐标或屏幕设备。
Screen.DeviceName属性是通过调用GetMonitorInfo()并传递MONITORINFOEX结构(请参见底部的声明)来检索的。它可以作为GDI的CreateDC函数的lpszDriver
参数使用。它将返回Graphics.FromHdc可以使用以创建有效的Graphics对象的显示的hDC,从而可以在特定屏幕上进行绘制。
假设至少有两个可用的显示器:
[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);
声明如下:
像这样声明MONITORINFOREX
:
(如果是主监视器,则将dwFlags
设置为0x00000001
)
[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];
}