Search code examples
pythonmultithreadingtkinterprogress-barcustomtkinter

CustomTkinter progress bar with a stoppable thread


I am trying to combine a ctk progress bar from https://stackoverflow.com/a/75728288/7611214 on a thread that is stoppable like https://stackoverflow.com/a/325528/7611214. I think I am close but I just can't quite pull it together. Can someone please help me complete my code so the bar will progress a certain distance before the thread is interrupted after 5 seconds in this case (akin to someone hitting a cancel button)?

import customtkinter as ctk
import threading
import time

class StoppableThread(threading.Thread):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._stop_event = threading.Event()
    def stop(self):
        self._stop_event.set()
    def stopped(self):
        return self._stop_event.is_set()

class App(ctk.CTk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.Progress_Bar = (ctk.CTkProgressBar(master=self, height=15, width=150, mode='determinate'))
        self.Progress_Bar.place(x=10, y=10)
        self.Progress_Bar.set(0)
        self.update()
        my_thread = Algorithm()
        my_thread.start()
        time.sleep(5)
        my_thread.stop()

class Algorithm(StoppableThread):
    def test(self):
        n = 50
        iter_step = 1 / n
        progress_step = iter_step
        self.Progress_Bar.start()

        while not self.stopped():
            for x in range(n):
                progress_step += iter_step
                self.Progress_Bar.set(progress_step)
                self.update_idletasks()
            self.Progress_Bar.stop()

app = App()
app.mainloop()

Solution

  • unfortunately, there is no easy solution for this and your functions need to be aware of the threads that they are running in. The thread class you provided can be used like:

    import tkinter as tk
    from tkinter import ttk
    from threading import Thread
    import threading
    import time
    
    alive_threads = dict()
    
    class StoppableThread(Thread):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self._stop_event = threading.Event()
        def stop(self):
            self._stop_event.set()
        def stopped(self):
            return self._stop_event.is_set()
    
    def start(alive_threads=alive_threads, thread_name = 'prog'):
        alive_threads[thread_name] = StoppableThread(target = computation, args=(alive_threads,thread_name))
        alive_threads[thread_name].start()
        
    def stop(alive_threads=alive_threads, thread_name = 'prog'):
        alive_threads[thread_name].stop()
        
    def computation(alive_threads=alive_threads, thread_name = 'prog'):
        # Main computation loop
        for i in range(20):
            if (alive_threads[thread_name].stopped()):
                break
            progress['value'] = (i+1)*5
            root.update_idletasks()
            time.sleep(0.2) 
        # Resource cleanup or logging
        print("Done!")
        alive_threads.pop(thread_name)
      
    
    root = tk.Tk()
    progress = ttk.Progressbar(root, orient = 'horizontal', length = 100, mode = 'determinate') 
    progress.pack(pady = 10) 
    
    # This button will start the progress bar 
    ttk.Button(root, text = 'Start', command = start).pack(pady = 10) 
    # This button will stop the progress bar 
    ttk.Button(root, text = 'stop', command = stop).pack(pady = 10) 
    root.mainloop()
    

    but the key takeaway is that the function needs to check the status of the thread to abort whatever its doing. If you use generators that process one item at a time, this becomes much easier to deal with; rather than trying to break the loop you can use: while not thread.stopped(): and next(YOUR_GENERATOR) to do whatever you want, one at a time.

    When I write GUIs in tkinter, I usually have a thread manager that deals with dynamically updating the buttons to either do a process or abort the process based on the status of the computations in a pool. needless to say you can generify the start method to take in *args and *kwargs (similar to the stoppable thread class) to make your life easier.