在多显示器环境下使用SetWindowPos函数

21

使用 user32.dll 和 C# 编写了下面的方法。它将使用窗口处理句柄,将窗口位置设置为提供的 (x, y) 位置。

但是,在多监视器环境中,下面的代码仅将窗口位置设置为主要监视器。我希望也能选择哪个监视器。
请问如何使用 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的评论提供的解决方案。

这是我的监视器配置:

enter image description here

请注意,我有一个位于主显示器左侧的次要显示器。在阅读Jimi提供的“虚拟监视器”链接后,我发现要将窗口移动到次要显示器上,必须使用负x值,因为它在主显示器的原点(左上角或(0, 0))左侧。

因此,如果我想将窗口位置设置为次要监视器的<0,0>坐标,则必须从主监视器的原点中减去次要监视器的x宽度,如下所示:

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

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

SetWindowPosition(Process p, -1920, 0);

注意:我不知道如果监视器有不同的分辨率,你会做什么。那是一个更复杂的话题,也不是我正在问的问题。此外,我没有看到需要深入探讨这个主题,因为上面的简单例子解决了我所有的问题。

1个回答

43

系统显示配置和虚拟屏幕

在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

如果我们通过系统小程序更改主屏幕引用,将其设置为\\.\DISPLAY3,那么坐标将相应地被修改:

Multi Display Disposition 1

虚拟屏幕
虚拟屏幕是一个虚拟显示器,其尺寸由以下参数表示: 起点:最左边屏幕的原点坐标 宽度:所有屏幕宽度之和 高度:最高屏幕的高度
这些尺寸可以通过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, MonitorFromPointMonitorFromRect

[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];
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接