Tkinter - Thread communication and shared result - with progressbar - python

I am building an interface in Tkinter in which the main window (let's call it 'root') contains a button (say, 'create'). Furthermore, assume I have already defined a function 'f'. I would like to create the following effect: clicking on 'create' would execute 'f' in the background and at the same time open an indeterminate progress bar in a new window. Moreover, and this is the tricky part for me, I want the progress bar to close automatically after 'f' is done executing. How can I achieve this? Could you please provide a minimal working example? I think that key lies on constructing a proper function to pass as 'command' option to 'create'.
This is what I have thus far. It is not even running properly, as the progress bar runs indefinitely and the task starts being executed only after the progress bar is closed (or after closing 'root'). However, it feels like this is really close, and there is some small issue that I should fix but that I cannot see:
from tkinter import *
from tkinter.ttk import *
import threading
import time
root = Tk() # Main window
def create_command():
stop_flag = threading.Event() # create a flag to stop the progress bar
def f():
# function to do some task
print("Starting task...")
time.sleep(5) # simulate some time-consuming task
print("Task complete.")
stop_flag.set() # set the stop flag to indicate that progress_check() should stop
progress_bar_window = Toplevel(root) # Progress bar window
progress_bar = Progressbar(progress_bar_window, orient= 'horizontal', length= 300, mode= 'indeterminate') # Create progress bar
progress_bar.pack()
progress_bar.start()
def progress_check():
# function to run an infinite loop
while not stop_flag.is_set():
print("Running infinite loop...")
time.sleep(1)
progress_bar.stop()
progress_bar_window.destroy()
progress_bar_window.mainloop() # Start mainloop for progress bar window
# create separate threads to run the functions
thread1 = threading.Thread(target=f, args=())
thread2 = threading.Thread(target=progress_check, args=())
thread1.start() # start executing f
thread2.start() # start the progress_check
# wait for f to finish before stopping the infinite loop
thread2.join()
stop_flag.set() # set the stop flag to indicate that progress_bar() should stop
create_button = Button(root, text= "Create", command= create_command)
create_button.pack()
root.mainloop()

Look at this:
from tkinter import ttk
import tkinter as tk
import threading
import time
root = tk.Tk() # Main window
def create_command():
# create a flag to stop the progress bar
stop_flag = threading.Event()
def f():
print("Starting task...\n", end="")
time.sleep(5)
print("Task complete.\n", end="")
# set the stop flag to indicate that progress_check() should stop
stop_flag.set()
progress_bar_window = tk.Toplevel(root)
progress_bar = ttk.Progressbar(progress_bar_window, orient="horizontal",
length=300, mode="indeterminate")
progress_bar.pack()
progress_bar.start()
def progress_check():
# If the flag is set (function f has completed):
if stop_flag.is_set():
# Stop the progressbar and destroy the toplevel
progress_bar.stop()
progress_bar_window.destroy()
else:
# If the function is still running:
print("Running infinite loop...\n", end="")
# Schedule another call to progress_check in 100 milliseconds
progress_bar.after(100, progress_check)
# start executing f in another thread
threading.Thread(target=f, daemon=True).start()
# Start the tkinter loop
progress_check()
create_button = tk.Button(root, text= "Create", command=create_command)
create_button.pack()
root.mainloop()
Explanation:
To run a loop alongside tkinter, you should use .after, like in this question. I changed progress_check so that tkinter calls it every 100 milliseconds until stop_flag is set. When stop_flag is set, the progressbar stops and the Toplevel is destroyed.
A few minor points:
from ... import * is discouraged
With tkinter, you don't need more than 1 .mainloop() unless you are using .quit(). .mainloop() doesn't stop until all tk.Tk windows have been destroyed.
There is no point in creating a new thread, if you are going to call .join() right after.

First of all, don't use wildcard imports!
Wildcard-imports can lead to name conflicts, for instance swap the wildcard imports from ttk and tkinter. You end up using tkinter buttons even if you want to use ttk buttons. Same issue might appear with PhotoImage and pillow. The magic word is "qualified-names".
Also I like to have some sort of structure in my code, I prefer classes. However, even in a procedural code there can be some sort of structure. For instance:
imports
1.0) built-in modules
1.1) import external modules
1.2) import own modules
Constants and global variables
free functions
main window definitions
...
every logical block can be separated with comments that indicates what the following code might do or represents. This could also be useful to "jump" with the search function of your IDE to the point you want to work next, in larger scripts and modules this becomes handy.
A slightly different version of your code can be found below and it is not intended to be used:
import tkinter as tk
from tkinter import ttk
import threading
import time
def start_worker_thread():
'This function starts a thread and pops up a progressbar'
def generate_waiting_window():
'nested function to generate progressbar'
#disable button to inform user of intended use
start_btn.configure(state=tk.DISABLED)
#toplevel definitions
toplevel = tk.Toplevel(root)
toplevel.focus()
#progressbar definitions
progress = ttk.Progressbar(
toplevel, orient=tk.HORIZONTAL, length=300, mode='indeterminate')
progress.pack(fill=tk.BOTH, expand=True)
progress.start()
return toplevel
def long_blocking_function():
'This function simulates a long blocking call'
stopped = threading.Event()
n = 0
while not stopped.is_set():
n += 1
print('working in turn', n)
time.sleep(0.5)
if n == 10:
stopped.set()
nonlocal thread_info
thread_info = n
#important!! last logical line
toplevel.destroy()
return None
toplevel = generate_waiting_window()
thread_info = None
thread = threading.Thread(target=long_blocking_function)
thread.start()
toplevel.wait_window()
start_btn.configure(state='normal')
result_lbl.configure(text='Result is: '+str(thread_info))
print('thread exited on turn', thread_info)
#Main window definitions
root = tk.Tk()
start_btn = ttk.Button(root, text="Start", command=start_worker_thread)
start_btn.pack()
result_lbl = tk.Label(root, text='Result is: None')
result_lbl.pack()
#start the application
root.mainloop()
#after application is destroyed
While this code is efficient for this simple task, it requires understanding what it does to debug it. That is why you won't find code like this often. It is here for demonstrative purposes. So what is wrong with the code and how does it differ from the meanwhile canonical way of using threads in tkinter.
First of all, it uses nested function. While this might not an issue here, computing the same function over and over again, can slow down your code significantly.
Second it uses tkwait and therefore has some caveats over the linked answer.
Also threading.Event is a low-level primitive for communication, while there are cases you could use it, tkinter offers own tools for it and these should be preferred.
In addition it does not use a threadsafe storage for the data and this could also lead to confusion and non reliable data.
A better approach and a slight improvement to the canonical way can be found here:
import tkinter as tk
from tkinter import ttk
import threading
import sys
import queue
import time
inter_thread_storage = queue.Queue()
temporary_toplevel = None
EXIT = False
def on_thread_ended_event(event):
start_btn.configure(state=tk.NORMAL)
result = inter_thread_storage.get_nowait()
result_lbl.configure(text='Result is: '+str(result))
global temporary_toplevel
temporary_toplevel.destroy()
temporary_toplevel = None
def worker_thread_function():
'Simulates a long blocking function'
n = 0
while n < 10 and not EXIT:
n += 1
print('working in turn', n)
time.sleep(0.5)
if not EXIT:
inter_thread_storage.put(n)
root.event_generate('<<ThreadEnded>>')
def start_worker_thread():
'This function starts a thread and pops up a progressbar'
#toplevel definitions
toplevel = tk.Toplevel(root)
toplevel.focus()
#progressbar definitions
progress = ttk.Progressbar(
toplevel, orient=tk.HORIZONTAL, length=300, mode='indeterminate')
progress.pack(fill=tk.BOTH, expand=True)
progress.start()
#thread definitions
thread = threading.Thread(target=worker_thread_function)
thread.start()
#disable button to inform user of intended use
start_btn.configure(state=tk.DISABLED)
#store toplevel temporary
global temporary_toplevel
temporary_toplevel = toplevel
#Main window definitions
root = tk.Tk()
root.bind('<Destroy>',lambda e:setattr(sys.modules[__name__], 'EXIT', True))
root.bind('<<ThreadEnded>>', on_thread_ended_event)
start_btn = ttk.Button(root, text="Start", command=start_worker_thread)
start_btn.pack()
result_lbl = tk.Label(root, text='Result is: None')
result_lbl.pack()
#start the application
root.mainloop()
#after application is destroyed
This is how it works:
generate a new event
Make sure your toplevel can be reached, with global or alternatives.
store data threadsafe like in a Queue
fire the event and let tkinter call your function safely in the mainloop.
it has a flag for the edge case, where the user closes the main window before the thread finished.
Let me know, if you have questions to my answer.

Related

Alternative To Time.Sleep() for pausing a function

I created an example code because my original is too big and has private information(My own) in it.
While running a program from a Tkinter GUI, it runs the program but makes the GUI unresponsive because of time.sleep() blocking the GUI from updating.
I am trying to avoid using timers because it fires a different function after a duration instead of simply pausing the function and then continuing the same function.
Is there an alternative that does not block the GUI but still adds a delay inside of the function?
Example Code:
from tkinter import *
import time
wn = Tk()
wn.geometry("400x300")
MyLabel = Label(wn, text="This is a Status Bar")
MyLabel.pack()
def MyFunction():
Value = 1
while Value < 10:
print("Do something")
time.sleep(1) **# - here blocks everything outside of the function**
MyLabel.config(text=Value)
# A lot more code is under here so I cannot use a timer that fires a new function
Value = 1
MyButton = Button(wn, text="Run Program", command=MyFunction)
MyButton.pack()
wn.mainloop()
Edit: Thanks so much, you're answers were fast and helpful, I changed the code and added "wn.mainloop()" after the delay and replaced "time.sleep(1)" with wn.after(100, wn.after(10, MyLabel.config(text=Value))
here is the final code:
from tkinter import *
import time
wn = Tk()
wn.geometry("400x300")
MyLabel = Label(wn, text="This is a Status Bar")
MyLabel.pack()
def MyFunction():
Value = 0
while Value < 10:
print("Do something")
wn.after(10, MyLabel.config(text=Value))
Value += 1
wn.mainloop()
MyButton = Button(wn, text="Run Program", command=MyFunction)
MyButton.pack()
wn.mainloop()
The short answer is that you can use wn.after() to request a callback after a certain amount of time. That's how you handle it. You get a timer tick at a one-per-second rate, and you have enough state information to let you proceed to the next state, then you go back to the main loop.
Put another way, timers are exactly how you have to solve this problem.
Fundamentally, any callback function in Tkinter runs in the main GUI thread, and so the GUI thread will block until the function exits. Thus you cannot add a delay inside the function without causing the GUI thread to be delayed.
There are two ways to solve this. One would be to refactor your function into multiple pieces so that it can schedule the remaining work (in a separate function) via .after. This has the advantage of ensuring that all of your functions are running in the main thread, so you can perform GUI operations directly.
The other way is to run your function in a separate thread that is kicked off whenever your main callback is executed. This lets you keep all the logic inside the one function, but it can no longer perform GUI operations directly - instead, any GUI operations would have to go through an event queue that you manage from the main thread.
You can combine after() and wait_variable() to simulate time.sleep() without blocking tkinter from handling pending events and updates:
def tk_sleep(delay):
v = wn.IntVar()
# update variable "delay" ms later
wn.after(delay, v.set, 0)
# wait for update of variable
wn.wait_variable(v)
Using tk_sleep() in your while loop:
def MyFunction():
Value = 1
while Value < 10:
print("Do something")
tk_sleep(1000) # waits for one second
MyLabel.config(text=Value)
# A lot more code is under here so I cannot use a timer that fires a new function
Value += 1

Display a countdown timer in TKinter while making the code BLOCKING but do not freeze the GUI

Please read my question carefully - I know there are plenty of ways to implement a countdown timer on Tkinter without freezing the window, but all of the existing solutions also cause the code to be non-blocking. For my use case, I need to schedule a task to run automatically after time's up while keeping the GUI active (not frozen). My guess is that I need to somehow block the execution of the next task, but that will also freeze the GUI window. So is there any way out?
What I have so far:
root = Tk.Tk()
def countdown(time, msg='Counting down'):
def tick():
nonlocal time
status(f'{msg} ({60 - time}sec)')
time += 1
root.after(1000, tick)
where status() is simply a function that updates the text of some buttons.
The current count down function does not work by itself as I don't have a way to stop the after() after the timeout period.
The other parts of the program will be like:
countdown(10) # I need this line to be blocking or somehow prevents the code from going to next line
print('starting scheduled job...')
job()
I have tried to use threading but as I said earlier on, this causes the code to be non-blocking, and the moment I use Thread.join(), the entire GUI freezes again.
Currently, your question doesn't make a lot of sense to me. From what I understand you want your job() function to be called after the countdown.
Using thread for this is unnecessary. You can just use after and once the timer reaches 0 call the job() function.
Here is a minimal example
import tkinter as tk
def job():
status.config(text="starting job")
def countdown(time, msg='Counting down'):
time -= 1
status.config(text=f'{msg} ({time}sec)')
if time != 0:
root.after(1000, countdown, time)
else:
job() # if job is blocking then create a thread
root = tk.Tk()
status = tk.Label(root)
status.pack()
countdown(20)
root.mainloop()
Note: I may have overthought this and the other answer may actually be simpler perhaps
From my understanding you want to create a program that will after a certain time run another task, but neither the task, nor countdown should interfere with the GUI (but the task must run only after countdown), explanation in code comments:
# import what is needed
from tkinter import Tk, Button, Label
from threading import Thread
from queue import Queue, Empty
import time
# the function that the button will call to start the countdown
# and after that the task
def start_countdown():
# disable button so not to accidentally run the task again
# before it has even started
button.config(state='disabled')
# create a queue object
queue = Queue()
update_label(queue)
# set daemon=True to kill thread if the main thread exits
Thread(target=countdown, args=(queue, ), daemon=True).start()
# the task you want to do after countdown
def do_task():
for _ in range(10):
print('doing task...')
time.sleep(0.5)
# the actual countdown (btw using `time.sleep()` is more precise
# and only the thread will sleep)
# put data in queue so that it can easily be accessed
# from the main thread
def countdown(queue):
seconds = 10
for i in range(1, seconds + 1):
queue.put(f'Seconds left: {seconds + 1 - i}')
time.sleep(1)
queue.put('Starting task')
# place a sentinel to tell the reading part
# that it can stop
queue.put('done')
# do the task, this will run it in the same thread
# so it won't block the main thread
do_task()
# function to update the label that shows the users how many seconds left
def update_label(queue):
try:
# since block=False it will raise an exception
# if nothing is in queue
data = queue.get(block=False)
except Empty: # therefore except it and simply pass
pass
else:
# if no error was raised check if data is sentinel,
# if it is, stop this loop and enable the button (if needed)
if data == 'done':
button.config(state='normal')
return
# otherwise just update the label with data in the queue
label.config(text=data)
finally:
# and (almost) no matter what happens (nothing much should) loop this again
root.after(100, update_label, queue)
# basic tkinter setup
root = Tk()
root.geometry('300x200')
button = Button(root, text='Start countdown', command=start_countdown)
button.pack(expand=True)
label = Label(root)
label.pack(expand=True)
root.mainloop()

Python tkinter after method causes window to freeze

I've looked around stackoverflow and am pretty sure this isn't a duplicate. I need to poll a queue every 1ms (or as quickly as possible), and this has to run in the same thread as my tkinter window otherwise I can't update my labels from the queue data. (someone correct me if i'm wrong here). Currently my code looks like this:
def newData():
global gotNewData
if q.get == 1: #if there is new data
updateVariables() #call the function to update my labels with said data
q.queue.clear() #clear the queue
gotNewData = 0 #no new data to get
q.put(gotNewData)
MainPage.after(1, newData)
else:
MainPage.after(1, newData)
however when I run this code, my tkinter window freezes instantly. I commented out the line which calls the other function and it still freezes so i'm pretty sure it's this function which is causing the problem. Any help is greatly appreciated.
So what I would do if you must have threading is to use a StringVar() in the threaded function instead of having to work with a widget directly.
I feel like 1000 times a second is excessive. Maybe do 10 times a sec instead.
Take a look at this example and let me know if you have any questions.
import tkinter as tk
import threading
root = tk.Tk()
lbl = tk.Label(root, text="UPDATE ME")
lbl.pack()
q_value = tk.StringVar()
q = tk.Entry(root, textvariable=q_value)
q.pack()
def updateVariables(q):
lbl.config(text=q)
def newData(q):
if q.get() != '':
updateVariables(q.get())
root.after(100, lambda: newData(q))
else:
root.after(100, lambda: newData(q))
print("not anything")
thread = threading.Thread(target=newData, args=(q_value, ))
thread.start()
root.mainloop()

Threading with Tkinter done right (with or without queues)

i have a much more advanced code, but it all comes to this simple example:
from Tkinter import *
import time
def destroyPrint():
global printOut
try:
printOut.destroy()
except:
pass
def sendData():
global new
global printOut
for i in range(6):
destroyPrint()
time.sleep(1)
printOut=Label(new,text=i,font=('arial',15,'bold'))
printOut.place(x=300,y=500)
def newWindow():
global new
print("ok")
new=Toplevel()
new.minsize(800,600)
functionButton=Button(new,text="Send me",width=20,height=20, command=sendData)
functionButton.place(x=300,y=150)
main = Tk()
main.minsize(800, 600)
menu=Button(main,text="Send data",width=20,height=20, command=newWindow)
menu.place(x=300,y=150)
mainloop()
In this simple example, i want to start sendData function, which will update printOut Label accordingly on every loop iteration. We all know that it doesn't, and that it hangs, until function is done, and prints last number (5).
I tried countless examples with threading and queueing, and i am failing badly.
Please, just simple clarification on this example, how to do threading correctly when you have Tkinter elements in a function that needs to be performed in another thread.
I am really getting frustrated here and i spent last 2 days on this step...
You have to add update_idletasks() to update the label. Instead of destroying and creating, just update the text , and use after() in Tkinter instead of sleep as it spawns a new process whereas time.sleep() hangs the program while sleeping.
from Tkinter import *
import time
def sendData():
global new
##global printOut
printOut=Label(new,text="0",font=('arial',15,'bold'))
printOut.place(x=300,y=500)
for x in range(6):
##destroyPrint()
printOut.config(text=str(x))
new.update_idletasks()
time.sleep(1)
def newWindow():
global new
print("ok")
new=Toplevel()
new.minsize(800,600)
functionButton=Button(new,text="Send me",width=20,
height=20, command=sendData)
functionButton.place(x=300,y=150)
main = Tk()
main.minsize(800, 600)
menu=Button(main,text="Send data",width=20,height=20, command=newWindow)
menu.place(x=300,y=150)
main.mainloop()

Destroying a Toplevel in a thread locks up root

I have a program I've been writing that began as a helper function for me to find a certain report on a shared drive based on some information in that report. I decided to give it a GUI so I can distribute it to other employees, and have ran into several errors on my first attempt to implement tkinter and threading.
I'm aware of the old adage "I had one problem, then I used threads, now I have two problems." The thread did, at least, solve the first problem -- so now on to the second....
My watered down code is:
class GetReport(threading.Thread):
def __init__(self,root):
threading.Thread.__init__(self)
# this is just a hack to get the StringVar in the new thread, HELP!
self.date = root.getvar('date')
self.store = root.getvar('store')
self.report = root.getvar('report')
# this is just a hack to get the StringVar in the new thread, HELP!
self.top = Toplevel(root)
ttk.Label(self.top,text="Fooing the Bars into Bazes").pack()
self.top.withdraw()
def run(self):
self.top.deiconify()
# a function call that takes a long time
self.top.destroy() #this crashes the program
def main():
root = Tk()
date,store,report = StringVar(),StringVar(),StringVar()
#####
## labels and Entries go here that define and modify those StringVar
#####
def launchThread(rpt):
report.set(rpt)
# this is just a hack to get the StringVar in the new thread, HELP!
root.setvar('date',date.get())
root.setvar('store',store.get())
root.setvar('report',report.get())
# this is just a hack to get the StringVar in the new thread, HELP!
reportgetter = GetReport(root)
reportgetter.start()
ttk.Button(root,text="Lottery Summary",
command=lambda: launchThread('L')).grid(row=1,column=3)
root.mainloop()
My expected output is for root to open and populate with Labels, Entries, and Buttons (some of which are hidden in this example). Each button will pull data from the Entries and send them to the launchThread function, which will create a new thread to perform the foos and the bars needed to grab the paperwork I need.
That thread will launch a Toplevel basically just informing the user that it's working on it. When it's done, the Toplevel will close and the paperwork I requested will open (I'm using ShellExecute to open a .pdf) while the Thread exits (since it exits its run function)
What's ACTUALLY happening is that the thread will launch its Toplevel, the paperwork will open, then Python will become non-responsive and need to be "end processed".
As far as I know you cannot use Threading to alter any GUI elements. Such as destroying a Toplevel window.
Any Tkinter code needs to be done in the main loop of your program.
Tkinter cannot accept any commands from threads other than the main thread, so launching a TopLevel in a thread will fail by design since it cannot access the Tk in the other thread. To get around this, use the .is_alive method of threads.
def GetReport(threading.Thread):
def __init__(self,text):
self.text = text
super().__init__()
def run(self):
# do some stuff that takes a long time
# to the text you're given as input
def main():
root = Tk()
text = StringVar()
def callbackFunc(text):
top = Toplevel(root)
ttk.Label(top,text="I'm working here!").pack()
thread = GetReport(text)
thread.start()
while thread.is_alive():
root.update() # this keeps the GUI updating
top.destroy() # only when thread dies.
e_text = ttk.Entry(root,textvariable=text).pack()
ttk.Button(root,text="Frobnicate!",
command = lambda: callbackFunc(text.get())).pack()

Categories