I wrote this simple particle flow animation with Tkinter. I wanted to ask if there is some major improvements you can think of regarding the animation motor or the documentation of my code. I noticed that sometimes the "Stop animation" functionality takes some time to run, but I don't know how to improve it.
This is a university course project, and later we will add some user inputs (radius and length of the pipe, pressure difference and flow liquid) and implement calculations for the speeds of the rows, possibly changing the colour of the particles also depending on the flow liquid. This is why I'm deleting the particles and drawing them again when "Stop animation" is pressed.
Below you can see the code of my animation.
from tkinter import *
size = 15
gap = 5
columns = 25
rows = 7
downspace = 40 # space for the buttons below the animation
screenwidth = columns*size + (columns-2)*gap
screenheight = rows*size + rows*gap + downspace
color = "sky blue"
xspeed = [2, 3, 4, 5, 4, 3, 2] # list for the speeds of the particle rows
class Animation:
"""Class for the animation and buttons"""
def __init__(self):
"""
Creates the canvas, lines, buttons and runs the mainloop of the tkinter window
self.particlelist is a list for the rows of particles
"""
self.root = Tk()
self.canvas = Canvas(self.root, width=screenwidth, height=screenheight)
self.canvas.pack()
self.root.title("Laminar flow in a pipe")
self.canvas.create_line(0, gap, screenwidth, gap, width=2)
self.canvas.create_line(0, rows*size + (rows+2)*gap, screenwidth, rows*size + (rows+2)*gap, width=2)
self.particlelist = []
self.stop_animation = False
self.start = Button(self.root, text="Start animation", command=self.start_anim)
self.start.pack()
self.stop = Button(self.root, text="Stop animation", command=self.stop_anim)
self.stop.pack()
self.init()
self.root.mainloop()
def start_anim(self):
"""Starts the animation"""
self.stop_animation = False
self.start.configure(state=DISABLED)
self.stop.configure(state=NORMAL)
self.update()
def stop_anim(self):
"""
Stops the animation and deletes the particles
Runs the function init to draw new particles to the beginning positions
"""
self.stop_animation = True
self.start.configure(state=NORMAL)
self.stop.configure(state=DISABLED)
self.update()
for i in range(len(self.particlelist)):
xlist = self.particlelist[i]
for particle in xlist:
particle.delete()
self.particlelist[i] = []
self.init()
def init(self):
"""
Creates lists of particle rows and appends them to self.particlelist
* ypos: vertical postition of the particle
* xpos: horizontal position of the particle
"""
ypos = 10
for i in range(rows):
xpos = 0
xlist = []
for t in range(columns):
xlist.append(Particle(self.canvas, xpos, ypos, color, xspeed[i]))
xpos += size + gap
self.particlelist.append(xlist)
ypos += size + gap
def update(self):
"""
Updates the screen
* t: update interval in milliseconds
"""
t = 20
if self.stop_animation == False:
for i in range(len(self.particlelist)):
xlist = self.particlelist[i]
for particle in xlist:
particle.move()
self.canvas.after(t, self.update)
class Particle():
"""
Class for particles
Parameters:
* canvas: canvas from the class "Animation"
* xpos: x coordinate for creation of the shape
* ypos: y coordinate for creation of the shape
* color: fill color of the shape
* xspeed: speed of the shape to the horizontal direction
"""
def __init__(self, canvas, xpos, ypos, color, xspeed):
self.canvas = canvas
self.shape = self.canvas.create_oval(xpos, ypos, xpos+size, ypos+size, fill=color)
self.xspeed = xspeed
def move(self):
"""
Function for moving the shape.
* "pos" gives a vector of position [x0, y0, x1, y1] where 0 is the left upper corner
and 1 is the right down corner of the shape
"""
self.canvas.move(self.shape, self.xspeed, 0)
pos = self.canvas.coords(self.shape)
if pos[0] >= screenwidth: # returning the shape to the left side of the screen
overlap = (pos[0] - screenwidth)
self.canvas.coords(self.shape, gap-size+overlap, pos[1], gap+overlap, pos[3])
def delete(self):
"""Function for deleting the shape"""
self.canvas.delete(self.shape)
Animation()
Any comments are appreciated.
-
\$\begingroup\$ Thanks a lot for posting this, it served as a good starting point into my [own project]:(github.com/tpo/flow). As a critique of your code I'd propose to disambiguate symbols. I have added a "step" button to my project. That also calls "update", however it doesn't schedule the next automatic update. So I've renamed the "update" method to "step". "step" will schedule the next step if "self.loop" is set (which is ugly and calls for further refactoring). So the next rename would be "self.stop_animation" to "self.loop", which also changes semantics and makes them clearer. \$\endgroup\$Tomáš Pospíšek– Tomáš Pospíšek2019年09月22日 11:15:32 +00:00Commented Sep 22, 2019 at 11:15
1 Answer 1
Overview
The code layout is great, and you used meaningful names for classes, functions and variables. The docstrings are excellent.
UX
The GUI is excellent. It has a good title, and the 2 buttons have meaningful labels. There is no doubt about how to use this, which is quite rare for GUIs.
Naming
It is great that you gave names to you magic number constants. The PEP 8 style guide recommends constant names be all upper case:
size = 15
gap = 5
would be:
SIZE = 15
GAP = 5
It is common to pluralize array variable names. particlelist
might be better as particles
.
Variable screenwidth
is easier to read as screen_width
.
Simpler
The code in the stop_anim
function can be simplified
a little by eliminating the xlist
intermediate variable:
for i in range(len(self.particlelist)):
for particle in self.particlelist[i]:
particle.delete()
self.particlelist[i] = []
The same goes for the update
function.
Since the t
variable is not used inside this for
loop in the init
function:
for t in range(columns):
the _
placeholder can be used:
for _ in range(len(order)):
DRY
This expression is repeated twice in the Animate.__init__
function:
rows*size + (rows+2)*gap
You could assign it to a variable.
tkinter imports
The following line unnecessarily imports many unused items:
from tkinter import *
It is common to use the following:
import tkinter as tk
This requires you to prefix everything from tkinter
with tk
, such as:
self.root = tk.Tk()
self.canvas = tk.Canvas(self.root, width=screenwidth, height=screenheight)
However, the benefit is that the code is more self-documenting in the sense
that we don't need to guess where Canvas
came from.
Tools
You could run code development tools to automatically find some style issues with your code.
For example, ruff
finds:
E712 Avoid equality comparisons to `False`; use `if not self.stop_animation:` for false checks
|
| t = 20
|
| if self.stop_animation == False:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E712
| for i in range(len(self.particlelist)):
| xlist = self.particlelist[i]
|
= help: Replace with `not self.stop_animation`
I ran a spell-checker to find this typo in a docstring: "postition".