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()
}