0

I am writing a Tkinter app and i need an indeterminate progressbar that shows over the main window, while, in the back, the function called does some operations. Because i don't know how long this operation will be, i don't want to have to time the progress bar. Also, the function called will show an info window, so, there should the progressbar stop. I've looked up multiple ways of doing it, and i think i found the right method. I want to let the function to run in a separate thread, measure the thread execution time, and then pass the time, somehow, to the progressbar to stop. I don't know if this is the right way. Also, it doesn't work, mostly because i don't know what i am doing, because is my first time working with threads. Below is my code:

  • the progress bar:
def loading_page(time):
    def close_loading():
        root.destroy()

    root = tk.Tk()
    root.title("Loading Page")

    root.resizable(False, False)

    loading_label = ttk.Label(root, text="Loading...", font=("Montserat", 12))
    loading_label.pack(pady=15, padx=15)

    progress_bar = ttk.Progressbar(root, length=200, mode="indeterminate")
    progress_bar.pack(padx=15, pady=15)

    progress_bar.after(time, close_loading())

    progress_bar.start(10)

    root.mainloop()
  • the threads:
def task(*args):
    loading_page(*args)


def thread_time_decorator(thread):
    @wraps(thread)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        thread(*args, **kwargs)
        end = time.perf_counter()
        threading.current_thread().thread_duration = end - start

    return wrapper


def worker(func, *args):
    t1 = threading.Thread(target=func, args=(*args,))
    t1.start()


def ini_task(c):
    t1 = threading.Thread(target=generate_file, args=(c,))
    wrapped_t1 = thread_time_decorator(t1)
    t1.start()
    task(wrapped_t1)
    t1.join()
  • the function that will be called from the main window:
def some_function(c):
    with contextlib.suppress(Exception):
        helper.close_one_drive()
    try:
        #some file operations
    except Exception:
        if kill_prompt := messagebox.askokcancel(
            title="Excel about to close",
            message="This operation cannot be performed because the file is already opened."
            "\n If you press 'OK' to continue the process, all opened Excel files will be closed.",
        ):
            os.system("taskkill /T /IM EXCEL.exe")

            file_gen = some_function(c)
    #this is where the progress bar shoud stop, or be destroyed immediately after the whole function finishes 
    if prompt := messagebox.askokcancel(
        title=f"Operation complete for {c}",
        message=f" Would you like to go to {c} ?",
    ):
        return c, subprocess.call(
            [file_gen],
            shell=True,
        )
3
  • Typo error missing quote message=f Would you like to go to {c} ?", ): Commented Jul 6, 2023 at 10:16
  • Use lambda.............progress_bar.after(time, lambda: close_loading(4)) Commented Jul 6, 2023 at 10:22
  • 1
    Fixed the typos. Also, while using a lambda seems to be a better idea, i can't get the thread execution time correctly Commented Jul 6, 2023 at 11:24

1 Answer 1

0

After some time and multiple readings of other answers on SO, i've come up with this solution, that actually solves the initial problem, using another approach than timing a thread. Below, the code:

def loading_page(thread, close_event, on_closing):
    import gc

    def close_loading():
        root.withdraw()  # Hide the root window
        root.quit() 
        gc.collect()

    def check_thread():
        if thread.is_alive():
            root.after(3000, check_thread)
            root.update()  # Allow the GUI to update
        else:
            close_loading()
            close_event.set()  # Set the close event to indicate completion

    root = tk.Tk()
    root.title("Loading Page")
    root.resizable(False, False)
    root.protocol("WM_DELETE_WINDOW", on_closing)

    # Create a label for the loading message
    loading_label = ttk.Label(root, text="Loading...", font=("Montserrat", 12))
    loading_label.pack(pady=15, padx=15)

    # Create a progress bar
    progress_bar = ttk.Progressbar(root, length=200, mode="indeterminate")
    progress_bar.pack(padx=15, pady=15)

    progress_bar.start(10)
    progress_bar.after(100, progress_bar.update_idletasks)
    progress_bar.after(1000, check_thread)
    root.mainloop()


def load_proc(*args):
    t2return = ThreadWithReturnValue(target=generate_file, args=(*args,))
    t2return.start()

    close_event = Event()

    def on_closing():
        if not close_event.is_set():
            pass

    loading_thread = Thread(
        target=loading_page, args=(t2return, close_event, on_closing)
    )
    loading_thread.start()

    # Once the main window is closed, set the close event to stop the loading window.
    close_event.set()

    # Wait for the loading thread to complete.
    loading_thread.join()

    # Return the result after the loading thread finishes.
    return open_file(t2return.join())

def generate_file(*args):
    #does file generation stuff

ThreadwithReturnValue is actually a class taken from another post on SO, where OP wanted a thread to return a value that can be used in a function:

class ThreadWithReturnValue(Thread):
    def __init__(self, group=None, target=None, name=None, args=(), kwargs=None):
        Thread.__init__(self, group, target, name, args, kwargs)
        self._return = None

    def run(self):
        if self._target is not None:
            self._return = self._target(*self._args, **self._kwargs)

    def join(self, *args):
        Thread.join(self, *args)
        return self._return

Because it took some time, i can't put a list with all the sources needed to come up with this solution

Not the answer you're looking for? Browse other questions tagged or ask your own question.