pythontkintertkinter-canvas

How can I run a long-running task in Tkinter without freezing the UI (while keeping real-time updates on a Canvas)?


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?


Solution

  • 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.