0

So I am making a program that has a timer and the timer works, now I am working with a pause function. After some research, I found a function called after_cancel. This function supposedly should cancel the after function as the after function in this situation creates an infinite loop. How do I use the after_cancel properly in this situation or are there any other possible solutions?

Thanks in advance.

t = 60000

global timerState
timerState = True

def pause():
    timerLabel.after_cancel(countdown)
    timerState = False
    timerButton.config(text="Play", command=countdown)


def countdown():
    global t

    if t == 0:
        timer = "00:00"
        timerLabel.config(text=timer)
        return

    if timerState == False:
        timerLabel.after_cancel(countdown)
        timerButton.config(text="Play", command=countdown)
        return

    mins = t / 60000

    secs = t / 1000
    secs = secs - int(mins) * 60

    mills = t

    mills = mills - int(secs) * 1000



    if timerState == True:
        timer = "{:02d}:{:02d}".format(int(mins),int(secs))
        timerLabel.config(text=timer)
        t -= 1
        timerLabel.after(1, countdown)

        timerButton.config(text="Pause", command=pause)

2 Answers2

1

Most of the time .after_cancel scripts can be avoided by just using if statements. For example look at this:

import tkinter as tk

t = 60000

def pause():
    global timerState
    timerState = False
    timerButton.config(text="Play", command=start_countdown)

def start_countdown():
    global timerState
    timerState = True
    timerButton.config(text="Pause", command=pause)
    countdown()

def countdown():
    global t

    if timerState:
        timerLabel.config(text=t)
        t -= 1
        if t > 0:
            timerLabel.after(1, countdown)


root = tk.Tk()

timerLabel = tk.Label(root, text="")
timerLabel.pack()

timerButton = tk.Button(root, text="Play", command=start_countdown)
timerButton.pack()

root.mainloop()

I modified your code to show t without making it in the mm:ss format. The main point is that if timerState is False the timerLabel.after(1, countdown) will never be called so there is no point to having a .after_cancel.

Note: You haven't considered the time taken for your other code so t isn't really in milliseconds (at least for my slow computer).

TheLizzard
  • 7,248
  • 2
  • 11
  • 31
  • That's another way to do it, but I'm still gonna use after_cancel as it is way simpler. Thanks anyways! – Dave Wilson Jul 08 '21 at 11:26
  • @DaveWilson I don't want to argue but using `if` statements is faster and more intuitive for me at least. – TheLizzard Jul 08 '21 at 11:29
  • Yes I agree, for next projects most likely I will use if statements. But because in my situation its just easier to use the ```.after_cancel()``` function. – Dave Wilson Jul 08 '21 at 11:32
0

Here is a demonstration of after and after_cancel

Every after needs to be cancelled in order to clear the event queue.

In this program, each time the button is pressed a time delay event is generated. The event ID is stored in self.after_time

I have set the delay value to increase by 100 ms with each button press, for demo purposes. it withdraws the master from view.

When the time delay event is complete it calls self.action

self.action cancels the event with after_cancel( self.after_time ) and the master is made visible, ready for the next button press.


import tkinter

class after_demo:

    delay = 100

    def __init__( self ):

        self.master = tkinter.Tk()
        self.master.title( 'After Demo' )
        self.control = tkinter.Button(
            self.master, text = 'Begin Demo',
            width = 40, command = self.pause )
        self.control.grid(row=0,column=0,sticky='nsew')

    def action( self ):

        self.master.after_cancel( self.after_time )
        self.control[ 'text' ] = 'Delay( {} ) ms'.format( self.delay )

        self.master.deiconify()
        
    def pause( self ):

        self.after_time = self.master.after( self.delay, self.action )
        self.delay += 100

        self.master.withdraw()

if __name__ == '__main__':

    timer = after_demo( )
    tkinter.mainloop()
Derek
  • 1,916
  • 2
  • 5
  • 15
  • From what I know you do not need to cancel `.after` scripts if they are being executed/have been executed. – TheLizzard Jul 08 '21 at 11:30
  • I think it's good programming practice. Although if it's only used once... – Derek Jul 08 '21 at 12:03
  • I am pretty sure that `tkinter` will clean up after itself when it is done with the `.after` script. The `.after_cancel` is there to stop future `.after` scripts that haven't ran yet. I will check this. – TheLizzard Jul 08 '21 at 12:06
  • Just checked. `tkinter` deletes the commands after they are executed. To test it add `print(timerLabel._tclCommands)` to the start of the `countdown` function in my answer. So there is no need to call `.after_cancel` if the command has been/is being executed. – TheLizzard Jul 08 '21 at 12:11
  • `after` creates a queue that `after_cancel` clears. This can be shown if instead of storing the `after event` as a single variable you append it to a list then view the list. Without `after_cancel` it just grows and grows. – Derek Jul 08 '21 at 12:12
  • Even if you use `after_cancel`, it will still grow. Try it. A object (like the `.after` id) can't delete itself in python so the list that you create will keep growing no matter what you do. If you leave my answer running, the memory usage will no increase. Also look [here](https://stackoverflow.com/a/10599462/11106801) and [here](https://stackoverflow.com/a/34029360/11106801). They never call `.after_cancel`. – TheLizzard Jul 08 '21 at 12:55
  • For even stronger proof look [here](https://github.com/python/cpython/blob/a3739b207adb5d17e3b53347df05b54b1a8b87f0/Lib/tkinter/__init__.py#L841). It's the official cpython code. It calls `self.deletecommand(name)` after it calls your function (`func(*args)`). So there is no need for the `.after_cancel`. `tkinter` will delete the command without you doing anything. – TheLizzard Jul 08 '21 at 12:58
  • If that's the case then what is the purpose of `after_cancel` ? – Derek Jul 08 '21 at 12:58
  • Is the function in the `.after` script hasn't been called, you can use `.after_cancel` to cancel it. Try this: `root = tk.Tk(); id = root.after(5000, lambda: print("aaa")); root.after_cancel(id)`. You will notice that nothing is printed on the screen because it is cancelled before being called – TheLizzard Jul 08 '21 at 13:01
  • I see what you're saying @TheLizzard but the question remains, what is the purpose of `after_cancel` is it redundant? – Derek Jul 08 '21 at 13:17
  • 1
    No. It has 1 use only. If you call `.after`, then for some reason you change your mind **before** the function is called you can cancel it. Think of it as a schedule. You can cancel events that are going to happen in the future but there is no point in cancelling events that have already happened. It's very rare that you would need to cancel an event if you are smart about your scheduling. That is why I said that *"Most of the time `.after_cancel` scripts can be avoided by just using `if` statements."* – TheLizzard Jul 08 '21 at 13:20