Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

WinWait framework (do something to a window when it appears)


  • Please log in to reply
8 replies to this topic
berban
  • Members
  • 202 posts
  • Last active: Apr 12 2019 01:08 AM
  • Joined: 30 Dec 2009
Updated September 21, 2014 to make it a lot better

The issue: Sometimes you want to do the same thing each time a particular window pops up. For example, in the documentation for SetTimer, Chris gives one example where the user might want to dismiss Microsoft Outlook error dialogs.

; Example #1: Close unwanted windows whenever they appear:

#Persistent
SetTimer, CloseMailWarnings, 250
Return

CloseMailWarnings:
WinClose, Microsoft Outlook, A timeout occured while communicating
WinClose, Microsoft Outlook, A connection to the server could not be established
Return
However, this is a somewhat inefficient method for this particular task because AutoHotkey now has to execute a subroutine 4x per second. Worse, it introduces a latency of up to 250 milliseconds before action is taken. (If a window appears immediately after the timer executes, nothing will be done about it until the next execution of the timer subroutine.)


The solution: My solution for this particular issue is to perform the check every time the active window changes instead of on a timer. This way no code will execute for as long as the same window is active. Also it’ll mean that if an action is desired, it’ll take place immediately upon encountering a new window.

;==================================================Configuration==================================================

WA_CheckNewTitles = %True% ; Will check each window again if its title changes. False will only check again once a window (as identified by ahk_id) loses focus.
WA_TitleMatchMode = RegEx ; Title match mode for the first and second column of WA_Definitions.

; A = Continue action until window goes away
; B = Once each time window becomes active
; C = Once for each window only

WA_Definitions = ; A tab-delimited table with four columns. The 2nd column (wintext) may be omitted for any entries
(
WinTitle1    WinText1    A    Action1
WinTitle2    WinText2    B    Action2
WinTitle3    WinText3    C    Action3
...          ...         ...  ...
)

;====================================================================================================

SetWinDelay, -1                                                               ; Removes the normal delay which occurs after the script waits for a window to become active
WA_Definitions := RegExReplace(WA_Definitions, "\t\K\t+")                     ; Turns the plain text table into an array of single-tab-delimited lists
DetectHiddenWindows, On                                                       ; Necessary to detect all C windows that already exist
SetTitleMatchMode, %WA_TitleMatchMode%
WA_Excluded := ",", WA_Count := 0
Loop, Parse, WA_Definitions, `n, `r                                           ; Parses the table
{
    WA_Count += 1
    StringSplit, WA_%WA_Count%_, A_LoopField, %A_Tab%                         ; Splits each item, creating a two-dimensional pseudoarray
    If (WA_%WA_Count%_4 = "")                                                 ; If the WinText parameter has been omitted then the variables are adjusted accordingly
        WA_%WA_Count%_4 := WA_%WA_Count%_3, WA_%WA_Count%_3 := WA_%WA_Count%_2, WA_%WA_Count%_2 := ""
    If WA_%WA_Count%_3 Not In A,B,C                                           ; If the third column isn't A, B, or C, then that row is ignored
        WA_%WA_Count%_4 := "", WA_Count -= 1
    Else If (WA_%WA_Count%_3 = "C") {                                         ; If the third column is C (only once per window), then find all windows that currently match the criteria and add them to the excluded list
        WinGet, @, List, % WA_%WA_Count%_1, % WA_%WA_Count%_2
        Loop, %@%
            WA_Excluded .= WA_Count @%A_Index% ","
    }
}
WA_Definitions := WA_Title := ""                                              ; Clear the definitions variable
DetectHiddenWindows, Off
SetTimer, WA_Loop, -500                                                       ; Sets a timer so that changing TitleMatchMode won't be considered in the auto-execute portion of the script and consequently won't affect your subroutines
Return

WA_Loop:
Loop {                                                                        ; Infinite loop to check for new active windows
    FoundA := False
    Sleep 40                                                                  ; Wait a small amount of time for the next window to become active. Not sure if this is really necessary or not. At the very least it keeps a buggy category A definition from using 100% of the CPU
    WinGet, WA_ID, ID, A                                                      ; Stores the HWND of the active window for later use
    If WA_CheckNewTitles {
        WinGetTitle, WA_Title, ahk_id %WA_ID%
        SetTitleMatchMode, %WA_TitleMatchMode%                                ; Turn off type 3 title match mode
    }
    Loop %WA_Count%                                                           ; Loop through each row of the table and see if the active window is one that you are searching for.
    {
        IfWinActive, % WA_%A_Index%_1 " ahk_id " WA_ID, % WA_%A_Index%_2      ; If WinActive() for this criteria evaluates true, then take action
        {
            If WA_%A_Index%_3 = A                                             ; If a window of category A has been found, remember this. This will allow the function to repeat the action once it tests for other criteria.
                FoundA := True
            Else If WA_%A_Index%_3 = C                                        ; If the category of this window is C and it's been seen before, then ignore it
            {
                If InStr(WA_Excluded, "," A_Index WA_ID ",")
                    Continue
                WA_Excluded .= A_Index WA_ID ","                              ; Otherwise add its HWND to the list of excluded windows and then take the specified action
            }
            Else If Holds =                                                   ; If it is not A or C it is B
                Holds := WA_ID "," A_Index ","                                ; %Holds% remembers which B actions have triggered for this instance of the specific window
            Else If (InStr(Holds, WA_ID) = 1) and InStr(Holds, "," A_Index ",")
                Continue
            Else
                Holds .= A_Index ","
            Do(WA_%A_Index%_4)                                                ; Carry out the action
            Sleep 50                                                          
        }
    }
    If FoundA                                                                 ; If there was an A action that matched, return to the top so it can be repeated
        Continue
    If WA_CheckNewTitles                                                      ; If title changes are being monitored, set the title match mode to be EXACT
        SetTitleMatchMode, 3
    WinWaitNotActive, %WA_Title% ahk_id %WA_ID%                               ; Wait for the title to change, or for a different window to become active.
    Holds =                                                                   ; Reset holds when the window changes
}                                                                             ; Start over

;==================================================Required function==================================================

Do(Action)
{
    Loop, Parse, Action, CSV
    {
        Transform, Cmd, Deref, %A_LoopField%
        Func := False, RegExMatch(Cmd, "s)^(?P<Name>\w*)(?:\((?P<Func>.*?)\)|,? ?(?P<Input>.*))$", Cmd)
        If CmdFunc and IsFunc(CmdName)
        { ; Will attempt to use a function if you used function notation, i.e. Command(Input1,Input2,Input3)
            Loop, Parse, CmdFunc, CSV
                CmdFunc%A_Index% := A_LoopField, n := A_Index
            Func := True, FuncReturn := !n ? %CmdName%() : n = 1 ? %CmdName%(CmdFunc1) : n = 2 ? %CmdName%(CmdFunc1, CmdFunc2) : n = 3 ? %CmdName%(CmdFunc1, CmdFunc2, CmdFunc3) : n = 4 ? %CmdName%(CmdFunc1, CmdFunc2, CmdFunc3, CmdFunc4) : n = 5 ? %CmdName%(CmdFunc1, CmdFunc2, CmdFunc3, CmdFunc4, CmdFunc5) : n = 6 ? %CmdName%(CmdFunc1, CmdFunc2, CmdFunc3, CmdFunc4, CmdFunc5, CmdFunc6) : n = 7 ? %CmdName%(CmdFunc1, CmdFunc2, CmdFunc3, CmdFunc4, CmdFunc5, CmdFunc6, CmdFunc7) : IsFunc(@ := "Error") ? %@%("Too many parameters (" n ") passed to function " A_ThisFunc "()!") : ""
        }
        Else If CmdName = Run
        {
            Run, %CmdInput%, , UseErrorLevel
            If ErrorLevel
                ToolTip, %A_ScriptName%, Could not run "%CmdInput%"
        }
        Else If CmdName = Send
        {
            Send, %CmdInput%
        }
        Else If CmdName = SendInput
        {
            SendInput, %CmdInput%
        }
        Else If CmdName = Goto
        {
            SetTimer, %CmdInput%, -1
        }
        Else If CmdName = Gosub
        {
            Gosub, %CmdInput%
        }
        Else If CmdName = Sleep
        {
            Sleep, %CmdInput%
        }
        Else If IsLabel(RegExReplace(Cmd, "\s"))
        {
            SetTimer, % RegExReplace(Cmd, "\s"), -1
        }
        Else If IsFunc(CmdName)
        {
            %CmdName%(CmdInput)
        }
        Else
        {
            Send %Cmd%
        }
    }
    If Func
        Return FuncReturn
}
There are two important differences between this method and the SetTimer one that you should take note of:
  • Since it introduces a bit of an overhead which runs each time the active window changes, this method becomes more practical as the probability of encountering a desired window increases. I.e, if you only are looking for one window and it rarely shows up, then this might be wasteful.
  • This method only targets windows that are active. (Chris’s example closes any Outlook error window regardless of whether it is active or not.) However I find it’s rarely necessary to close non-active windows because they aren’t stealing focus from the current task. If these windows do eventually become active then their action will trigger at that time.
How to use it: Settings are defined in a table which is assigned to the variable %WindowActions%. Any number of tabs delimits the various rows.
  • The first column contains the WinTitle criteria that will define that row’s window. Treat this item exactly as you would treat the WinTitle parameter for any AutoHotkey command, for instance you may indicate an ahk_class or ahk_group .
  • The second column contains the WinText param. This column may be omitted. (Note that there is no ExcludeTitle or ExcludeText parameter.)
  • The third column contains either the letter A, B, C, or some other character.
    • If this column contains A then the action (fourth column) will happen continuously until the window is closed or no longer active.
    • If this column contains B then the action will happen once each time that window becomes active.
    • If this column contains C then the action will only happen once per window upon its first activation. (Windows are identified by their HWND.) Windows that exist when the script is loaded are automatically excluded.If this column contains anything besides a, b, or c, then that row is ignored. This can be useful for temporarily deactivating items – for instance, you can add an asterisk (A*) and that window will be ignored until you remove the asterisk.
  • The fourth column contains the action that will be performed. The format for the action is borrowed from my function MultiTap() – I’ve repackaged it into a new function with the very helpful name Do(). The text in the fourth column is passed to Do() and will be interpreted in one of several ways (see the function’s code for more details or to change this):
    • If the action is in the format Function(Params,In,CSV,Format) then the script will call that function with the respective comma-separated items for each parameter of that function.
    • If the action starts with Run and then a space, the script will run the following item. The same applies for Send, SendInput, Gosub, and Goto.
    • If the action is a label, or would be a label if any spaces were removed, then that label will have a timer of -1 set, which is basically the same as a Goto.
    • If none of the above criteria match, the script will try to run the item (as a filepath or url), and if that fails it will treat it as keystrokes and send those keystrokes.Note that while the action may resemble AutoHotkey code, it isn’t functional AutoHotkey code. As you can see from the examples, I generally put some keystrokes or a label if I want to do something more complex.
  • Within a subroutine you can refer to the variable %WinWaitWin% which will contain the current window’s HWND.
Some examples: Below are some working examples from my window criteria (which currently number 74). Note that I'm using SetTitleMatchMode, Regex because this allows for more precision filtering of windows. The criteria need to be precise because every window that gains focus will be evaluated.
The delimiter must be tabs, so if the code copies with spaces from the forum then you’ll need to correct it.


; 1) The first one is similar to Chris's example: it closes the message that occurs when the Windows Live Mail connection times out. Many dialog boxes with the title of "Windows Live Mail" come up so I need to specify some window text as well. When this window appears, the script will press {Enter} repeatedly until the window closes or loses focus, because it is in category A
; 2) This waits for the error message that sometimes comes up in my MButton script. It then reloads the script.
; 3) It annoys me when certain programs launch in a non-maximized window. This entry will maximize new windows of the given window classes. The label NewWinMax can be anywhere in your script.

WA_Definitions =
(
^Windows Live Mail$ ahk_class #32770               timed out      a    {Enter}
^MButton\.ahk$ ahk_class #32770                                   b    Run mbutton.ahk
ahk_class (IEFrame|WMPlayerApp|SOURCE_VIEWER)                     c    NewWinMax
)

NewWinMax: ; relevant subroutine
WinMaximize, ahk_id %WinWaitWin%
Return

Find me on the new AutoHotkey forums and send me a message if you have a question about any of the scrips I've posted to this forum!


MasterFocus
  • Moderators
  • 4323 posts
  • Last active: Jan 28 2016 01:38 AM
  • Joined: 08 Apr 2009
Related script:

WinTrigger: Watch [de]activation/[un]existance of windows... - <!-- l --><a class="postlink-local" href="http://www.autohotkey.com/community/viewtopic.php?f=2&t=63673">viewtopic.php?f=2&t=63673</a><!-- l -->

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Antonio França -- git.io -- github.com -- ahk4.net -- sites.google.com -- ahkscript.org

Member of the AHK community since 08/Apr/2009. Moderator since mid-2012.


LarryC
  • Guests
  • Last active:
  • Joined: --
Sorry, I cannot get this script to work, using a simple exercise with calculator and soundbeep as such:
(Carefully used Tab).

WindowActions = ; A tab-delimited table with four columns. The 2nd column may be omitted for any entries
(
Calculator A soundbeep
)



guest3456
  • Members
  • 1704 posts
  • Last active: Nov 19 2015 11:58 AM
  • Joined: 10 Mar 2011
well done

you could also hook the shell to receive a callback when the active window changes
thats an alternative instead of having an infinite loop
<!-- l --><a class="postlink-local" href="http://www.autohotkey.com/community/viewtopic.php?p=123323">viewtopic.php?p=123323</a><!-- l -->

berban_
  • Members
  • 202 posts
  • Last active: Aug 05 2014 11:52 PM
  • Joined: 16 Mar 2011

Sorry, I cannot get this script to work, using a simple exercise with calculator and soundbeep as such:
(Carefully used Tab).

WindowActions = ; A tab-delimited table with four columns. The 2nd column may be omitted for any entries
(
Calculator A soundbeep
)


Hey LarryC,
"Soundbeep" isn't by default recognized by my function "Do()". You'd need to create a label like this:
Soundbeep:
SoundBeep, 1000, 100
Return
Either that or you could add recognition for "Soundbeep" to the code for Do().



well done

you could also hook the shell to receive a callback when the active window changes
thats an alternative instead of having an infinite loop
<!-- l --><a class="postlink-local" href="http://www.autohotkey.com/community/viewtopic.php?p=123323">viewtopic.php?p=123323</a><!-- l -->


Wow.
No kidding I could also do it that way. In fact it seems a much better way to do it! Thanks for the heads up!
I'll post an update here when I update this with the message hook method :)

jjohnston2
  • Members
  • 13 posts
  • Last active: Aug 23 2015 07:20 AM
  • Joined: 03 Aug 2012

This code is great... thanks for sharing.

 

I had two issues getting this to run out of the box, presumably because there were partial updates.

 

  1. Without a #persistant flag, the main SetTimer -500 call will execute, followed immediately by Return, which makes the script execution end before the timer can execute the infinite do loop to keep itself alive.  I initially added a delay before the return call before realizing there was no persistent flag set... either one will work to keep the script from exiting immediately.
  2. The %WinWaitWin% variable referenced is now %WA_ID%, but wasn't updated in the describing text, or in the third set of code showing an example call.


capeably
  • Members
  • 61 posts
  • Last active: Jun 10 2016 06:50 AM
  • Joined: 18 May 2013

Thanks, I'm looking forward to trying this out.

 

I'm kind of new to AutoHotkey so I'm sorry if this is a stupid question.  Is it possible to use mouse movement as a contextual #IfWinActive hotkey that could trigger whatever it is you want to happen when a certain window appears?

 

Example, I'd like a custom Evernote Search GUI to show every time ahk_class ENMainFrame window is active. If just a general mouse movement when ahk_class ENMainFrame is active could be used as a trigger to restore the Custom Search GUI, that might be cool. I just tried this and it seemed to work okay-ish:

#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn  ; Enable warnings to assist with detecting common errors.
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.
#SingleInstance Force
SetTitleMatchMode, 2

#IfWinActive ahk_class ENMainFrame
~LButton::
WinActivate Evernote Search Menu ahk_class AutoHotkeyGUI
WinActivate ahk_class ENMainFrame
Return
#IfWinActive


Skrell
  • Members
  • 384 posts
  • Last active: Jul 07 2016 05:03 PM
  • Joined: 23 Aug 2011

I'm sorry for being daft but how is this framework different/better than simply using the regular WinWait command ?  Please explain as if i'm a noob :)



LarryC
  • Members
  • 68 posts
  • Last active: Aug 14 2016 07:53 PM
  • Joined: 28 Oct 2012

Well, I am no expert, I am thinking the WinWait command will do just that, wait and wait and wait. Nothing else can be done, script is waiting. If no window shows up, nothing else can be done. To use WinWait for another window, you would have to launch another script. I think the author addressed that "This method only targets windows that are active", and again, "Since it introduces a bit of an overhead which runs each time the active window changes, this method becomes more practical as the probability of encountering a desired window increases. I.e, if you only are looking for one window and it rarely shows up, then this might be wasteful". Just guessing, because I see the WinWait command has a timeout feature. Not sure if that was always there or changed over the time, as Chris put a lot of work into improving AHK over the years. I cannot locate the changelog.