c# - 將SetWindowPos與多個監視器一起使用



windows user32 (1)

系統顯示處置和VirtualScreen

在Windows系統中, 主屏幕 (編程透視圖)是顯示設備,其左上角位置設置為 Point(0,0)

這意味著位於主屏幕 左側 的顯示器將具有 X 坐標(如果顯示器處於縱向佈局,則 Y 坐標可能為負)。
右側 的顯示將具有 X 坐標(如果顯示處於縱向佈局,則 Y 坐標可能為負)。

顯示在主屏幕的 左側
換句話說,具有 Point.X 原點的顯示
Point.X origin是所有前面 Screens[].Width 的總和 Screens[].Width ,從主屏幕的 Point.X 原點坐標中減去。

顯示在主屏幕的 右側
換句話說,具有 Point.X 原點的 顯示器
Point.X origin是所有前面 Screens[].Width 的總和 Screens[].Width Primary包含 ,添加到主屏幕的原點 Point.X 坐標。

重要說明
如果應用程序不是DPIAware,則係統執行的虛擬化和自動DPI縮放可能會影響所有這些措施。 所有度量將 統一 為默認的96 Dpi:應用程序將獲得縮放值。 這還包括從非DpiAware Win32 Api函數檢索的值。 看到:

Windows上的高DPI桌面應用程序開發

啟用對 app.manifest 文件中所有目標系統的支持,取消註釋所需的部分。

app.manifest 文件中添加/取消註釋 DpiAware和DpiAwareness部分
可以在 app.config 文件中設置 PerMonitorV2 Dpi Awareness 模式(可從Windows 10 Creators Edition獲得)。

例:
考慮一個帶有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



如果我們使用System applet更改主屏幕參考,將其設置為 \\.\DISPLAY3 ,則相應地修改坐標:


虛擬屏幕

虛擬屏幕是虛擬顯示,其尺寸表示為:
原點 :最左側 Screen 的原點坐標
寬度 :所有 Screens 寬度的總和。
高度 :最高 Screen 的高度

這些度量由 SystemInformation.VirtualScreen 報告
主屏幕 Size SystemInformation.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


顯示區域內的窗口位置

Screen類 提供了多種方法,可用於確定當前顯示特定窗口的屏幕:

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

Screen.FromHandle([Window Handle])
返回 Screen 對象,該對象包含 Handle 引用的Window \ Control的最大部分

Screen.FromPoint([Point])
返回包含特定 PointScreen 對象

Screen.FromRectangle([Rectangle])
返回包含指定 Rectangle 的最大部分的 Screen 對象

Screen.GetBounds() (重載)
返回一個 Rectangle 結構,該結構引用包含以下內容的Screen Bounds:
- 一個特定的 Point
- 指定 Rectangle 最大部分
- Control 參考

要確定 \\.\DISPLAY[N] 當前表單的 \\.\DISPLAY[N] ,請調用(例如):

Screen.FromHandle(this);

要確定在哪個屏幕中顯示輔助表格:
(使用示例中的示例顯示)

form2 = new Form2();
form2.Location = new Point(-1400, 100);
form2.Show();
Rectangle screenSize = Screen.GetBounds(form2);
Screen screen = Screen.FromHandle(form2.Handle);

screenSize 將= = \\.\DISPLAY3 Bounds。
screen 將是表示 \\.\DISPLAY3 屬性的 Screen 對象。

screen 像還將報告 \\.\DISPLAY[N] form2Screen\\.\DISPLAY[N] 名稱。

獲取Screen對象的 hMonitor 句柄

.NET參考源 顯示返回 hMonitor ,調用 [Screen].GetHashCode();

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

獲取屏幕設備上下文的句柄
檢索任何可用顯示的hDC的通用方法。

當僅需要特定的屏幕參考時,可以使用先前描述的方法之一來確定屏幕坐標或屏幕設備。

Screen.DeviceName 屬性可以用作GDI + CreateDC 函數的 lpszDriver 參數。 它將返回顯示的hDC, 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);

使用 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的評論。

這是我的顯示器配置:

注意我的主監視器左側有一個輔助監視器。 在閱讀Jimi提供的虛擬監視器鏈接後,我發現要將窗口移動到輔助監視器,我必須使用負x值,因為它位於主監視器的原點左側(左上角或<0,0>)。

因此,如果我想將窗口位置設置為輔助監視器的<0,0>坐標,那麼我必須從主監視器的原點中減去輔助監視器的x寬度,如下所示:

<0,0> - <1920,0> = < - 1920,0>

現在,當我在客戶端代碼中調用SetWindowPosition時,我將其稱為:

SetWindowPosition(Process p, -1920, 0);

注意:如果顯示器具有不同的分辨率,我不知道你會怎麼做。 這是一個更複雜的話題,而不是我要問的問題。 此外,我沒有看到有必要深入探討這個主題,因為上面的簡單例子解決了我的所有問題。





user32