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