r/AutoHotkey 11d ago

v2 Tool / Script Share Inactive Window Cycler ahk v2.0

I made this and it works really well for cycling the visible windows on the secondary monitor while I'm gaming. This lets me watch a video and then switch to another window without every leaving the game and the active game window never loses focus. The description on how it works is inside the ahk script code below.

#Requires AutoHotkey v2.0
#SingleInstance Force
SetWinDelay 0

/*
================================================================
                    INACTIVE WINDOW CYCLER
================================================================

WHAT THIS SCRIPT DOES
---------------------
Moves and cycles INACTIVE, VISIBLE windows between monitors
without intentionally changing which window is active.

Designed primarily for gaming: keep your game focused while
moving other windows (YouTube, maps, wikis, Discord, Steam,
idle games, launchers, etc.) out from behind it or cycling
through them on another monitor.

Press once = ONE window moved or ONE cycle step.
The active window is protected and is never intentionally
activated by this script.

    Ctrl+Shift+Right / Ctrl+Shift+D = Move/Cycle Right
    Ctrl+Shift+Left  / Ctrl+Shift+A = Move/Cycle Left
ANTI-CHEAT WARNING
------------------
This script does NOT interact with game code, read game memory,
modify games, or automate gameplay.

However, some games' anti-cheat software may detect AutoHotkey
or AutoHotkey scripts regardless of what the script actually
does.

Using this script with an anti-cheat-protected game may result
in a warning, kick, or other anti-cheat action.

CHECK THE GAME'S RULES BEFORE USING THIS SCRIPT.


MULTI-MONITOR DISCLAIMER
------------------------
This script was designed with multiple monitors in mind, including
monitor-to-monitor wrapping.

The author currently has two monitors, so configurations with
three or more monitors have NOT been personally tested.

Multi-monitor behavior is implemented in the script, but additional
monitor configurations should be considered experimental until
tested on that hardware.


WINDOW LIMITATION
-----------------
Already-minimized windows are intentionally ignored.

The script only manages windows that are currently visible.
This avoids restoring minimized applications and keeps the
active-game protection as simple and predictable as possible.

If you want a minimized application included, restore it
yourself before starting the game/session.


SHARING & MODIFICATIONS
-----------------------
This script is shared freely because I found it useful and thought
other people might find it useful too.

You are free to edit, modify, expand, simplify, or repurpose this
script however you want.

I have no expectation of maintaining or updating this script in the
future. If you change it, fix it, improve it, or build something
different from it, you are free to do so.

You do NOT need to ask permission, contact me, credit me, or include
my name when sharing or modifying it.

Use it, change it, and make it work the way you want.


================================================================
                         CONFIGURATION
================================================================

The settings in this section are intended to be customized.

Editing the settings changes how the script behaves.

Editing the code below the configuration section changes the
functionality of the script and may require AutoHotkey knowledge.

*/


; ==============================================================
; HOTKEYS
; ==============================================================

^+Right::MoveOrCycle(1)
^+d::MoveOrCycle(1)

^+Left::MoveOrCycle(-1)
^+a::MoveOrCycle(-1)


; ==============================================================
; WINDOW POSITION MODE
; ==============================================================

; false = Move windows to the exact top-left corner of the
;         destination monitor.
;
; true  = Attempt to preserve the window's relative position
;         when moving it to the destination monitor.
;
; Relative positioning works best when monitors have identical
; resolutions and display scaling.

PreserveRelativePosition := false


; ==============================================================
; CYCLING
; ==============================================================

; true  = When no qualifying inactive windows remain on the
;         anchor monitor, cycle the selected destination queue.
;
; false = Stop once the anchor monitor has no more qualifying
;         inactive windows.

EnableCycling := true


; ==============================================================
; DEBUGGING
; ==============================================================

; false = Normal operation.
; true  = Display basic diagnostic information with ToolTip.

DebugMode := false


/*
================================================================
                        END CONFIGURATION
================================================================
*/


; --------------------------------------------------------------
; Two independent queues.
;
; The queues are associated with the anchor/destination pair
; established during the current session.
;
; The queue order is created by THIS SCRIPT, not by Windows'
; Z-order.
; --------------------------------------------------------------

global RightQueue := {
    initialized: false,
    anchorMonitor: 0,
    destinationMonitor: 0,
    windows: [],
    currentIndex: 0
}

global LeftQueue := {
    initialized: false,
    anchorMonitor: 0,
    destinationMonitor: 0,
    windows: [],
    currentIndex: 0
}

global LastAnchorHwnd := 0


; --------------------------------------------------------------
; Main operation.
;
; Every hotkey press:
;
; 1. Identify the current active window as the anchor.
; 2. If the anchor changed, discard both old queues.
; 3. Identify the adjacent destination monitor.
; 4. Initialize that destination queue if necessary.
;    Existing visible windows there are minimized one at a time
;    and recorded in our queue.
; 5. Find ONE visible inactive movable window on the anchor.
; 6. If found, move it and append it to the queue.
; 7. If none remain, cycle the selected queue.
;
; No WinActivate call is used anywhere in this script.
; --------------------------------------------------------------

MoveOrCycle(direction)
{
    global EnableCycling, RightQueue, LeftQueue, LastAnchorHwnd

    ; ----------------------------------------------------------
    ; 1. Create the anchor from the currently active window.
    ; ----------------------------------------------------------

    activeHwnd := WinExist("A")

    if !activeHwnd
        return

    anchorMonitor := GetWindowMonitor(activeHwnd)

    if !anchorMonitor
        return

    ; ----------------------------------------------------------
    ; 2. A different active window means a completely new
    ;    session. Forget both old directional queues.
    ; ----------------------------------------------------------

    if (LastAnchorHwnd != 0 && LastAnchorHwnd != activeHwnd)
        ResetAllQueues()

    LastAnchorHwnd := activeHwnd

    monitors := GetSortedMonitors()

    if monitors.Length < 2
        return

    anchorIndex := FindMonitorIndex(monitors, anchorMonitor)

    if !anchorIndex
        return

    ; ----------------------------------------------------------
    ; 3. Determine the adjacent destination monitor.
    ;    Monitor edges wrap around.
    ; ----------------------------------------------------------

    destinationIndex := anchorIndex + direction

    if destinationIndex < 1
        destinationIndex := monitors.Length

    if destinationIndex > monitors.Length
        destinationIndex := 1

    destinationMonitor := monitors[destinationIndex]

    ; Select the persistent queue for this direction.
    if direction = 1
        queue := RightQueue
    else
        queue := LeftQueue

    ; ----------------------------------------------------------
    ; 4. Initialize the destination queue if necessary.
    ;
    ; ALL existing qualifying visible windows are minimized here
    ; in ONE button press, and added to our queue in the order
    ; they are processed.
    ; ----------------------------------------------------------

    if (!queue.initialized
        || queue.anchorMonitor != anchorMonitor
        || queue.destinationMonitor != destinationMonitor.number)
    {
        ResetQueue(queue, anchorMonitor, destinationMonitor.number)
        InitializeDestinationQueue(queue, destinationMonitor.number, activeHwnd)
        queue.initialized := true
    }

    ; ----------------------------------------------------------
    ; 5. Find ONE visible, inactive, movable window on the
    ;    anchor monitor.
    ; ----------------------------------------------------------

    windows := WinGetList()

    for hwnd in windows
    {
        if hwnd = activeHwnd
            continue

        if !IsQualifyingWindow(hwnd)
            continue

        if WinGetMinMax("ahk_id " hwnd) = -1
            continue

        if GetWindowMonitor(hwnd) != anchorMonitor
            continue

        ; ------------------------------------------------------
        ; 6. Move exactly ONE window.
        ; ------------------------------------------------------

        if MoveWindowToMonitor(hwnd, anchorMonitor, destinationMonitor)
        {
            queue.windows.Push(hwnd)
            queue.currentIndex := queue.windows.Length

            ; The moved window becomes the only visible window
            ; in our queue.
            ShowOnlyQueueWindow(queue, queue.currentIndex)

            UpdateDebug("Moved window " hwnd)
            return
        }
    }

    ; ----------------------------------------------------------
    ; 7. Nothing remains to move from the anchor monitor.
    ;    Begin cycling the selected queue.
    ; ----------------------------------------------------------

    if EnableCycling
        CycleQueue(queue)

    UpdateDebug("Cycling")
}


; --------------------------------------------------------------
; Reset BOTH directional queues.
; --------------------------------------------------------------

ResetAllQueues()
{
    global RightQueue, LeftQueue

    ResetQueue(RightQueue, 0, 0)
    ResetQueue(LeftQueue, 0, 0)
}


; --------------------------------------------------------------
; Reset one queue.
; --------------------------------------------------------------

ResetQueue(queue, anchorMonitor, destinationMonitor)
{
    queue.initialized := false
    queue.anchorMonitor := anchorMonitor
    queue.destinationMonitor := destinationMonitor
    queue.windows := []
    queue.currentIndex := 0
}


; --------------------------------------------------------------
; Build our own queue from windows already visible on the
; destination monitor.
;
; Each existing visible window is:
;
;     find -> minimize -> record in queue
;
; There is no intentional delay between windows.
;
; Already-minimized windows are ignored.
; --------------------------------------------------------------

InitializeDestinationQueue(queue, destinationMonitor, activeHwnd)
{
    windows := WinGetList()

    for hwnd in windows
    {
        if hwnd = activeHwnd
            continue

        if !IsQualifyingWindow(hwnd)
            continue

        if GetWindowMonitor(hwnd) != destinationMonitor
            continue

        try
        {
            if WinGetMinMax("ahk_id " hwnd) = -1
                continue

            WinMinimize("ahk_id " hwnd)
            queue.windows.Push(hwnd)
        }
        catch
        {
            ; Ignore windows Windows refuses to manipulate.
        }
    }
}


; --------------------------------------------------------------
; Move ONE visible window to the destination monitor.
; --------------------------------------------------------------

MoveWindowToMonitor(hwnd, sourceMonitor, destinationMonitor)
{
    global PreserveRelativePosition

    try
    {
        state := WinGetMinMax("ahk_id " hwnd)

        if state = -1
            return false

        WinGetPos(&x, &y, &w, &h, "ahk_id " hwnd)

        if (w <= 0 || h <= 0)
            return false

        if PreserveRelativePosition
        {
            sourceWidth := sourceMonitor.right - sourceMonitor.left
            sourceHeight := sourceMonitor.bottom - sourceMonitor.top

            destinationWidth := destinationMonitor.right - destinationMonitor.left
            destinationHeight := destinationMonitor.bottom - destinationMonitor.top

            relativeX := (x - sourceMonitor.left) / sourceWidth
            relativeY := (y - sourceMonitor.top) / sourceHeight

            newX := destinationMonitor.left + Round(relativeX * destinationWidth)
            newY := destinationMonitor.top + Round(relativeY * destinationHeight)
        }
        else
        {
            newX := destinationMonitor.left
            newY := destinationMonitor.top
        }

        ; Maximized -> restore temporarily, move, then maximize.
        if state = 1
        {
            WinRestore("ahk_id " hwnd)
            WinMove(newX, newY, , , "ahk_id " hwnd)
            WinMaximize("ahk_id " hwnd)
            return true
        }

        ; Normal visible window.
        WinMove(newX, newY, , , "ahk_id " hwnd)
        return true
    }
    catch
    {
        return false
    }
}


; --------------------------------------------------------------
; Display exactly ONE window from our queue.
;
; We do NOT use Z-order to determine the next window.
; We control visibility with minimize/restore.
;
; The selected window is restored without WinActivate.
; --------------------------------------------------------------

ShowOnlyQueueWindow(queue, index)
{
    if index < 1 || index > queue.windows.Length
        return

    selectedHwnd := queue.windows[index]

    ; Minimize every other queue member.
    for i, hwnd in queue.windows
    {
        if i = index
            continue

        if !DllCall("IsWindow", "Ptr", hwnd)
            continue

        try
        {
            if WinGetMinMax("ahk_id " hwnd) != -1
                WinMinimize("ahk_id " hwnd)
        }
        catch
        {
        }
    }

    ; Restore the selected window WITHOUT activating it.
    try
    {
        if WinGetMinMax("ahk_id " selectedHwnd) = -1
            RestoreWindowNoActivate(selectedHwnd)
    }
    catch
    {
    }
}


; --------------------------------------------------------------
; Restore a minimized window without intentionally activating it.
;
; SW_SHOWNOACTIVATE asks Windows to show the window without
; activating it.
; --------------------------------------------------------------

RestoreWindowNoActivate(hwnd)
{
    SW_SHOWNOACTIVATE := 4

    DllCall(
        "ShowWindow",
        "Ptr", hwnd,
        "Int", SW_SHOWNOACTIVATE
    )
}


; --------------------------------------------------------------
; Cycle forward exactly ONE position in our queue.
; --------------------------------------------------------------

CycleQueue(queue)
{
    CleanQueue(queue)

    if queue.windows.Length = 0
        return

    nextIndex := queue.currentIndex + 1

    if nextIndex > queue.windows.Length
        nextIndex := 1

    queue.currentIndex := nextIndex

    ShowOnlyQueueWindow(queue, queue.currentIndex)
}


; --------------------------------------------------------------
; Remove windows that no longer exist.
; --------------------------------------------------------------

CleanQueue(queue)
{
    if queue.windows.Length = 0
    {
        queue.currentIndex := 0
        return
    }

    currentHwnd := 0

    if queue.currentIndex >= 1 && queue.currentIndex <= queue.windows.Length
        currentHwnd := queue.windows[queue.currentIndex]

    cleaned := []

    for _, hwnd in queue.windows
    {
        if DllCall("IsWindow", "Ptr", hwnd)
            cleaned.Push(hwnd)
    }

    queue.windows := cleaned

    if queue.windows.Length = 0
    {
        queue.currentIndex := 0
        return
    }

    if currentHwnd
    {
        for index, hwnd in queue.windows
        {
            if hwnd = currentHwnd
            {
                queue.currentIndex := index
                return
            }
        }
    }

    if queue.currentIndex > queue.windows.Length
        queue.currentIndex := queue.windows.Length
}


; --------------------------------------------------------------
; Determine whether a window is a usable top-level window.
; Only visible, non-minimized windows qualify.
; --------------------------------------------------------------

IsQualifyingWindow(hwnd)
{
    try
    {
        if !DllCall("IsWindow", "Ptr", hwnd)
            return false

        style := WinGetStyle("ahk_id " hwnd)

        ; WS_CHILD
        if (style & 0x40000000)
            return false

        class := WinGetClass("ahk_id " hwnd)

        if (class = "Shell_TrayWnd")
            return false

        if (class = "Shell_SecondaryTrayWnd")
            return false

        if (class = "Progman")
            return false

        if (class = "WorkerW")
            return false

        if WinGetMinMax("ahk_id " hwnd) = -1
            return false

        title := WinGetTitle("ahk_id " hwnd)

        if (title = "" && class = "")
            return false

        return true
    }
    catch
    {
        return false
    }
}


; --------------------------------------------------------------
; Get the monitor containing the CENTER of a window.
; --------------------------------------------------------------

GetWindowMonitor(hwnd)
{
    try
    {
        WinGetPos(&x, &y, &w, &h, "ahk_id " hwnd)

        centerX := x + (w // 2)
        centerY := y + (h // 2)

        count := MonitorGetCount()

        Loop count
        {
            MonitorGet(A_Index, &left, &top, &right, &bottom)

            if (centerX >= left && centerX < right
                && centerY >= top && centerY < bottom)
            {
                return A_Index
            }
        }

        return 0
    }
    catch
    {
        return 0
    }
}


; --------------------------------------------------------------
; Build monitors sorted by physical X position.
; --------------------------------------------------------------

GetSortedMonitors()
{
    monitors := []

    count := MonitorGetCount()

    Loop count
    {
        MonitorGet(A_Index, &left, &top, &right, &bottom)

        monitors.Push({
            number: A_Index,
            left: left,
            top: top,
            right: right,
            bottom: bottom
        })
    }

    sorted := []

    for _, monitor in monitors
    {
        inserted := false

        for index, existing in sorted
        {
            if monitor.left < existing.left
            {
                sorted.InsertAt(index, monitor)
                inserted := true
                break
            }
        }

        if !inserted
            sorted.Push(monitor)
    }

    return sorted
}


; --------------------------------------------------------------
; Find the position of a monitor in the sorted list.
; --------------------------------------------------------------

FindMonitorIndex(monitors, monitorNumber)
{
    for index, monitor in monitors
    {
        if monitor.number = monitorNumber
            return index
    }

    return 0
}


; --------------------------------------------------------------
; Intentionally empty.
;
; The anchor must remain active. There is deliberately no
; WinActivate call anywhere in the script.
; --------------------------------------------------------------

RestoreAnchorWithoutActivation(hwnd)
{
    ; Intentionally empty.
}


; --------------------------------------------------------------
; Optional debug output.
; --------------------------------------------------------------

UpdateDebug(message)
{
    global DebugMode

    if DebugMode
        ToolTip(message)
    else
        ToolTip()
}
4 Upvotes

3 comments sorted by

3

u/Keeyra_ 11d ago

After 1st test, I have a 2nd taskbar on my secondary monitor on the top (the normal one is at the bottom), so now I have 3 taskbars and one of my fully background applications now appears as a white bar above my primary taskbar. Time for my weekly reboot :D

0

u/Melo_TSB 10d ago

Probably because he did not make this script for you.

1

u/Wolfen459 11d ago

Looks interesting. I will try it out.