Search code examples
scriptingautohotkey

assign different border to different windows?


Hotkeys and hotstrings problem :

Now It seems to be working well and the flashing problem no longer occurs But I have this script for test only :

  • when pressing m, a MsgBox should apppear but It does not.
  • when commenting the script that draws borders or the SetTimer and then when pressing m the MsgBox appears. Is the problem from the border's script or what?
#SingleInstance Force
#Requires AutoHotkey v2.0
SendMode("Input")
Persistent
SetWorkingDir(A_ScriptDir) ; Ensures a consistent starting directory
SetTitleMatchMode(2)
ProcessSetPriority("High")
SetTimer test, 1000
test() {
    WinWaitActive("ahk_class Notepad")
    WinWaitNotActive("ahk_class Notepad")
    SoundBeep(1000)
}
; =================================================================
; used in SetWinEventHook (for eventMin and eventMax)
; check here for additional events: https://learn.microsoft.com/en-us/windows/win32/winauto/event-constants
global EVENT_SYSTEM_ALERT := 0x2 ; 2 - alert has been generated
global EVENT_SYSTEM_FOREGROUND := 0x3 ; 3 - foreground window changed
global EVENT_SYSTEM_MENUSTART := 0x4 ; 4 - menu-bar menu opened
global EVENT_SYSTEM_MENUEND := 0x5 ; 5 - menu-bar menu closed
global EVENT_SYSTEM_MENUPOPUPSTART := 0x6 ; 6 - pop-up menu opened
global EVENT_SYSTEM_MENUPOPUPEND := 0x7 ; 7 - pop-up menu closed
global EVENT_SYSTEM_CAPTURESTART := 0x8 ; 8 - mouse click start
global EVENT_SYSTEM_CAPTUREEND := 0x9 ; 9 - mouse click end
global EVENT_SYSTEM_MOVESIZESTART := 0xA ; 10 - window move/resize start
global EVENT_SYSTEM_MOVESIZEEND := 0xB ; 11 - window move/resize end
global EVENT_SYSTEM_SCROLLINGSTART := 0x12 ; 18 - scrolling started on a scrol bar
global EVENT_SYSTEM_SCROLLINGEND := 0x13 ; 19 - scrolling ended on a scrol bar
global EVENT_SYSTEM_SWITCHSTART := 0x14 ; 20 - alt+tab pressed
global EVENT_SYSTEM_SWITCHEND := 0x15 ; 21 - alt+tab released
global EVENT_SYSTEM_MINIMIZESTART := 0x16 ; 22 - window minimized
global EVENT_SYSTEM_MINIMIZEEND := 0x17 ; 23 - window restored from minimize
global WINDOW_DRAG_STARTED := 0x19 ; 25 - window drag start (documentation not found for this)
global EVENT_SYSTEM_DESKTOPSWITCH := 0x20 ; 32 - active desktop has switched
global EVENT_OBJECT_CREATE := 0x8000 ; 32768 - an object has been created
global EVENT_OBJECT_DESTROY := 0x8001 ; 32769 - an object has been destroyed
global EVENT_OBJECT_SHOW := 0x8002 ; 32770 - a hidden object is shown
global EVENT_OBJECT_LOCATIONCHANGE := 0x800B ; 32779 - an object has changed location, shape, or size
global EVENT_OBJECT_NAMECHANGE := 0x800C ; 32780 - an object's Name property has changed
global EVENT_OBJECT_VALUECHANGE := 0x800E ; 32782 - an object's Value property has changed
global EVENT_OBJECT_PARENTCHANGE := 0x800F ; 32783 - an object has a new parent object
global EVENT_OBJECT_CLOAKED := 0x8017 ; 32791 - a window is cloaked. A cloaked window still exists, but is invisible to the user.
global EVENT_OBJECT_UNCLOAKED := 0x8018 ; 32792 - a window is uncloaked. A cloaked window still exists, but is invisible to the user.
; used in SetWinEventHook (for dwFlags)
global WINEVENT_OUTOFCONTEXT := 0x0000 ; Events are ASYNC
global WINEVENT_SKIPOWNTHREAD := 0x0001 ; Don't call back for events generated by this thread
global WINEVENT_SKIPOWNPROCESS := 0x0002 ; Don't call back for events generated by threads of this process
; used with the callback function of SetWinEventHook (for idObject and idChild)
global OBJID_WINDOW := 0x00000000 ; 0
global OBJID_SYSMENU := 0xFFFFFFFF ; -1
global OBJID_TITLEBAR := 0xFFFFFFFE ; -2
global OBJID_MENU := 0xFFFFFFFD ; -3
global OBJID_CLIENT := 0xFFFFFFFC ; -4
global OBJID_VSCROLL := 0xFFFFFFFB ; -5
global OBJID_HSCROLL := 0xFFFFFFFA ; -6
global OBJID_SIZEGRIP := 0xFFFFFFF9 ; -7
global OBJID_CARET := 0xFFFFFFF8 ; -8
global OBJID_CURSOR := 0xFFFFFFF7 ; -9
global OBJID_ALERT := 0xFFFFFFF6 ; -10
global OBJID_SOUND := 0xFFFFFFF5 ; -11
global OBJID_QUERYCLASSNAMEIDX := 0xFFFFFFF4 ; -12
global OBJID_NATIVEOM := 0xFFFFFFF3 ; -13
global CHILDID_SELF := 0
; saved for releasing resources appropriately
global hookProcAdr := ""
global hWinEventHooks := []
; list of windows for which to create a border
; Title is what will be matched with WinActive(Title)
; if the final list entry's Title is "A", it will make the current active window be the "fallback" if none of the others match
global rectList := [{
    Title: "ahk_class PotPlayer64",
    Rect: DrawRect(2, 'green', 0)
}, {
    Title: "ahk_exe vivaldi.exe",
    Rect: DrawRect(2, 'yellow', 2)
}, {
    Title: "ahk_exe Reverso.exe",
    Rect: DrawRect(2, 'red', 0)
}, {
    ;     Title: "Program Manager",
    ;     Rect: DrawRect(2, '33cc33', 2)
    ; }, {
    Title: "- Notepad",
    Rect: DrawRect(4, 'green', 1)
}, {
    Title: "Microsoft Store",
    Rect: DrawRect(2, 'red', 3)
}, {
    Title: "A",
    Rect: DrawRect()
}
]
class DrawRect extends Gui {
    __New(border_thickness := "3", border_color := "red", offset := 0) {
        super.__New("+AlwaysOnTop -Caption +ToolWindow", "GUI4Boarder")
        super.BackColor := border_color
        super.Opt("+E0x08080028") ; WS_EX_NOACTIVATE|WS_EX_LAYERED|WS_EX_TRANSPARENT|WS_EX_TOPMOST
        DllCall("SetLayeredWindowAttributes", "Ptr", super.Hwnd, "Ptr", 0, "UChar", 255, "UInt", 2, "Int") ; allows this "Layered Window" to be visible and makes it fully opaque (i.e. 255)
        this.offset := offset
        this.parentHwnd := 0
        this.x := 0
        this.y := 0
        this.w := 0
        this.h := 0
        this.outerX := offset
        this.outerY := offset
        this.innerX := border_thickness + offset
        this.innerY := border_thickness + offset
        this.IsShowing := false
    }
    UpdatePosition(hwnd) {
        this.parentHwnd := hwnd
        super.Opt("+Owner" this.parentHwnd)
        WinGetPos(&x, &y, &w, &h, this.parentHwnd)
        if (x == this.x && y == this.y && w == this.w && h == this.h)
            return false ; position is the same as the last time this was checked
        this.x := x
        this.y := y
        this.w := w
        this.h := h
        return true
    }
    Show() {
        outerX2 := this.w - this.outerX
        outerY2 := this.h - this.outerY
        innerX2 := this.w - this.innerX
        innerY2 := this.h - this.innerY
        WinSetRegion(this.outerX "-" this.outerY " " outerX2 "-" this.outerY " " outerX2 "-" outerY2 " " this.outerX "-" outerY2 " " this.outerX "-" this.outerY " " this.innerX "-" this.innerY " " innerX2 "-" this.innerY " " innerX2 "-" innerY2 " " this.innerX "-" innerY2 " " this.innerX "-" this.innerY, super.Hwnd)
        ; super.Show("w" . this.w . " h" . this.h . " x" . this.x . " y" . this.y . " NoActivate")
        ; Similar to super.OptShow("+E0x800A8"whxy) but also places it at the top of the z-order
        DllCall("SetWindowPos", "Ptr", super.Hwnd, "Ptr", 0, "Int", this.x, "Int", this.y, "Int", this.w, "Int", this.h, "UInt", 0x4250, "Int") ; SWP_NOACTIVATE|SWP_SHOWWINDOW|SWP_NOOWNERZORDER|SWP_ASYNCWINDOWPOS (0x10|0x40|0x200|0x4000)
        this.IsShowing := true
    }
    Hide() {
        super.Hide()
        this.IsShowing := false
    }
}
ShowRect(hWinEventHook, Event, hWnd, idObject := 0, idChild := 0, dwEventThread := 0, dwmsEventTime := 0) {
    Critical -1 ; turns on Critical (making the thread uninterruptible) but disables message checks (via -1)
    ; ignore events for objects that are not windows and events for child objects
    if (idObject !== OBJID_WINDOW || idChild !== CHILDID_SELF)
        return
    for _, value in rectList {
        ; the latter two WinActive checks will exclude the Windows Desktop itself from matching but you can remove them if you aren't using a WinActive("A") gui
        if ((winHwnd := WinActive(value.Title)) && !WinActive("ahk_class WorkerW ahk_exe explorer.exe") && !WinActive("ahk_class Progman ahk_exe explorer.exe")) {
            ; current active window matches one in the list
            try {
                if (WinGetMinMax("ahk_id " winHwnd) == 0) {
                    ; window is not maximized or minimized
                    if (value.Rect.UpdatePosition(winHwnd) || !value.Rect.IsShowing) {
                        ; window is in a new position or the gui was previously hidden
                        value.Rect.Show()
                    }
                    continue
                }
            }
            ; active window has been maximized, minimized, or no longer exists, so hide the rect
            value.Rect.Hide()
            continue
        }
        ; hide any existing rects for non-matching windows
        if (value.Rect.IsShowing) {
            value.Rect.Hide()
            continue
        }
    }
}
SetWinEventHook(eventMin, eventMax, hmodWinEventProc, lpfnWinEventProc, idProcess, idThread, dwFlags) {
    return DllCall("SetWinEventHook"
        , "UInt", eventMin ; start of event range
        , "UInt", eventMax ; end of event range
        , "UInt", hmodWinEventProc ; handle to DLL that contains the hook function or null
        , "UInt", lpfnWinEventProc ; the callback
        , "UInt", idProcess ; process IDs of interest (0 = all)
        , "UInt", idThread ; thread IDs of interest (0 = all)
        , "UInt", dwFlags ; flags
        , "Ptr") ; returns HWINEVENTHOOK
}
ReleaseResources(ExitReason, ExitCode) {
    global hookProcAdr, hWinEventHook
    loop hWinEventHooks.Length {
        DllCall("UnhookWinEvent", "Ptr", hWinEventHooks.Pop()) ; free up memory from SetWinEventHook
    }
    if (hookProcAdr) {
        CallbackFree(hookProcAdr) ; free up allocated memory from CallbackCreate
        hookProcAdr := ""
    }
}
OnExit(ReleaseResources)
hookProcAdr := CallbackCreate(ShowRect, , 7) ; avoiding fast mode because it may be called from a different thread
; hook on active window change
hWinEventHooks.Push(SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, 0, hookProcAdr, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS))
; hook on window move/resize
hWinEventHooks.Push(SetWinEventHook(EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE, 0, hookProcAdr, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS))
ShowRect(0, EVENT_SYSTEM_FOREGROUND, WinActive("A")) ; Make a pseudo call when the script is called in order to affect the current active window as well
; =================================================================
m:: MsgBox("m")

Solution

  • Version 1

    Here is one way to do what @PGilm was suggesting. It's not the most eloquent solution, but it should work for your needs.

    You could put something like this inside the Call() method of DrawRect, after these first four lines

            if (win := this.func()) && WinGetMinMax(win) == 0 {
                WinGetPos(&x, &y, &w, &h, win)
                offset := this.offset
                border_thickness := this.border_thickness
    

    add

                title := WinGetTitle("ahk_id " win)
                if (title ~= "- Notepad$") {
                    border_thickness := 4
                    offset := 1
                    super.BackColor := "green"
                } else if (title == "Microsoft Store") {
                    border_thickness := 2
                    offset := 3
                    super.BackColor := "red"
                } else {
                    super.BackColor := this.border_color
                }
    

    additionally, you'll need to change the line in the __New() method from

            super.BackColor := border_color
    

    to

            this.border_color := border_color
    

    in order to store the default border_color.

    What's happening is that SetTimer is being passed a function object and its Call() method will be called every 100 milliseconds.

    If I were writing it from scratch, to make it more versatile, I would probably have separate objects set up and then call the right one depending on the current active window.


    Version 2

    An alternative way of doing this is to watch for the EVENT_SYSTEM_FOREGROUND and EVENT_OBJECT_LOCATIONCHANGE messages being sent by the system via SetWinEventHook and then show/hide the rect's appropriately.

    By doing is this way, you won't have a bunch of SetTimers running in the background eating up cpu or memory. However, the rects also won't go away after a predetermined period of time this way. EVENT_OBJECT_LOCATIONCHANGE should account for this in most situations, but in the event that you want to watch for additional system events, I'll include a list of a number of them at the top and also a link to view more of them.

    Persistent ; Keep the script running even when there are no hotkeys or timers
    
    ; used in SetWinEventHook (for eventMin and eventMax)
    ; check here for additional events: https://learn.microsoft.com/en-us/windows/win32/winauto/event-constants
    global EVENT_SYSTEM_ALERT := 0x2 ; 2 - alert has been generated
    global EVENT_SYSTEM_FOREGROUND := 0x3 ; 3 - foreground window changed
    global EVENT_SYSTEM_MENUSTART := 0x4 ; 4 - menu-bar menu opened
    global EVENT_SYSTEM_MENUEND := 0x5 ; 5 - menu-bar menu closed
    global EVENT_SYSTEM_MENUPOPUPSTART := 0x6 ; 6 - pop-up menu opened
    global EVENT_SYSTEM_MENUPOPUPEND := 0x7 ; 7 - pop-up menu closed
    global EVENT_SYSTEM_CAPTURESTART := 0x8 ; 8 - mouse click start
    global EVENT_SYSTEM_CAPTUREEND := 0x9 ; 9 - mouse click end
    global EVENT_SYSTEM_MOVESIZESTART := 0xA ; 10 - window move/resize start
    global EVENT_SYSTEM_MOVESIZEEND := 0xB ; 11 - window move/resize end
    global EVENT_SYSTEM_SCROLLINGSTART := 0x12 ; 18 - scrolling started on a scrol bar
    global EVENT_SYSTEM_SCROLLINGEND := 0x13 ; 19 - scrolling ended on a scrol bar
    global EVENT_SYSTEM_SWITCHSTART := 0x14 ; 20 - alt+tab pressed
    global EVENT_SYSTEM_SWITCHEND := 0x15 ; 21 - alt+tab released
    global EVENT_SYSTEM_MINIMIZESTART := 0x16 ; 22 - window minimized
    global EVENT_SYSTEM_MINIMIZEEND := 0x17 ; 23 - window restored from minimize
    global WINDOW_DRAG_STARTED := 0x19 ; 25 - window drag start (documentation not found for this)
    global EVENT_SYSTEM_DESKTOPSWITCH := 0x20 ; 32 - active desktop has switched
    global EVENT_OBJECT_CREATE := 0x8000 ; 32768 - an object has been created
    global EVENT_OBJECT_DESTROY := 0x8001 ; 32769 - an object has been destroyed
    global EVENT_OBJECT_SHOW := 0x8002 ; 32770 - a hidden object is shown
    global EVENT_OBJECT_LOCATIONCHANGE := 0x800B ; 32779 - an object has changed location, shape, or size
    global EVENT_OBJECT_NAMECHANGE := 0x800C ; 32780 - an object's Name property has changed
    global EVENT_OBJECT_VALUECHANGE := 0x800E ; 32782 - an object's Value property has changed
    global EVENT_OBJECT_PARENTCHANGE := 0x800F ; 32783 - an object has a new parent object
    global EVENT_OBJECT_CLOAKED := 0x8017 ; 32791 - a window is cloaked. A cloaked window still exists, but is invisible to the user.
    global EVENT_OBJECT_UNCLOAKED := 0x8018 ; 32792 - a window is uncloaked. A cloaked window still exists, but is invisible to the user.
    
    ; used in SetWinEventHook (for dwFlags)
    global WINEVENT_OUTOFCONTEXT := 0x0000 ; Events are ASYNC
    global WINEVENT_SKIPOWNTHREAD := 0x0001 ; Don't call back for events generated by this thread
    global WINEVENT_SKIPOWNPROCESS := 0x0002 ; Don't call back for events generated by threads of this process
    
    ; used with the callback function of SetWinEventHook (for idObject and idChild)
    global OBJID_WINDOW := 0x00000000 ; 0
    global OBJID_SYSMENU := 0xFFFFFFFF ; -1
    global OBJID_TITLEBAR := 0xFFFFFFFE ; -2
    global OBJID_MENU := 0xFFFFFFFD ; -3
    global OBJID_CLIENT := 0xFFFFFFFC ; -4
    global OBJID_VSCROLL := 0xFFFFFFFB ; -5
    global OBJID_HSCROLL := 0xFFFFFFFA ; -6
    global OBJID_SIZEGRIP := 0xFFFFFFF9 ; -7
    global OBJID_CARET := 0xFFFFFFF8 ; -8
    global OBJID_CURSOR := 0xFFFFFFF7 ; -9
    global OBJID_ALERT := 0xFFFFFFF6 ; -10
    global OBJID_SOUND := 0xFFFFFFF5 ; -11
    global OBJID_QUERYCLASSNAMEIDX := 0xFFFFFFF4 ; -12
    global OBJID_NATIVEOM := 0xFFFFFFF3 ; -13
    global CHILDID_SELF := 0
    
    ; saved for releasing resources appropriately
    global hookProcAdr := ""
    global hWinEventHooks := []
    
    ; list of windows for which to create a border
    ; Title is what will be matched with WinActive(Title)
    ; if the final list entry's Title is "A", it will make the current active window be the "fallback" if none of the others match
    global rectList := [
        {Title: "ahk_class PotPlayer64", Rect: DrawRect(2, 'green', 0)},
        {Title: "ahk_exe vivaldi.exe", Rect: DrawRect(2, 'yellow', 2)},
        {Title: "ahk_exe Reverso.exe", Rect: DrawRect(2, 'red', 0)},
        {Title: "ahk_group desktop", Rect: DrawRect(2, '33cc33', 2)},
        {Title: "- Notepad", Rect: DrawRect(4, 'green', 1)},
        {Title: "Microsoft Store", Rect: DrawRect(2, 'red', 3)},
        {Title: "A", Rect: DrawRect()}
    ]
    
    class DrawRect extends Gui {
        __New(border_thickness := "3", border_color := "red", offset := 0) {
            super.__New("+AlwaysOnTop -Caption +ToolWindow", "GUI4Boarder")
            super.BackColor := border_color
            super.Opt("+E0x08080028") ; WS_EX_NOACTIVATE|WS_EX_LAYERED|WS_EX_TRANSPARENT|WS_EX_TOPMOST
            DllCall("SetLayeredWindowAttributes", "Ptr",super.Hwnd, "Ptr",0, "UChar",255, "UInt",2, "Int") ; allows this "Layered Window" to be visible and makes it fully opaque (i.e. 255)
            this.offset := offset
            this.parentHwnd := 0
            this.x := 0
            this.y := 0
            this.w := 0
            this.h := 0
            this.outerX := offset
            this.outerY := offset
            this.innerX := border_thickness + offset
            this.innerY := border_thickness + offset
            this.IsShowing := false
        }
        UpdatePosition(hwnd) {
            this.parentHwnd := hwnd
            super.Opt("+Owner" this.parentHwnd)
            WinGetPos(&x, &y, &w, &h, this.parentHwnd)
            if (x == this.x && y == this.y && w == this.w && h == this.h)
                return false ; position is the same as the last time this was checked
            this.x := x
            this.y := y
            this.w := w
            this.h := h
            return true
        }
        Show() {
            outerX2 := this.w - this.outerX
            outerY2 := this.h - this.outerY
            innerX2 := this.w - this.innerX
            innerY2 := this.h - this.innerY
            WinSetRegion(this.outerX "-" this.outerY " " outerX2 "-" this.outerY " " outerX2 "-" outerY2 " " this.outerX "-" outerY2 " " this.outerX "-" this.outerY " " this.innerX "-" this.innerY " " innerX2 "-" this.innerY " " innerX2 "-" innerY2 " " this.innerX "-" innerY2 " " this.innerX "-" this.innerY, super.Hwnd)
            ; super.Show("w" . this.w . " h" . this.h . " x" . this.x . " y" . this.y . " NoActivate")
            ; Similar to super.Show(whxy) but also places it at the top of the z-order
            DllCall("SetWindowPos", "Ptr",super.Hwnd, "Ptr",0, "Int",this.x, "Int",this.y, "Int",this.w, "Int",this.h, "UInt",0x4250, "Int") ; SWP_NOACTIVATE|SWP_SHOWWINDOW|SWP_NOOWNERZORDER|SWP_ASYNCWINDOWPOS (0x10|0x40|0x200|0x4000)
            this.IsShowing := true
        }
        Hide() {
            super.Hide()
            this.IsShowing := false
        }
    }
    
    ShowRect(hWinEventHook, Event, hWnd, idObject:=0, idChild:=0, dwEventThread:=0, dwmsEventTime:=0) {
        Critical -1 ; turns on Critical (making the thread uninterruptible) but disables message checks (via -1)
        ; ignore events for objects that are not windows and events for child objects
        if (idObject !== OBJID_WINDOW || idChild !== CHILDID_SELF)
            return
        for _,value in rectList {
            ; the latter two WinActive checks will exclude the Windows Desktop itself from matching but you can remove them if you aren't using a WinActive("A") gui
            if ((winHwnd := WinActive(value.Title)) && !WinActive("ahk_class WorkerW ahk_exe explorer.exe") && !WinActive("ahk_class Progman ahk_exe explorer.exe")) {
                ; current active window matches one in the list
                try {
                    if (WinGetMinMax("ahk_id " winHwnd) == 0) {
                        ; window is not maximized or minimized
                        if (value.Rect.UpdatePosition(winHwnd) || !value.Rect.IsShowing) {
                            ; window is in a new position or the gui was previously hidden
                            value.Rect.Show()
                        }
                        continue
                    }
                }
                ; active window has been maximized, minimized, or no longer exists, so hide the rect
                value.Rect.Hide()
                continue
            }
            ; hide any existing rects for non-matching windows
            if (value.Rect.IsShowing) {
                value.Rect.Hide()
                continue
            }
        }
    }
    
    SetWinEventHook(eventMin, eventMax, hmodWinEventProc, lpfnWinEventProc, idProcess, idThread, dwFlags) {
        return DllCall("SetWinEventHook"
            ,"UInt",eventMin ; start of event range
            ,"UInt",eventMax ; end of event range
            ,"UInt",hmodWinEventProc ; handle to DLL that contains the hook function or null
            ,"UInt",lpfnWinEventProc ; the callback
            ,"UInt",idProcess ; process IDs of interest (0 = all)
            ,"UInt",idThread ; thread IDs of interest (0 = all)
            ,"UInt",dwFlags ; flags
            ,"Ptr") ; returns HWINEVENTHOOK
    }
    
    ReleaseResources(ExitReason, ExitCode) {
        global hookProcAdr, hWinEventHook
        loop hWinEventHooks.Length {
            DllCall("UnhookWinEvent", "Ptr",hWinEventHooks.Pop()) ; free up memory from SetWinEventHook
        }
        if (hookProcAdr) {
            CallbackFree(hookProcAdr) ; free up allocated memory from CallbackCreate
            hookProcAdr := ""
        }
    }
    
    OnExit(ReleaseResources)
    
    hookProcAdr := CallbackCreate(ShowRect, , 7) ; avoiding fast mode because it may be called from a different thread
    
    ; hook on active window change
    hWinEventHooks.Push(SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, 0, hookProcAdr, 0, 0, WINEVENT_OUTOFCONTEXT|WINEVENT_SKIPOWNPROCESS))
    ; hook on window move/resize
    hWinEventHooks.Push(SetWinEventHook(EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE, 0, hookProcAdr, 0, 0, WINEVENT_OUTOFCONTEXT|WINEVENT_SKIPOWNPROCESS))
    
    ShowRect(0, EVENT_SYSTEM_FOREGROUND, WinActive("A")) ; Make a pseudo call when the script is called in order to affect the current active window as well
    

    I also moved some of the expressions to the __New() method for a minimal optimization.

    Edit:

    Added an UpdatePosition(hwnd) method that returns false if the position is unchanged and modified the Show() method and callsites of it accordingly.


    Version 2.5

    Here is a version that I was playing around with until I settled on the EVENT_OBJECT_LOCATIONCHANGE event. To be clear, this is an "incomplete" solution compared to Version 2, but it may assist you in debugging things yourself.

    Just replace the ShowRect method in Version 2 with the one below and modify the additional line below that. There are some superfluous switch cases accounted for, since the only events that will show up are the ones that you include in the SetWinHookEvent. However, the main problem with this version is that it won't account for situations like where you maximize the window.

    ShowRect(hWinEventHook, Event, hWnd, idObject:=0, idChild:=0, dwEventThread:=0, dwmsEventTime:=0) {
        ; ignore events for objects that are not windows and events for child objects
        if (idObject !== OBJID_WINDOW || idChild !== CHILDID_SELF)
            return
        switch (Event) {
            case EVENT_SYSTEM_SCROLLINGSTART
                , EVENT_SYSTEM_SCROLLINGEND
                , EVENT_OBJECT_CREATE
                , EVENT_OBJECT_DESTROY
                , EVENT_OBJECT_SHOW
                , EVENT_OBJECT_NAMECHANGE
                , EVENT_OBJECT_VALUECHANGE
                , EVENT_OBJECT_PARENTCHANGE:
                ; do nothing
                return
            case EVENT_SYSTEM_FOREGROUND
                , EVENT_SYSTEM_MOVESIZESTART
                , EVENT_SYSTEM_MOVESIZEEND
                , EVENT_SYSTEM_MENUSTART
                , EVENT_SYSTEM_MENUEND
                , EVENT_SYSTEM_MENUPOPUPSTART
                , EVENT_SYSTEM_MENUPOPUPEND
                , EVENT_SYSTEM_CAPTURESTART
                , EVENT_SYSTEM_CAPTUREEND
                , EVENT_SYSTEM_SWITCHEND
                , EVENT_SYSTEM_MINIMIZEEND
                , WINDOW_DRAG_STARTED
                , EVENT_OBJECT_UNCLOAKED:
                for _,value in rectList {
                    ; the latter two WinActive checks will exclude the Windows Desktop itself from matching but you can remove them if you aren't using a WinActive("A") gui
                    if ((winHwnd := WinActive(value.Title)) && !WinActive("ahk_class WorkerW ahk_exe explorer.exe") && !WinActive("ahk_class Progman ahk_exe explorer.exe")) {
                        ; current active window matches one in the list
                        try {
                            if (WinGetMinMax("ahk_id " winHwnd) == 0) {
                                ; window is not maximized or minimized
                                if (value.Rect.UpdatePosition(winHwnd) || !value.Rect.IsShowing) {
                                    ; window is in a new position or the gui was previously hidden
                                    value.Rect.Show()
                                }
                                continue
                            }
                        }
                        ; active window has been maximized, minimized, or no longer exists, so hide the rect
                        value.Rect.Hide()
                        continue
                    }
                    ; hide any existing rects for non-matching windows
                    if (value.Rect.IsShowing) {
                        value.Rect.Hide()
                        continue
                    }
                }
            case EVENT_SYSTEM_ALERT
                , EVENT_SYSTEM_MINIMIZESTART
                , EVENT_SYSTEM_SWITCHSTART
                , EVENT_SYSTEM_DESKTOPSWITCH
                , EVENT_OBJECT_CLOAKED:
                ; hide any rects
                for _,value in rectList {
                    if (value.Rect.IsShowing) {
                        value.Rect.Hide()
                        break
                    }
                }
            default:
                ; hide any rects
                for _,value in rectList {
                    if (value.Rect.IsShowing) {
                        value.Rect.Hide()
                        break
                    }
                }
                return
        }
    }
    

    Then you can change the hooks to only include this one, which will hook on every event from EVENT_SYSTEM_FOREGROUND through EVENT_SYSTEM_DESKTOPSWITCH :

    hWinEventHooks.Push(SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_DESKTOPSWITCH, 0, hookProcAdr, 0, 0, WINEVENT_OUTOFCONTEXT|WINEVENT_SKIPOWNPROCESS))
    

    Or, alternatively, include only the exact ones you want to account for, similar to Version 2.