tkinter progress bar won't update when called from an other app - python

I am developing a GUI with tkinter to manage images in a database (import file, load file, query, ...)
When a new directory and its sub-directories are scanned for new images to put in the database, a dedicated GUI is launched:
It consists of a Text widget where the name of the directory currently analysed is printed, and a progress-bar showing the progress of the scan.
When I call this GUI alone, the progressbar updates and progresses correctly as long as I use update() after each change in the progressbar. On the other hand,
the Text widget is corretly updating even if I do no use update.
However, the progress bar does not update as it should when I call it from the main GUI, while the Text widget updates correctly.
I hope someone can help!
Below is the code for the progressbar GUI. I am using Python 3.6.
from tkinter.filedialog import *
from tkinter.ttk import *
class ScanDirectoryForJPG(Tk):
"""
Inherited from the Tk class
"""
def __init__(self, parent, Path=None):
Tk.__init__(self, parent)
self.parent = parent
self.PathDicom = Path
if self.Path == None:
self.Path = askdirectory(title='Select a directory to scan')
self.title('Scan {} for JPG files'.format(self.Path))
self.status_string = 'Scanning the content of {} folder\n'.format(self.Path)
self.initialize_gui()
self.scan_directory()
def initialize_gui(self):
# Style
self.style = Style()
self.style.theme_use('vista')
# Main window
self.grid()
self.grid_columnconfigure([0], weight=1)
self.grid_rowconfigure([0], weight=1)
# Status
self.status_label = Text(self)
self.status_label.grid(row=0, column=0, sticky='NSEW')
self.status_label.insert(END, 'Looking for JPG files in {}\n'.format(self.Path))
# Progress Bar
self.p = DoubleVar()
self.progress_bar = Progressbar(self, orient='horizontal', mode='determinate', variable=self.p, maximum=100)
self.p.set(0)
self.progress_bar.grid(row=1, column=0, rowspan=1, sticky='EW')
def scan_directory(self):
"""
"""
number_of_files = sum([len(files) for r, d, files in os.walk(self.Path)])
count = 0
for dirName, subdirList, fileList in os.walk(self.Path):
self.status_label.insert(END, '\t-exploring: {}\n'.format(dirName))
self.update()
for filename in fileList:
count += 1
value = count / number_of_files * self.progress_bar['maximum']
if value >= (self.progress_bar['value'] + 1):
# update the progress bar only when its value is increased by at least 1 (avoid too much updates of the progressbar)
self.p.set(self.progress_bar['value'] + 1)
self.update()
file = os.path.join(dirName, filename)
# if the file is a JPG, load it into the database
# ...
# ...
# ..
self.status_label.insert(END, 'FINISH\n')
self.update()
if __name__ == '__main__':
app = ScanDirectoryForJPG(None, Path='D:\Data\Test')
app.mainloop()
print('App closed')

If you have to call update() in tkinter you are doing it wrong.
In this case you have a loop that walks the directory tree. For the whole time your code is within that loop you are not processing events promptly so you have to call update() all the time.
Instead, capture the output of the os.walk or simply collect the toplevel directory contents and then use after to process a single item at a time, passing the iterator or list along so once you process a single item, you call after again to process the next one. This way the mainloop will handle UI events promptly with your directory tree processing being queued as events along with everything else. You should fine the application more responsive once you rework it in this manner.
Example
To demonstrate this, os.walk returns a generator so we can use after events to schedule each directory as each call of next(generator) yields the next directory with its files.
To monitor the progress we need some way to count the number of directories or files to be visited and if this demo is used for a whole filesystem that is where the app will appear to freeze. This could be broken into event-based code too to prevent this effect.
I used after(10, ...) to have it show the effect but for maximum speed, use after_idle instead.
import sys
import os
import tkinter as tk
import tkinter.ttk as ttk
from tkinter.filedialog import askdirectory
class App(ttk.Frame):
def __init__(self, parent, title):
#tk.Frame.__init__(self, parent)
super(App, self).__init__(parent)
parent.wm_withdraw()
parent.wm_title(title)
self.create_ui()
self.grid(sticky = "news")
parent.wm_protocol("WM_DELETE_WINDOW", self.on_destroy)
parent.grid_rowconfigure(0, weight=1)
parent.grid_columnconfigure(0, weight=1)
parent.wm_deiconify()
def create_ui(self):
textframe = ttk.Frame(self)
self.text = text = tk.Text(textframe)
vs = ttk.Scrollbar(textframe, orient=tk.VERTICAL, command=text.yview)
text.configure(yscrollcommand=vs.set)
text.grid(row=0, column=0, sticky=tk.NSEW)
vs.grid(row=0, column=1, sticky=tk.NS)
textframe.grid_columnconfigure(0, weight=1)
textframe.grid_rowconfigure(0, weight=1)
textframe.grid(row=0, column=0, columnspan=2, sticky=tk.NSEW)
self.progressvar = tk.IntVar()
self.progress = ttk.Progressbar(self, variable=self.progressvar)
test_button = ttk.Button(self, text="Walk", command=self.on_walk)
exit_button = ttk.Button(self, text="Exit", command=self.on_destroy)
self.progress.grid(row=1, column=0, sticky=tk.NSEW)
test_button.grid(row=1, column=0, sticky=tk.SE)
exit_button.grid(row=1, column=1, sticky=tk.SE)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
def on_destroy(self):
self.master.destroy()
def on_walk(self):
root = askdirectory()
self.walk(root)
def walk(self, root=None):
if root:
# this is potentially costly, but how to find the number of files to be examined?
count = sum([len(files) for (root,dirs,files) in os.walk(root)])
self.text.delete("1.0", "end")
self.progress.configure(maximum=count)
self.progressvar.set(0)
walker = os.walk(root)
self.after(100, self.do_one, walker)
def do_one(self, walker):
try:
root,dirs,files = next(walker)
for file in files:
self.text.insert(tk.END, os.path.join(root, file), "PATH", "\n", "")
self.text.see(tk.END)
self.progressvar.set(self.progressvar.get() + 1)
self.after(10, self.do_one, walker)
except StopIteration:
pass
def main(args):
root = tk.Tk()
app = App(root, "Walk directory tree")
root.mainloop()
if __name__ == '__main__':
sys.exit(main(sys.argv))

Related

Why is my threaded process still freezing?

I'm writing an app using tkinter for the GUI, and I want an indeterminate progress bar to be going back and forth while the main function is running, which sometimes takes a few seconds, depending on user input. Normally, the whole program freeze while the main function is running, so I am trying to establish a threaded process for the progress bar so it moves while main() is doing its thing (both functions are called withinscan_and_display()).
from tkinter import filedialog
from tkinter import *
from tkinter.ttk import Progressbar
from main import main
import graphs
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from PIL import ImageTk, Image
import threading
root = Tk()
launch_frame = Frame(root)
button_frame = Frame(root)
graph_frame = Frame(root, height=1000, width=1200)
# log directory button/text
logDirButton = Button(master=launch_frame, text='Select log storage location...',
command=lambda: get_directory(log_text), width=22)
log_text = Text(master=launch_frame, height=1, width=25)
logDirButton.grid(row=1, column=0)
log_text.grid(row=1, column=1)
# scan directory button/text
dirButton = Button(master=launch_frame, text="Select scan directory...", command=lambda: get_directory(t), width=22)
t = Text(master=launch_frame, height=1, width=25)
dirButton.grid(row=2, column=0)
t.grid(row=2, column=1)
# main scan button
mainButton = Button(master=launch_frame, text="SCAN!", state=DISABLED,
width=50, height=10, bg='#27b355')
mainButton.grid(row=3, column=0, columnspan=2)
# progress bar
progress = Progressbar(launch_frame, orient=HORIZONTAL, length=100, mode='indeterminate')
progress.grid(row=4, column=0, columnspan=2)
launch_frame.grid(row=0, column=0, sticky=NW)
def get_directory(text):
# first clear form if it already has text
try:
text.delete("1.0", END)
except AttributeError:
pass
directory = filedialog.askdirectory()
# store the first directory for later specific reference
text.insert(END, directory)
# disable scan button until user has given necessary info to run (log storage location, scan directory)
enable_scan_button(log_text, t)
return directory
def enable_scan_button(logText, dirText):
if logText.get("1.0", END) != '\n' and dirText.get('1.0', END) != '\n':
mainButton['state'] = NORMAL
mainButton['command'] = lambda: scan_and_display()
else:
mainButton['state'] = DISABLED
def scan_and_display():
threading.Thread(target=bar_start).start()
# get scan directory and log directory from text fields
log_directory = log_text.get("1.0", END)[:-1]
scan_directory = t.get("1.0", END)[:-1]
# store the initial scan directory for later reference
top_dir = scan_directory
# runs the main scan function. Passes scan_directory and log_directory arguments
data, scanDate = main(log_directory, scan_directory)
display(scan_directory, data, scanDate, top_dir)
def bar_start():
print("BAR START CALLED")
progress.start(10)
With this setup (and various other configurations I've trieD), the bar still freezes while main() is doing it's thing, and I need it to move to indicate to the user that something is happening.
You need to thread the long-running function, not the progressbar.
def scan_and_display():
# get scan directory and log directory from text fields
# It's best to do all tkinter calls in the main thread
log_directory = log_text.get("1.0", END)[:-1]
scan_directory = t.get("1.0", END)[:-1]
th = threading.Thread(target=bar_start, args=(log_directory, scan_directory))
th.start()
progress.start()
def bar_start(log_directory, scan_directory):
print("BAR START CALLED")
# store the initial scan directory for later reference
top_dir = scan_directory
# runs the main scan function. Passes scan_directory and log_directory arguments
data, scanDate = main(log_directory, scan_directory)
display(scan_directory, data, scanDate, top_dir)
progress.stop()
EDIT: here's a MCVE:
import tkinter as tk
from tkinter.ttk import Progressbar
import time
from threading import Thread
def long_running_function(arg1, arg2, result_obj):
"""accept some type of object to store the result in"""
time.sleep(3) # long running function
result_obj.append(f'DONE at {int(time.time())}')
root.event_generate("<<LongRunningFunctionDone>>") # trigger GUI event
def long_func_start():
x = 'spam'
y = 'eggs'
t = Thread(target=long_running_function, args=(x,y,data))
t.start()
result.config(text='awaiting result')
progress.start()
def long_func_end(event=None):
progress.stop()
result.config(text=f"process finished with result:\n{data[-1]}")
root = tk.Tk()
root.geometry('200x200')
btn = tk.Button(root, text='start long function', command=long_func_start)
btn.pack()
progress = Progressbar(root, orient=tk.HORIZONTAL, length=100, mode='indeterminate')
progress.pack()
result = tk.Label(root, text='---')
result.pack()
root.bind("<<LongRunningFunctionDone>>", long_func_end) # tell GUI what to do when thread ends
data = [] # something to store data
root.mainloop()

How to call a function in tkinter after an other function has finished to execute?

Hi,
I know that event driven programming is very different than "traditional programming" where the instructions are executed sequentially by the program. However, I am new to it, and I do not understand 100% how does event driven programming works, and I think this is the reason of my problem here.
I built an example from the answer to this post:tkinter progress bar won't update when called from an other app
In this example, I would like that my program do an action after it has finished to scan the given path. Naively, I tried adding the call to action routine after the call to walk:
import sys
import os
import tkinter as tk
import tkinter.ttk as ttk
from tkinter.filedialog import askdirectory
class App(ttk.Frame):
def __init__(self, parent, title):
#tk.Frame.__init__(self, parent)
super(App, self).__init__(parent)
parent.wm_withdraw()
parent.wm_title(title)
self.create_ui()
self.grid(sticky = "news")
parent.wm_protocol("WM_DELETE_WINDOW", self.on_destroy)
parent.grid_rowconfigure(0, weight=1)
parent.grid_columnconfigure(0, weight=1)
parent.wm_deiconify()
def create_ui(self):
textframe = ttk.Frame(self)
self.text = text = tk.Text(textframe)
vs = ttk.Scrollbar(textframe, orient=tk.VERTICAL, command=text.yview)
text.configure(yscrollcommand=vs.set)
text.grid(row=0, column=0, sticky=tk.NSEW)
vs.grid(row=0, column=1, sticky=tk.NS)
textframe.grid_columnconfigure(0, weight=1)
textframe.grid_rowconfigure(0, weight=1)
textframe.grid(row=0, column=0, columnspan=2, sticky=tk.NSEW)
self.progressvar = tk.IntVar()
self.progress = ttk.Progressbar(self, variable=self.progressvar)
test_button = ttk.Button(self, text="Walk", command=self.on_walk)
exit_button = ttk.Button(self, text="Exit", command=self.on_destroy)
self.progress.grid(row=1, column=0, sticky=tk.NSEW)
test_button.grid(row=1, column=0, sticky=tk.SE)
exit_button.grid(row=1, column=1, sticky=tk.SE)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
def on_destroy(self):
self.master.destroy()
def on_walk(self):
root = askdirectory()
self.walk(root)
self.action()
def walk(self, root=None):
if root:
# this is potentially costly, but how to find the number of files to be examined?
count = sum([len(files) for (root,dirs,files) in os.walk(root)])
self.text.delete("1.0", "end")
self.progress.configure(maximum=count)
self.progressvar.set(0)
walker = os.walk(root)
self.after(100, self.do_one, walker)
def do_one(self, walker):
try:
root,dirs,files = next(walker)
for file in files:
self.text.insert(tk.END, os.path.join(root, file), "PATH", "\n", "")
self.text.see(tk.END)
self.progressvar.set(self.progressvar.get() + 1)
self.after(10, self.do_one, walker)
except StopIteration:
pass
def action(self):
print('DO SOMETHING HERE, AFTER WALK HAS FINISHED\n')
By doing so, the action function is called before the end of the call to walk.
I figured a workaround by calling action after the exception in the do_one function:
def do_one(self, walker):
try:
root,dirs,files = next(walker)
for file in files:
self.text.insert(tk.END, os.path.join(root, file), "PATH", "\n", "")
self.text.see(tk.END)
self.progressvar.set(self.progressvar.get() + 1)
self.after(10, self.do_one, walker)
except StopIteration:
self.action()
I guess that there is a better way to do it. Is there a way to create an event that tells to the program that the tasks performed by walk is finished?
Thanks

Tkinter splash screen & multiprocessing outside of mainloop

I have implemented a splash screen that is shown while my application loads the database from remote cloud storage on startup. The splash screen is kept alive (there's a progressbar on it) with calls to .update() and is destroyed once the separate loading process ends. After this, the mainloop is started and the app runs normally.
The code below used to work fine on my Mac with python 3.6 and tcl/tk 8.5.9. However, after the update to Sierra I was forced to update tk to ActiveTcl 8.5.18. Now, the splash screen is not displayed until the separate process finishes, but then appears and stays on screen together with the root window (even though its .destroy() method is called).
import tkinter as tk
import tkinter.ttk as ttk
import multiprocessing
import time
class SplashScreen(tk.Toplevel):
def __init__(self, root):
tk.Toplevel.__init__(self, root)
self.geometry('375x375')
self.overrideredirect(True)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.label = ttk.Label(self, text='My Splashscreen', anchor='center')
self.label.grid(column=0, row=0, sticky='nswe')
self.center_splash_screen()
print('initialized splash')
def center_splash_screen(self):
w = self.winfo_screenwidth()
h = self.winfo_screenheight()
x = w / 2 - 375 / 2
y = h / 2 - 375 / 2
self.geometry("%dx%d+%d+%d" % ((375, 375) + (x, y)))
def destroy_splash_screen(self):
self.destroy()
print('destroyed splash')
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.start_up_app()
self.title("MyApp")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.application_frame = ttk.Label(self, text='Rest of my app here', anchor='center')
self.application_frame.grid(column=0, row=0, sticky='nswe')
self.mainloop()
def start_up_app(self):
self.show_splash_screen()
# load db in separate process
process_startup = multiprocessing.Process(target=App.startup_process)
process_startup.start()
while process_startup.is_alive():
# print('updating')
self.splash.update()
self.remove_splash_screen()
def show_splash_screen(self):
self.withdraw()
self.splash = SplashScreen(self)
#staticmethod
def startup_process():
# simulate delay while implementation is loading db
time.sleep(5)
def remove_splash_screen(self):
self.splash.destroy_splash_screen()
del self.splash
self.deiconify()
if __name__ == '__main__':
App()
I do not understand why this is happening and how to solve it. Can anybody help? Thanks!
Update:
The splash screen is displayed correctly if you outcomment the line self.overrideredirect(True). However, I don't want window decorations and it still stays on screen at the end of the script. It is being destroyed internally though, any further method calls on self.splash (e.g. .winfo_...-methods) result in _tkinter.TclError: bad window path name ".!splashscreen".
Also, this code works fine under windows and tcl/tk 8.6. Is this a bug/problem with window management of tcl/tk 8.5.18 on Mac?
I came across this while looking for an example on how to make a tkinter splash screen that wasn't time dependent (as most other examples are). Sam's version worked for me as is. I decided to make it an extensible stand-alone class that handles all the logic so it can just be dropped into an existing program:
# Original Stackoverflow thread:
# https://stackoverflow.com/questions/44802456/tkinter-splash-screen-multiprocessing-outside-of-mainloop
import multiprocessing
import tkinter as tk
import functools
class SplashScreen(tk.Toplevel):
def __init__(self, root, **kwargs):
tk.Toplevel.__init__(self, root, **kwargs)
self.root = root
self.elements = {}
root.withdraw()
self.overrideredirect(True)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
# Placeholder Vars that can be updated externally to change the status message
self.init_str = tk.StringVar()
self.init_str.set('Loading...')
self.init_int = tk.IntVar()
self.init_float = tk.DoubleVar()
self.init_bool = tk.BooleanVar()
def _position(self, x=.5,y=.5):
screen_w = self.winfo_screenwidth()
screen_h = self.winfo_screenheight()
splash_w = self.winfo_reqwidth()
splash_h = self.winfo_reqheight()
x_loc = (screen_w*x) - (splash_w/2)
y_loc = (screen_h*y) - (splash_h/2)
self.geometry("%dx%d+%d+%d" % ((splash_w, splash_h) + (x_loc, y_loc)))
def update(self, thread_queue=None):
super().update()
if thread_queue and not thread_queue.empty():
new_item = thread_queue.get_nowait()
if new_item and new_item != self.init_str.get():
self.init_str.set(new_item)
def _set_frame(self, frame_funct, slocx=.5, sloxy=.5, ):
"""
Args:
frame_funct: The function that generates the frame
slocx: loction on the screen of the Splash popup
sloxy:
init_status_var: The variable that is connected to the initialization function that can be updated with statuses etc
Returns:
"""
self._position(x=slocx,y=sloxy)
self.frame = frame_funct(self)
self.frame.grid(column=0, row=0, sticky='nswe')
def _start(self):
for e in self.elements:
if hasattr(self.elements[e],'start'):
self.elements[e].start()
#staticmethod
def show(root, frame_funct, function, callback=None, position=None, **kwargs):
"""
Args:
root: The main class that created this SplashScreen
frame_funct: The function used to define the elements in the SplashScreen
function: The function when returns, causes the SplashScreen to self-destruct
callback: (optional) A function that can be called after the SplashScreen self-destructs
position: (optional) The position on the screen as defined by percent of screen coordinates
(.5,.5) = Center of the screen (50%,50%) This is the default if not provided
**kwargs: (optional) options as defined here: https://www.tutorialspoint.com/python/tk_toplevel.htm
Returns:
If there is a callback function, it returns the result of that. Otherwise None
"""
manager = multiprocessing.Manager()
thread_queue = manager.Queue()
process_startup = multiprocessing.Process(target=functools.partial(function,thread_queue=thread_queue))
process_startup.start()
splash = SplashScreen(root=root, **kwargs)
splash._set_frame(frame_funct=frame_funct)
splash._start()
while process_startup.is_alive():
splash.update(thread_queue)
process_startup.terminate()
SplashScreen.remove_splash_screen(splash, root)
if callback: return callback()
return None
#staticmethod
def remove_splash_screen(splash, root):
splash.destroy()
del splash
root.deiconify()
class Screen(tk.Frame):
# Options screen constructor class
def __init__(self, parent):
tk.Frame.__init__(self, master=parent)
self.grid(column=0, row=0, sticky='nsew')
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
### Demo ###
import time
def splash_window_constructor(parent):
"""
Function that takes a parent and returns a frame
"""
screen = SplashScreen.Screen(parent)
label = tk.Label(screen, text='My Splashscreen', anchor='center')
label.grid(column=0, row=0, sticky='nswe')
# Connects to the tk.StringVar so we can updated while the startup process is running
label = tk.Label(screen, textvariable=parent.init_str, anchor='center')
label.grid(column=0, row=1, sticky='nswe')
return screen
def startup_process(thread_queue):
# Just a fun method to simulate loading processes
startup_messages = ["Reticulating Splines","Calculating Llama Trajectory","Setting Universal Physical Constants","Updating [Redacted]","Perturbing Matrices","Gathering Particle Sources"]
r = 10
for n in range(r):
time.sleep(.2)
thread_queue.put_nowait(f"Loading database.{'.'*n}".ljust(27))
time.sleep(1)
for n in startup_messages:
thread_queue.put_nowait(n)
time.sleep(.2)
for n in range(r):
time.sleep(.2)
thread_queue.put_nowait(f"Almost Done.{'.'*n}".ljust(27))
for n in range(r):
time.sleep(.5)
thread_queue.put_nowait("Almost Done..........".ljust(27))
time.sleep(.5)
thread_queue.put_nowait("Almost Done......... ".ljust(27))
def callback(text):
# To be run after the splash screen completes
print(text)
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.callback_return = SplashScreen.show(root=self,
frame_funct=splash_window_constructor,
function=startup_process,
callback=functools.partial(callback,"Callback Done"))
self.title("MyApp")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.application_frame = tk.Label(self, text='Rest of my app here', anchor='center')
self.application_frame.grid(column=0, row=0, sticky='nswe')
self.mainloop()
if __name__ == "__main__":
App()
Apparently this is due to a problem with the window stacking order when windows are not decorated by the window manager after calling overrideredirect(True). It seems to have occurred on other platforms as well.
Running the following code on macOS 10.12.5 with Python 3.6.1 and tcl/tk 8.5.18, toplevel windows do not appear after the button 'open' is clicked:
import tkinter as tk
class TL(tk.Toplevel):
def __init__(self):
tk.Toplevel.__init__(self)
self.overrideredirect(True)
# self.after_idle(self.lift)
tl_label = tk.Label(self, text='this is a undecorated\ntoplevel window')
tl_label.grid(row=0)
b_close = tk.Button(self, text='close', command=self.close)
b_close.grid(row=1)
def close(self):
self.destroy()
def open():
TL()
root = tk.Tk()
label = tk.Label(root, text='This is the root')
label.grid(row=0)
b_open = tk.Button(root, text='open', command=open)
b_open.grid(row=1)
root.mainloop()
Uncommenting the line self.after_idle(self.lift) fixes the problem (simply calling self.lift() does too. But using after_idle()prevents the window from flashing up for a fraction of a second before it is moved to its position and resized, which is another problem I have experienced repeatedly with tkinter and keeps me wondering whether I should move on to learn PyQT or PySide2...).
As to the problem with closing an undecorated window in my original question: calling after_idle(window.destroy()) instead of window.destroy() seems to fix that too. I do not understand why.
In case other people reproduce this and somebody hints me towards where to report this as a bug, I am happy to do so.

tkinter cursor not changing until after action despite update_idletasks()

I am trying to change the cursor in my tkinter program to show the program is working but the cursor only changes to the working cursor until after the work is done, this is as compressed as I can make the code
warning: to demonstrate working it will count to 99,999,999 when you press go to page one
import tkinter as tk # python 3
from tkinter import font as tkfont # python 3
#import Tkinter as tk # python 2
#import tkFont as tkfont # python 2
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (StartPage, PageOne):
page_name = F.__name__
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("StartPage")
def show_frame(self, page_name):
'''Show a frame for the given page name'''
frame = self.frames[page_name]
frame.tkraise()
class StartPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is the start page", font=controller.title_font)
label.pack(side="top", fill="x", pady=10)
button1 = tk.Button(self, text="Go to Page One",
command=self.go)
button1.pack()
def go(self):
# do something for like 5 seconds to demonstrate working
working(True)
l = [x for x in range(99999999)]
self.controller.show_frame('PageOne')
class PageOne(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is page 1", font=controller.title_font)
label.pack(side="top", fill="x", pady=10)
button = tk.Button(self, text="Go to the start page",
command=self.back)
button.pack()
def back(self):
working(False)
self.controller.show_frame('StartPage')
def working(yesorno):
if yesorno==True:
app.config(cursor='wait')
else:
app.config(cursor='')
app.update_idletasks()
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
Edit: I would like to thank Switch between two frames in tkinter for this app layout example
This code was tested in windows 10 and Python 3. I found the cursor would not change until control was returned to mainloop. The code here outlines how to consistently display the busy cursor during a long running task. Further, this code demonstrates how to retrieve the data from the long running task (like results from a database query).
#! python3
'''
Everything you need to run I/O in a separate thread and make the cursor show busy
Summary:
1. Set up to call the long running task, get data from windows etc.
1a. Issue a callback to the routine that will process the data
2. Do the long running task - absolutely no tkinter access, return the data
3. Get the data from the queue and process away. tkinter as you will
'''
import tkinter as tk
import tkinter.ttk as ttk
from threading import Thread
from threading import Event
import queue
class SimpleWindow(object):
def __init__(self):
self._build_widgets()
def _build_widgets(self):
# *************************************************************************************************
# * Build buttons and some entry boxes
# *************************************************************************************************
g_col = 0
g_row = 0
WaiterFrame = ttk.Frame()
WaiterFrame.pack( padx=50)
i = 0
g_row += 1
longWaitButton = ttk.Button(WaiterFrame, text='Long Wait',command=self.setup_for_long_running_task)
longWaitButton.grid(row = g_row, column = i, pady=4, padx=25)
i += 1
QuitButton = ttk.Button(WaiterFrame, text='Quit', command=self.quit)
QuitButton.grid(row = g_row, column = i,pady=4, padx=25)
i += 1
self.Parm1Label = ttk.Label(WaiterFrame, text="Parm 1 Data")
self.Parm1Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm1 = ttk.Entry(WaiterFrame)
self.Parm1.grid(row = g_row, column = i, pady=4, padx=2)
i += 1
self.Parm2Label = ttk.Label(WaiterFrame, text="Parm 2 Data")
self.Parm2Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm2 = ttk.Entry(WaiterFrame)
self.Parm2.grid(row = g_row, column = i, pady=4, padx=2)
i += 1
self.Parm3Label = ttk.Label(WaiterFrame, text="Parm 3 Data")
self.Parm3Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm3 = ttk.Entry(WaiterFrame)
self.Parm3.grid(row = g_row, column = i, pady=4, padx=2)
i += 1
self.Parm4Label = ttk.Label(WaiterFrame, text="Parm 4 Data")
self.Parm4Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm4 = ttk.Entry(WaiterFrame)
self.Parm4.grid(row = g_row, column = i, pady=4, padx=2)
def quit(self):
root.destroy()
root.quit()
def setup_for_long_running_task(self):
# ********************************************************************************************************
# * Do what needs to be done before starting the long running task in a thread
# ********************************************************************************************************
Parm1, Parm2, Parm3, Parm4 = self.Get_Parms()
root.config(cursor="wait") # Set the cursor to busy
# ********************************************************************************************************
# * Set up a queue for thread communication
# * Invoke the long running task (ie. database calls, etc.) in a separate thread
# ********************************************************************************************************
return_que = queue.Queue(1)
workThread = Thread(target=lambda q, w_self, p_1, p_2, p_3, p_4: \
q.put(self.long_running_task(Parm1, Parm2, Parm3, Parm4)),
args=(return_que, self, Parm1, Parm2, Parm3, Parm4))
workThread.start()
# ********************************************************************************************************
# * Busy cursor won't appear until this function returns, so schedule a callback to accept the data
# * from the long running task. Adjust the wait time according to your situation
# ********************************************************************************************************
root.after(500,self.use_results_of_long_running_task,workThread,return_que) # 500ms is half a second
# ********************************************************************************************************
# * This is run in a thread so the cursor can be changed to busy. NO tkinter ALLOWED IN THIS FUNCTION
# ********************************************************************************************************
def long_running_task(self, p1,p2,p3,p4):
Event().wait(3.0) # Simulate long running task
p1_out = f'New {p1}'
p2_out = f'New {p2}'
p3_out = f'New {p3}'
p4_out = f'New {p4}'
return [p1_out, p2_out, p3_out, p4_out]
# ********************************************************************************************************
# * Waits for the thread to complete, then gets the data out of the queue for the listbox
# ********************************************************************************************************
def use_results_of_long_running_task(self, workThread,return_que):
ThreadRunning = 1
while ThreadRunning:
Event().wait(0.1) # this is set to .1 seconds. Adjust for your process
ThreadRunning = workThread.is_alive()
while not return_que.empty():
return_list = return_que.get()
self.LoadWindow(return_list)
root.config(cursor="") # reset the cursor to normal
def LoadWindow(self, data_list):
self.Parm1.delete(0, tk.END)
self.Parm2.delete(0, tk.END)
self.Parm3.delete(0, tk.END)
self.Parm4.delete(0, tk.END)
i=0; self.Parm1.insert(0,data_list[i])
i+=1; self.Parm2.insert(0,data_list[i])
i+=1; self.Parm3.insert(0,data_list[i])
i+=1; self.Parm4.insert(0,data_list[i])
# ********************************************************************************************************
# * The long running task thread can't get to the tkinter self object, so pull these parms
# * out of the window and into variables in the main process
# ********************************************************************************************************
def Get_Parms(self):
p1 = self.Parm1Label.cget("text")
p2 = self.Parm2Label.cget("text")
p3 = self.Parm3Label.cget("text")
p4 = self.Parm4Label.cget("text")
return p1,p2,p3,p4
def WaitForBigData():
global root
root = tk.Tk()
root.title("Wait with busy cursor")
waitWindow = SimpleWindow()
root.mainloop()
if __name__ == '__main__':
WaitForBigData()
I suspect that all events need to be proceeded to change a cursor's look, because cursor depends on operating system and there're some events to handle (I assume that), since update_idletask has no effect - your cursor really change look only when code flow reaches a mainloop. Since you can treat an update as mainloop(1) (very crude comparison) - it's a good option if you know what you doing, because noone wants an endless loop in code.
Little snippet to represent idea:
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
import time
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.button = tk.Button(self, text='Toggle cursor', command=self.toggle_business)
self.button.pack()
def toggle_business(self):
if self['cursor']:
self.config(cursor='')
else:
self.config(cursor='wait')
# self.update_idletasks() # have no effect at all
# self.update() # "local" mainloop(1)
# simulate work with time.sleep
# time.sleep(3)
# also your work can be scheduled so code flow can reach a mainloop
# self.after(500, lambda: time.sleep(3))
app = App()
app.mainloop()
To overcome this problem you can use:
update method (note warnings)
after method for scheduled work (opportunity to reach a mainloop for a code flow)
threading for "threaded" work (another opportunity, but GUI is responsive, you can handle other events and even simulate unresponsiveness, in other hand threading adds complexity, so use it if you really need it).
Note: There's no difference in behaviour between universal and native cursors on Windows platform.

How can I create a non-unique browse button in python?

I am using; Python 3.4, Windows 8, tkinter. I am trying to create a generic browse button that will get a file name and assign it to a variable.
I have created the following code to do this.
from tkinter import *
from tkinter import filedialog
from tkinter import ttk
class Application(Frame):
# A GUI Application.
# Initialize the Frame
def __init__(self, master):
Frame.__init__(self, master)
nbook = ttk.Notebook(root)
nbook.pack(fill='both', expand='yes')
f1 = ttk.Frame(nbook)
nbook.add(f1, text='QC1')
self.qc1_tab(f1)
# create QC1 tab contents
def qc1_tab(self, tab_loc):
# Set up file name entry.
Label(tab_loc, text="Select file:").grid(pady=v_pad, row=0, column=0, sticky=W)
self.flnm = ttk.Entry(tab_loc, width=60)
self.flnm.focus_set()
self.flnm.grid(pady=v_pad, row=0, column=1, columnspan=2, sticky=W)
ttk.Button(tab_loc, text="Browse...", width=10, command=self.browse).grid(row=0, column=3)
def browse(self):
temp = filedialog.askopenfilename()
self.flnm.delete(0, END)
self.flnm.insert(0, temp)
root = Tk()
app = Application(root)
root.mainloop()
The only problem with this is that the browse button is tied to self.flnm and cannot be used for anything else. I plan to use the browse button several times to acquire the file name of several different files and would rather not have multiple browse commands.
I need to call it from a button and somehow assign it to a variable afterwards.
I was thinking of something like
ttk.Button(..., command=lambda: self.flnm = self.browse)
...
def browse(self):
filename = filedialog.askopenfilename()
return filename
but that failed terribly.
How can I make a general purpose browse button?
You can write:
def browse(self, target):
temp = filedialog.askopenfilename()
target.delete(0, END)
target.insert(0, temp)
ttk.Button(..., command=lambda: self.browse(self.flnm))

Categories