Reputation: 446
[This has been fairly significantly edited in the light of comments to the original post, and to make the context - 2 modules - clearer and to summarise what I think is the key underlying issue. The code is also updated. I have a working version but am not at all sure its done the right way.] (Disclaimer ... Im learning Tkinter as I go along!)
Im attempting to display a progress bar while an app is running (eg walking a music library folder tree but that doesnt matter here).
I'd like to implement this as a class in a separate module from the main app so I can use it elsewhere (also the app itself is actually in 2 modules).
For that reason, and also because I don't want to upset the app's main window design Id like the progress bar to appear in a separate window.
I've tried this two ways ... my own crudely drawn progress bar using a text widget, and - once I discovered it - ttk.Progressbar. Im now focusing on using ttk.Progressbar.
Note however I had essentially the same problem with either approach, which is getting the contents of the progress window to display properly without preventing control reverting back to the calling module.
My class for this (ProgressBar) has methods to start, update, and stop the progress bar. As I understand it there are three ways to force refreshing of the status window in the class methods. All three seem to have drawbacks.
So the basic questions are - What is the correct way to force the window to update (eg the Set method); and why is update_idletasks() blanking the progress window.
I believe the following code reflects the suggestions made but I have adapted it to reflect the intended import class.
# dummy application importing the StatusBar class.
# this reflects app is itslef using tkinter
from ProgressBar12 import ProgressBar
import tkinter as Tk
import time
import os
def RunAppProcess():
print('App running')
Bar = ProgressBar(tkroot) # '' arg to have ProgressBar create its tkroot
Bar.start('Progress...', 0) # >0 determinate (works) / 0 for indeterminate (doesnt!)
print('starting process')
# this simulates some process, (eg for root, dirs, files = os.walk(lib))
for k in range(10):
Bar.step(5) # (should be) optional for indeterminate
time.sleep(.2)
Bar.stop('done') # '' => kill the window; or 'message' to display in window
def EndAppProcess():
tkroot.withdraw()
tkroot.destroy()
# Application init code, the application is using tkinter
# (should probably be in an init procedure etc, but this will serve)
tkroot = Tk.Tk()
tkroot.title("An Application")
tkroot.geometry("100x100")
tkroot.configure(bg='khaki1')
# a 2 button mini window: [Start] and [Quit]
Tk.Button(tkroot, text='Start', bg='orange', command=RunAppProcess).grid(sticky=Tk.W)
Tk.Button(tkroot, text="Quit", bg="orange", command=EndAppProcess).grid(sticky=Tk.W)
tkroot.mainloop()
ProgressBar Module
# determinate mode
import tkinter as Tk
import tkinter.font as TkF
from tkinter import ttk
import time
# print statements are for tracing execution
# changes from the sample code previsouly given reflect:
# - suggestions made in the answer and in comments
# - to reflect the actual usage with the class imported into a calling module rather than single module solution
# - consistent terminology (progress not status)
# - having the class handle either determinate or indeterminate progress bar
class ProgressBar():
def __init__(self, root):
print('progress bar instance init')
if root == '':
root = tkInit()
self.master=Tk.Toplevel(root)
# Tk.Button(root, text="Quit all", bg="orange", command=root.quit).grid() A bit rude to mod the callers window
self.customFont2 = TkF.Font(family="Calibri", size=12, weight='bold')
self.customFont5 = TkF.Font(family="Cambria", size=16, weight='bold')
self.master.config(background='ivory2')
self.create_widgets()
self.N = 0
self.maxN = 100 # default for %
def create_widgets(self):
self.msg = Tk.Label(self.master, text='None', bg='ivory2', fg='blue4') #, font=self.customFont2)
self.msg.grid(row=0, column=0, sticky=Tk.W)
self.bar = ttk.Progressbar(self.master, length=300, mode='indeterminate')
self.bar.grid(row=1, column=0, sticky=Tk.W)
#self.btn_abort = Tk.Button(self.master, text=' Abort ', command=self.abort, font=self.customFont2, fg='maroon')
#self.btn_abort.grid(row=2,column=0, sticky=Tk.W)
#self.master.rowconfigure(2, pad=3)
print('progress bar widgets done')
def start(self, msg, maxN):
if maxN <= 0:
#indeterminate
self.msg.configure(text=msg)
self.bar.configure(mode='indeterminate')
self.maxN = 0
self.bar.start()
self.master.update()
else: # determinate
self.msg.configure(text=msg)
self.bar.configure(mode='determinate')
self.maxN = maxN
self.N = 0
self.bar['maximum'] = maxN
self.bar['value'] = 0
def step(self, K):
#if self.maxN == 0: return # or raise error?
self.N = min(self.maxN, K+self.N)
self.bar['value'] = self.N
self.master.update() # see set(..)
def set(self, K):
#if self.maxN == 0: return
self.N = min(self.maxN, K)
self.bar['value'] = self.N
#self.master.mainloop() # <<< calling module does not regain control. Pointless.
#self.master.update_idletasks # <<< works, EXCEPT statusbar window is blank! Also pointless. But calling module regains control
self.master.update() # <<< works in all regards, BUT I've read this is dangerous.
def stop(self, msg):
print('progress bar stopping')
self.msg.configure(text=msg)
if self.maxN <= 0:
self.bar.stop()
else:
self.bar['value'] = self.maxN
#self.bar.stop()
if msg == '':
self.master.destroy()
else: self.master.update()
def abort(self):
# eventually will raise an error to the calling routine to stop the process
self.master.destroy()
def tkInit():
print('progress bar tk init')
tkroot = Tk.Tk()
tkroot.title("Progress Bar")
tkroot.geometry("250x50")
tkroot.configure(bg='grey77')
tkroot.withdraw()
return tkroot
if (__name__ == '__main__'):
print('start progress bar')
tkroot = tkInit()
tkroot.configure(bg='ivory2')
Bar = ProgressBar(tkroot)
Bar.start('Demo', 10)
for k in range(11):
Bar.set(k)
time.sleep(.2)
Bar.stop('done, you can close me')
else:
# called from another module
print('progress bar module init. (nothing) done.')
This is based on the first of the solutions in the answer; as an alternative I will try the second using after() .... I first have to understand exactly what that does.
Upvotes: 0
Views: 6975
Reputation: 385940
So the basic questions are - What is the correct way to force the window to update (eg the Set method); and why is update_idletasks() blanking the progress window.
The correct way to force the window to update is to allow it to happen naturally via mainloop
. In rare circumstances it's reasonable to call update_idletasks
to update the display. It's also possible to call update
but that has some serious ramifications 1
There's no escaping the fact that for a GUI to be responsive, it needs to be able to constantly process events. If you have a long running process that prevents that, there are a couple of different strategies you can employ.
One solution is to break your long running problem into small pieces, and let mainloop
run one piece at a time. For example, if I were to write a function to find every occurrence of the word "the" in a million-line document, I wouldn't want to do the search all at once. Instead, I would do one search at a time (or maybe as many as I can do in 100ms), highlight them, and then schedule another search to happen in a few milliseconds. Between those calls, mainloop
is able to process events as normal.
For certain classes of problems, that's all it takes -- break the problem down into steps that take about 200ms or less, and run one step at a time. There are several examples of this on the internet and on this site, often related to animation (such as moving images across a screen).
The other option is to move all of your long-running code into a separate thread or separate process. This requires more overhead and adds complexity, but it is the best solution if you can't refactor your code to work in chunks.
The main difficulty with using threads or processes is that these threads or processes can't safely interact directly with the widgets. Tkinter isn't thread safe, so you'll have to set up a mechanism by which the GUI thread can communicate with the worker thread or process. Often this is done with a thread-safe queue, where the worker thread puts requests on a queue, and the GUI thread polls this queue and does work on behalf of the worker.
1 Calling update
does much more than refresh the display. It will process any pending events, including events such as key presses and button clicks. If one of those key presses or button clicks results in calling code that also calls update
, you now have, in effect, two mainloops running. Calling update
is perfectly safe if there is no way any event can kick off another call to update
, but it can be difficult to make that guarantee.
Upvotes: 3
Reputation:
First, you don't call mainloop() anywhere. The following code displays a moving progress bar until you hit the abort button. The for() loop in your code above serves no purpose as it does nothing except stop program execuption for 0.3*20 seconds. If you want to update the progress bar yourself, then see the 2nd example and how it uses "after" to call an update function until the progress bar completes. And note that everything associated with it is contained in the class, which is one of the reasons you would use a class. You could also call the update function from outside the class, but the update function would still be in the same class that created the progress bar.
import Tkinter as Tk
import tkFont as TkF
import ttk
import time
class StatusBar():
def __init__(self, root):
self.master=Tk.Toplevel(root)
Tk.Button(root, text="Quit all", bg="orange", command=root.quit).grid()
self.customFont2 = TkF.Font(family="Calibri", size=12, weight='bold')
self.customFont5 = TkF.Font(family="Cambria", size=16, weight='bold')
self.master.config(background='ivory2')
self.ctr=0
self.create_widgets()
def create_widgets(self):
self.msg = Tk.Label(self.master, text='None', bg='ivory2', fg='blue4',
font=self.customFont2, width=5)
self.msg.grid(row=0, column=0, sticky=Tk.W)
self.bar = ttk.Progressbar(self.master, length=300, mode='indeterminate')
self.bar.grid(row=1, column=0, sticky=Tk.W)
self.btn_abort = Tk.Button(self.master, text=' Abort ', command=self.abort, font=self.customFont2, fg='maroon')
self.btn_abort.grid(row=2,column=0, sticky=Tk.W)
self.master.rowconfigure(2, pad=3)
print('widgets done')
def Start(self, msg):
self.msg.configure(text=msg)
self.bar.start()
def Stop(self, msg):
self.msg.configure(text=msg)
self.bar.stop()
def abort(self):
# eventually will raise an error to the calling routine to stop the process
self.master.destroy()
if (__name__ == '__main__'):
print('start')
tkroot = Tk.Tk()
tkroot.title("Status Bar")
tkroot.geometry("500x75")
tkroot.configure(bg='ivory2')
Bar = StatusBar(tkroot)
Bar.Start('Demo')
tkroot.mainloop()
Uses after() to update the progress bar
try:
import Tkinter as tk ## Python 2.x
except ImportError:
import tkinter as tk ## Python 3.x
import ttk
class TestProgress():
def __init__(self):
self.root = tk.Tk()
self.root.title('ttk.Progressbar')
self.increment = 0
self.pbar = ttk.Progressbar(self.root, length=300)
self.pbar.pack(padx=5, pady=5)
self.root.after(100, self.advance)
self.root.mainloop()
def advance(self):
# can be a float
self.pbar.step(5)
self.increment += 5
if self.increment < 100:
self.root.after(500, self.advance)
else:
self.root.quit()
TP=TestProgress()
Upvotes: 0