GuideProgramming + ScriptingPowershell Save and Restore window positions using Powershell script Updated January 27 2026
I have an annoying issue where after I use remote desktop to my computer, when I log back in locally the windows do not go back to the proper monitors. This happens every time.

I used Google Gemini to help me build a powershell script which lets you save window positions and restore them. This page has 3 different scripts on it, the first is an interactive one that lets you save and restore positions. The next lets you save window positions to a file, and the third lets you restore positions. I normally use the third script, I have a shortcut to it using this syntax:
PowerShell.exe -windowstyle hidden -ExecutionPolicy Bypass -File "C:\Path\To\Script.ps1"
I set a keyboard shortcut so I can easily run it by pressing a few keys.

Copy to Clipboard # --- [ 1. Unified WinApi Block ] --- if (-not ("WinApi_Master" -as [type])) { $Assemblies = @("System.Drawing", "System.Windows.Forms") Add-Type -ReferencedAssemblies $Assemblies -TypeDefinition @" using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using System.Drawing; public class WinApi_Master { [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] public struct WINDOWPLACEMENT { public int length; public int flags; public int showCmd; public Point ptMinPosition; public Point ptMaxPosition; public RECT rcNormalPosition; } } "@ } # --- [ 2. Settings ] --- $BaseDir = "C:\Users\$env:USERNAME\Documents\Workspaces" if (-not (Test-Path $BaseDir)) { New-Item -ItemType Directory -Path $BaseDir | Out-Null } $ProcessAllowList = @("chrome", "msedge", "calc", "notepad", "shotcut", "code") # --- [ 3. Core Functions ] --- function Save-Layout ($FileName) { $Path = Join-Path $BaseDir "$FileName.json" $windowList = New-Object System.Collections.Generic.List[PSCustomObject] $seenHandles = New-Object System.Collections.Generic.HashSet[intptr] $callback = { param($hwnd, $lparam) if ([WinApi_Master]::IsWindowVisible($hwnd) -and -not $seenHandles.Contains($hwnd)) { $processId = 0 [WinApi_Master]::GetWindowThreadProcessId($hwnd, [ref]$processId) $pName = (Get-Process -Id $processId -ErrorAction SilentlyContinue).ProcessName if ($ProcessAllowList -contains $pName.ToLower()) { $sb = New-Object System.Text.StringBuilder 256 [WinApi_Master]::GetWindowText($hwnd, $sb, $sb.Capacity) $wp = New-Object WinApi_Master+WINDOWPLACEMENT $wp.length = [System.Runtime.InteropServices.Marshal]::SizeOf($wp) [WinApi_Master]::GetWindowPlacement($hwnd, [ref]$wp) $rect = New-Object WinApi_Master+RECT [WinApi_Master]::GetWindowRect($hwnd, [ref]$rect) $null = $seenHandles.Add($hwnd) # Fixed Hash Literal Syntax $windowList.Add([PSCustomObject]@{ ProcessName = $pName Title = $sb.ToString() X = $rect.Left Y = $rect.Top Width = $rect.Right - $rect.Left Height = $rect.Bottom - $rect.Top State = $wp.showCmd Handle = $hwnd.ToInt64() }) } } return $true } [WinApi_Master]::EnumWindows($callback, [IntPtr]::Zero) $windowList | ConvertTo-Json | Out-File $Path Write-Host "`n[✔] Layout '$FileName' saved for user $env:USERNAME" -ForegroundColor Green } function Restore-Layout ($FileName) { $Path = Join-Path $BaseDir "$FileName.json" if (-not (Test-Path $Path)) { return Write-Warning "File not found!" } $SavedData = Get-Content $Path | ConvertFrom-Json foreach ($item in $SavedData) { $hwnd = [IntPtr]$item.Handle # Fallback if handle is dead if (-not [WinApi_Master]::IsWindowVisible($hwnd)) { $p = Get-Process -Name $item.ProcessName -ErrorAction SilentlyContinue | Select-Object -First 1 if ($p) { $hwnd = $p.MainWindowHandle } } if ($hwnd -ne [IntPtr]::Zero) { [WinApi_Master]::ShowWindow($hwnd, 1) # Unmaximize Start-Sleep -Milliseconds 50 [WinApi_Master]::SetWindowPos($hwnd, [IntPtr]::Zero, $item.X, $item.Y, $item.Width, $item.Height, 0x0044) if ($item.State -eq 3) { [WinApi_Master]::ShowWindow($hwnd, 3) } } } Write-Host "`n[✔] Layout '$FileName' restored!" -ForegroundColor Cyan } # --- [ 4. Interactive Menu ] --- do { Clear-Host Write-Host "--- Workspace Manager ($env:USERNAME) ---" -ForegroundColor Yellow Write-Host "1. Save Current Layout" Write-Host "2. Load a Layout" Write-Host "3. List Saved Layouts" Write-Host "Q. Quit" $choice = Read-Host "`nSelect an option" switch ($choice) { "1" { $name = Read-Host "Enter name for this layout" if ($name) { Save-Layout $name; Start-Sleep -Seconds 1 } } "2" { $files = Get-ChildItem $BaseDir -Filter *.json if ($files) { Write-Host "`nAvailable Layouts:" for ($i=0; $i -lt $files.Count; $i++) { Write-Host "$($i+1). $($files[$i].BaseName)" } $input = Read-Host "`nSelect number" if ($input -match '^\d+$') { $idx = [int]$input - 1 if ($idx -ge 0 -and $idx -lt $files.Count) { Restore-Layout $files[$idx].BaseName Start-Sleep -Seconds 1 } } } else { Write-Warning "No layouts found."; Start-Sleep -Seconds 1 } } "3" { Get-ChildItem $BaseDir -Filter *.json | Select-Object @{N="LayoutName"; E={$_.BaseName}}, LastWriteTime | Out-GridView } } } while ($choice -ne "q")




I also built two separate scripts which Save positions and another restores them:
Save Window Positions:
Copy to Clipboard if (-not ("WinApi_Final" -as [type])) { # We must explicitly tell the C# compiler to reference System.Drawing.dll $Assemblies = @("System.Drawing", "System.Windows.Forms") Add-Type -ReferencedAssemblies $Assemblies -TypeDefinition @" using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using System.Drawing; // This tells C# where to find 'Point' public class WinApi_Final { // Window Enumeration & Text [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); // Positioning & State [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); // Helper Structures [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] public struct WINDOWPLACEMENT { public int length; public int flags; public int showCmd; public Point ptMinPosition; // Now correctly recognized public Point ptMaxPosition; public RECT rcNormalPosition; } } "@ } $FilePath = "C:\Users\$env:USERNAME\WorkspaceLayout.json" # --- CONFIGURATION: Only save these processes --- $ProcessAllowList = @("chrome", "msedge", "calc", "notepad", "mremoteng") function Save-Workspace { $windowList = New-Object System.Collections.Generic.List[PSCustomObject] $seenHandles = New-Object System.Collections.Generic.HashSet[intptr] $callback = { param($hwnd, $lparam) if ([WinApi_Final]::IsWindowVisible($hwnd) -and -not $seenHandles.Contains($hwnd)) { $processId = 0 [WinApi_Final]::GetWindowThreadProcessId($hwnd, [ref]$processId) $pName = (Get-Process -Id $processId -ErrorAction SilentlyContinue).ProcessName # --- FILTER LOGIC --- # Only proceed if the process name is in our Allow List if ($ProcessAllowList -contains $pName.ToLower()) { $sb = New-Object System.Text.StringBuilder 256 [WinApi_Final]::GetWindowText($hwnd, $sb, $sb.Capacity) $title = $sb.ToString() $null = $seenHandles.Add($hwnd) $wp = New-Object WinApi_Final+WINDOWPLACEMENT $wp.length = [System.Runtime.InteropServices.Marshal]::SizeOf($wp) [WinApi_Final]::GetWindowPlacement($hwnd, [ref]$wp) $rect = New-Object WinApi_Final+RECT [WinApi_Final]::GetWindowRect($hwnd, [ref]$rect) $windowList.Add([PSCustomObject]@{ ProcessName = $pName Title = $title X = $rect.Left Y = $rect.Top Width = $rect.Right - $rect.Left Height = $rect.Bottom - $rect.Top State = $wp.showCmd Handle = $hwnd.ToInt64() }) } } return $true } [WinApi_Final]::EnumWindows($callback, [IntPtr]::Zero) $windowList | ConvertTo-Json | Out-File $FilePath Write-Host "Success: Saved $($windowList.Count) filtered windows to $FilePath" -ForegroundColor Green } Save-Workspace



Restore Window Positions:
Copy to Clipboard if (-not ("WinApi_Final" -as [type])) { # We must explicitly tell the C# compiler to reference System.Drawing.dll $Assemblies = @("System.Drawing", "System.Windows.Forms") Add-Type -ReferencedAssemblies $Assemblies -TypeDefinition @" using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using System.Drawing; // This tells C# where to find 'Point' public class WinApi_Final { // Window Enumeration & Text [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); // Positioning & State [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); // Helper Structures [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] public struct WINDOWPLACEMENT { public int length; public int flags; public int showCmd; public Point ptMinPosition; // Now correctly recognized public Point ptMaxPosition; public RECT rcNormalPosition; } } "@ } $FilePath = "C:\Users\$env:USERNAME\WorkspaceLayout.json" function Restore-Workspace { if (-not (Test-Path $FilePath)) { return Write-Warning "Save file not found." } $SavedData = Get-Content $FilePath | ConvertFrom-Json foreach ($item in $SavedData) { $hwnd = [IntPtr]0 # --- FALLBACK LOGIC --- # 1. Try the saved Handle first $savedHandle = [IntPtr]$item.Handle if ([WinApi_Final]::IsWindowVisible($savedHandle)) { $hwnd = $savedHandle } # 2. If handle is dead, find the window by Name and Title else { $procs = Get-Process -Name $item.ProcessName -ErrorAction SilentlyContinue foreach ($p in $procs) { if ($p.MainWindowHandle -ne 0) { $hwnd = $p.MainWindowHandle break # Grab the first available window for this process } } } # --- EXECUTION LOGIC --- if ($hwnd -ne [IntPtr]::Zero) { # 1. Unmaximize/Restore to Normal (1) to 'unlock' the window [WinApi_Final]::ShowWindow($hwnd, 1) Start-Sleep -Milliseconds 50 # Small buffer for Windows to process state change # 2. Move to coordinates # Flags: 0x0044 (NoZOrder + Show) [WinApi_Final]::SetWindowPos($hwnd, [IntPtr]::Zero, $item.X, $item.Y, $item.Width, $item.Height, 0x0044) # 3. Re-maximize if that was the saved state (3) if ($item.State -eq 3) { [WinApi_Final]::ShowWindow($hwnd, 3) } } } Write-Host "Restoration complete (Handle + Process Fallback applied)!" -ForegroundColor Cyan } Restore-Workspace



Search Keywords: layout, workspace




©2024 - Some portions of this website are Copyrighted.
Your IP: 216.73.216.131     Referring URL:
Browser: Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)
Terms and Conditions, Privacy Policy, and Security Policy