""" Example showing for tkinter and ttk how to do: -- Simple animation -- on a tkinter Canvas. References: -- https://effbot.org/tkinterbook/canvas.htm This is the simplest explanation, but very old and possibly somewhat out of date. Everywhere that it says "pack" use "grid" instead. -- The tkinter.pdf document in this project. This is by far the most complete reference work for tkinter and ttk. It is for reference, NOT a tutorial. -- https://tkdocs.com/tutorial/canvas.html This is a more complete and up-to-date tutorial than the one above. It shows each example in four different languages. Python is the fourth (last) one. Ignore the other-language examples. The key ideas are: 1. Drawing (and hence animation) is on a tkinter.Canvas. 2. You put an object onto a Canvas with: id = canvas.create_XXX(POSITION, OTHER-OPTIONS) where XXX can be any of: oval, arc, bitmap, image, line, polygon, rectangle, text, window, and where the specifics of POSITION and OTHER-OPTIONS depends on the type of object being created. See the example in the code below for an oval. See the above reference work for details on other types. 3. The ID returned by a call to create_XXX is how you keep track of objects on a Canvas for future animation (movements, color changes, etc.). 4. There are three basic methods for animating (changing) an object. Each method is a Canvas method whose first argument is the ID of the object on the Canvas. You can: a. MOVE an object BY a given amount by: canvas.move(ID, delta_x, delta_y) b. MOVE an object TO a certain position by: canvas.coords(ID, NEW_POSITION ...) where the specifics of NEW_POSITION depend on the type of the object. c. CHANGE OTHER CHARACTERISTICS of objects as in this example: canvas.coords(ID, fill="blue") # Changes the fill color to "blue" The specifics of what you can change (and how) depends on the type of object. See the above reference work for details. 5. You must FIRST construct everything needed for the animation, and THEN do the root.mainloop() to start the GUI running. The code below shows one way to accomplish that, using this structure: a. The main method constructs and then starts an Animation object. b. The Animation object constructs the GUI, passing itself to the GUI so that the GUI can later ask the Animation to do stuff. c. The GUI contains: -- The one-and-only tkinter.Tk object. -- Frame(s) and other widgets as desired. -- A tkinter.Canvas on a Frame. d. When the GUI is constructed, you include all the tkinter/ttk code that you have seen in previous examples EXCEPT not (yet) the root.mainloop() e. The GUI includes a start method that contains: root.mainloop() f. The Animation object (which constructed the GUI) calls the GUI's start method to start the animation running. g. The Animation object has a method: run_one_cycle that makes all the changes to all the objects in the Animation, for ONE cycle of the animation, by using the Canvas methods: move coords itemconfigure The Animation has access to the Canvas because the Animation constructed (and stores) the GUI, and the GUI makes and stores the Canvas. h. The Animation's run_one_cycle method is called repeatedly BY THE GUI as follows, all in the GUI class: def __init__(self, animation): self.animation = animation self.root = tkinter.Tk() ... self.root.after(1000, self.animation_loop) def animation_loop(self): self.animation.run_one_cycle() self.root.after(10, self.animation_loop) The after method sets a TIMER that is triggered after the given number of milliseconds (1000 ms in the first call to after in the above, and 10 ms in the second call to after). Because it is a TIMER, Tkinter is able to react to button presses and other stuff while the TIMER is waiting to ring its alarm. When the TIMER rings its alarm, it calls the second argument to the after method, which is self.animation_loop in the above. So, self.animation_loop is called the first time after 1 second (1000 ms), and it runs one cycle of the animation at that time. Thereafter it repeatedly: -- Waits 10 ms (via a TIMER that allows other stuff to happen) -- Calls animation_loop again -- Runs one cycle of the animation. In the actual code below, instead of running every 10 ms, it runs every animation.cycle_ms, so that the Animation object can control the "refresh rate" of the animation. See the code below for an example that uses the above structure. While you are not REQUIRED to use the same structure, it is probably a good idea to do so for any video-game style game. This example does NOT include any message-passing with MQTT to other computers. Other examples cover that topic. SEE THE UML CLASS DIAGRAM include with this project. Authors: David Mutchler and his colleagues at Rose-Hulman Institute of Technology. """ import random import tkinter from tkinter import ttk def main(): animation = Animation() animation.start() class Animation(object): """ An animation of Ball objects (per the Ball class defined below). """ def __init__(self): # Construct the GUI, which constructs and stores a Canvas. # Store that Canvas in THIS object too, so that animated objects can # act upon it. Here, our animated objects are all Ball objects, # stored in the self.balls list, which starts with a single Ball. # Each Ball needs to have the Canvas so that the Ball can change its # position and fill color (and anything else it might want to change). self.gui = GUI(self) self.canvas = self.gui.canvas ball = Ball(self.canvas) # Note how each Ball gets the Canvas self.balls = [ball] self.cycle_ms = 10 # Run an animation step every 10 ms (approximately) def start(self): # Called after the GUI, the Animation, and all the animated objects # are constructed. The GUI's start method starts the mainloop # in which the program remains for the remainder of its run. self.gui.start() def run_one_cycle(self): """ Must make whatever changes animated objects need to make on the Canvas, for one iteration (cycle) of the animation loop. """ # One out of every 200 cycles, make a new Ball. r = random.randrange(1, 201) # r is between 1 and 200, inclusive if r == 1: self.balls.append(Ball(self.canvas)) # Animate each ball. for ball in self.balls: ball.run_one_cycle() class GUI(object): def __init__(self, animation): """ Stores the given Animation object in order to call the Animation object's run_one_cycle method repeatedly, by using root.after(...) Constructs all the GUI widgets, but does NOT (yet) call root.mainloop. :type animation: Animation """ self.animation = animation # The usual Tk and Frame objects, plus any other widgets you want. self.root = tkinter.Tk() self.frame = ttk.Frame(self.root, padding=10) self.frame.grid() self.canvas = self.make_canvas() # Starts the animation loop AFTER 1000 ms (i.e., 1 second). self.root.after(1000, self.animation_loop) def make_canvas(self): canvas_width = 400 canvas_height = 300 canvas = tkinter.Canvas(self.frame, width=canvas_width, height=canvas_height) canvas.width = canvas_width canvas.height = canvas_height canvas.grid() return canvas def start(self): # Called by the Animation object when the program is ready to enter the # Tk object's mainloop and remain there for the remainder of the run. self.root.mainloop() def animation_loop(self): # Tells the Animation to run one cycle of the animation. # Then sets up a timer to call this same method again after a few ms. self.animation.run_one_cycle() self.root.after(self.animation.cycle_ms, self.animation_loop) class Ball(object): def __init__(self, canvas): """ The Ball needs the Canvas so that it can update its characteristics (position, fill color, etc) as the animation runs. :type canvas: tkinter.Canvas """ self.canvas = canvas # Set the characteristics of the Ball: # specific x, y and diameter, with a random color. x = 200 y = 200 self.diameter = 20 self.colors = ["red", "green", "blue"] r = random.randrange(len(self.colors)) self.color = self.colors[r] # Make the item on the Canvas for drawing the Ball, storing its ID # for making changes to the Ball (moving it, changing color, etc.). # Here, each Ball is a filled circle (actually an oval), # defined by its upper-left and lower-right corners. self.id = self.canvas.create_oval(x, y, x + self.diameter, y + self.diameter, fill=self.color) def run_one_cycle(self): """ Illustrates the 3 basic ways to change (animate) an item. """ # Move RED balls BY a small random amount # (using the Canvas move method): if self.color == "red": delta_x = random.randrange(-5, 6) # Between -5 and 5, inclusive delta_y = random.randrange(-2, 3) # Between -2 and 2, inclusive self.canvas.move(self.id, delta_x, delta_y) # Move GREEN balls TO a certain position, randomly inside a box near # the upper-left of the window (using the Canvas coords method): elif self.color == "green": x = random.randrange(50, 101) # Between 50 and 100, inclusive y = random.randrange(20, 41) # Between 20 and 40, inclusive self.canvas.coords(self.id, x, y, x + self.diameter, y + self.diameter) # Change balls to a random color, every 100 cycles or so, # about once a second (using the Canvas itemconfigure method): r1 = random.randrange(1, 101) # Random between 1 and 100, inclusive if r1 == 1: r2 = random.randrange(len(self.colors)) self.color = self.colors[r2] self.canvas.itemconfigure(self.id, fill=self.color) main()