P03: Window Layout Manager
P03: Window Layout Manager
A comprehensive guide to building a window arrangement tool with AutoHotkey v2
Project Overview
What youโll build: A tool that saves and restores window arrangementsโdefine layouts like โcodingโ (IDE left, terminal right, browser on second monitor) and restore them with a hotkey.
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Advanced |
| Time Estimate | 1-2 weeks (15-25 hours) |
| Programming Language | AutoHotkey v2 |
| Knowledge Area | Windows Automation, Window Management, Multi-Monitor, DPI Scaling |
| Prerequisites | Intermediate AutoHotkey, understanding of Windows GUI concepts, P01 and P02 completed |
The Core Question Youโre Answering
โHow do I programmatically identify, track, and reposition windows across multiple displays with different DPI settings, in a way that survives application restarts?โ
This question encapsulates several fundamental challenges:
- Identity: Windows (HWND) are ephemeralโhow do you recognize โVS Codeโ after a restart?
- Geometry: Coordinates span multiple monitors with different resolutions and scaling
- State: Windows can be minimized, maximized, or normalโeach requires different handling
- Persistence: Layouts must survive reboots and monitor configuration changes
Concepts You Must Understand First
Before diving into implementation, ensure you understand these foundational concepts:
1. The Windows Window Model
- Every visible rectangle is a โwindowโ with a handle (HWND)
- Windows exist in a parent-child hierarchy (Desktop -> Applications -> Controls)
- Handles are valid only while the window existsโthey change on restart
2. Coordinate Systems
- Windows uses a โvirtual desktopโ spanning all monitors
- Primary monitor starts at (0,0); other monitors can have negative coordinates
- Each monitor may have different DPI scaling, affecting โlogicalโ vs โphysicalโ pixels
3. Window States
- Normal: Has position and size you can save
- Maximized: Fills a monitorโposition is stored internally, not visible
- Minimized: Hiddenโhas a โrestore positionโ Windows remembers
4. Process vs. Window Relationship
- One process can create multiple windows (Chrome tabs, dialog boxes)
- Window identification requires combining process name, class, and title patterns
Questions to Guide Your Design
Answer these before writing code:
-
Window Identity: How will you identify โVS Codeโ after a restart? What combination of attributes is unique enough?
-
Monitor Portability: If the user saves a layout at work (3 monitors) and restores at home (1 monitor), what happens to windows from missing monitors?
-
Application Launch: What if a layout references an app that isnโt running? Should you launch it? Skip it? Warn?
-
Timing: Some apps take seconds to initialize their window. How do you handle this during restore?
-
Conflicts: What if two saved windows match the same current window? Which takes priority?
-
User Experience: How do you provide feedback during a restore operation that might take several seconds?
Thinking Exercise
Before reading further, spend 10 minutes designing your window matching algorithm on paper:
- Given a saved window (
processName: "Code.exe", class: "Chrome_WidgetWin_1", title: "index.js - MyProject") - And current windows: several Chrome windows, several VS Code windows with different files open
- How would you score each current window against the saved one?
- What happens when the user has two VS Code windows but the layout only saved one?
Write pseudocode for a MatchWindow(saved, currentWindows) function that returns the best match or null.
1. Learning Objectives
By the end of this project, you will be able to:
Core Skills
- Enumerate Windows windows - Use Win32 APIs (via AHK) to list all visible top-level windows and filter system windows
- Understand window handles (HWND) - Work with Windowsโ window identification system, understanding handle lifecycle and validity
- Manipulate window positions - Move, resize, and control window states programmatically using WinMove, WinMaximize, WinRestore
- Handle multi-monitor setups - Navigate virtual desktop coordinates, detect monitors, calculate relative positions
- Identify windows reliably - Match windows across process restarts using process name, class, and title pattern combinations
- Persist and restore state - Save complex hierarchical state to disk (INI/JSON) and recreate it accurately
- Work with DPI scaling - Handle high-DPI monitors correctly, understanding logical vs physical coordinates
Applied Knowledge
- Design scoring algorithms - Create heuristics that rank potential matches by multiple weighted criteria
- Handle state machines - Windows can be normal/minimized/maximized; transitions require specific sequences
- Implement graceful degradation - When monitors change or apps arenโt running, degrade smoothly without errors
- Build configuration systems - Store and retrieve complex settings across multiple layouts
Transferable Concepts
- Fuzzy Matching - The window matching algorithm applies to any โfind best matchโ problem
- Coordinate Transformations - Converting between coordinate systems is fundamental in graphics and games
- State Serialization - Saving and restoring complex object graphs is universal in software
2. Real World Outcome
When this project is complete, you will have a genuinely useful tool that transforms how you work with multiple applications. Here is exactly what you will see and experience:
2.1 Summary of Capabilities
- Remembers your layouts - Define multiple arrangements (work, gaming, video editing)
- Save with Ctrl+Win+S - Current window positions are recorded instantly
- Restore with Ctrl+Win+1/2/3 - Windows snap back into place in under 2 seconds
- Multi-monitor support - Layouts work across 2+ monitors with different resolutions
- Smart matching - Finds windows even after app restart using fuzzy matching
2.2 System Tray Presence
When your script starts, it lives quietly in the system tray:
Windows Taskbar
+-----------------------------------------------------------------------+
| |
| [Start] [Search] [Edge] [Explorer] [^] [Wifi] [Vol] [Clock]
| |
+-----------------------------------------------------------------------+
|
+----------+----------+
| Notification Area |
+---------------------+
| [Wifi] [Vol] [BT] |
| [OneDrive] [LAYOUT] | <-- Your icon!
+---------------------+
Right-clicking the tray icon shows:
+------------------------+
| Window Layout Manager |
|------------------------|
| > Save Current Layout |
| > Restore Layout |
| +------------------+ |
| | Work | |
| | Gaming | |
| | Video Editing | |
| +------------------+ |
|------------------------|
| Settings... |
| Exit |
+------------------------+
2.3 Saving a Layout - Step by Step
Scenario: Youโve arranged your windows perfectly for coding work.
Step 1: Arrange your windows
+====================+====================+====================+
| | | |
| MONITOR 1 | MONITOR 2 | MONITOR 3 |
| (Primary) | (Left) | (Right) |
| 2560x1440 | 1920x1080 | 1920x1080 |
| | | |
+====================+====================+====================+
Monitor 1 (Primary): Monitor 2 (Left): Monitor 3 (Right):
+----------+----------+ +--------------------+ +--------------------+
| VS Code | Terminal | | | | |
| main.py | PowerShell | Chrome | | Slack |
| 50% | 50% | | (Documentation) | | (Team Chat) |
| | | | | | |
+----------+----------+ +--------------------+ +--------------------+
Step 2: Press Ctrl+Win+S
A dialog appears:
+------------------------------------------+
| Save Window Layout [X] |
+------------------------------------------+
| |
| Layout Name: [Coding Work ] |
| |
| Detected 4 windows: |
| +------------------------------------+ |
| | [x] VS Code - main.py | |
| | [x] Windows Terminal | |
| | [x] Chrome - React Docs | |
| | [x] Slack | |
| +------------------------------------+ |
| |
| Monitor Configuration: |
| - Monitor 1: 2560x1440 @ (0, 0) |
| - Monitor 2: 1920x1080 @ (-1920, 0) |
| - Monitor 3: 1920x1080 @ (2560, 0) |
| |
| [ Cancel ] [ Save ] |
+------------------------------------------+
Step 3: Click Save
You see a tooltip confirmation:
+-------------------------------+
| Layout "Coding Work" saved |
| 4 windows on 3 monitors |
+-------------------------------+
(disappears after 2 seconds)
Console output (if debug mode enabled):
[14:30:15] === SAVE LAYOUT: Coding Work ===
[14:30:15] Detected 3 monitors:
[14:30:15] Monitor 1: 2560x1440, DPI=100%, origin=(0,0)
[14:30:15] Monitor 2: 1920x1080, DPI=100%, origin=(-1920,0)
[14:30:15] Monitor 3: 1920x1080, DPI=125%, origin=(2560,0)
[14:30:15] Enumerating windows...
[14:30:15] Found: Code.exe - "main.py - MyProject - Visual Studio Code"
[14:30:15] Class=Chrome_WidgetWin_1, Pos=(0,0,1280,1440), State=Normal, Monitor=1
[14:30:15] Found: WindowsTerminal.exe - "PowerShell"
[14:30:15] Class=CASCADIA_HOSTING_WINDOW_CLASS, Pos=(1280,0,1280,1440), State=Normal, Monitor=1
[14:30:15] Found: chrome.exe - "React Documentation - Chrome"
[14:30:15] Class=Chrome_WidgetWin_1, Pos=(-1920,0,1920,1080), State=Maximized, Monitor=2
[14:30:15] Found: slack.exe - "Slack | MyTeam"
[14:30:15] Class=Chrome_WidgetWin_1, Pos=(2560,0,1920,1080), State=Normal, Monitor=3
[14:30:15] Skipped 3 system windows (Explorer, taskbar)
[14:30:15] Layout saved to: C:\Users\{you}\AppData\Roaming\LayoutManager\layouts.ini
[14:30:15] === SAVE COMPLETE ===
2.4 Restoring a Layout - Step by Step
Scenario: Youโve been gaming, windows are scattered, you need to get back to work.
Current mess:
+--------------------+--------------------+
| Discord | |
| (random pos) | Steam |
| | (full screen) |
| Chrome | |
| (somewhere) | VS Code |
| | (minimized) |
+--------------------+--------------------+
Step 1: Press Ctrl+Win+1 (for โCoding Workโ layout)
You see a progress tooltip:
+------------------------------------+
| Restoring "Coding Work"... |
| [=====> ] 2/4 |
+------------------------------------+
Step 2: Windows animate to their positions (2 seconds total)
Time T=0ms: VS Code restored from minimized state
Time T=100ms: VS Code moves to left half of Monitor 1
Time T=200ms: Terminal moves to right half of Monitor 1
Time T=400ms: Chrome moves to Monitor 2, maximizes
Time T=600ms: Slack moves to Monitor 3
Time T=700ms: Complete!
Step 3: Final result
+------------------------------------+
| Restored 4 of 4 windows |
| Layout: Coding Work |
+------------------------------------+
(disappears after 2 seconds)
Final state matches the saved layout perfectly:
Monitor 1 (Primary): Monitor 2 (Left): Monitor 3 (Right):
+----------+----------+ +--------------------+ +--------------------+
| VS Code | Terminal | | | | |
| main.py | PowerShell | Chrome | | Slack |
| 50% | 50% | | (Documentation) | | (Team Chat) |
| | | | | | |
+----------+----------+ +--------------------+ +--------------------+
2.5 Handling Edge Cases
Scenario: App not running
[14:35:00] Restoring layout "Coding Work"...
[14:35:00] VS Code: FOUND, moving to position
[14:35:00] Terminal: FOUND, moving to position
[14:35:00] Chrome: NOT FOUND (app not running)
[14:35:00] Slack: FOUND, moving to position
[14:35:01] Restored 3 of 4 windows (1 not found)
Tooltip shows:
+------------------------------------+
| Restored 3 of 4 windows |
| Missing: Chrome |
+------------------------------------+
Scenario: Different monitor setup
Saved with 3 monitors, now only 2 connected:
[14:40:00] WARNING: Monitor configuration changed
[14:40:00] Saved: 3 monitors
[14:40:00] Current: 2 monitors
[14:40:00] Adjusting positions for windows on missing Monitor 3...
[14:40:00] Slack: Moved to Monitor 1 (overflow)
Tooltip shows:
+------------------------------------+
| Restored 4 of 4 windows |
| 1 window repositioned (monitor |
| not available) |
+------------------------------------+
Scenario: Window has multiple instances
Saved: 1 VS Code window with "main.py"
Current: 3 VS Code windows open (main.py, test.py, README.md)
[14:45:00] Matching windows for VS Code...
[14:45:00] Candidate 1: "main.py - Project" - Score: 180 (process+class+title match)
[14:45:00] Candidate 2: "test.py - Project" - Score: 150 (process+class match only)
[14:45:00] Candidate 3: "README.md - Project" - Score: 150 (process+class match only)
[14:45:00] Selected Candidate 1 (highest score)
2.6 Debug Mode Output
When troubleshooting, enable verbose logging to see exactly whatโs happening:
=== WINDOW LAYOUT MANAGER DEBUG LOG ===
Timestamp: 2025-12-27 14:30:15
Version: 1.0.0
OS: Windows 11 Build 22631
DPI Awareness: Per-Monitor V2
--- MONITOR DETECTION ---
SysGet(MonitorCount) = 3
Monitor 1 (PRIMARY):
Device: \\.\DISPLAY1
Bounds: (0, 0) to (2560, 1440)
Work Area: (0, 0) to (2560, 1400) [Taskbar: 40px bottom]
DPI: 96 (100% scaling)
Monitor 2:
Device: \\.\DISPLAY2
Bounds: (-1920, 0) to (0, 1080)
Work Area: (-1920, 0) to (0, 1080)
DPI: 96 (100% scaling)
Monitor 3:
Device: \\.\DISPLAY3
Bounds: (2560, 0) to (4480, 1080)
Work Area: (2560, 0) to (4480, 1080)
DPI: 120 (125% scaling) <-- High DPI!
--- WINDOW ENUMERATION ---
Total window handles: 127
After filtering (visible, has title): 15
After filtering (not system): 4
Window 1:
HWND: 0x00A30B2C
Title: "main.py - MyProject - Visual Studio Code"
Class: Chrome_WidgetWin_1
Process: Code.exe (PID: 14532)
Position: x=0, y=0, w=1280, h=1440
State: SW_NORMAL
Monitor: 1
Relative Position: x=0, y=0 (0% from left, 0% from top)
Window 2:
HWND: 0x00B40C3D
Title: "PowerShell"
Class: CASCADIA_HOSTING_WINDOW_CLASS
Process: WindowsTerminal.exe (PID: 8876)
Position: x=1280, y=0, w=1280, h=1440
State: SW_NORMAL
Monitor: 1
Relative Position: x=1280, y=0 (50% from left, 0% from top)
[... continues for all windows ...]
2.7 The Layouts File
Your layouts are stored in a human-readable INI file:
Location: C:\Users\{username}\AppData\Roaming\WindowLayoutManager\layouts.ini
Contents:
; Window Layout Manager - Layout Configuration
; Generated: 2025-12-27 14:30:15
; Do not edit manually unless you know what you're doing!
[CodingWork]
SavedAt=20251227143015
MonitorCount=3
WindowCount=4
Monitor1_Device=\\.\DISPLAY1
Monitor1_Left=0
Monitor1_Top=0
Monitor1_Right=2560
Monitor1_Bottom=1440
Monitor1_DPI=96
Monitor2_Device=\\.\DISPLAY2
Monitor2_Left=-1920
Monitor2_Top=0
Monitor2_Right=0
Monitor2_Bottom=1080
Monitor2_DPI=96
Monitor3_Device=\\.\DISPLAY3
Monitor3_Left=2560
Monitor3_Top=0
Monitor3_Right=4480
Monitor3_Bottom=1080
Monitor3_DPI=120
Window1_ProcessName=Code.exe
Window1_ClassName=Chrome_WidgetWin_1
Window1_TitlePattern=.*Visual Studio Code.*
Window1_MonitorIndex=1
Window1_RelativeX=0
Window1_RelativeY=0
Window1_Width=1280
Window1_Height=1440
Window1_State=normal
Window2_ProcessName=WindowsTerminal.exe
Window2_ClassName=CASCADIA_HOSTING_WINDOW_CLASS
Window2_TitlePattern=.*PowerShell.*
Window2_MonitorIndex=1
Window2_RelativeX=1280
Window2_RelativeY=0
Window2_Width=1280
Window2_Height=1440
Window2_State=normal
; ... more windows ...
[Gaming]
SavedAt=20251226201045
MonitorCount=2
WindowCount=3
; ... gaming layout config ...
[VideoEditing]
SavedAt=20251225143022
MonitorCount=3
WindowCount=7
; ... video editing layout config ...
3. Deep Theoretical Foundation
3.1 The Windows Window Hierarchy
Windows organizes its graphical interface as a hierarchy of windows. Every visible element is a window or a child of a window. Understanding this hierarchy is fundamental to window management.
The hierarchy:
KERNEL SPACE
+------------------------------------------+
| WINDOW MANAGER |
| (Maintains all window data structures) |
+---------------------+--------------------+
|
- - - - - - - - - - - | - - - - - - - - - - - -
|
USER SPACE
v
+------------------------------------------+
| DESKTOP WINDOW (root) |
| HWND: GetDesktopWindow() |
| Covers entire virtual screen |
+---------------------+--------------------+
|
+---------------+----------+-----------+---------------+
| | | |
v v v v
+-----------+ +-----------+ +-----------+ +-----------+
| Taskbar | | Start Menu| | Notepad | | Chrome |
| explorer | | (hidden) | | HWND: | | HWND: |
| .exe | | | | 0x001A... | | 0x002B... |
+-----------+ +-----------+ +-----+-----+ +-----+-----+
| |
+-----------+------+ +----+-----+
| | | | |
v v v v v
+--------+ +--------+ +--------+ +--------+
| Menu | | Edit | | Status | | Tab |
| Bar | | Area | | Bar | | Strip |
| (child)| | (child)| | (child)| | (child)|
+--------+ +--------+ +--------+ +--------+
What is a window?
A window is a rectangular region managed by the Windows operating system. From Windows Security Internals by James Forshaw:
โA window object is a kernel object that represents a rectangular region on the screen. It has associated properties for drawing, input focus, and parent-child relationships.โ
Every window has:
- Handle (HWND) - A unique 32/64-bit identifier assigned by the kernel
- Class name - Describes the window type (e.g., โNotepadโ, โChrome_WidgetWin_1โ)
- Title - The text in the title bar (can change dynamically!)
- Position and size - Coordinates relative to virtual screen or parent
- State - Normal, minimized, maximized, hidden
- Owner process - The executable that created it
HWND: The Window Handle
Every window has a unique HWND (Handle to WiNDow). From a systems programming perspective, this is an opaque handle that refers to a kernel window object.
Window "Untitled - Notepad"
โโโ HWND: 0x000A0B2C <-- This is what Win32 APIs use to identify the window
โโโ Class: "Notepad" <-- Registered window class (stable across instances)
โโโ PID: 1234 <-- Process ID (changes each launch)
โโโ TID: 5678 <-- Thread ID that owns the window's message queue
โโโ Position: x=100, y=200 <-- Top-left corner in virtual screen coordinates
โโโ Size: w=800, h=600 <-- Client area dimensions
โโโ State: Normal <-- WS_NORMAL, WS_MINIMIZED, or WS_MAXIMIZED
โโโ Styles: 0x14CF0000 <-- Combination of WS_* flags (visible, has title bar, etc.)
โโโ ExStyles: 0x00000100 <-- Extended styles (WS_EX_* flags)
Critical Insight: HWNDs are valid only while the window exists. This is the fundamental challenge of layout management:
Timeline:
T1: User launches Notepad -> HWND = 0x000A0B2C
T2: Layout saved with HWND 0x000A0B2C
T3: User closes Notepad -> HWND 0x000A0B2C is INVALID
T4: User launches Notepad -> HWND = 0x000C1D3E (NEW!)
T5: Layout restore fails if using saved HWND
Solution: Don't save HWNDs. Save identification patterns:
- ProcessName: "notepad.exe"
- ClassName: "Notepad"
- TitlePattern: ".*Notepad"
3.2 Process vs. Window Identification
A single process can create multiple windows. This is one of the key challenges in window management:
Process: chrome.exe (PID: 5678)
โ
โโโ Window: "Google - Gmail - Chrome"
โ โโโ HWND: 0x00A10000
โ โโโ Class: "Chrome_WidgetWin_1"
โ โโโ Visible: Yes (main browser window)
โ
โโโ Window: "DevTools - https://example.com"
โ โโโ HWND: 0x00A20000
โ โโโ Class: "Chrome_WidgetWin_1" <-- SAME CLASS as main window!
โ โโโ Visible: Yes (separate developer tools window)
โ
โโโ Window: "" (empty title)
โ โโโ HWND: 0x00A30000
โ โโโ Class: "Chrome_WidgetWin_0"
โ โโโ Visible: No (hidden helper/renderer window)
โ
โโโ Window: "Downloads"
โโโ HWND: 0x00A40000
โโโ Class: "Chrome_WidgetWin_1" <-- SAME CLASS again!
โโโ Visible: Yes
The challenge: How do you identify โthe Chrome window with Gmail openโ reliably?
Identification Strategies - Comparison:
| Method | Stability | Uniqueness | Complexity | Use Case |
|---|---|---|---|---|
| HWND | Session only | Perfect | Low | Real-time tracking (no persistence) |
| Window Title | Changes often | Usually unique | Low | Single-instance apps with stable titles |
| Window Class | Very stable | Not unique | Low | Filtering by app type |
| Process Name | Very stable | Not unique | Low | First-level filtering |
| PID | Session only | Not unique | Low | Supplementary check |
| Combination | Stable | Unique enough | Medium | Production layout managers |
The Scoring Algorithm (Best Practice):
MatchScore(savedWindow, currentWindow):
score = 0
// REQUIRED: Process name must match exactly
if currentWindow.processName != savedWindow.processName:
return 0 // Immediate disqualification
score += 100 // Base score for process match
// HIGHLY VALUABLE: Window class match
if currentWindow.className == savedWindow.className:
score += 50
// VALUABLE: Title pattern match
if RegexMatch(currentWindow.title, savedWindow.titlePattern):
score += 30
// SUPPLEMENTARY: Position similarity (for disambiguation)
if PositionSimilar(currentWindow.position, savedWindow.position):
score += 10
return score
// Usage:
candidates = currentWindows.filter(w => MatchScore(saved, w) >= 100)
bestMatch = candidates.maxBy(w => MatchScore(saved, w))
Book Reference: For deep understanding of Windows window management, see Windows Security Internals by James Forshaw, Chapter 8 โUser Interface Securityโ - particularly the sections on window objects and handle management.
3.3 Virtual Desktop Coordinates
Windows treats all monitors as one large virtual desktop. The primary monitorโs top-left corner is defined as (0,0), and all other coordinates are relative to this origin. This is conceptually similar to the Cartesian coordinate system you learned in mathematics.
Single Monitor - The Simple Case:
Virtual Desktop Coordinate System (single 1920x1080 monitor):
X-axis โ
0 960 1920
โโโโโโโโโโโโฌโโโโโโโโโโโ 0
โ โ โ โ
โ โ โ
โ (0,0) โ โ Y-axis
โ โ โ โ
โ โ โ 540
โ โ โ
โโโโโโโโโโโโผโโโโโโโโโโโค
โ โ โ
โ โ โ
โ โ โ
โ โ โ 1080
โโโโโโโโโโโโดโโโโโโโโโโโ
Window at (100, 200, 400, 300):
- Top-left corner: (100, 200)
- Width: 400 pixels
- Height: 300 pixels
- Bottom-right: (500, 500)
Two Monitors (Side by Side) - Common Setup:
Virtual Desktop Coordinate System (two 1920x1080 monitors):
Monitor 1 (Primary) Monitor 2 (Extended)
โโโโโโโโโโ 1920px โโโโโโโโโโโ โโโโโโโโโโ 1920px โโโโโโโโโโโ
0 1920 3840
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ 0
โ (0,0) is here โ โ
โ โ โ (1920, 0) is here โ โ
โ โ โ โ
โ โ โ Y
โ โ โ
โ PRIMARY โ EXTENDED โ
โ โ โ 1080
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
A window on Monitor 2: Position might be (2400, 300, 800, 600)
- x=2400 means: 1920 (Monitor 1 width) + 480 into Monitor 2
Complex Setup - Monitors Left/Above Primary:
Physical arrangement: Virtual coordinate system:
+--------+ (-1920,0) (0,0) (1920,0)
| Mon 2 | +--------+ โ โ โ
| (left) | | Mon 1 | โโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโ
+--------+ |(primary)| โ Monitor 2 โ Monitor 1 โ Monitor 3 โ
+--------+ โ x: -1920 โ x: 0 โ x: 1920 โ
+--------+ โ to 0 โ to 1920 โ to 3840 โ
| Mon 3 | โ โ (PRIMARY) โ โ
| (right)| โโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโ
+--------+ 1080 1080 1080
KEY INSIGHT: Coordinates can be NEGATIVE!
A window on Monitor 2 might have position: (-1500, 200, 800, 600)
- Negative X means "to the left of primary monitor"
Coordinate Transformation for Portable Layouts:
To make layouts work across different monitor setups, convert absolute coordinates to relative:
Absolute Position (tied to specific monitor config):
Window at (2400, 300) on a 3-monitor setup
Relative Position (portable):
Window on Monitor 2, at offset (480, 300) from monitor's top-left
Conversion formulas:
relativeX = absoluteX - monitor.left
relativeY = absoluteY - monitor.top
absoluteX = monitor.left + relativeX
absoluteY = monitor.top + relativeY
Example:
Monitor 2: bounds = (1920, 0, 3840, 1080)
Window absolute: (2400, 300)
Window relative: (2400 - 1920, 300 - 0) = (480, 300)
Later, Monitor 2 moves to position (-1920, 0):
Window absolute: (-1920 + 480, 0 + 300) = (-1440, 300)
Book Reference: See Computer Systems: A Programmerโs Perspective by Bryant and OโHallaron, Chapter 10 for understanding coordinate systems and how graphics hardware maps logical to physical pixels.
DPI Scaling: The Modern Complication
High-DPI monitors (4K, Retina) use scaling to keep text readable. A 150% scaled monitor reports coordinates differently.
The problem:
Monitor: 3840x2160 physical pixels at 150% scaling
Reported resolution: 2560x1440 "logical" pixels
You think a window is at (100, 100)
But it's actually at (150, 150) in physical pixels
DPI-aware vs unaware:
- DPI-aware apps: Report real coordinates, handle scaling themselves
- DPI-unaware apps: Windows lies to them about coordinates
Your layout manager needs to:
- Query the DPI of each monitor
- Convert coordinates when saving/restoring
- Handle mixed-DPI setups (100% on one monitor, 150% on another)
Window State: Normal, Minimized, Maximized
Windows can be in different states:
| State | Description | Position Meaningful? |
|---|---|---|
| Normal | Windowed, resizable | Yes |
| Minimized | In taskbar | No (uses restore position) |
| Maximized | Full screen | No (fills monitor) |
The restore position:
When you maximize a window, Windows remembers where it was before. When you restore it, it returns to that position.
Window at (100, 100, 800, 600) -> Maximize -> fullscreen
-> Restore -> back to (100, 100, 800, 600)
Your layout manager should save:
- Current state (normal/min/max)
- โNormalโ position (for when itโs restored)
Project Specification
Functional Requirements
| ID | Requirement | Priority |
|---|---|---|
| F1 | Enumerate all visible top-level windows | Must Have |
| F2 | Get window position, size, and state | Must Have |
| F3 | Save current layout to file | Must Have |
| F4 | Restore layout from file | Must Have |
| F5 | Support multiple named layouts | Must Have |
| F6 | Match windows after app restart | Must Have |
| F7 | Support multi-monitor setups | Must Have |
| F8 | Handle minimized/maximized windows | Should Have |
| F9 | Support DPI-scaled monitors | Should Have |
| F10 | Gracefully handle missing windows | Should Have |
| F11 | Quick-save current layout (hotkey) | Should Have |
| F12 | Layout selection GUI | Nice to Have |
| F13 | Auto-restore on login | Nice to Have |
| F14 | Per-application layout rules | Nice to Have |
Non-Functional Requirements
| ID | Requirement |
|---|---|
| NF1 | Restore completes in < 2 seconds for 20 windows |
| NF2 | Save completes in < 500ms |
| NF3 | Works without administrator privileges |
| NF4 | Handles monitor disconnect gracefully |
Solution Architecture
Component Design
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Window Layout Manager โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ
โ โ Window Enumeratorโ โ Hotkey Handler โ โ
โ โ - ListWindows() โ โ - Save (^#s) โ โ
โ โ - GetWindowInfo()โ โ - Restore (^#1)โ โ
โ โโโโโโโโโโฌโโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโโ โ
โ โ โ โ
โ v v โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Layout Manager โ โ
โ โ - layouts: Map<name, Layout> โ โ
โ โ - SaveLayout(name) โ โ
โ โ - RestoreLayout(name) โ โ
โ โ - MatchWindow(saved, current) โ โ
โ โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโโโโโโดโโโโโโโโโโโโ โ
โ v v โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โ
โ โ Persistenceโ โ Monitor โ โ
โ โ Layer โ โ Manager โ โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Data Structures
; Information about a single window
WindowInfo := {
; Identification (for matching after restart)
processName: "Code.exe",
className: "Chrome_WidgetWin_1",
titlePattern: ".*Visual Studio Code.*",
; Position and size
x: 0,
y: 0,
width: 960,
height: 1080,
; State
state: "normal", ; "normal", "minimized", "maximized"
; Monitor info (for multi-monitor)
monitorIndex: 1,
relativeX: 0, ; Position relative to monitor
relativeY: 0
}
; A complete layout
Layout := {
name: "Work",
createdAt: "20251226143000",
monitorConfig: [...], ; Monitor arrangement when saved
windows: [WindowInfo, WindowInfo, ...]
}
; All layouts
Layouts := Map(
"Work", Layout1,
"Gaming", Layout2,
"Presentation", Layout3
)
Save Flow
[User presses Ctrl+Win+S]
|
v
GetCurrentMonitorConfig()
|
+-- For each monitor:
| - Get bounds (x, y, w, h)
| - Get DPI scaling
| - Get monitor ID
|
v
EnumerateWindows()
|
+-- For each visible window:
| |
| +-- Get HWND
| +-- Get process name (WinGetProcessName)
| +-- Get window class (WinGetClass)
| +-- Get title (WinGetTitle)
| +-- Get position (WinGetPos)
| +-- Get state (min/max/normal)
| +-- Calculate which monitor
| +-- Calculate relative position
| |
| +-- Create WindowInfo object
|
v
PromptForLayoutName()
|
v
CreateLayout(name, monitorConfig, windows)
|
v
SaveToFile(layout)
Restore Flow
[User presses Ctrl+Win+1]
|
v
LoadLayout("Layout1")
|
v
GetCurrentMonitorConfig()
|
+-- Compare to saved config
| |
| +-- Same monitors? Use exact coords
| +-- Different? Adjust positions
|
v
EnumerateCurrentWindows()
|
v
For each saved window:
|
+-- MatchWindow(saved, currentWindows)
| |
| +-- Match by process + class + title
| +-- If not found: skip (or warn)
|
+-- If matched:
| |
| +-- Calculate target position
| | (adjust for monitor changes)
| |
| +-- Restore state first (if minimized)
| |
| +-- WinMove(hwnd, x, y, w, h)
| |
| +-- Restore max/min state if needed
|
v
Report: "Restored X of Y windows"
Window Matching Algorithm
MatchWindow(saved, currentWindows):
candidates := []
for window in currentWindows:
score := 0
// Process name must match
if window.processName != saved.processName:
continue
score += 100 // Base score for process match
// Window class should match
if window.className == saved.className:
score += 50
// Title pattern matching
if RegExMatch(window.title, saved.titlePattern):
score += 30
candidates.Push({window: window, score: score})
// Return best match if score high enough
Sort(candidates, by score descending)
if candidates.Length > 0 && candidates[1].score >= 100:
return candidates[1].window
return null // No match found
Implementation Guide
Phase 1: Window Enumeration (2-3 hours)
Goal: List all visible windows with their properties.
Steps:
- Use
WinGetto get list of all windows - Filter to visible, top-level windows
- For each window, extract: title, class, process, position, size
- Display in console or message box
Verification: Running script shows all your open applications with correct info.
Phase 2: Single Layout Save/Restore (3-4 hours)
Goal: Save current positions and restore them.
Steps:
- Create
SaveCurrentLayout()function - Enumerate windows, build array of WindowInfo objects
- Save to JSON or INI file
- Create
RestoreLayout()function - Load file, iterate windows, call
WinMovefor each
Verification: Save layout, shuffle windows, restore, windows return to positions.
Phase 3: Window Matching (2-3 hours)
Goal: Find windows after apps restart.
Steps:
- Close and reopen an application
- Try to restore layout (will fail - HWND changed)
- Implement
MatchWindow()function - Use process name + class + title pattern
- Handle โno match foundโ gracefully
Verification: Close VS Code, reopen it, restore layout, VS Code moves correctly.
Phase 4: Multi-Monitor Support (3-4 hours)
Goal: Handle windows on different monitors.
Steps:
- Create
GetMonitorConfig()function - Detect all monitors and their bounds
- For each window, determine which monitor itโs on
- Save relative position (to monitor, not screen)
- On restore, recalculate absolute position
Verification: Layout works when restored on different monitor arrangement.
Phase 5: Multiple Layouts (2-3 hours)
Goal: Support named layouts with hotkey switching.
Steps:
- Modify save to include layout name
- Store multiple layouts in single file
- Create layout selection menu/GUI
- Assign hotkeys: Ctrl+Win+1, Ctrl+Win+2, etc.
Verification: Switch between โWorkโ and โEntertainmentโ layouts with hotkeys.
Phase 6: Edge Cases and Polish (2-4 hours)
Goal: Handle real-world complications.
Steps:
- Handle minimized/maximized windows
- Handle DPI scaling
- Skip system windows (taskbar, etc.)
- Add feedback (tooltip showing restore progress)
- Handle missing windows gracefully (app not open)
Code Hints
Hint 1: Window Enumeration
#Requires AutoHotkey v2.0
GetAllWindows() {
windows := []
; Get list of all window IDs
ids := WinGetList()
for hwnd in ids {
try {
; Skip windows without titles
title := WinGetTitle("ahk_id " . hwnd)
if (title = "")
continue
; Skip hidden windows
if (!WinExist("ahk_id " . hwnd . " ahk_class"))
continue
; Get window info
WinGetPos(&x, &y, &w, &h, "ahk_id " . hwnd)
processName := WinGetProcessName("ahk_id " . hwnd)
className := WinGetClass("ahk_id " . hwnd)
; Skip system windows
if (processName = "explorer.exe" && className = "Shell_TrayWnd")
continue
if (className = "Progman" || className = "WorkerW")
continue
windows.Push({
hwnd: hwnd,
title: title,
processName: processName,
className: className,
x: x,
y: y,
width: w,
height: h
})
}
}
return windows
}
Hint 2: Get Window State (Min/Max/Normal)
GetWindowState(hwnd) {
minMax := WinGetMinMax("ahk_id " . hwnd)
switch minMax {
case -1: return "minimized"
case 1: return "maximized"
default: return "normal"
}
}
; When restoring, handle state:
RestoreWindowState(hwnd, savedState, x, y, w, h) {
currentState := GetWindowState(hwnd)
; If minimized, restore first
if (currentState = "minimized")
WinRestore("ahk_id " . hwnd)
; If currently maximized, restore to move it
if (currentState = "maximized")
WinRestore("ahk_id " . hwnd)
; Move to position (only works in normal state)
if (savedState = "normal") {
WinMove(x, y, w, h, "ahk_id " . hwnd)
}
; Apply final state
if (savedState = "maximized")
WinMaximize("ahk_id " . hwnd)
else if (savedState = "minimized")
WinMinimize("ahk_id " . hwnd)
}
Hint 3: Monitor Detection
GetMonitorInfo() {
monitors := []
count := MonitorGetCount()
Loop count {
MonitorGetWorkArea(A_Index, &left, &top, &right, &bottom)
MonitorGetName(A_Index, &name)
monitors.Push({
index: A_Index,
name: name,
left: left,
top: top,
right: right,
bottom: bottom,
width: right - left,
height: bottom - top
})
}
return monitors
}
GetWindowMonitor(x, y, monitors) {
; Find which monitor contains this point
for monitor in monitors {
if (x >= monitor.left && x < monitor.right
&& y >= monitor.top && y < monitor.bottom) {
return monitor
}
}
return monitors[1] ; Default to first monitor
}
; Convert absolute to relative (to monitor)
ToRelativePosition(x, y, monitor) {
return {
relX: x - monitor.left,
relY: y - monitor.top,
monitorIndex: monitor.index
}
}
; Convert relative back to absolute
ToAbsolutePosition(relX, relY, monitorIndex, monitors) {
monitor := monitors[monitorIndex]
return {
x: monitor.left + relX,
y: monitor.top + relY
}
}
Hint 4: Saving Layout to File
SaveLayout(name) {
; Get current state
monitors := GetMonitorInfo()
windows := GetAllWindows()
layout := {
name: name,
savedAt: A_Now,
monitors: monitors,
windows: []
}
for win in windows {
monitor := GetWindowMonitor(win.x, win.y, monitors)
relative := ToRelativePosition(win.x, win.y, monitor)
layout.windows.Push({
processName: win.processName,
className: win.className,
titlePattern: EscapeRegex(win.title),
relX: relative.relX,
relY: relative.relY,
width: win.width,
height: win.height,
monitorIndex: relative.monitorIndex,
state: GetWindowState(win.hwnd)
})
}
; Save to file (simple INI format)
SaveLayoutToFile(layout)
}
SaveLayoutToFile(layout) {
filePath := A_AppData . "\WindowLayoutManager\layouts.ini"
DirCreate(A_AppData . "\WindowLayoutManager")
section := layout.name
IniWrite(layout.savedAt, filePath, section, "SavedAt")
IniWrite(layout.windows.Length, filePath, section, "WindowCount")
for idx, win in layout.windows {
prefix := "Window" . idx . "_"
IniWrite(win.processName, filePath, section, prefix . "ProcessName")
IniWrite(win.className, filePath, section, prefix . "ClassName")
IniWrite(win.titlePattern, filePath, section, prefix . "TitlePattern")
IniWrite(win.relX, filePath, section, prefix . "RelX")
IniWrite(win.relY, filePath, section, prefix . "RelY")
IniWrite(win.width, filePath, section, prefix . "Width")
IniWrite(win.height, filePath, section, prefix . "Height")
IniWrite(win.monitorIndex, filePath, section, prefix . "MonitorIndex")
IniWrite(win.state, filePath, section, prefix . "State")
}
}
Hint 5: Window Matching
MatchSavedToCurrentWindows(savedWindows, currentWindows) {
matches := []
for saved in savedWindows {
bestMatch := FindBestMatch(saved, currentWindows)
matches.Push({
saved: saved,
current: bestMatch ; null if not found
})
}
return matches
}
FindBestMatch(saved, currentWindows) {
candidates := []
for current in currentWindows {
; Process name must match
if (current.processName != saved.processName)
continue
score := 100 ; Base score
; Class name bonus
if (current.className = saved.className)
score += 50
; Title match bonus
if (RegExMatch(current.title, saved.titlePattern))
score += 30
candidates.Push({window: current, score: score})
}
; Sort by score
; ... sorting code ...
if (candidates.Length > 0)
return candidates[1].window
return "" ; No match
}
Hint 6: Restore Layout
RestoreLayout(layoutName) {
layout := LoadLayoutFromFile(layoutName)
if (!layout) {
MsgBox("Layout '" . layoutName . "' not found")
return
}
; Get current state
currentMonitors := GetMonitorInfo()
currentWindows := GetAllWindows()
; Match saved windows to current windows
matches := MatchSavedToCurrentWindows(layout.windows, currentWindows)
restored := 0
failed := 0
for match in matches {
if (!match.current) {
failed++
continue
}
; Calculate target position
; Adjust for monitor changes if needed
targetMonitor := currentMonitors[match.saved.monitorIndex]
if (!targetMonitor)
targetMonitor := currentMonitors[1]
pos := ToAbsolutePosition(
match.saved.relX,
match.saved.relY,
match.saved.monitorIndex,
currentMonitors
)
; Restore the window
RestoreWindowState(
match.current.hwnd,
match.saved.state,
pos.x,
pos.y,
match.saved.width,
match.saved.height
)
restored++
}
ToolTip("Restored " . restored . " of " . (restored + failed) . " windows")
SetTimer(() => ToolTip(), -2000)
}
Testing Strategy
Unit Tests
| Test | Steps | Expected |
|---|---|---|
| Window enumeration | Run GetAllWindows() | Returns list with open apps |
| Position retrieval | Open Notepad, check position | Correct x, y, w, h |
| State detection | Minimize window, check state | Returns โminimizedโ |
| Monitor detection | Window on each monitor | Correct monitor index |
Integration Tests
| Test | Steps | Expected |
|---|---|---|
| Save/restore cycle | Save, move windows, restore | Windows return to saved positions |
| App restart | Save, close app, reopen, restore | Window still matched and moved |
| Multi-monitor | Window on monitor 2, save, restore | Returns to monitor 2 |
| Maximized window | Save maximized, restore | Window maximized on correct monitor |
Edge Cases
| Test | Steps | Expected |
|---|---|---|
| Missing app | Save layout with app, close app, restore | Graceful skip, report โ1 not foundโ |
| Changed monitors | Save on 2 monitors, unplug one, restore | Windows moved to remaining monitor |
| DPI change | Save at 100%, switch to 150%, restore | Positions approximately correct |
Common Pitfalls and Debugging
Problem: Windows donโt move to exact position
Causes: Window chrome, DPI scaling, window minimum size.
Solutions:
- Account for window border in position calculation
- Check DPI scaling factor
- Some windows have minimum sizes
Problem: Canโt find window after restart
Causes: Title changed, process name different, window class mismatch.
Solutions:
- Use title patterns (regex) instead of exact match
- Verify process name matches
- Add fuzzy matching on title
Problem: Windows on wrong monitor
Causes: Monitor order changed, different resolution.
Solutions:
- Save relative position to monitor
- Use monitor serial/ID if available
- Fall back to primary monitor
Problem: System windows captured
Causes: Taskbar, desktop, hidden helpers included.
Solutions:
- Filter by window class (exclude โShell_TrayWndโ, โProgmanโ)
- Skip windows with empty titles
- Skip windows with no visible area
Extensions and Challenges
Easy Extensions
- System tray icon - Quick access to save/restore
- Quick layout swap - Single hotkey to toggle between two layouts
- Layout preview - Show minimap before restoring
- Export/import - Share layouts between computers
Medium Extensions
- Auto-save - Save layout periodically
- Per-application rules - โChrome always on monitor 2โ
- Layout profiles by time - Morning vs. evening layouts
- Window groups - Save/restore subsets of windows
Advanced Extensions
- Virtual desktop support - Layouts per virtual desktop
- Window snapping zones - Define grid, snap windows to zones
- Monitor profiles - Different layouts for docked vs. undocked
- Cloud sync - Sync layouts across machines
The Interview Questions Theyโll Ask
Technical Questions
- โHow do you uniquely identify a window?โ
- HWND is unique but transient (changes on restart)
- Use combination: process name + window class + title pattern
- Implement a scoring algorithm for fuzzy matching
- Handle multiple windows from same process with position hints
- โWhatโs the difference between absolute and relative coordinates?โ
- Absolute: Position on virtual desktop (can be negative if monitor is left of primary)
- Relative: Position within a specific monitor (always positive)
- Relative is portable across monitor configuration changes
- Conversion:
absoluteX = monitor.left + relativeX
- โHow do you handle DPI scaling?โ
- Query DPI for each monitor using GetDpiForMonitor or MonitorGetDpi
- Windows reports logical coordinates at 96 DPI baseline
- Convert:
physicalPos = logicalPos * (DPI / 96) - Handle mixed-DPI setups where monitors have different scaling
- โWhat happens if a window resists being moved?โ
- Some apps lock their position (games, certain utilities)
- Some apps have minimum/maximum window sizes
- Verify position after move and report discrepancy
- Consider using SetWindowPos with FRAMECHANGED flag
- โHow do you handle virtual desktops in Windows 10/11?โ
- Use IVirtualDesktopManager COM interface
- Each desktop has its own set of visible windows
- Decision: restore to current desktop or move window to original desktop
- Windows API doesnโt make this easy - consider third-party libraries
System Design Questions
- โHow would you make this work with thousands of windows?โ
- Batch file I/O operations (donโt write after every window)
- Parallel window enumeration if possible
- Incremental saves instead of full rewrites
- Consider indexing by process name for faster lookup
- โHow would you sync layouts across multiple computers?โ
- Abstract monitor by resolution and DPI, not index
- Use percentage-based positioning within monitor
- Normalize application paths (different install locations)
- Consider cloud storage with conflict resolution
Hints in Layers
Use these hints progressively - only look at the next layer if youโre stuck.
Layer 1: Conceptual Hints (Start Here)
Window Enumeration: Start with WinGetList() which returns all window handles. Youโll need to filter this list.
Monitor Detection: MonitorGetCount() tells you how many monitors, then loop with MonitorGetWorkArea().
File Format: INI is simpler than JSON for AutoHotkey. Use IniWrite and IniRead.
Layer 2: Structural Hints (If Stuck on Basics)
Filtering System Windows: Check for class names like โShell_TrayWndโ, โProgmanโ, โWorkerWโ. Also skip windows with empty titles.
Relative Positioning: Store relativeX = windowX - monitorLeft and monitorIndex. On restore, calculate absoluteX = newMonitorLeft + relativeX.
Window Matching: Score based on: process name (required, +100), class match (+50), title regex match (+30). Take highest score.
Layer 3: Implementation Hints (Only If Really Stuck)
See the Code Hints section above for complete function implementations.
Books That Will Help
| Topic | Book | Chapter | Why This Helps |
|---|---|---|---|
| Windows Internals | Windows Security Internals by James Forshaw | Ch. 8: User Interface Security | Deep dive into HWND, window stations, and desktops. Explains the kernel objects behind windows and why some operations require special privileges. |
| Coordinate Systems | Computer Systems: A Programmerโs Perspective by Bryant & OโHallaron | Ch. 10: System-Level I/O | Foundational understanding of how systems represent and transform coordinates. Essential for understanding DPI scaling. |
| Process Management | The Linux Programming Interface by Michael Kerrisk | Ch. 6: Processes | Though Linux-focused, the concepts of PIDs, process identification, and handle lifecycle transfer directly to Windows development. |
| Pattern Matching | Algorithms, Fourth Edition by Sedgewick & Wayne | Ch. 5.3: Substring Search | The window matching algorithm is fundamentally a fuzzy search problem. Understanding string algorithms helps design better matchers. |
| State Management | Game Programming Patterns by Robert Nystrom | Ch. 7: State Pattern | Window state (normal/min/max) is a classic state machine. This chapter shows elegant ways to handle state transitions. |
| Configuration Design | The Pragmatic Programmer by Hunt & Thomas | Topic 32: Flexible Configuration | How to design configuration systems (your layouts file) that are robust to changes and easy to extend. |
| Graphics Fundamentals | Computer Graphics from Scratch by Gabriel Gambetta | Ch. 1-3: Coordinate Systems | Visual explanation of coordinate transformations that helps understand multi-monitor coordinate math. |
Reading Order Recommendation
- Start with: AutoHotkey v2 Documentation (essential for syntax)
- Then read: Game Programming Patterns - State chapter (understand state machines)
- While building: The Pragmatic Programmer - Configuration topics (good practices)
- For debugging: Windows Security Internals - Window sections (deep understanding)
- For optimization: Algorithms - Substring Search (better matching)
Self-Assessment Checklist
Use this checklist to verify your understanding and implementation quality.
Conceptual Understanding
- I can explain the Windows window hierarchy (Desktop -> Applications -> Controls)
- I understand why HWNDs are transient and how to identify windows without them
- I can describe the virtual desktop coordinate system including negative coordinates
- I understand DPI scaling and can explain logical vs physical coordinates
- I can explain the three window states and the correct transition sequences
Implementation Quality
- All visible windows are enumerated correctly (no system windows, no hidden windows)
- Window positions and sizes are saved accurately (verified by manual inspection)
- Windows can be restored to saved positions within 10 pixels of target
- Window matching works after app restart (process+class+title pattern)
- Multiple named layouts can be saved (at least 3)
- Multi-monitor setups are handled correctly (relative positioning)
- Minimized/maximized states are preserved (restore to correct state)
- Missing windows are handled gracefully (reported, not crashed)
- System windows (taskbar, etc.) are filtered out
- Hotkeys work reliably for save/restore
Code Quality
- Code is organized into logical functions (enumerate, save, restore, match)
- Variable names are descriptive (WindowInfo not wi, monitorIndex not mi)
- Comments explain WHY, not just WHAT
- Error handling is present (try/catch around file operations)
- Magic numbers are named (MAX_LAYOUTS := 9, MIN_MATCH_SCORE := 100)
Edge Cases Tested
- Works with single monitor (no multi-monitor code errors)
- Works with app not running (skips gracefully with message)
- Works with different DPI on each monitor (positions scale correctly)
- Works after monitor order changes (falls back to primary)
- Works with windows on negative coordinates (left-of-primary monitor)
Final Reflection Questions
Answer these without looking at the code:
- Why do we save relative coordinates instead of absolute?
- Whatโs the minimum score for a window match, and why did we choose that threshold?
- How do you handle a maximized window that needs to move to a different monitor?
- What happens if two current windows match the same saved window?
- Why might WinMove place a window 8 pixels off from where you specified?
- Whatโs the difference between MonitorGet and MonitorGetWorkArea?
Resources
Official Documentation
- WinGetPos - Get window position and size
- WinMove - Move and resize windows
- MonitorGet - Get monitor bounds
- WinGetMinMax - Get window state
Community Resources
- AutoHotkey Forums - Active community for troubleshooting
- AHK Reddit - Examples and discussions
Related Projects for Inspiration
- FancyZones (PowerToys) - Microsoftโs window manager with zones
- DisplayFusion - Commercial multi-monitor management
- AquaSnap - Window snapping utility with layout features
Begin with Phase 1 and build your understanding layer by layer. The window matching algorithm is the most intellectually interesting part - spend extra time getting it right. By the end, youโll have a tool you use every day and deep understanding of Windows window management.