I’m building a Tkinter app where I visualize sorting algorithms on a Canvas. The problem: whenever I run the sorting function, the entire UI freezes until the function finishes.
Here’s a simplified version of my code:
import tkinter as tk
import time
def bubble_sort(canvas, data):
n = len(data)
for i in range(n):
for j in range(0, n-i-1):
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
draw_data(canvas, data)
time.sleep(0.1) # <-- freezes the UI
def draw_data(canvas, data):
canvas.delete("all")
for i, val in enumerate(data):
canvas.create_rectangle(i*20, 200-val, (i+1)*20, 200, fill="blue")
def start_sort():
bubble_sort(canvas, [5,3,8,4,2])
root = tk.Tk()
canvas = tk.Canvas(root, width=200, height=200)
canvas.pack()
btn = tk.Button(root, text="Start", command=start_sort)
btn.pack()
root.mainloop()
Issues I face:
The UI freezes during sorting, and I can’t interact with the window.
If I try after() instead of sleep, I get confused about how to rewrite the nested loops.
I’ve seen people suggest using threads, but then I run into errors when updating the Canvas from a thread (since Tkinter isn’t thread-safe).
What’s the best practice in Tkinter for running long tasks (like sorting) while keeping the GUI responsive?
Should I restructure the algorithm to work with after() instead of sleep?
Or is it better to use threads/processes with some kind of queue to update the Canvas?
For this program the simplest thing would be to add root.update()
in in some places in long running function - ie. before sleep()
- and tkinter will have time to update changes in GUI and it will not freeze.
As @JEarls suggested in comment above I added also root.update_idletasks()
import tkinter as tk
import time
def bubble_sort(canvas, data):
n = len(data)
for i in range(n):
for j in range(0, n-i-1):
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
draw_data(canvas, data)
root.update() # <-- force tkinter to update widgets in GUI
root.update_idletasks()
time.sleep(0.1) # <-- freezes the UI
def draw_data(canvas, data):
canvas.delete("all")
for i, val in enumerate(data):
canvas.create_rectangle(i*20, 200-val, (i+1)*20, 200, fill="blue")
def start_sort():
bubble_sort(canvas, [5,3,8,4,2])
root = tk.Tk()
canvas = tk.Canvas(root, width=200, height=200)
canvas.pack()
btn = tk.Button(root, text="Start", command=start_sort)
btn.pack()
root.mainloop()
But if sleep()
would use bigger value then it would look again like freezed and it would need to use different method - ie. Threading
with Queue
.
Version which runs long-running code in thread and it uses queue
to send data back to main thread which use after
to periodically check if there is new data and redraw it on canvas
import tkinter as tk
import time
import random
import threading
import queue
def bubble_sort(canvas, data, q):
n = len(data)
for i in range(n):
for j in range(0, n-i-1):
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
#draw_data(canvas, data)
q.put(data) # <-- send data to main thread
time.sleep(0.5) # <-- freezes the UI
q.put('end') # <-- send info to main thread
def draw_data(canvas, q):
global thread
if q.not_empty:
data = q.get()
if data == 'end':
thread = None # <-- set None so it can be run again
return
canvas.delete("all")
for i, val in enumerate(data):
canvas.create_rectangle(i*20, 200-val, (i+1)*20, 200, fill="blue")
# repeat after 100ms
root.after(100, draw_data, canvas, q)
def start_sort():
global thread
if thread is None:
q = queue.Queue()
data = [random.randint(0,100) for _ in range(20)]
thread = threading.Thread(target=bubble_sort, args=(canvas, data, q) )
thread.start()
root.after(0, draw_data, canvas, q)
else:
print("thread is already running")
# -----
thread = None # <-- used as information if thread is running or not
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=200)
canvas.pack()
btn = tk.Button(root, text="Start", command=start_sort)
btn.pack()
root.mainloop()
Actually threads share memory and it could be done without queue
but with queue
it was simpler for me.