Created
December 26, 2025 11:28
-
-
Save edwinclement08/b0a8af0449a8441a43f3ef8620e5b290 to your computer and use it in GitHub Desktop.
Swapping Monitor horizontal orientation for Win11(2 side by side monitors usecase).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| param( | |
| [switch]$DryRun, | |
| [switch]$UseUnicode, | |
| [switch]$Apply | |
| ) | |
| # Usage: | |
| # powershell -NoProfile -ExecutionPolicy Bypass -File "monitor_swap.ps1" -Apply | |
| # Two Add-Type signatures: ANSI (original) and Unicode (alternate) — use -UseUnicode to pick the Unicode one for testing. | |
| $SignatureAnsi = @" | |
| using System; | |
| using System.Runtime.InteropServices; | |
| [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] | |
| public struct DEVMODE { | |
| [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] | |
| public string dmDeviceName; | |
| public short dmSpecVersion; | |
| public short dmDriverVersion; | |
| public short dmSize; | |
| public short dmDriverExtra; | |
| public int dmFields; | |
| public int dmPositionX; | |
| public int dmPositionY; | |
| public int dmDisplayOrientation; | |
| public int dmDisplayFixedOutput; | |
| public short dmColor; | |
| public short dmDuplex; | |
| public short dmYResolution; | |
| public short dmTTOption; | |
| public short dmCollate; | |
| [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] | |
| public string dmFormName; | |
| public short dmLogPixels; | |
| public short dmBitsPerPel; | |
| public int dmPelsWidth; | |
| public int dmPelsHeight; | |
| public int dmDisplayFlags; | |
| public int dmDisplayFrequency; | |
| } | |
| [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] | |
| public struct DISPLAY_DEVICE { | |
| public int cb; | |
| [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] | |
| public string DeviceName; | |
| [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] | |
| public string DeviceString; | |
| public int StateFlags; | |
| [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] | |
| public string DeviceID; | |
| [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] | |
| public string DeviceKey; | |
| } | |
| public class User32 { | |
| [DllImport("user32.dll")] | |
| public static extern bool EnumDisplayDevices(string lpDevice, int iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, int dwFlags); | |
| [DllImport("user32.dll")] | |
| public static extern int EnumDisplaySettings(string lpszDeviceName, int iModeNum, ref DEVMODE lpDevMode); | |
| [DllImport("user32.dll")] | |
| public static extern int ChangeDisplaySettingsEx(string lpszDeviceName, ref DEVMODE lpDevMode, IntPtr hwnd, int dwFlags, IntPtr lParam); | |
| public const int ENUM_CURRENT_SETTINGS = -1; | |
| public const int CDS_UPDATEREGISTRY = 0x01; | |
| public const int DM_POSITION = 0x20; | |
| } | |
| "@ | |
| $SignatureUnicode = $SignatureAnsi -replace 'CharSet = CharSet.Ansi','CharSet = CharSet.Unicode' | |
| # select signature | |
| $Signature = if ($UseUnicode) { $SignatureUnicode } else { $SignatureAnsi } | |
| # add the P/Invoke types only if they are not already loaded | |
| if (-not ('User32' -as [type])) { | |
| try { | |
| Add-Type -TypeDefinition $Signature -ErrorAction Stop | |
| } | |
| catch { | |
| Write-Error "Failed to Add-Type User32: $_" | |
| throw | |
| } | |
| } | |
| # --- Monitor Re-arrangement Logic --- | |
| # Diagnostic helper: test Marshal sizes and EnumDisplayDevices for indices 0..9 | |
| function Test-EnumDisplayDevices { | |
| Write-Host "--- Diagnostics: Marshal.SizeOf and EnumDisplayDevices test ---" | |
| $dd = New-Object DISPLAY_DEVICE | |
| $dm = New-Object DEVMODE | |
| $dd.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($dd) | |
| $dm.dmSize = [System.Runtime.InteropServices.Marshal]::SizeOf($dm) | |
| Write-Host "DISPLAY_DEVICE.cb = $($dd.cb)" | |
| Write-Host "DEVMODE.dmSize = $($dm.dmSize)" | |
| for ($i=0; $i -lt 10; $i++) { | |
| $d = New-Object DISPLAY_DEVICE | |
| $d.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($d) | |
| $ok = [User32]::EnumDisplayDevices($null, $i, [ref]$d, 0) | |
| if ($ok) { | |
| Write-Host "$i -> OK Name='$($d.DeviceName)' StateFlags=0x$('{0:X}' -f $d.StateFlags)" | |
| } else { | |
| Write-Host "$i -> FALSE" | |
| } | |
| } | |
| Write-Host "--- End diagnostics ---" | |
| } | |
| # Run diagnostics if requested | |
| if ($DryRun) { | |
| Test-EnumDisplayDevices | |
| } | |
| # 1. Get all active monitors | |
| $monitors = New-Object System.Collections.Generic.List[PSObject] | |
| for ($i=0; $i -lt 10; $i++) { | |
| $device = New-Object DISPLAY_DEVICE | |
| # Use full namespace here: | |
| $device.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($device) | |
| if ([User32]::EnumDisplayDevices($null, $i, [ref]$device, 0)) { | |
| if ($device.StateFlags -band 0x1) { # 0x1 = Attached to desktop | |
| $mode = New-Object DEVMODE | |
| $mode.dmSize = [System.Runtime.InteropServices.Marshal]::SizeOf($mode) | |
| [User32]::EnumDisplaySettings($device.DeviceName, [User32]::ENUM_CURRENT_SETTINGS, [ref]$mode) | |
| $monitors.Add([PSCustomObject]@{ | |
| Name = $device.DeviceName | |
| X = $mode.dmPositionX | |
| W = $mode.dmPelsWidth | |
| Mode = $mode | |
| }) | |
| } | |
| } | |
| } | |
| # If P/Invoke found nothing, fall back to managed enumeration so the script can at least list screens | |
| $EnumerationMethod = 'PInvoke' | |
| # Try to obtain a DEVMODE for a given device name via EnumDisplaySettings. | |
| function Try-GetDevMode { | |
| param([string]$DeviceName) | |
| $mode = New-Object DEVMODE | |
| $mode.dmSize = [System.Runtime.InteropServices.Marshal]::SizeOf($mode) | |
| try { | |
| $ret = [User32]::EnumDisplaySettings($DeviceName, [User32]::ENUM_CURRENT_SETTINGS, [ref]$mode) | |
| if ($ret -ne 0) { | |
| return $mode | |
| } | |
| else { | |
| return $null | |
| } | |
| } | |
| catch { | |
| Write-Host "EnumDisplaySettings failed for '$DeviceName': $_" | |
| return $null | |
| } | |
| } | |
| if ($monitors.Count -eq 0) { | |
| Write-Host "P/Invoke enumeration returned no devices - falling back to managed enumeration (System.Windows.Forms.Screen)" | |
| try { | |
| Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop | |
| foreach ($s in [System.Windows.Forms.Screen]::AllScreens) { | |
| # Try to obtain a DEVMODE for the managed screen DeviceName so we can apply changes later. | |
| $mode = Try-GetDevMode $s.DeviceName | |
| if ($mode) { | |
| Write-Host "Managed screen $($s.DeviceName) -> obtained DEVMODE via EnumDisplaySettings" | |
| } | |
| else { | |
| Write-Host "Managed screen $($s.DeviceName) -> no DEVMODE available via EnumDisplaySettings" | |
| } | |
| $monitors.Add([PSCustomObject]@{ | |
| Name = $s.DeviceName | |
| X = $s.Bounds.X | |
| W = $s.Bounds.Width | |
| Mode = $mode | |
| }) | |
| } | |
| $EnumerationMethod = 'Managed' | |
| } | |
| catch { | |
| Write-Error "Managed fallback failed to load System.Windows.Forms: $_" | |
| } | |
| } | |
| # 2. Identify Left and Right based on X position | |
| if ($monitors.Count -ge 2) { | |
| $sorted = $monitors | Sort-Object X | |
| $leftMonitor = $sorted[0] | |
| $rightMonitor = $sorted[1] | |
| } else { | |
| $sorted = @() | |
| $leftMonitor = $null | |
| $rightMonitor = $null | |
| } | |
| # output current arrangement | |
| Write-Host "Current Monitor Arrangement:" | |
| $monitors | ForEach-Object { | |
| Write-Host "$($_.Name): X=$($_.X), W=$($_.W)" | |
| } | |
| if ($leftMonitor -and $rightMonitor) { | |
| Write-Host "Moving $($leftMonitor.Name) to the right of $($rightMonitor.Name)..." | |
| # 3. Calculate New Position | |
| # The new X for the left monitor is the Right monitor's X + its Width | |
| $newX = $rightMonitor.X + $rightMonitor.W | |
| $devMode = $leftMonitor.Mode | |
| Write-Host "Using DEVMODE for $($leftMonitor.Name): $($devMode.dmPositionX), $($devMode.dmPelsWidth)" | |
| # what are the keys available in leftMonitor? | |
| $canApply = $true | |
| if (-not $devMode) { | |
| Write-Host "No DEVMODE available for $($leftMonitor.Name) (managed fallback supplied). Cannot perform P/Invoke apply." | |
| $canApply = $false | |
| } | |
| if ($canApply) { | |
| $devMode.dmPositionX = $newX | |
| $devMode.dmFields = [User32]::DM_POSITION | |
| } | |
| Write-Host "Proposed new X for $($leftMonitor.Name): $newX" | |
| if ($DryRun -or -not $Apply) { | |
| Write-Host "Dry-run or not requested to apply. Use -Apply to perform the ChangeDisplaySettingsEx call." | |
| } | |
| if ($Apply) { | |
| if (-not $canApply) { | |
| Write-Error "Cannot apply because no DEVMODE was available for the target monitor. Aborting apply." | |
| } | |
| else { | |
| # 4. Apply | |
| $res = [User32]::ChangeDisplaySettingsEx($leftMonitor.Name, [ref]$devMode, [IntPtr]::Zero, [User32]::CDS_UPDATEREGISTRY, [IntPtr]::Zero) | |
| if ($res -eq 0) { Write-Host "Success!" -ForegroundColor Green } | |
| else { Write-Error "Failed with code $res" } | |
| } | |
| } | |
| } | |
| else { | |
| Write-Host "Not enough monitors found to swap (need >= 2)." | |
| } | |
| # Try to obtain a DEVMODE for a given device name via EnumDisplaySettings. | |
| function Try-GetDevMode { | |
| param([string]$DeviceName) | |
| $mode = New-Object DEVMODE | |
| $mode.dmSize = [System.Runtime.InteropServices.Marshal]::SizeOf($mode) | |
| try { | |
| $ret = [User32]::EnumDisplaySettings($DeviceName, [User32]::ENUM_CURRENT_SETTINGS, [ref]$mode) | |
| if ($ret -ne 0) { | |
| return $mode | |
| } | |
| else { | |
| return $null | |
| } | |
| } | |
| catch { | |
| Write-Host "EnumDisplaySettings failed for '$DeviceName': $_" | |
| return $null | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment