I wanted a nice Alt+` window switching experience, and I thought of a way to do it using the actual Alt+Tab UI, no mimicry required.
This script listens for Alt+` and converts it to an Alt+Tab after filtering the windows seen by the task switcher, by setting an attribute on the windows. When you release Alt, it unfilters the windows by removing the attribute.
You can use ` or Tab while Alt is held to cycle the list, and you can hold Shift to cycle backwards, including when opening the switcher.
Limitations:
- Windows are hidden from the task bar as well, which can be distracting,
especially with taskbar button labels enabled, as it animates the taskbar buttons collapsing and expanding.
Windows has a setting to disable taskbar animation, but it doesn't seem to work in Windows 11.
- Some windows are not hidden from the task switcher, such as the Task Manager, due to permission errors.
- Running as administrator fixes this.
- UWP windows, such as Windows's Settings app, are not filtered out either. They don't play well with any of the methods I've tried.
- There may be side effects on the windows that get hidden, since it temporarily changes their window type, essentially. If something goes wrong, windows may be left excluded from the task bar and task switcher, and without minimize or maximize buttons. I've only seen this happen once, and it may have been due to a work-in-progress version of the script.
I don't use UWP apps much, so this is good enough for me, but I would love it if someone found a solution for hiding UWP windows.
Usage:
- Install AutoHotkey v2
- Save script as
window-switcher.ahk
or similar
- Double click the new file to run it once.
- To run at startup with administrator privileges:
- Open Task Scheduler
- Action > Create Task...
- Check "Run with highest privileges" in "Security options" in General tab
- In Triggers tab, click "New..." and set the type to "At log on"
- For the Action, you can browse for the script. (You may want to move it somewhere where you'd like to keep it long term first, since it will use the file path to locate the script.)
; Requires AutoHotkey v2
;--------------------------------------------------------
; Alt+` to switch between windows of the same application
;--------------------------------------------------------
; This script piggybacks on the built-in Alt+Tab window switcher,
; filtering it to show only windows from the same process as the active window.
; It listens for Alt+` and Alt+Shift+` and converts them to Alt+Tab and Alt+Shift+Tab, respectively,
; after hiding windows from the task switcher by setting their WS_EX_TOOLWINDOW style,
; and then unhiding them after the switcher is closed.
; Pressing ` again while holding Alt will tab through the windows of the same application,
; and Shift+` will tab through them in reverse.
; Tab or Shift+Tab also works (automatically, since that's what the switcher normally uses.)
; Limitations:
; - Windows are hidden from the task bar as well, which can be distracting,
; especially with taskbar button labels enabled, as it animates the taskbar buttons collapsing and expanding.
; - Some windows are not hidden from the task switcher, such as the Task Manager, due to permission errors.
; - Running as administrator fixes this.
; - UWP windows, such as Windows's Settings app, are not filtered out either.
; - They don't play well with any of the methods I've tried.
; - There may be side effects on the windows that get hidden, since it changes their window type, essentially, temporarily.
; - I now have seen it leave File Explorer with no minimize or maximize buttons, stuck on a tool window style,
; and permanently excluded from the task bar and task switcher.
; (This may have only been due to a work-in-progress version of this script, or it may be a real issue.)
; I didn't see this before adding removal of WS_EX_APPWINDOW, so that may be the cause (if it's not a fluke.)
; Actually it might not be the removal of WS_EX_APPWINDOW, but the code supporting that,
; which allows for changes to other styles while hidden. If I change that, it might be fine.
; I've changed it to restore all styles for now, but I don't know if that's even related.
; TODO: remove windows from task switcher only, and not the task bar.
; Adding WS_EX_TOOLWINDOW is much faster than WinHide/WinShow (it makes the actual interaction instantaneous!),
; but it still causes distracting animation in the taskbar, particularly when taskbar button labels are enabled.
; Is there a less obtrusive way to remove windows from the task switcher?
#MaxThreadsPerHotkey 2
WS_EX_APPWINDOW := 0x00040000
WS_EX_TOOLWINDOW := 0x00000080
WS_CHILD := 0x40000000
TempHiddenWindows := []
OriginalExStyles := Map()
!+`:: {
FilteredWindowSwitcher()
}
!`:: {
FilteredWindowSwitcher()
}
FilteredWindowSwitcher() {
if TempHiddenWindows.Length {
; Needs #MaxThreadsPerHotkey 2 to handle Alt+`+`+`... to tab through windows with `, while waiting for Alt to be released
; Needs {Blind} to handle Alt+Shift+` to go in reverse
Send "{Blind}{Tab}"
return
}
try {
ActiveProcessName := WinGetProcessName("A")
} catch TargetError {
MakeSplash("Window Switcher", "Active window not found.", 1000)
return
}
WinClassCount := WinGetCount("ahk_exe " ActiveProcessName)
if WinClassCount = 1 {
return
}
WindowsOfApp := WinGetList("ahk_exe " ActiveProcessName)
AllWindows := WinGetList()
Messages := []
for Window in AllWindows {
SameApp := false
for WindowOfApp in WindowsOfApp {
if Window = WindowOfApp {
SameApp := true
break
}
}
if !SameApp {
if Switchable(Window) {
try {
; MsgBox("Would hide:`n`n" DescribeWindow(Window), "Window Switcher")
ExStyle := WinGetExStyle(Window) ; redundantly accessed in Switchable...
OriginalExStyles[Window] := ExStyle
if WinGetClass(Window) = "ApplicationFrameWindow" {
; This is a Windows UWP app. It doesn't work to add WS_EX_TOOLWINDOW (though it doesn't generate an error).
; In fact, not even replacing all styles works:
; WinSetExStyle(WS_EX_TOOLWINDOW, Window)
; WinSetStyle(WS_CHILD, Window)
; WinHide doesn't work either, for UWP apps.
; It hides the window itself, but it doesn't hide it from the task switcher or the task bar.
; TODO: Find a way to hide UWP apps from the task switcher. This is pretty annoying!
; My only real idea is to move the window to a different virtual desktop,
; which would only work well with "Show all open windows when I press Alt+Tab" set to "Only on the desktop I'm using",
; and ideally with "On the taskbar, show all open windows" set to "On all desktops",
; which theoretically could avoid the taskbar animation, which could be nice for other windows as well.
; (These settings are in Multitasking in Settings.)
; No idea if it would be performant enough. There's a library for this though: https://github.com/FuPeiJiang/VD.ahk
; Perhaps that just speaks to the complexity of the solution though.
; It might be better to reimplement a task switcher from scratch at that point, though it would never look quite the same.
; WinHide(Window)
; Um, MakeSplash is no good here, since it blocks execution. But it's useful for debugging.
; MakeSplash("Window Switcher", "Hiding UWP app window: " WinGetTitle(Window), 1000)
; MakeSplash("Window Switcher", "Can't hide UWP app window from task switcher: " WinGetTitle(Window), 1000)
} else {
; I have not seen any benefit to removing WS_EX_APPWINDOW, but I don't know if I've seen any windows with it.
; It may help in some cases, if I've done it right, but I don't know.
WinSetExStyle(ExStyle | WS_EX_TOOLWINDOW & ~WS_EX_APPWINDOW, Window)
}
TempHiddenWindows.Push(Window)
} catch Error as e {
; It can get permission errors for certain windows, such as the Task Manager.
; But it's better to leave some extraneous windows in the list than to throw an error message up
; (especially while some windows are hidden, though I've made an array to delay the messages now.)
; Messages.Push("Error hiding window from the task switcher.`n`n" DescribeWindow(Window) "`n`n" e.Message)
}
}
}
}
Send "{LAlt Down}"
Send "{Blind}{Tab}" ; Tab or Shift+Tab to go in reverse
KeyWait "LAlt"
; MakeSplash("Window Switcher", "Alt (physical key) released", 1000)
for Window in TempHiddenWindows {
; If WinShow is ever used for a fallback, it should not be called for all windows, and it should be called at the end, so it doesn't slow things down for every window.
; WinShow(Window)
; Don't need to remember WS_EX_TOOLWINDOW state, since we're not matching windows with WS_EX_TOOLWINDOW.
; Restore WS_EX_APPWINDOW, if it was set.
; My first instinct was to allow other styles to change while hidden, as this may avoid problems with some apps,
; but styles may be forced to change as a result of WS_EX_TOOLWINDOW / removing WS_EX_APPWINDOW, I'm not sure.
try {
; WinSetExStyle(WinGetExStyle(Window) & ~WS_EX_TOOLWINDOW | (OriginalExStyles[Window] & WS_EX_APPWINDOW), Window)
WinSetExStyle(OriginalExStyles[Window], Window)
; MsgBox("Would show:`n`n" DescribeWindow(Window), "Window Switcher")
} catch Error as e {
; Delay error messages until after the switcher is closed and all windows are unhidden that can be.
Messages.Push("Failed to unhide window from the task switcher.`n`n" DescribeWindow(Window) "`n`n" e.Message)
}
}
TempHiddenWindows.Length := 0
; MakeSplash("Window Switcher", "Closing switcher (triggering logical release of Alt)", 1000)
Send "{LAlt Up}" ; This could be earlier, couldn't it?
; MakeSplash("Window Switcher", "Switcher closed.", 1000)
for message in Messages {
MsgBox(message, "Window Switcher", 0x10)
}
}
Switchable(Window) {
; Heuristics determine if a window is in the taskbar
; https://stackoverflow.com/a/2262791
ExStyle := WinGetExStyle(Window)
if ExStyle & WS_EX_TOOLWINDOW {
return false
}
if ExStyle & WS_EX_APPWINDOW {
return true
}
Style := WinGetStyle(Window)
return !(Style & WS_CHILD)
; Not sure of the specific rules, or how much the priority of the cases matters.
; AI-autocompleted logic is slightly different:
; Style := WinGetStyle(Window)
; ExStyle := WinGetExStyle(Window)
; if Style & WS_CHILD {
; return false
; }
; if ExStyle & WS_EX_APPWINDOW {
; return true
; }
; if ExStyle & WS_EX_TOOLWINDOW {
; return false
; }
; return true
}
DescribeWindow(Window) {
return "Window Title: " WinGetTitle(Window) "`nWindow Class: " WinGetClass(Window) "`nProcess Name: " WinGetProcessName(Window)
}
;--------------------------------------------------------
; Win+Tab to switch between windows (TODO: between apps)
;--------------------------------------------------------
; #Tab::
; {
; Send "{LAlt Down}{Tab}"
; KeyWait "LWin" ; Wait to release left Win key
; Send "{LAlt Up}" ; Close switcher on hotkey release
; }
; return
;--------------------------------------------------------
; AUTO RELOAD THIS SCRIPT
;--------------------------------------------------------
~^s:: {
if WinActive(A_ScriptName) {
MakeSplash("AHK Auto-Reload", "`n Reloading " A_ScriptName " `n", 500)
Reload
}
}
MakeSplash(Title, Text, Duration := 0) {
SplashGui := Gui(, Title)
SplashGui.Opt("+AlwaysOnTop +Disabled -SysMenu +Owner") ; +Owner avoids a taskbar button.
SplashGui.Add("Text", , Text)
SplashGui.Show("NoActivate") ; NoActivate avoids deactivating the currently active window.
if Duration {
Sleep(Duration)
SplashGui.Destroy()
}
return SplashGui
}