Dhillan Kara
Dhillan Kara

Reputation: 63

How to activate a function everytime the target window becomes active in AutoHotkey

I would like to activate a sound profile every time I switch to a specific program and change back to the default profile every time I leave it. This action is turned on in a GUI via a radio button.

The workaround I have created is:

Auto_Ftsps:
    gui, Submit, NoHide
    While (Rad3==1)
    {
        Previous_window:= WinActive("A")
        Sleep,1000
        Current_window:= WinActive("A")
        If (Previous_window =Current_window)
            {}
        Else If (Previous_window !=Current_window) 
        {
            If(WinActive("Fortnite"))
                Run_Peace_Profile("Ftsps")
            Else 
                Run_Peace_Profile("Graphic EQ")
        }
        Sleep,2000
    }
    return

Is there a better way to do this? I looked on forums and tutorials with no success.

Upvotes: 5

Views: 2337

Answers (1)

Josh Brobst
Josh Brobst

Reputation: 2080

OnWin.ahk is somewhat similar to your method; it uses SetTimer to periodically check for the events you register with it, so unlike your method it's asynchronous in terms of AHK threads. Don't quote me on this, but I think internally WinWaitActive is similar as well.

There is however another way that doesn't involve periodically checking the active window ourselves and instead allows us to react to Windows' "active window change" events - a shell hook. Typically SetWindowsHookEx with WH_SHELL would be used for this, but I don't think it's even possible to use it with AHK alone (you have to make a DLL), and it's kind of complicated to get everything right. Luckily there's RegisterShellHookWindow, which allows us to receive shell events as Windows messages rather than injecting a DLL into other threads. We can then use AHK's OnMessage to react to these messages, which, in your case, means having a function that tests for to wParam being HSHELL_WINDOWACTIVATED or HSHELL_RUDEAPPACTIVATED (i.e., bit 3 is set) and changes the sound profile accordingly. As for turning this functionality on/off, we can have the radio buttons' g-label contain the logic for controlling whether we want to receive the shell messages via (De)RegisterShellHookWindow.

#SingleInstance Force

Gui +AlwaysOnTop +HwndhWnd
Gui Add, Text,, Automatic sound profile change
Gui Add, Radio, gHookRadioHandler Checked, On
Gui Add, Radio, gHookRadioHandler X+, Off
Gui Font,, Consolas
Gui Add, Edit, HwndhLog xm w800 r30 ReadOnly -Wrap -WantReturn

ftspsActive := false

; Get the dynamic identifier for shell messages and assign our callback to handle these messages
SHELL_MSG := DllCall("RegisterWindowMessage", "Str", "SHELLHOOK", "UInt")
OnMessage(SHELL_MSG, Func("ShellCallback"))

if (!SetHook(true)) {
    GuiControl,, Off, 1
}

Gui Show


GuiClose() {
    ExitApp
}

; Dummy implementation that logs the changes to an edit control for demonstration purposes
Run_Peace_Profile(profile) {
    Println("Switched to " profile)
}

; Sets whether the shell hook is registered
SetHook(state) {
    global hWnd
    static shellHookInstalled := false
    if (!shellHookInstalled and state) {
        if (!DllCall("RegisterShellHookWindow", "Ptr", hWnd)) {
            Println("Failed to register shell hook")
            return false
        }
        Println("Registered shell hook")
        shellHookInstalled := true
    }
    else if (shellHookInstalled and !state) {
        if (!DllCall("DeregisterShellHookWindow", "Ptr", hWnd)) {
            Println("Failed to deregister shell hook")
            return false
        }
        Println("Deregistered shell hook")
        shellHookInstalled := false
    }

    return true
}

; Radio button handler that controls registration of the sound profile hook
HookRadioHandler() {
    state := A_GuiControl == "On"
    if (!SetHook(state)) {
        GuiControl,, % (state ? "Off" : "On"), 1
    }
}

; Shell messages callback
ShellCallback(wParam, lParam) {
    ; HSHELL_WINDOWACTIVATED = 4, HSHELL_RUDEAPPACTIVATED = 0x8004
    if (wParam & 4) {
        ; lParam = hWnd of activated window
        global ftspsActive
        WinGet fnHWnd, ID, Fortnite

        WinGetTitle t, ahk_id %lParam%
        Println("active window: " t)

        if (!ftspsActive and fnHWnd = lParam) {
            Run_Peace_Profile("Ftsps")
            ftspsActive := true
        }
        else if (ftspsActive and fnHWnd != lParam) {
            Run_Peace_Profile("Graphic EQ")
            ftspsActive := false
        }
    }
}

; Prints a line to the logging edit box
Println(s) {
    global hLog
    static MAX_LINES := 1000, LINE_ADJUST := 200, nLines := 0
    ; EM_SETSEL = 0xB1, EM_REPLACESEL = 0xC2, EM_LINEINDEX = 0xBB
    if (nLines = MAX_LINES) {
        ; Delete the oldest LINE_ADJUST lines
        SendMessage 0xBB, LINE_ADJUST,,, ahk_id %hLog%
        SendMessage 0xB1, 0, ErrorLevel,, ahk_id %hLog%
        SendMessage 0xC2, 0, 0,, ahk_id %hLog%
        nLines -= LINE_ADJUST
    }
    ++nLines
    ; Move to the end by selecting all and deselecting
    SendMessage 0xB1, 0, -1,, ahk_id %hLog%
    SendMessage 0xB1, -1, -1,, ahk_id %hLog%
    ; Add the line
    str := "[" A_Hour ":" A_Min "] " s "`r`n"
    SendMessage 0xC2, 0, &str,, ahk_id %hLog%
}

Note that I added some feedback messages in the form of an edit control just so this script can serve as a small stand-alone demonstration.

A possible drawback of this approach comes right from the top of the RegisterShellHookWindow documentation:

This function is not intended for general use. It may be altered or unavailable in subsequent versions of Windows.

Additionally, I have no idea what a "rude app" is or why they have their own constant. This question says it has to do with full-screen applications, but the asker and I receive HSHELL_RUDEAPPACTIVATED for seemingly every program.

As an alternative, there's also SetWinEventHook, which can be called with EVENT_SYSTEM_FOREGROUND and WINEVENT_OUTOFCONTEXT to install a callback from AHK which is called every time the foreground window changes. Note that this will be called for child windows coming to the foreground, unlike the RegisterShellHookWindow method.

#SingleInstance Force

Gui +AlwaysOnTop
Gui Add, Text,, Automatic sound profile change
Gui Add, Radio, gHookRadioHandler Checked, On
Gui Add, Radio, gHookRadioHandler X+, Off
Gui Font,, Consolas
Gui Add, Edit, HwndhLog xm w800 r30 ReadOnly -Wrap -WantReturn

ftspsActive := false

fcAddr := RegisterCallback(Func("FgCallback"))

if (!SetHook(true)) {
    GuiControl,, Off, 1
}

Gui Show


GuiClose() {
    ExitApp
}

; Dummy implementation that logs the changes to an edit control for demonstration purposes
Run_Peace_Profile(profile) {
    Println("Switched to " profile)
}

; Sets whether the foreground hook is installed
SetHook(state) {
    global fcAddr
    static hook, fgHookInstalled := false

    if (!fgHookInstalled and state) {
        ; EVENT_SYSTEM_FOREGROUND = 3, WINEVENT_OUTOFCONTEXT = 0
        hook := DllCall("SetWinEventHook", "UInt", 3, "UInt", 3, "Ptr", 0, "Ptr", fcAddr, "Int", 0, "Int", 0, "UInt", 0, "Ptr")
        if (!hook) {
            Println("Failed to set foreground hook")
            return false
        }
        Println("Set foreground hook")
        fgHookInstalled := true
    }
    else if (fgHookInstalled and !state) {
        if (!DllCall("UnhookWinEvent", "Ptr", hook)) {
            Println("Failed to unset foreground hook")
            return false
        }
        Println("Unset foreground hook")
        fgHookInstalled := false
    }

    return true
}

; Radio button handler that controls installation of the sound profile hook
HookRadioHandler() {
    state := A_GuiControl == "On"
    if (!SetHook(state)) {
        GuiControl,, % (state ? "Off" : "On"), 1
    }
}

; Foreground window change callback
FgCallback(hWinEventHook, event, hWnd, idObject, idChild, dwEventThread, dwmsEventTime) {
    global ftspsActive
    WinGet fnHWnd, ID, Fortnite

    WinGetTitle t, ahk_id %hWnd%
    Println("fg window: " t)

    if (!ftspsActive and fnHWnd = hWnd) {
        Run_Peace_Profile("Ftsps")
        ftspsActive := true
    }
    else if (ftspsActive and fnHWnd != hWnd) {
        Run_Peace_Profile("Graphic EQ")
        ftspsActive := false
    }
}

; Prints a line to the logging edit box
Println(s) {
    global hLog
    static MAX_LINES := 1000, LINE_ADJUST := 200, nLines := 0
    ; EM_SETSEL = 0xB1, EM_REPLACESEL = 0xC2, EM_LINEINDEX = 0xBB
    if (nLines = MAX_LINES) {
        ; Delete the oldest LINE_ADJUST lines
        SendMessage 0xBB, LINE_ADJUST,,, ahk_id %hLog%
        SendMessage 0xB1, 0, ErrorLevel,, ahk_id %hLog%
        SendMessage 0xC2, 0, 0,, ahk_id %hLog%
        nLines -= LINE_ADJUST
    }
    ++nLines
    ; Move to the end by selecting all and deselecting
    SendMessage 0xB1, 0, -1,, ahk_id %hLog%
    SendMessage 0xB1, -1, -1,, ahk_id %hLog%
    ; Add the line
    str := "[" A_Hour ":" A_Min "] " s "`r`n"
    SendMessage 0xC2, 0, &str,, ahk_id %hLog%
}

Upvotes: 8

Related Questions