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:

  1. Identity: Windows (HWND) are ephemeralโ€”how do you recognize โ€œVS Codeโ€ after a restart?
  2. Geometry: Coordinates span multiple monitors with different resolutions and scaling
  3. State: Windows can be minimized, maximized, or normalโ€”each requires different handling
  4. 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:

  1. Window Identity: How will you identify โ€œVS Codeโ€ after a restart? What combination of attributes is unique enough?

  2. 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?

  3. Application Launch: What if a layout references an app that isnโ€™t running? Should you launch it? Skip it? Warn?

  4. Timing: Some apps take seconds to initialize their window. How do you handle this during restore?

  5. Conflicts: What if two saved windows match the same current window? Which takes priority?

  6. 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:

  1. Given a saved window (processName: "Code.exe", class: "Chrome_WidgetWin_1", title: "index.js - MyProject")
  2. And current windows: several Chrome windows, several VS Code windows with different files open
  3. How would you score each current window against the saved one?
  4. 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

  1. Enumerate Windows windows - Use Win32 APIs (via AHK) to list all visible top-level windows and filter system windows
  2. Understand window handles (HWND) - Work with Windowsโ€™ window identification system, understanding handle lifecycle and validity
  3. Manipulate window positions - Move, resize, and control window states programmatically using WinMove, WinMaximize, WinRestore
  4. Handle multi-monitor setups - Navigate virtual desktop coordinates, detect monitors, calculate relative positions
  5. Identify windows reliably - Match windows across process restarts using process name, class, and title pattern combinations
  6. Persist and restore state - Save complex hierarchical state to disk (INI/JSON) and recreate it accurately
  7. Work with DPI scaling - Handle high-DPI monitors correctly, understanding logical vs physical coordinates

Applied Knowledge

  1. Design scoring algorithms - Create heuristics that rank potential matches by multiple weighted criteria
  2. Handle state machines - Windows can be normal/minimized/maximized; transitions require specific sequences
  3. Implement graceful degradation - When monitors change or apps arenโ€™t running, degrade smoothly without errors
  4. Build configuration systems - Store and retrieve complex settings across multiple layouts

Transferable Concepts

  1. Fuzzy Matching - The window matching algorithm applies to any โ€œfind best matchโ€ problem
  2. Coordinate Transformations - Converting between coordinate systems is fundamental in graphics and games
  3. 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

  1. Remembers your layouts - Define multiple arrangements (work, gaming, video editing)
  2. Save with Ctrl+Win+S - Current window positions are recorded instantly
  3. Restore with Ctrl+Win+1/2/3 - Windows snap back into place in under 2 seconds
  4. Multi-monitor support - Layouts work across 2+ monitors with different resolutions
  5. 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:

  1. Query the DPI of each monitor
  2. Convert coordinates when saving/restoring
  3. 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:

  1. Current state (normal/min/max)
  2. โ€œ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:

  1. Use WinGet to get list of all windows
  2. Filter to visible, top-level windows
  3. For each window, extract: title, class, process, position, size
  4. 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:

  1. Create SaveCurrentLayout() function
  2. Enumerate windows, build array of WindowInfo objects
  3. Save to JSON or INI file
  4. Create RestoreLayout() function
  5. Load file, iterate windows, call WinMove for 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:

  1. Close and reopen an application
  2. Try to restore layout (will fail - HWND changed)
  3. Implement MatchWindow() function
  4. Use process name + class + title pattern
  5. 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:

  1. Create GetMonitorConfig() function
  2. Detect all monitors and their bounds
  3. For each window, determine which monitor itโ€™s on
  4. Save relative position (to monitor, not screen)
  5. 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:

  1. Modify save to include layout name
  2. Store multiple layouts in single file
  3. Create layout selection menu/GUI
  4. 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:

  1. Handle minimized/maximized windows
  2. Handle DPI scaling
  3. Skip system windows (taskbar, etc.)
  4. Add feedback (tooltip showing restore progress)
  5. 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:

  1. Account for window border in position calculation
  2. Check DPI scaling factor
  3. Some windows have minimum sizes

Problem: Canโ€™t find window after restart

Causes: Title changed, process name different, window class mismatch.

Solutions:

  1. Use title patterns (regex) instead of exact match
  2. Verify process name matches
  3. Add fuzzy matching on title

Problem: Windows on wrong monitor

Causes: Monitor order changed, different resolution.

Solutions:

  1. Save relative position to monitor
  2. Use monitor serial/ID if available
  3. Fall back to primary monitor

Problem: System windows captured

Causes: Taskbar, desktop, hidden helpers included.

Solutions:

  1. Filter by window class (exclude โ€œShell_TrayWndโ€, โ€œProgmanโ€)
  2. Skip windows with empty titles
  3. Skip windows with no visible area

Extensions and Challenges

Easy Extensions

  1. System tray icon - Quick access to save/restore
  2. Quick layout swap - Single hotkey to toggle between two layouts
  3. Layout preview - Show minimap before restoring
  4. Export/import - Share layouts between computers

Medium Extensions

  1. Auto-save - Save layout periodically
  2. Per-application rules - โ€œChrome always on monitor 2โ€
  3. Layout profiles by time - Morning vs. evening layouts
  4. Window groups - Save/restore subsets of windows

Advanced Extensions

  1. Virtual desktop support - Layouts per virtual desktop
  2. Window snapping zones - Define grid, snap windows to zones
  3. Monitor profiles - Different layouts for docked vs. undocked
  4. Cloud sync - Sync layouts across machines

The Interview Questions Theyโ€™ll Ask

Technical Questions

  1. โ€œ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
  2. โ€œ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
  3. โ€œ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
  4. โ€œ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
  5. โ€œ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

  1. โ€œ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
  2. โ€œ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

  1. Start with: AutoHotkey v2 Documentation (essential for syntax)
  2. Then read: Game Programming Patterns - State chapter (understand state machines)
  3. While building: The Pragmatic Programmer - Configuration topics (good practices)
  4. For debugging: Windows Security Internals - Window sections (deep understanding)
  5. 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:

  1. Why do we save relative coordinates instead of absolute?
  2. Whatโ€™s the minimum score for a window match, and why did we choose that threshold?
  3. How do you handle a maximized window that needs to move to a different monitor?
  4. What happens if two current windows match the same saved window?
  5. Why might WinMove place a window 8 pixels off from where you specified?
  6. Whatโ€™s the difference between MonitorGet and MonitorGetWorkArea?

Resources

Official Documentation

Community Resources


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.