Interrupting a running function
This is a common question and this tutorial explains how to do it within the limitations of Autoit.
AutoIt queues function calls in both OnEvent and MessageLoop modes. This means that it waits until a function is finished and the code is back in your idle While...WEnd loop before running the next.
Here are 2 short scripts in both modes to show this in action. Start Function 1 by pressing its button and then immediately try to run Function 2 by pressing its button a couple of times. You will see from the console output that Function 2 will only run AFTER Function 1 has terminated - and then it will run as many times as you pressed the button.
We start with MessageLoop mode:
#include <GUIConstantsEx.au3>
$hGUI = GUICreate("Test", 500, 500)
$hButton_1 = GUICtrlCreateButton("Func One", 10, 10, 80, 30)
$hButton_2 = GUICtrlCreateButton("Func Two", 10, 50, 80, 30)
GUISetState()
While 1
Switch GUIGetMsg()
Case $GUI_EVENT_CLOSE
Exit
Case $hButton_1
_Func_1()
Case $hButton_2
_Func_2()
EndSwitch
WEnd
Func _Func_1()
For $i = 1 To 20
ConsoleWrite("-Func 1 Running" & @CRLF)
Sleep(100)
Next
ConsoleWrite(">Func 1 Ended" & @CRLF)
EndFunc
Func _Func_2()
For $i = 1 To 3
ConsoleWrite("+Func 2 Running" & @CRLF)
Sleep(100)
Next
ConsoleWrite(">Func 2 Ended" & @CRLF)
EndFunc
And now in OnEvent mode:
#include <GUIConstantsEx.au3>
Opt("GUIOnEventMode", 1)
$hGUI = GUICreate("Test", 500, 500)
GUISetOnEvent($GUI_EVENT_CLOSE, "_Exit")
$hButton_1 = GUICtrlCreateButton("Func One", 10, 10, 80, 30)
GUICtrlSetOnEvent($hButton_1, "_Func_1")
$hButton_2 = GUICtrlCreateButton("Func Two", 10, 50, 80, 30)
GUICtrlSetOnEvent($hButton_2, "_Func_2")
GUISetState()
While 1
Sleep(10)
WEnd
Func _Func_1()
For $i = 1 To 20
ConsoleWrite("-Func 1 Running" & @CRLF)
Sleep(100)
Next
ConsoleWrite(">Func 1 Ended" & @CRLF)
EndFunc
Func _Func_2()
For $i = 1 To 3
ConsoleWrite("+Func 2 Running" & @CRLF)
Sleep(100)
Next
ConsoleWrite(">Func 2 Ended" & @CRLF)
EndFunc
Func _Exit()
Exit
EndFunc
So is it impossible to interrupt a running function? Of course not or this tutorial would serve no purpose - but there are limitations on the type of function which can be interrupted.
If you are in OnEvent mode then there is an easy way to interrupt a running function, as long as the function is started within the main code and not by an OnEvent call. This script shows how to do it by using a flag and checking if it is set - in this way the function is started by the main script and can be interrupted.
#include <GUIConstantsEx.au3>
; Declare a flag
Global $fRunOne = False
Opt("GUIOnEventMode", 1)
$hGUI = GUICreate("Test", 500, 500)
GUISetOnEvent($GUI_EVENT_CLOSE, "_Exit")
$hButton_1 = GUICtrlCreateButton("Func One", 10, 10, 80, 30)
GUICtrlSetOnEvent($hButton_1, "_Func_1")
$hButton_2 = GUICtrlCreateButton("Func Two", 10, 50, 80, 30)
GUICtrlSetOnEvent($hButton_2, "_Func_2")
GUISetState()
While 1
Sleep(10)
; Check if the flag has been set by the OnEvent function
If $fRunOne Then
; Now start the "real" function from within the main code
_Func_1_Run()
EndIf
WEnd
Func _Func_1()
; Set the flag within the OnEvent function
$fRunOne = True
EndFunc ;==>_Func_1
Func _Func_1_Run()
For $i = 1 To 20
ConsoleWrite("-Func 1 Running" & @CRLF)
Sleep(100)
Next
ConsoleWrite(">Func 1 Ended" & @CRLF)
Global $fRunOne = False
EndFunc ;==>_Func_1_Run
Func _Func_2()
For $i = 1 To 3
ConsoleWrite("+Func 2 Running" & @CRLF)
Sleep(100)
Next
ConsoleWrite(">Func 2 Ended" & @CRLF)
EndFunc ;==>_Func_2
Func _Exit()
Exit
EndFunc ;==>_Exit
However, this method does not work in MessageLoop mode, nor when you want to interrupt a function which was started by an OnEvent call in OnEvent mode. In these cases the function you want to interrupt must have regular checks of a flag which we set when we want to interrupt. If the function does not have a suitable loop where such a check is easy to make, you might not be able to interrupt it.
Let us now look at the 2 ways an AutoIt script can break into a suitable function: a HotKey and a Windows message handler. The first is easier to code, but cannot be linked to a control on your GUI; the latter is a little more complicated to code, but can be linked to a control.
How do they work?
Let us start with the HotKey. From the AutoIt Help file: A hotkey-press *typically* interrupts the active AutoIt function/statement and runs its user function until it completes or is interrupted. So a HotKey press is designed to interrupt your running function - just what we need. But the running function will resume after the HotKey function terminates - so, as explained above, we use the HotKey function to set a flag which is looked for by our running function. Remember that this flag must be declared as a Global variable because it needs to be seen by both functions.
Now many coders do not like using HotKeys because they remain active while a script is running and can interfere with other apps. One way to get around this is to use Accelerator keys - these look very much like HotKeys to the user, but are only active in the script in which they are declared. Although Accelerator keys look like HotKeys, they are tied to GUI controls and you must use the message handler method explained below if you want to use them to interrupt a running function.
The Windows message handler method is a little more complicated and if you are not used to working with them, you might want to read the GUIRegisterMsg tutorial first.
What we do here is intercept the message that your GUI control sends when it is activated - the same message that AutoIt queues to run the required code once the running function terminates. We use our own message to get a sneak preview of this queuing and use it to set a similar flag to that described above. If our running function sees the flag, it terminates and AutoIt then runs the function waiting in the queue - simple really!
Here are scripts using both methods to show how HotKeys, Accelerator keys and GUI controls can interrupt a running function. Just as before, start Function_1 by pressing its button - but now you can interrupt it at will!
First in MessageLoop mode:
#include <GUIConstantsEx.au3>
#include <WindowsConstants.au3>
; Set a HotKey
HotKeySet("x", "_Interrupt")
; Declare a flag
$fInterrupt = 0
$hGUI = GUICreate("Test", 500, 500)
$hButton_1 = GUICtrlCreateButton("Func One", 10, 10, 80, 30)
$hButton_2 = GUICtrlCreateButton("Func Two", 10, 50, 80, 30)
; Create a dummy control for the Accelerator to action when pressed
$hAccelInterupt = GUICtrlCreateDummy()
; Set an Accelerator key to action the dummy control
Dim $AccelKeys[1][2]=[ ["z", $hAccelInterupt] ]
GUISetAccelerators($AccelKeys)
GUISetState()
; Intercept Windows command messages with out own handler
GUIRegisterMsg($WM_COMMAND, "_WM_COMMAND")
While 1
Switch GUIGetMsg()
Case $GUI_EVENT_CLOSE
Exit
Case $hButton_1
_Func_1()
Case $hButton_2
_Func_2()
EndSwitch
WEnd
Func _Func_1()
; Make sure the flag is cleared
$fInterrupt = 0
For $i = 1 To 20
ConsoleWrite("-Func 1 Running" & @CRLF)
; Look for the flag
If $fInterrupt <> 0 Then
; The flag was set
Switch $fInterrupt
Case 1
ConsoleWrite("!Func 1 interrrupted by Func 2" & @CRLF)
Case 2
ConsoleWrite("!Func 1 interrrupted by HotKey" & @CRLF)
Case 3
ConsoleWrite("!Func 1 interrrupted by Accelerator" & @CRLF)
EndSwitch
Return
EndIf
Sleep(100)
Next
ConsoleWrite(">Func 1 Ended" & @CRLF)
EndFunc
Func _Func_2()
For $i = 1 To 3
ConsoleWrite("+Func 2 Running" & @CRLF)
Sleep(100)
Next
ConsoleWrite(">Func 2 Ended" & @CRLF)
EndFunc
Func _Interrupt()
; The HotKey was pressed so set the flag
$fInterrupt = 2
EndFunc
Func _WM_COMMAND($hWnd, $Msg, $wParam, $lParam)
; The Func 2 button was pressed so set the flag
If BitAND($wParam, 0x0000FFFF) = $hButton_2 Then $fInterrupt = 1
; The dummy control was actioned by the Accelerator key so set the flag
If BitAND($wParam, 0x0000FFFF) = $hAccelInterupt Then $fInterrupt = 3
Return $GUI_RUNDEFMSG
EndFunc ;==>_WM_COMMAND
And now in OnEvent mode:
#include <GUIConstantsEx.au3>
#include <WindowsConstants.au3>
; Set a HotKey
HotKeySet("x", "_Interrupt_HotKey")
; Declare a flag
$fInterrupt = 0
Opt("GUIOnEventMode", 1)
$hGUI = GUICreate("Test", 500, 500)
GUISetOnEvent($GUI_EVENT_CLOSE, "_Exit")
$hButton_1 = GUICtrlCreateButton("Func One", 10, 10, 80, 30)
GUICtrlSetOnEvent($hButton_1, "_Func_1")
$hButton_2 = GUICtrlCreateButton("Func Two", 10, 50, 80, 30)
GUICtrlSetOnEvent($hButton_2, "_Func_2")
; Create a dummy control for the Accelerator to action
$hAccelInterupt = GUICtrlCreateDummy()
GUICtrlSetOnEvent($hAccelInterupt, "_Interrupt_Accel")
; Set an Accelerator key to action the dummy control
Dim $AccelKeys[1][2]=[ ["z", $hAccelInterupt] ]
GUISetAccelerators($AccelKeys)
GUISetState()
; Intercept Windows command messages with out own handler
GUIRegisterMsg($WM_COMMAND, "_WM_COMMAND")
While 1
Sleep(10)
WEnd
Func _Func_1()
; Make sure the flag is cleared
$fInterrupt = 0
For $i = 1 To 20
ConsoleWrite("-Func 1 Running" & @CRLF)
; Look for the flag
If $fInterrupt <> 0 Then
; The flag was set
Switch $fInterrupt
Case 1
ConsoleWrite("!Func 1 interrrupted by Func 2" & @CRLF)
Case 2
ConsoleWrite("!Func 1 interrrupted by HotKey" & @CRLF)
Case 3
ConsoleWrite("!Func 1 interrrupted by Accelerator" & @CRLF)
EndSwitch
Return
EndIf
Sleep(100)
Next
ConsoleWrite(">Func 1 Ended" & @CRLF)
EndFunc
Func _Func_2()
For $i = 1 To 3
ConsoleWrite("+Func 2 Running" & @CRLF)
Sleep(100)
Next
ConsoleWrite(">Func 2 Ended" & @CRLF)
EndFunc
Func _Exit()
Exit
EndFunc
Func _Interrupt_HotKey()
; The HotKey was pressed so set the flag
$fInterrupt = 2
EndFunc
Func _Interrupt_Accel()
; This is an empty function for the dummy control linked to the Accelerator key
EndFunc
Func _WM_COMMAND($hWnd, $Msg, $wParam, $lParam)
; The Func 2 button was pressed so set the flag
If BitAND($wParam, 0x0000FFFF) = $hButton_2 Then $fInterrupt = 1
; The dummy control was actioned by the Accelerator key so set the flag
If BitAND($wParam, 0x0000FFFF) = $hAccelInterupt Then $fInterrupt = 3
Return $GUI_RUNDEFMSG
EndFunc ;==>_WM_COMMAND
But what if you want to have a longish Sleep or some other function which waits for a fair time inside the running function? If you add a Sleep(5000) to Func_1 in the above examples, you will see that you have to wait until the function returns from the Sleep before the interrupt is actioned, which is not what we want:
Func _Func_1()
; Make sure the flag is cleared
$fInterrupt = 0
For $i = 1 To 20
ConsoleWrite("-Func 1 Running" & @CRLF)
; Add a long Sleep ; <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
Sleep(5000)
; Look for the flag
If $fInterrupt <> 0 Then
; The flag was set
Switch $fInterrupt
Case 1
ConsoleWrite("!Func 1 interrrupted by Func 2" & @CRLF)
Case 2
ConsoleWrite("!Func 1 interrrupted by HotKey" & @CRLF)
Case 3
ConsoleWrite("!Func 1 interrrupted by Accelerator" & @CRLF)
EndSwitch
Return
EndIf
Sleep(100)
Next
ConsoleWrite(">Func 1 Ended" & @CRLF)
EndFunc ;==>_Func_1
The answer is to put the blocking function inside a wrapper function which does check for the interrupt flag at regular intervals. Replace the Sleep line with a call to the wrapper function and add the wrapper function to the script:
; Use a wrapper function in place of the blocking function
Func _Func_1()
; Make sure the flag is cleared
$fInterrupt = 0
For $i = 1 To 20
ConsoleWrite("-Func 1 Running" & @CRLF)
; Run a modified Sleep function which checks for the interrupt
If _Interrupt_Sleep(5000) Then ; <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
; The flag was set
Switch $fInterrupt
Case 1
ConsoleWrite("!Func 1 interrrupted by Func 2" & @CRLF)
Case 2
ConsoleWrite("!Func 1 interrrupted by HotKey" & @CRLF)
Case 3
ConsoleWrite("!Func 1 interrrupted by Accelerator" & @CRLF)
EndSwitch
Return
EndIf
Sleep(100)
Next
ConsoleWrite(">Func 1 Ended" & @CRLF)
EndFunc ;==>_Func_1
; And here is the wrapper function itself
Func _Interrupt_Sleep($iDelay)
; Get a timestamp
Local $iBegin = TimerInit()
; And run a tight loop
Do
; Use a minimum Sleep (could also be a WinWaitActive with a short timeout)
Sleep(10)
; Look for the interrrupt
If $fInterrupt Then
; And return True immediately if set
Return True
EndIf
Until TimerDiff($iBegin) > $iDelay
; Return False if timed out and no interrupt was set
Return False
EndFunc ;==>_Interrupt_Sleep
So there are the ways available in AutoIt to interrupt a running function. Remember that you must be in OnEvent mode and start the function from the main code - or have a function which checks internally whether it ought to allow an interruption. You may need to use a wrapper function if you want to use a blocking function such as Sleep or WinWaitActive. In all other cases you are are not going to be able to interrupt your function.