How to run and update Tkinter widgets when running time-consuming functions?
Asked Answered
P

0

0

I have a time consuming task that I'd like to show in UI the progress of, or at least just tell the user the program's not frozen. I'd like to show this in two ways: through Label and ProgressBar. For a more specific context, I'm using Python 3.6.7 for this.

I am aware of the indeterminate and determinate modes of the Progressbar, and the difference in implementation of the two, but my main problem is how I can make the Progressbar appear and not freeze before the time consuming function is run.

Same goes with the Label. I'm okay with some text not shown or skipped if the operation is fast enough, but some text should be left to remind the user what's currently happening.

I have tried the solutions in these answers, both the threaded and non-threaded ones.

Originally, my code is similar to this:

import threading
import time # to simulate work

from tkinter import *
from tkinter import filedialog

from tkinter.ttk import Progressbar

class DemoWindow(Frame):
    def __init__(self, parent):
        Frame.__init__(self, parent)

        self.status = StringVar()
        self.status.set("Click when ready")
        self.label = Label(text="Click when ready")
        self.label.grid(row=0)

        self.button = Button(text="Click me", command=self.run_foo)
        self.button.grid(row=1)

        self.indeterminate_bar = Progressbar(orient='horizontal', mode='indeterminate')
        self.determinate_bar = Progressbar(orient='horizontal', mode='determinate')

    def run_foo(self):
        # to simulate asking user for something
        self.label.configure(text="Where's the sauce?")
        foo_dir = filedialog.askdirectory()

        if foo_dir is not None:
            # start the progress bars
            # it should be moving at this point
            self.show_progress()
            self.start_progress()
            self.label.configure(text="Please wait...")

            self.time_consuming_task()

            # stop or freeze progress bar while
            # asking the user about something again
            self.stop_progress()

            # ask something again...
            self.label.configure(text="Where's the meat?")
            bar_dir = filedialog.askdirectory()
            if bar_dir is not None:

                # start it again
                self.start_progress()
                self.label.configure(text="Please wait...")

                self.another_time_consuming_task()

        # stop and hide progress bars
        self.hide_progress()
        self.label.configure(text="Done!")

    def show_progress(self):
        self.indeterminate_bar.grid(row=2)
        self.determinate_bar.grid(row=3)

        self.indeterminate_bar.update_idletasks()
        self.determinate_bar.update_idletasks()

    def start_progress(self):
        self.indeterminate_bar.start(50)
        self.determinate_bar.start(50)

        self.indeterminate_bar.update_idletasks()
        self.determinate_bar.update_idletasks()

    def stop_progress(self):
        self.indeterminate_bar.stop()
        self.determinate_bar.stop()

        self.indeterminate_bar.update_idletasks()
        self.determinate_bar.update_idletasks()

    def hide_progress(self):
        self.indeterminate_bar.stop()
        self.determinate_bar.stop()

        self.indeterminate_bar.grid_forget()
        self.determinate_bar.grid_forget()

        self.indeterminate_bar.update_idletasks()
        self.determinate_bar.update_idletasks()

    def time_consuming_task(self):
        time.sleep(5)

    def another_time_consuming_task(self):
        time.sleep(3)

if __name__ == '__main__':
    root = Tk()
    root.title("The UI Dilemma")
    root.geometry("400x100")
    root.resizable(0,0)
    DemoWindow(root).grid()
    root.mainloop()

Here's my attempt at the threaded approach, inspired by the linked questions' answers:

import threading
import time # to simulate work

from tkinter import *
from tkinter import filedialog

from tkinter.ttk import Progressbar

class DemoWindow(Frame):
    def __init__(self, parent):
        Frame.__init__(self, parent)

        self.status = StringVar()
        self.status.set("Click when ready")
        self.label = Label(text="Click when ready")
        self.label.grid(row=0)

        self.button = Button(text="Click me", command=self.start_helper_thread)
        self.button.grid(row=1)

        self.indeterminate_bar = Progressbar(orient='horizontal', mode='indeterminate')
        self.determinate_bar = Progressbar(orient='horizontal', mode='determinate')

    def run_foo(self):
        # to simulate asking user for something
        self.label.configure(text="Where's the sauce?")
        foo_dir = filedialog.askdirectory()

        if foo_dir != "":
            # start the progress bars
            # it should be moving at this point
            self.show_progress()
            self.start_progress()
            self.label.configure(text="Please wait...")

            self.time_consuming_task()

            # stop or freeze progress bar while
            # asking the user about something again
            self.stop_progress()

            # ask something again...
            self.label.configure(text="Where's the meat?")
            bar_dir = filedialog.askdirectory()
            if bar_dir != "":

                # start it again
                self.start_progress()
                self.label.configure(text="Please wait...")

                self.another_time_consuming_task()

        # stop and hide progress bars
        self.hide_progress()
        self.label.configure(text="Done!")

    def start_helper_thread(self):
        global helper_thread
        helper_thread = threading.Thread(target=self.run_foo())
        helper_thread.daemon = True

        helper_thread.start()
        self.after(500, self.check_helper_thread)

    def check_helper_thread(self):
        if helper_thread.is_alive():
            self.after(500, self.check_helper_thread())
        else:
            self.hide_progress()

    def show_progress(self):
        self.indeterminate_bar.grid(row=2)
        self.determinate_bar.grid(row=3)

        self.indeterminate_bar.update_idletasks()
        self.determinate_bar.update_idletasks()

    def start_progress(self):
        self.indeterminate_bar.start(50)
        self.determinate_bar.start(50)

        self.indeterminate_bar.update_idletasks()
        self.determinate_bar.update_idletasks()

    def stop_progress(self):
        self.indeterminate_bar.stop()
        self.determinate_bar.stop()

        self.indeterminate_bar.update_idletasks()
        self.determinate_bar.update_idletasks()

    def hide_progress(self):
        self.indeterminate_bar.stop()
        self.determinate_bar.stop()

        self.indeterminate_bar.grid_forget()
        self.determinate_bar.grid_forget()

        self.indeterminate_bar.update_idletasks()
        self.determinate_bar.update_idletasks()

    def time_consuming_task(self):
        time.sleep(5)

    def another_time_consuming_task(self):
        time.sleep(3)

if __name__ == '__main__':
    root = Tk()
    root.title("The UI Dilemma")
    root.geometry("400x100")
    root.resizable(0,0)
    DemoWindow(root).grid()
    root.mainloop()

Basically the same as the former, but I added self.start_helper_thread() to start the thread that will run the self.run_foo() function, changed the command in self.button to call self.start_helper_thread instead, and added self.check_helper_thread() that supposedly checks if the helper_thread is still alive every 500 ms.

The former code block is quite similar already to the non-threaded solutions in the linked questions, right? I expected that update_idletasks() would do the trick for me. By that I mean:

  • After the first self.label.configure(text="Please wait...") line, self.indeterminate_bar and self.determinate_bar should be moving while self.time_consuming_task() is running. The Label's text that is displayed should be "Please wait..." before and after self.time_consuming_task().
  • Then, the two ProgressBars will stop() and self.label's text would be "Where's the meat?" by the time the second filedialog.askdirectory() runs.
  • If bar_dir is valid, the ProgressBars will start again, and the self.label's text would be "Please wait..." again.
  • self.another_time_consuming_task() will execute with the ProgressBars moving and the Label's text as Please wait..."
  • After self.another_time_consuming_task() ends, the ProgressBars will be hidden and the Label's text will be left at "Done!"

I expected the same too of the thread method. Actually I expected this behavior right off the bat before all of this- I coded them in that order, so why doesn't it execute in that order?

Parkland answered 16/8, 2019 at 17:47 Comment(4)
the problem is that you update your UI in the same thread where the time_consuming_task happens. in pretty much every UI library I know the changes you apply to the UI won't be applied until your UI changing code has finished running, and in this case it implies finishing the time consuming task. To fix that you need to separate the code dealing with the UI and the helper thread completely, and probably use a shared buffer to handle communication between the main (UI) thread and the helper thread.Petrolic
Any idea how I can do that? Thanks.Parkland
this page should help #16401033Petrolic
I linked that page in my question, I've tried to fit those solutions into my code, and here it is in the questionParkland

© 2022 - 2024 — McMap. All rights reserved.