将 SetWindowPos 与多个监视器一起使用

Using SetWindowPos with multiple monitors

使用 user32.dll 和 C# 我编写了您在下面看到的方法。使用 window 的进程句柄,它将在提供的 (x, y) 位置设置 window 位置。

但是,在多监控环境中,下面的代码仅将 window 位置设置为主监控器。我也希望能够select哪个显示器。
有人可以解释一下如何使用 SetWindowPos 或与另一个 user32.dll 函数的组合来实现吗?

[DllImport("user32.dll", SetLastError = true)]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, int uFlags);

private const int SWP_NOSIZE = 0x0001;
private const int SWP_NOZORDER = 0x0004;
private const int SWP_SHOWWINDOW = 0x0040;

public static void SetWindowPosition(Process p, int x, int y)
{
    IntPtr handle = p.MainWindowHandle;
    if (handle != IntPtr.Zero)
    {
        SetWindowPos(handle, IntPtr.Zero, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE | SWP_SHOWWINDOW);
    }
}

基于 Jimi 评论的解决方案。

这是我的显示器配置:

观察到我的主显示器左侧有一个辅助显示器。在阅读了 Jimi 提供的虚拟监视器 link 后,我发现要将 windows 移动到辅助监视器,我必须使用负 x 值,因为它位于主监视器原点(左上角,或 (0, 0)).

因此,如果我想将我的 window 位置设置为辅助监视器的 <0,0> 坐标,我必须从主监视器的原点减去辅助监视器的 x 宽度,像这样:

(0, 0) - (1920, 0) = (-1920, 0)

现在,当我在客户端代码中调用 SetWindowPosition 时,我这样调用它:

SetWindowPosition(Process p, -1920, 0);

注意:我不知道如果显示器有不同的分辨率你会怎么做。这是一个更复杂的话题,而不是我要问的问题。此外,我认为没有必要深入探讨该主题,因为上面的简单示例解决了我所有的问题。

系统显示配置和VirtualScreen

在Windows系统中,Primary Screen(编程角度)是显示设备,其左上角位置设置在Point(0,0).

这意味着位于主屏幕 左侧 的显示器将具有 X 坐标( Y 如果显示器处于纵向布局,则坐标可能为负数。
右侧 的显示器将具有 X 坐标(如果显示器在纵向布局)。

显示在主屏幕左侧:
换句话说,具有 negative Point.X origin 的显示器.
Point.X 原点是前面所有 Screens[].Width 的总和,从主屏幕的 Point.X 原点坐标中减去。

显示在主屏幕右侧:
换句话说,具有 positive Point.X origin 的显示器.
Point.X 原点是所有前面的 Screens[].Width 包含的主屏幕 的总和,添加到主屏幕的原点 Point.X 坐标。


关于 Dpi 感知的重要说明:
如果应用程序不是 DPI Aware,所有这些措施都可能受到系统执行的虚拟化和自动 DPI 缩放的影响。所有度量都将 统一 到默认的 96 Dpi:应用程序将接收缩放值。这也包括从非 Dpi 软件 Win32 API 函数中检索到的值。参见:

High DPI Desktop Application Development on Windows

app.manifest 文件中启用对所有目标系统的支持,取消对所需部分的注释。

Add/Uncomment app.manifest 文件中的 DpiAware and DpiAwareness sections
PerMonitorV2 Dpi Awareness模式可以在app.config文件中设置(可从Windows 10 Creators Edition获得)。

另请参阅:

DPI and Device-Independent Pixels
Mixed-Mode DPI Scaling and DPI-aware APIs


示例:
考虑具有 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

如果我们更改,使用系统小程序,主屏幕参考,将其设置为 \.\DISPLAY3,坐标将相应修改:

虚拟屏幕

Virtual Screen 是一个虚拟显示器,其尺寸表示为:
Origin:最左边的原点坐标Screen
宽度:所有Screens宽度的总和。
Height:最高Screen.

的Height

这些措施由 SystemInformation.VirtualScreen
报告 主屏幕 SizeSystemInformation.PrimaryMonitorSize
报告 还可以使用 Screen.AllScreens 并检查每个 \.\DISPLAY[N] 属性来检索所有屏幕当前度量和位置。

参考前面的例子,在第一个配置中,VirtualScreen边界是:

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

Window 显示区域内的位置:

Screen class 提供了多种方法,可用于确定当前显示特定 window 的屏幕:

Screen.FromControl([Control reference])
Returns 包含指定 Control 引用的最大部分的 Screen 对象。

Screen.FromHandle([Window Handle])
Returns Screen 对象包含 Handle

引用的 Window\Control 的最大部分

Screen.FromPoint([Point])
Returns 包含特定 Point

Screen 对象

Screen.FromRectangle([Rectangle])
Returns 包含指定 Rectangle

的最大部分的 Screen 对象

Screen.GetBounds()(过载)
Returns 引用屏幕边界的 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 对象还将报告显示 form2Screen\.\DISPLAY[N] 名称。


获取Screen对象的hMonitor句柄:

.NET Reference Source显示hMonitor返回调用[Screen].GetHashCode();

IntPtr monitorHwnd = new IntPtr([Screen].GetHashCode());

或使用相同的原生 Win32 函数:

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

获取屏幕的设备上下文句柄:
检索任何可用显示器的 hDC 的通用方法。

当只需要特定的屏幕参考时,可以使用前面描述的方法之一来确定屏幕坐标或屏幕设备。

Screen.DeviceName property can be used as the lpszDriver parameter of GDI's CreateDC function. It will return the hDC of the display that Graphics.FromHdc 可用于创建有效的 Graphics 对象,这将允许在特定屏幕上绘画。

此处,假设至少有两个显示器可用:

[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);