CustomTkinter progress bar with a stoppable thread

241 views Asked by At

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()
1

There are 1 answers

1
OM222O On BEST ANSWER

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.