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
mode
s 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
andself.determinate_bar
should be moving whileself.time_consuming_task()
is running. TheLabel
's text that is displayed should be"Please wait..."
before and afterself.time_consuming_task()
. - Then, the two
ProgressBar
s willstop()
andself.label
's text would be"Where's the meat?"
by the time the secondfiledialog.askdirectory()
runs. - If
bar_dir
is valid, theProgressBar
s will start again, and theself.label
's text would be"Please wait..."
again. self.another_time_consuming_task()
will execute with theProgressBar
s moving and theLabel
's text asPlease wait..."
- After
self.another_time_consuming_task()
ends, theProgressBar
s will be hidden and theLabel
'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?