How to automatically update a tkinter ui - python

I'm trying to make an UI in python with tkinter. I want it to show the very volatile value of something in the right corner (I'm getting the value through an api). This value can changes almost every second so I want to be sure it's up to date. I could make a button that refresh the ui but that's not really cool for the user. My question is how can I automatically refresh the value in the ui every x seconds with tkinter or is it isn't possible what's a better solution.

You can make a Thread that is running in the background and use StringVar() to update the value every x seconds in tkinter.
Example code that updates the time:
import tkinter as tk
from threading import Thread
import datetime
import time
class SampleApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
# Make a StringVar that will contain the value
self.value = tk.StringVar()
# Make a label that will show the value of self.value
self.label = tk.Label(self, textvariable=self.value).pack()
# Make a thread that will run outside the tkinter program
self.thread = Thread(target=self.show_time)
# set daemon to True (This means that the thread will stop when you stop the tkinter program)
self.thread.daemon = True
# start the thread
self.thread.start()
tk.Button(self, text="Click me", command=lambda: print("Hello World")).pack()
def show_time(self):
# The thread will execute this function in the background, so you need to while loop to update the value of self.value
while True:
# Get the time (in your case, you need to get the api data)
data = datetime.datetime.now().strftime('Time: %H:%M:%S, Milliseconds: %f')
# Update the StringVar variable
self.value.set(data)
# Pause the while loop with 1 second, so you can set an interval to update your value
time.sleep(1)
# rest of your code.
root = SampleApp()
root.mainloop()

Related

Tkinter - Thread communication and shared result - with progressbar

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.

Delete all from TkInter Canvas and put new items while in mainloop

The goal is to achieve different "screens" in TkInter and change between them. The easiest to imagine this is to think of a mobile app, where one clicks on the icon, for example "Add new", and new screen opens. The application has total 7 screens and it should be able to change screens according to user actions.
Setup is on Raspberry Pi with LCD+touchscreen attached. I am using tkinter in Python3. Canvas is used to show elements on the screen.
Since I am coming from embedded hardware world and have very little experience in Python, and generally high-level languages, I approached this with switch-case logic. In Python this is if-elif-elif...
I have tried various things:
Making global canvas object. Having a variable programState which determines which screen is currently shown. This obviously didn't work because it would just run once and get stuck at the mainloop below.
from tkinter import *
import time
root = Tk()
programState = 0
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
time.sleep(2)
canvas.delete(ALL) #delete all objects from canvas
programState = 1
elif(programState == 1):
....
....
....
root.mainloop()
Using root.after function but this failed and wouldn't show anything on the screen, it would only create canvas. I probably didn't use it at the right place.
Trying making another thread for changing screens, just to test threading option. It gets stuck at first image and never moves to second one.
from tkinter import *
from threading import Thread
from time import sleep
def threadFun():
while True:
backgroundImage = PhotoImage(file="image1.gif")
backgroundImage2 = PhotoImage(file="image2.gif")
canvas.create_image(0,0,image=backgroundImage, anchor=NW)
sleep(2)
canvas.delete(ALL)
canvas.create_image(0,0,image=backgroundImage2, anchor=NW)
root = Tk()
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
# daemon=True kills the thread when you close the GUI, otherwise it would continue to run and raise an error.
Thread(target=threadFun, daemon=True).start()
root.mainloop()
I expect this app could change screens using a special thread which would call a function which redraws elements on the canvas, but this has been failing so far. As much as I understand now, threads might be the best option. They are closest to my way of thinking with infinite loop (while True) and closest to my logic.
What are options here? How deleting whole screen and redrawing it (what I call making a new "screen") can be achieved?
Tkinter, like most GUI toolkits, is event driven. You simply need to create a function that deletes the old screen and creates the new, and then does this in response to an event (button click, timer, whatever).
Using your first canvas example
In your first example you want to automatically switch pages after two seconds. That can be done by using after to schedule a function to run after the timeout. Then it's just a matter of moving your redraw logic into a function.
For example:
def set_programState(new_state):
global programState
programState = new_state
refresh()
def refresh():
canvas.delete("all")
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
canvas.after(2000, set_programState, 1)
elif(programState == 1):
...
Using python objects
Arguably a better solution is to make each page be a class based off of a widget. Doing so makes it easy to add or remove everything at once by adding or removing that one widget (because destroying a widget also destroys all of its children)
Then it's just a matter of deleting the old object and instantiating the new. You can create a mapping of state number to class name if you like the state-driven concept, and use that mapping to determine which class to instantiate.
For example:
class ThisPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
class ThatPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
page_map = {0: ThisPage, 1: ThatPage}
current_page = None
...
def refresh():
global current_page
if current_page:
current_page.destroy()
new_page_class = page_map[programstate]
current_page = new_page_class()
current_page.pack(fill="both", expand=True)
The above code is somewhat ham-fisted, but hopefully it illustrates the basic technique.
Just like with the first example, you can call update() from any sort of event: a button click, a timer, or any other sort of event supported by tkinter. For example, to bind the escape key to always take you to the initial state you could do something like this:
def reset_state(event):
global programState
programState = 0
refresh()
root.bind("<Escape>", reset_state)

Pygame, Tkinter or something else for TUI and responding to keyboard input?

I'm quite new to programming and trying to write a fairly simple program that records the times in between different keyboard button presses (a bit like multiple reaction time tests) and prints these times on screen in an array, then terminates and saves the array after a certain period of time is up.
I've already written most of the program in pygame after giving up on Tkinter because it seemed to be the best thing for responding to keyboard input in real time. However, now that I'm wanting the text to scroll automatically once the screen fills up, add more columns to the array, and export to Excel, I'm starting to wonder whether I'd be better off with a module more suited to text handling.
Can anyone advise me on whether I'm making a mistake attempting this in pygame and whether responding immediately to multiple keyboard inputs in Tkinter is possible? I can provide more detail if necessary.
Using Tkinter, you can bind to <Any-KeyPress>. The function that is called is passed an event object that has a timestamp. You can use that to compute the time between events.
Here's a quick example that shows how to display the time between keypresses. You can of course add your own logic to count and track and display however you want.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
self.last_event = None
tk.Frame.__init__(self, parent)
self.label = tk.Label(self, text="")
self.text = tk.Text(self, wrap="word")
self.label.pack(side="top", fill="x")
self.text.pack(fill="both", expand=True)
self.text.bind("<Any-KeyRelease>", self.on_key_release)
def on_key_release(self, event):
if self.last_event is not None:
delta = event.time - self.last_event.time
self.label.configure(text="time since last event: %s ms" % delta)
self.last_event = event
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.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()

When I use update() with tkinter my Label writes another line instead of rewriting the same text

When I call the update() method using tkinter instead of rewriting the label it just writes the label under the previous call. I would like for this to rewrite over the previous line.
For Example:
root=Tk()
while True:
w=Label(root, text = (price, time))
w.pack()
root.update()
Your problem is simply this: when you do while True, you create an infinite loop. The code in that loop will run until you force the program to exit. In that loop you create a label. Thus, you will create an infinite number of labels.
If you want to update a label on a regular basis, take advantage of the already running infinite loop - the event loop. You can use after to schedule a function to be called in the future. That function can reschedule itself to run again, guaranteeing it will run until the program quits.
Here's a simple example:
import Tkinter as tk
import time
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.clock = tk.Label(self, text="")
self.clock.pack()
# start the clock "ticking"
self.update_clock()
def update_clock(self):
now = time.strftime("%H:%M:%S" , time.gmtime())
self.clock.configure(text=now)
# call this function again in one second
self.after(1000, self.update_clock)
if __name__== "__main__":
app = SampleApp()
app.mainloop()
No.
I suspect, without having seen it, that there are at least a couple of confusions in the code wDroter has written. In general, it is NOT necessary in well-structured Tkinter code to use update() at all. Here's a small example that illustrates updates to the text of a Label:
import Tkinter
import time
def update_the_label():
updated_text = time.strftime("The GM time now is %H:%M:%S.", time.gmtime())
w.configure(text = updated_text)
root = Tkinter.Tk()
w = Tkinter.Label(root, text = "Hello, world!")
b = Tkinter.Button(root, text = "Update the label", command = update_the_label)
w.pack()
b.pack()
root.mainloop()
Run this. Push the button. Each time you do so (as long as your pushes differ by at least a second), you'll see the text update.
you want to use .configure insted
while True:
w.Configure(text = (price, time))
root.update()
instead of
w.pack()
you can write
w.grid(row=0, column=0)
pack() in tkinter usually packs things in a single row/column. It lays things out along the sides of a box. Whereas, grid() has more of a table like structure. So when you write row=0 and column=0, it has no choice but to replace the previous if it exists. Because you have provided a very specific position instead of just pushing it to the window (which is hat pack() does)
The BadRoot class should demonstrate the problem that you are having. You can comment out the call to the class to verify with a complete, working example. If you run the code as written, it will update the label in the GoodRoot class. The first line that is commented out shows an alternative syntax for changing the text in your label.
from tkinter import Tk, Label
from time import sleep
from random import random
class BadRoot(Tk):
def __init__(self, price, time):
super().__init__()
self.labels = []
while True:
self.labels.append(Label(self, text=(price, time)))
self.labels[-1].pack()
self.update()
sleep(1)
class GoodRoot(Tk):
def __init__(self, callback):
super().__init__()
self.label = Label(self, text=str(callback()))
self.label.pack()
while True:
## self.label['text'] = str(callback())
self.label.configure(text=str(callback()))
self.update()
sleep(1)
if __name__ == '__main__':
## BadRoot('$1.38', '2:37 PM')
GoodRoot(random)
The problem with your original code is that a new label is created and packed into the interface each time through the loop. What you actually want to do is just edit the text being displayed by the label instead replacing the label with a new one. There are others ways of doing this, but this method should work for you.

Categories