Update the contents of Tkinter window - python

I'm creating a Python program that displays the time and weather in a Tkinter window. I need to have the time, weather, and anything else constantly updating. Here's my old code:
import time
from Tkinter import *
root = Tk()
while True:
now = time.localtime(time.time()) # Fetch the time
label = time.strftime("%I:%M", now) # Format it nicely
# We'll add weather later once we find a source (urllib maybe?)
w = Label(root, text=label) # Make our Tkinter label
w.pack()
root.mainloop()
I've never done anything with Tkinter before, and it's frustrating that the loop doesn't work. Apparently, Tkinter doesn't let you do anything like loops or anything non-Tkinter while it's running. I thought I could maybe do something with threading.
#!/usr/bin/env python
# Import anything we feel like importing
import threading
import time
# Thread for updating the date and weather
class TimeThread ( threading.Thread ):
def run ( self ):
while True:
now = time.localtime(time.time()) # Get the time
label = time.strftime("%I:%M", now) # Put it in a nice format
global label # Make our label available to the TkinterThread class
time.sleep(6)
label = "Weather is unavailable." # We'll add in weather via urllib later.
time.sleep(6)
# Thread for Tkinter UI
class TkinterThread ( threading.Thread ):
def run ( self ):
from Tkinter import * # Import Tkinter
root = Tk() # Make our root widget
w = Label(root, text=label) # Put our time and weather into a Tkinter label
w.pack() # Pack our Tkinter window
root.mainloop() # Make it go!
# Now that we've defined our threads, we can actually do something interesting.
TimeThread().start() # Start our time thread
while True:
TkinterThread().start() # Start our Tkinter window
TimeThread().start() # Update the time
time.sleep(3) # Wait 3 seconds and update our Tkinter interface
So that doesn't work either. Multiple empty windows appear and they glitch out a ton. I get tons of errors in my debugger too.
Do I need to stop and re-open my window when I update? Can I tell Tkinter to update with something like tkinter.update(root) or something like that?
Is there a workaround or solution, or am I missing something? If you see anything wrong with my code, let me know.
Thanks!
Alex

You can "nest" your after calls:
def update():
now = time.localtime(time.time())
label = time.strftime("%I:%M:%S", now)
w.configure(text=label)
root.after(1000, update)
Now you just have to call after once before the mainloop, and it updates every second from now on.

Related

Tkinter TopLevel not showing when it's supposed to

I have a very simple python code: a tkitner button that process some images in the background. I wanted to open a tkinter toplevel to show the user that it was doing something, but for my surprise is not working as I thought it would. The command on the tk.Button is the next method:
def processing(self):
"""Starts the images processing"""
# Open a Tk.Toplevel
aux_topLevel = Splash(self.window) # a simple Tk.Toplevel class, that works perfectly
self._process_images() # starts processing the images
# I wanted to kill here the topLevel created before
aux_topLevel.destroy()
My surprise: the window is displayed once the processing images is done (tried it out adding prints and time.sleep), however, i couldn't display the TopLevel when I wanted to.
Is there anything am I doing wrong? Any help is appreciated. Thank you.
Consider the following example and try to run it.
What you'd think should happen is that the new Toplevel window should open, some event happens for a period of time and then the window is destroyed.
What actually happens is the window is opened, but never displayed, the task occurs and then the window is destroyed.
from tkinter import *
import time
def processing():
new = Toplevel(root)
new.geometry("200x150")
lbl = Label(new,text="--")
lbl.grid()
for i in range(50):
time.sleep(0.1)
#Un-comment the line below to fix
#root.update()
print(i)
lbl['text'] = "{}".format(i)
new.destroy()
root = Tk()
root.geometry('200x100')
btnGo = Button(root,text="Go",command=processing)
btnGo.grid()
root.mainloop()
If you un-comment out the root.update() line and re-run the code, the window will be displayed.
There are better ways to deal with tasks that takes a while to process, such as threading.

Python: How do you obtain variables from another script which are constantly being updated

I've made a script that uses a while True loop to constantly update a series of variables based on UDP packets that I am constantly recieving. I want to ultimately create a GUI that displays that data and updates the screen constantly, which I plan to do with tkinter (using my_label.after in a function which then calls itself, not sure if this is a good plan).
Here is some testing scripts that I can't get to work properly:
GUI2.py (my test looping script)
import time
var = 0
while True:
var += 1
time.sleep(0.1)
GUI Testing.py (the script that would be accessing those variables)
from GUI2 import *
import time
print('never')
print(var)
time.sleep(1)
The second script never reaches the print('never') line, I think because it gets stuck in the other script's while True loop and never returns.
How should I go about this? I have one script that I want in a constant loop to update my variables to the correct values based on incoming packets, and then another script updating a tkinter window. I went this way as most examples I could find using Tkinter didn't use any sort of while True loops. Could I just put my packet recieving code inside the Tkinter mainloop, and would that effectively act as a while True?
EDIT (added Tkinter loop that I can't get working):
This opens a Tkinter window, but the label stays at 99, then reopens a window when I close it with the new x value (ie. 98, 97, etc). I want the label to update every second.
import tkinter as tk
import time
x = 99
while True:
root = tk.Tk()
label = tk.Label(root, text=x)
label.pack()
x -= 1
time.sleep(1)
root.mainloop()
Below is a sample script to show you how you can update the value in the label widget at a certain time interval. I have provided you the hyperlinks to help you understand tkinter's methods. Best regards.
Key points:
use the textvariable option of the tk.Label widget.
use tkinter's control variable. I have shown you how to set and get it's value.
you can use tkinter's widget method called .after() without having to explicitly use a while-statement and time.sleep() method. Tkinter has it's own event loop that you can use.
writing your tkinter GUI as a class makes it easier to implement what you need.
Example Script:
import tkinter as tk
class App(tk.Frame):
def __init__( self, master, *args, **kw ):
super().__init__( master )
self.master = master
self.create_label()
self.update_label()
def create_label( self ):
self.var = tk.IntVar() # Holds an int; default value 0
self.label = tk.Label(self, textvariable=self.var ) # Use textvariable not text
self.label.pack()
def update_label( self ):
value = self.get_value()
self.var.set( value ) # Set the label widget textvariable value.
self.after(1000, self.update_label) # Call this method after 1000 ms.
def get_value( self ):
'''To simulate calling a function to return a value'''
value = self.var.get() + 1
return value
if __name__ == "__main__":
root = tk.Tk()
root.geometry('100x100+0+24')
app = App( root )
app.pack()
root.mainloop() #This command activates tkinter's event loop
Edit:
As a clarification, this answer shows how to utilize the .after() and .mainloop() methods in GUI Testing.py, i.e. using tkinter event loop and not use two while-loops, to achieve what you wanted to do. This is a way to simplify your GUI script.
For more sophisticated algorithms, e.g. more than one while-loop is involved, you have to look into using threads(note it has its issues) or more recently I found a way of using python's Asyncio approach to do it. The learning curve for these two approaches is a lot steeper. To use the asyncio approach, you can explore modifying my answer to do what you want.
Best solution is to use threads however If you plan to do in simplest possible manner then implement the main loop inside your Tkinter GUI and once you read the packet simply update it on your GUI in same loop. Here is the Updated and working Code.
import tkinter as tk
import time
def setvalue(self, x):
self.label.config(text=x, )
root.update()
time.sleep(1)
def changevalues(self):
x = 99
self.label = tk.Label(root, text=x)
self.label.pack()
while x >0:
x -= 1
setvalue(root,x)
root = tk.Tk()
changevalues(root)
root.mainloop()

conflicts between tkinter and pygame.midi

I am attempting to write a Python 3 program which sets up a Gui with tkinter to display an image (in a label)
and set a number of parameters via various widgets. (This part works fine.)
I want that Gui to stay on the screen while I go off and run the rest of the program which uses pygame.midi
to input and output midi data. (It does not use pygame to present any screens.) (This part also works fine on its
own.)
From time to time, at the control of what happens in that part of the program I want to update the Gui and/or reset
some of the parameters and then go back to the midi stuff. (In other words, I'm happy to have the gui lie dormant until I
tell it to wake up.) It won't work.
I've tried placing the mainloop() command at the end of the gui setup. I've tried placing it at the very end of the program.
neither works. It looks to me as if the midi polling that pygame.midi does is not being allowed because both the gui and midi
polling involves threads in conflict. Am I correct? Is there a simple solution? Can you point me to where I might find it?
Code added:
#!/usr/local/bin/python3
from tkinter import Tk
from tkinter import ttk
from tkinter import Frame
from tkinter import E, W
from tkinter import StringVar
import sys
import os
import pygame.midi
def do_nothing(*args):
labelbox.update()
def do_midi():
global pygame_initialized, midi_out, midi_in, msgVar
if not pygame_initialized:
pygame.init()
pygame.midi.init() # Sets PortMidi timer to zero.
midi_in = pygame.midi.Input(3, 4096)
midi_out = pygame.midi.Output(2)
pygame_initialized = True
midi_out.write_short(176, 122, 00) # turn off echo
while True:
if midi_in.poll():
midi_event_list = midi_in.read(1)
event = midi_event_list[0][0][0]
if event == 248: # timing event ignore
continue
if event > 159 or event < 128: # non key-off or key-on midi events are passed through
midi_out.write(midi_event_list)
continue
# From here on we are dealing only with key-on or key-off events
key = midi_event_list[0][0][1]
vel = midi_event_list[0][0][2]
if key == 21: # right now this is the only way back to the gui to Quit
if vel != 0:
midi_out.write_short(176, 122, 127) # Turn local control back on
return
if vel != 0: # only do this for key-on events
msgVar.set(key)
midi_out.write_short(event, key, vel)
def cleanup():
global pygame_initialized, midi_out, midi_in
root.destroy()
if pygame_initialized:
del midi_out
del midi_in
pygame.midi.quit()
sys.exit()
if __name__ == '__main__':
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (0, 0)
pygame_initialized = False
global msgVar
message = "Push the Play button!"
root = Tk()
msgVar = StringVar()
msgVar.set(message)
msgVar.trace("w", do_nothing)
root.title("Testing midi")
root.geometry("900x600+200+100")
frame1 = Frame(root, width=900, height=600)
frame1.grid(column=0, row=0)
ttk.Button(frame1, text="Play", style='My.TButton', command=do_midi).grid(column=0, row=4, pady=(40, 0), sticky=W)
labelbox = ttk.Label(frame1)
labelbox.grid(column=1, row=4)
labelbox.configure(textvariable=msgVar)
ttk.Button(frame1, text="Quit", style='My.TButton', command=cleanup).grid(column=2, row=4, pady=(40, 0), sticky=E)
root.mainloop()
The last code listing incorporates what I learned. Basically I didn't understand the fact that the "variables" referred to in many postings as being updated were, in fact, tkinter vaiables (StringVar, BooleanVar, etc.). So python variables needed to set tkinter variables. I also thought that mainloop automatically looked for changes in tkinter variables and updated automatically. In other words, I didn't understand that I needed "trace" to be set for the variables I wanted to watch. After that, I needed to learn that "trace" doesn't update automatically, you have to use it to trigger an explicit "update".
In my actual code (too long to post here) I am using tkinter variables "set" by midi events to change selections in a listbox (via "clear", "activate", "selection_set", and "see") and "update" images displayed in a label (which are linked to the index of the item selected). As far as I can tell, nothing happens automatically.

label.configure works sometimes why?

Part of my code is as follows:
def get_songs():
label6.configure(text='Wait')
os.system('/home/norman/my-startups/grabsongs')
label6.configure(text='Done')
The label is not updated at the first .configure() but is at the second one.
Except if I cause a deliberate error immediately after the first one at which point it is updated and then the program terminates.
The system call takes about 2 minutes to complete so it isn't as if there isn't time to display the first one.
I am using Python 2.7.6
Does anyone know why please?
I'm going to guess you're using Tkinter. If so, as #albert just suggested, you'll want to call label.update_idletasks() or label.update() to tell Tkinter to refresh the display.
As a very crude example to reproduce your problem, let's make a program that will:
Wait 1 second
Do something (sleep for 2 seconds) and update the text to "wait"
Display "done" afterwards
For example:
import Tkinter as tk
import time
root = tk.Tk()
label = tk.Label(root, text='Not waiting yet')
label.pack()
def do_stuff():
label.configure(text='Wait')
time.sleep(2)
label.configure(text='Done')
label.after(1000, do_stuff)
tk.mainloop()
Notice that "Wait" will never be displayed.
To fix that, let's call update_idletasks() after initially setting the text:
import Tkinter as tk
import time
root = tk.Tk()
label = tk.Label(root, text='Not waiting yet')
label.pack()
def do_stuff():
label.configure(text='Wait')
label.update_idletasks()
time.sleep(2)
label.configure(text='Done')
label.after(1000, do_stuff)
tk.mainloop()
As far as why this happens, it actually is because Tkinter doesn't have time to update the label.
Calling configure doesn't automatically force a refresh of the display, it just queues one the next time things are idle. Because you immediately call something that will halt execution of the mainloop (calling an executable and forcing python to halt until it finishes), Tkinter never gets a chance to process the changes to the label.
Notice that while the gui displays "Wait" (while your process/sleep is running) it won't respond to resizing, etc. Python has halted execution until the other process finishes running.
To get around this, consider using subprocess.Popen (or something similar) instead of os.system. You'll then need to perodically poll the returned pipe to see if the subprocess has finished.
As an example (I'm also moving this into a class to keep the scoping from getting excessively confusing):
import Tkinter as tk
import subprocess
class Application(object):
def __init__(self, parent):
self.parent = parent
self.label = tk.Label(parent, text='Not waiting yet')
self.label.pack()
self.parent.after(1000, self.do_stuff)
def do_stuff(self):
self.label.configure(text='Wait')
self._pipe = subprocess.Popen(['/bin/sleep', '2'])
self.poll()
def poll(self):
if self._pipe.poll() is None:
self.label.after(100, self.poll)
else:
self.label.configure(text='Done')
root = tk.Tk()
app = Application(root)
tk.mainloop()
The key difference here is that we can resize/move/interact with the window while we're waiting for the external process to finish. Also note that we never needed to call update_idletasks/update, as Tkinter now does have idle time to update the display.

Python: Is it possible to create an tkinter label which has a dynamic string when a function is running in background?

I have created a tkinter GUI for my python script. When I run the script, I want a dynamic string in one of the Label widgets on the GUI window, which will display:
"Working."
Then:
"Working.."
then
"Working..."
and then start from "Working." again until the script is completed.
(Actually I'd prefer a progress bar in this area)
Is it possible?
I wrote two simple scripts to help demonstrate how to do what you want. The first is using the label:
import tkinter as tk
root = tk.Tk()
status = tk.Label(root, text="Working")
status.grid()
def update_status():
# Get the current message
current_status = status["text"]
# If the message is "Working...", start over with "Working"
if current_status.endswith("..."): current_status = "Working"
# If not, then just add a "." on the end
else: current_status += "."
# Update the message
status["text"] = current_status
# After 1 second, update the status
root.after(1000, update_status)
# Launch the status message after 1 millisecond (when the window is loaded)
root.after(1, update_status)
root.mainloop()
The next one is using a progressbar:
import tkinter as tk
# You will need the ttk module for this
from tkinter import ttk
def update_status(step):
# Step here is how much to increment the progressbar by.
# It is in relation to the progressbar's length.
# Since I made the length 100 and I am increasing by 10 each time,
# there will be 10 times it increases before it restarts
progress.step(step)
# You can call 'update_status' whenever you want in your script
# to increase the progressbar by whatever amount you want.
root.after(1000, lambda: update_status(10))
root = tk.Tk()
progress = ttk.Progressbar(root, length=100)
progress.pack()
progress.after(1, lambda: update_status(10))
root.mainloop()
Note however that I couldn't do too much with the progressbar script because progressbars are a little tricky and need to be customized to your script exactly. I just wrote it to maybe shed a little light on the subject. The main part of my answer though is the label script.
Yes, it is possible. There are two ways to do it:
Whenever you want to update the label from your code you can call the_widget.configure(the_text). This will change the text of the label.
You can create an instance of a tkinter.StringVar, and assign it to the textvariable attribute of a label. Whenever you change the value of the variable (via the_variable.set(the_text), the label will automatically update.
Note that for either of these to work, the event loop needs to be able to process events (ie: you won't see anything if your function takes a long time to run and you never call update_idletasks or re-enter the event loop).

Categories

Resources