I am trying to make an app in tkinter to visualize data structures and algorithms. However, I have an issue with the way the program is updating the canvas during the visualisation process. When the visualisation process takes more than ~2 seconds, the program freezes if the user clicks on the window (not responding), and resumes when the visualisation is complete. I am sure what causes this problem, and it doesn't arise if the user never clicks. Any ideas as to the actual problem and/or issue? The app is still in a testing phase, so it isn't super clean, apologies for that.
"""Module for visualizing data structures and algorithms"""
import random
import tkinter as tk
from tkinter import HORIZONTAL
def bubble(data, draw_data, speed):
data_length = len(data)
for i in range(data_length):
for j in range(0, data_length - i - 1):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
# if swapped then color becomes Green else stays Red
draw_data(data, ['Green' if x == j + 1 else 'Red' for x in range(len(data))])
window.after(int(speed * 1000), bubble, data, draw_data, int(speed * 1000))
# sorted elements generated with Green color
draw_data(data, ['Green' for _ in range(len(data))])
window = tk.Tk()
window.minsize(700, 580)
app_width, app_height = 700, 580
screen_width, screen_height = window.winfo_screenwidth(), window.winfo_screenheight()
mid_x = (screen_width - app_width) // 2
mid_y = (screen_height - app_height) // 2
window.title("StructViz")
window.iconbitmap("./assets/favicon.ico")
window.geometry(f'{app_width}x{app_height}+{mid_x}+{mid_y}')
select_alg = tk.StringVar()
data = []
def regenerate():
global data
minval = int(minEntry.get())
maxval = int(maxEntry.get())
sizeval = int(amountEntry.get())
data = []
for _ in range(sizeval):
data.append(random.randint(minval, maxval + 1))
draw_data(data, ['Red' for _ in range(len(data))])
def draw_data(data, colorlist):
structVizC.delete("all")
canvas_height = 380
canvas_width = 500
x_width = canvas_width / (len(data) + 1)
offset = 30
spacing = 10
normalized_data = [i / max(data) for i in data]
for i, height in enumerate(normalized_data):
x0 = i * x_width + offset + spacing
y0 = canvas_height - height * 340
x1 = ((i + 1) * x_width + offset)
y1 = canvas_height
structVizC.create_rectangle(x0, y0, x1, y1, fill=colorlist[i])
structVizC.create_text(x0 + 2, y0, anchor='se', text=str(data[i]))
window.update_idletasks()
def start_algorithm():
global data
bubble(data, draw_data, speedbar.get())
window.columnconfigure(0, weight=0)
window.columnconfigure(1, weight=45)
window.rowconfigure((0, 1), weight=1)
window.rowconfigure(2, weight=45)
navbarLB = tk.Listbox(window, selectmode=tk.SINGLE)
for item in ["Option 1", "Option 2", "Option 3"]:
navbarLB.insert(tk.END, item)
navbarLB.grid(row=0, column=0, rowspan=3, sticky='nsew')
userSettingsF = (tk.Frame(window, background='bisque2')
.grid(row=0, column=1, columnspan=2, rowspan=2, sticky='news', padx=7, pady=5))
# Change navbarLB.get(0) when user generates a selected option ( from func selected_item() )
AlgorithmL = tk.Label(userSettingsF, text=navbarLB.get(0), background='bisque2')
AlgorithmL.grid(row=0, column=1, sticky='nw', padx=10, pady=10)
amountEntry = tk.Scale(userSettingsF, from_=5, to=40, label='Amount', background='bisque2',
orient=HORIZONTAL, resolution=1, cursor='arrow')
amountEntry.grid(row=0, column=1, sticky='n', padx=10, pady=10)
minEntry = tk.Scale(userSettingsF, from_=0, to=10, resolution=1, background='bisque2',
orient=HORIZONTAL, label="Minimum Value")
minEntry.grid(row=1, column=1, sticky='s', padx=10, pady=10)
maxEntry = tk.Scale(userSettingsF, from_=10, to=100, resolution=1, background='bisque2',
orient=HORIZONTAL, label="Maximum Value")
maxEntry.grid(row=1, column=1, sticky='se', padx=10, pady=10)
speedbar = tk.Scale(userSettingsF, from_=0.10, to=2.0, length=100, digits=2, background='bisque2',
resolution=0.1, orient=HORIZONTAL, label="Speed")
speedbar.grid(row=0, column=1, sticky='ne', padx=10, pady=10)
tk.Button(userSettingsF, text="Start", bg="Blue", command=start_algorithm, background='bisque2').grid(
row=1, column=1, sticky='nw', padx=10, pady=10)
tk.Button(userSettingsF, text="Regenerate", bg="Red", command=regenerate, background='bisque2').grid(
row=1, column=1, sticky='sw', padx=10, pady=10)
structVizC = tk.Canvas(window, background='bisque2')
structVizC.grid(row=2, column=1, sticky='news', padx=5, pady=5)
window.mainloop()
During visualisation the app should display the process. On user click, it should do nothing. Instead, it freezes (not responding) until the process is complete.
GUI doesn't freeze when I use window.update()
instead of window.update_idletasks()
or when I use both together.
But real problem is bubble()
which runs window.after(...)
but it never exit bubble()
after window.after()
and it can't make delay before next drawing - so it can't control speed. And this also makes problem with update_idletasks()
.
It needs to exit nested loops but it makes problem how to jump back into nested loop. And this may need to use yield
which can exit function and later it can start function after yield
instead of starting from the beginning.
Here bubble()
with yield
but without window.after()
def bubble(data, draw_data):
data_length = len(data)
for i in range(data_length):
for j in range(0, data_length - i - 1):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
draw_data(data, ['Green' if x == j + 1 else 'Red' for x in range(data_length)])
yield
# sorted elements generated with Green color
draw_data(data, ['Green' for _ in range(len(data))])
# here python runs as default `return None`
And here function which runs bubble()
and it uses window.after()
generator = None
def repeater(data, draw_data, speed):
global generator
# run it only once - at start
if not generator:
generator = bubble(data, draw_data)
try:
# run function - it will raise `StopIteration` when it use `return` instead of `yield`
next(generator)
# set next execution after some time
window.after(speed, repeater, data, draw_data, speed)
# exit this function
return
except StopIteration: #
# reset value after last execution
generator = None
And now button starts repeater
instead of bubble
def start_algorithm():
repeater(data, draw_data, int(speedbar.get()*1000))
And now it can run with different speeds.
And it also works correctly without window.update()
and window.update_idletasks()
Full working code.
I increased Amount
to 100, and reduced Speed
to 0.01
- to see faster animation with more values.
I also added boolean variable running_animation
to stop animation when you press again button Start
during animation. But it works rather like pause
because when you press it again then it continues animation. I use it also to stop animation when you press Regenerate
but it needs some changes to work correctly.
import random
import tkinter as tk
# --- classes ---
# --- functions ---
def bubble(data, draw_data):
data_length = len(data)
for i in range(data_length):
#print('i:', i)
for j in range(0, data_length - i - 1):
#print('j:', j)
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
draw_data(data, ['Green' if x == j + 1 else 'Red' for x in range(data_length)])
#print('before yield')
yield
#print('after yield')
#print('finish')
# sorted elements generated with Green color
draw_data(data, ['Green' for _ in range(len(data))])
generator = None
def repeater(data, draw_data, speed):
global generator
if not generator:
generator = bubble(data, draw_data)
try:
#print('next')
next(generator)
if running_animation:
#print('after:', )
window.after(speed, repeater, data, draw_data, speed)
else:
generator = None
#print('return')
return
except StopIteration:
#print('StopIteration')
generator = None
def regenerate():
global data
global running_animation
print('regenerate')
if running_animation:
running_animation = False
minval = int(minEntry.get())
maxval = int(maxEntry.get())
sizeval = int(amountEntry.get())
data = []
for _ in range(sizeval):
data.append(random.randint(minval, maxval + 1))
draw_data(data, ['Red' for _ in range(len(data))])
def draw_data(data, colorlist):
print('draw')
structVizC.delete("all")
canvas_height = 380
canvas_width = 500
x_width = canvas_width / (len(data) + 1)
offset = 30
spacing = 10
normalized_data = [i / max(data) for i in data]
for i, height in enumerate(normalized_data):
x0 = i * x_width + offset + spacing
y0 = canvas_height - height * 340
x1 = ((i + 1) * x_width + offset)
y1 = canvas_height
structVizC.create_rectangle(x0, y0, x1, y1, fill=colorlist[i])
structVizC.create_text(x0 + 2, y0, anchor='se', text=str(data[i]))
#window.update_idletasks()
#window.update()
def start_algorithm():
global running_animation
if not running_animation:
running_animation = True
repeater(data, draw_data, int(speedbar.get()*1000))
start_button['text'] = 'Stop'
else:
running_animation = False
start_button['text'] = 'Start'
# --- main ---
running_animation = False
window = tk.Tk()
window.minsize(700, 580)
app_width, app_height = 700, 580
screen_width, screen_height = window.winfo_screenwidth(), window.winfo_screenheight()
mid_x = (screen_width - app_width) // 2
mid_y = (screen_height - app_height) // 2
window.title("StructViz")
#window.iconbitmap("./assets/favicon.ico")
window.geometry(f'{app_width}x{app_height}+{mid_x}+{mid_y}')
select_alg = tk.StringVar()
data = []
window.columnconfigure(0, weight=0)
window.columnconfigure(1, weight=45)
window.rowconfigure((0, 1), weight=1)
window.rowconfigure(2, weight=45)
navbarLB = tk.Listbox(window, selectmode=tk.SINGLE)
for item in ["Option 1", "Option 2", "Option 3"]:
navbarLB.insert(tk.END, item)
navbarLB.grid(row=0, column=0, rowspan=3, sticky='nsew')
userSettingsF = (tk.Frame(window, background='bisque2')
.grid(row=0, column=1, columnspan=2, rowspan=2, sticky='news', padx=7, pady=5))
# Change navbarLB.get(0) when user generates a selected option ( from func selected_item() )
AlgorithmL = tk.Label(userSettingsF, text=navbarLB.get(0), background='bisque2')
AlgorithmL.grid(row=0, column=1, sticky='nw', padx=10, pady=10)
amountEntry = tk.Scale(userSettingsF, from_=5, to=100, label='Amount', background='bisque2',
orient="horizontal", resolution=1, cursor='arrow')
amountEntry.grid(row=0, column=1, sticky='n', padx=10, pady=10)
minEntry = tk.Scale(userSettingsF, from_=0, to=10, resolution=1, background='bisque2',
orient="horizontal", label="Minimum Value")
minEntry.grid(row=1, column=1, sticky='s', padx=10, pady=10)
maxEntry = tk.Scale(userSettingsF, from_=10, to=100, resolution=1, background='bisque2',
orient="horizontal", label="Maximum Value")
maxEntry.grid(row=1, column=1, sticky='se', padx=10, pady=10)
speedbar = tk.Scale(userSettingsF, from_=0.01, to=1.0, length=100, digits=3, background='bisque2',
resolution=0.01, orient="horizontal", label="Speed")
speedbar.grid(row=0, column=1, sticky='ne', padx=10, pady=10)
start_button = tk.Button(userSettingsF, text="Start", bg="Blue", command=start_algorithm, background='bisque2')
start_button.grid(row=1, column=1, sticky='nw', padx=10, pady=10)
tk.Button(userSettingsF, text="Regenerate", bg="Red", command=regenerate, background='bisque2').grid(
row=1, column=1, sticky='sw', padx=10, pady=10)
structVizC = tk.Canvas(window, background='bisque2')
structVizC.grid(row=2, column=1, sticky='news', padx=5, pady=5)
window.mainloop()