Search code examples
pythonloopstkinterinfinite-loopcancellation

Unable to cancel infinite Tkinter (nested) after loop


TL;DR: I've got an after loop in Tkinter that I want to run indefinitely, but want to be able to kill if I want to restart the loop with new starting inputs. As each iteration of the loop calls the after function again, I can't work out how to get the correct after ID to the after_cancel function to kill the old loop when launching the new one.

More detailed post: So I'm trying to produce a (dot matrix) scrolling text bar via Tkinter for use in a virtual departure board. For this to work I need a given bit of text to scroll across the relevant part of the GUI indefinitely until an updated bit of text is ready to replace it. I've managed the scrolling indefinitely part just fine; however I'm having an issue stopping the old scroll ready for the new scroll to take it's place.

I feel like my problem is that once one of those loops gets going I can't easily 'communicate' with it; nor can my new loop 'tell' my old one to stop. In attempts to resolve this I tried having a couple toggle attributes within the GUI object, but these attempts failed (not to say it's not possible this way; just that I couldn't manage it). Further searching for a solution has lead to me feeling like the Tkinter function after_cancel is the likely solution, but I've been unable to implement this as the way I loop the text involves using the after function of Tkinter again (thus the after ID is constantly changing for the loop). I can't work this one out, and if I'm honest I'm unsure if it's me not fully getting the after and/or after_cancel functions, or if it's more that I'm looping my function in a poor way (i.e. whether I shouldn't be using the after function to achieve this).

Any help/advice would be much appreciated; and I'm more than happy any additional information if required.

Below is my minimum, reproducible example code. I've quickly put this together and it is very much like a simpler version of my main code (here I'm moving a square rather than text on an dot matrix display); this loops the functions in the same way as my real code, and has the same problem within it.

import tkinter as tk

class App(tk.Tk):

    def __init__(self, *args, **kwargs):

        tk.Tk.__init__(self, *args, **kwargs)

        self.background = "black"
        self.width = 600
        self.height = 150

        self.canvas = tk.Canvas(self, width=self.width, height=self.height, borderwidth=0, \
                                highlightthickness=0, background = self.background)

        self.canvas.pack(side="top", fill="both", expand="true")

        self.run_id_1 = self.after( 100, lambda: self.moving_square( "red", 50, 10 ) )

        self.run_id_2 = self.after( 5000, lambda: self.moving_square( "green", 200, 90, \
                                                                      prev_run_id = self.run_id_1 ) )

    def moving_square( self, color, x_top_left, y_top_left, last_square = None, prev_run_id = None ):

        if not prev_run_id == None:
            self.after_cancel( prev_run_id )

        if not last_square == None:
            self.canvas.delete( last_square )

        last_square = self.canvas.create_rectangle(x_top_left, y_top_left, x_top_left + 50, \
                                                   y_top_left + 50, outline=color, fill=color)

        if x_top_left + 50 < 0:
            self.after( 500, lambda: self.moving_square( color, self.width, y_top_left, last_square ) )
        else:
            self.after( 500, lambda: self.moving_square( color, x_top_left - 10, y_top_left, last_square ) )


if __name__ == "__main__":

    app = App( )
    app.mainloop()


Solution

  • Whenever you call the function again, you need to update that ID so that — as you suspected — you have a reference you can pass to after_cancel. You already store that reference as self.run_id_1, but you don't update one.

    The simplest way to do it would be to add a check if color == 'red' then set self.run_id_1 = self.after(...), and likewise for 'green' and self.run_id_2.

    However, I should note that the structure of your moving_square function is not ideal. One reason you might have found it hard to locate what to change is that it's already overly parametrized. You should either divide its work up a little, splitting into various functions that are more specific. Or you could create a table of data to store the relevant states and ids of the scrolling rows so you can streamline the logic.