TKINTER: Howto auto-update loop generated labels - python

I'm battling with tkinter's .after() functionality, and I see all the examples, these all work as demo in my environment if applied on a well-known amount of labels
The challenge I have here is that I have an unknown amount of labels generated by a loop.
I need to have these labels regenerated and overwritten every x seconds. The number of labels may change, text and color of the labels will change accordingly, as well.
I think the challenge here is to re-run the whole labels generation function _construct_label_colorise after x seconds, and have it replace previous labels.
Think of it as like these labels would be a list of windows processes, each time it constructs labels - some processes might go down and disappear, and some appear on top. so, every second the number of labels will be different. For this example, I limit the delta to 3-5.labels.
Please advise a way to correctly implement _construct_label_colorise function rerun, after x seconds intervals.
from tkinter import StringVar, Tk, Frame, Label, LabelFrame, mainloop
from platform import system
from random import randint
class Window(Tk):
def __init__(self):
super(Window, self).__init__()
# self.geometry('100x150')
self.build_ui()
def build_ui(self):
mainFrame = Frame(self)
mainFrame.pack()
status_frame = LabelFrame(mainFrame, text='EP status')
status_frame.pack(padx=5,pady=5)
self._construct_label_colorise(status_frame)
self.after(1000,self._update_status(status_frame))
#Note!: The number of LABELs delivered by this function in production changes dynamically, it is an unknown total label quantity at each refresh
def _construct_label_colorise(self,master):
self.label_list=[]
for x in range(0,randint(3,5)):
c = randint(0,1)
fg = "green" if c == 0 else "red"
label = StringVar
label = Label(master, text=f'color+{fg}', foreground= fg)
label.pack(padx=5,pady=5)
self.label_list.append (label)
def _update_status(self,master):
for label in self.label_list:
label.destroy()
self.after(1000,self._construct_label_colorise(master))
pass
window = Window()
window.mainloop()

Note that self.after(1000, self._update_status(status_frame)) will execute self._update_status(status_frame) immediately, not 1000ms later. Also you don't need self._update_status() at all for your case.
...
from random import randint, choice
class Window(Tk):
def __init__(self):
super(Window, self).__init__()
# self.geometry('100x150')
self.build_ui()
def build_ui(self):
mainFrame = Frame(self)
mainFrame.pack()
status_frame = LabelFrame(mainFrame, text='EP status')
status_frame.pack(padx=5,pady=5)
self.label_list = [] # initialize the list here
self._construct_label_colorise(status_frame) # start the after loop
#Note!: The number of LABELs delivered by this function in production changes dynamically, it is an unknown total label quantity at each refresh
def _construct_label_colorise(self,master):
# delete existing labels
for label in self.label_list:
label.destroy()
# clear the list
self.label_list.clear()
# create random number of labels with random foreground color
for x in range(0,randint(3,5)):
fg = choice(["green", "red"])
label = Label(master, text=f'color+{fg}', foreground= fg)
label.pack(padx=5,pady=5)
self.label_list.append (label)
# call this function one second later
self.after(1000, self._construct_label_colorise, master)
...

When you use after, it will only use the callback once.
If you want to keep repeating an action, you have to re-schedule it with after every time.
The _update_status method should call (not schedule) _construct_label_colorise and then re-schedule itself.

Related

How to dynamically wrap label text in tkinter with .bind and <Configure>

I'm trying to create a page that outputs a large amount of data, and wraps the text dynamically depending on window size. I started by setting wraplength = self.master.winfo_width(), which sets the text wrapping to the current window size, but it does not change when the window does. I found this answer, which seemed like it would solve the problem, but when trying to recreate it myself, something went wrong. I suspect that I'm misunderstanding something with .bind or <Configure>, but I can't be sure. My base code is as follows:
from tkinter import *
class Wrap_example(Frame):
def __init__(self):
Frame.__init__(self)
self.place(relx=0.5, anchor='n')
#Initalize list and variable that populates it
self.data_list = []
self.data = 0
#Button and function for creating a bunch of numbers to fill the space
self.button = Button(self, text = "Go", command = self.go)
self.button.grid()
def go(self):
for self.data in range(1, 20000, 100):
self.data_list.append(self.data)
#Label that holds the data, text = list, wraplength = current window width
self.data = Label(self, text = self.data_list, wraplength = self.master.winfo_width(), font = 'arial 30')
self.data.grid()
#Ostensibly sets the label to dynamically change wraplength to match new window size when window size changes
self.data.bind('<Configure>', self.rewrap())
def rewrap(self):
self.data.config(wraplength = self.master.winfo_width())
frame01 = Wrap_example()
frame01.mainloop()
A few things of note: I tried using the lambda directly as shown in the linked answer, but it didn't work. If I remove the rewrap function and use self.data.bind('<Configure>', lambda e: self.data.config(wraplength=self.winfo_width()), it throws a generic Syntax error, always targeting the first character after that line, (the d in def if the function is left in, the f in frame01 if it's commented out). Leaving rewrap as-is doesn't throw an error, but it doesn't perform any other apparent function, either. Clicking 'Go' will always spawn data that wraps at the current window size, and never changes.
There are few issues:
frame Wrap_example does not fill all the horizontal space when window is resized
label self.data does not fill all the horizontal space inside frame Wrap_example when the frame is resized
self.rewrap() will be executed immediately when executing the line self.data.bind('<Configure>', self.rewrap())
To fix the above issues:
set relwidth=1 in self.place(...)
call self.columnconfigure(0, weight=1)
use self.data.bind('<Configure>', self.rewrap) (without () after rewrap) and add event argument in rewrap()
from tkinter import *
class Wrap_example(Frame):
def __init__(self):
Frame.__init__(self)
self.place(relx=0.5, anchor='n', relwidth=1) ### add relwidth=1
self.columnconfigure(0, weight=1) ### make column 0 use all available horizontal space
#Initalize list and variable that populates it
self.data_list = []
self.data = 0
#Button and function for creating a bunch of numbers to fill the space
self.button = Button(self, text = "Go", command = self.go)
self.button.grid()
def go(self):
for self.data in range(1, 20000, 100):
self.data_list.append(self.data)
#Label that holds the data, text = list, wraplength = current window width
self.data = Label(self, text = self.data_list, wraplength = self.master.winfo_width(), font = 'arial 30')
self.data.grid()
#Ostensibly sets the label to dynamically change wraplength to match new window size when window size changes
self.data.bind('<Configure>', self.rewrap) ### remove () after rewrap
def rewrap(self, event): ### add event argument
self.data.config(wraplength = self.master.winfo_width())
frame01 = Wrap_example()
frame01.mainloop()

Why does my progress bar in tkinter only change colour when value is used as an option over variable?

I'm using a progress bar in tkinter like a meter which indicates the signal strength being received. I would like to change the colour of my progress bar so that it turns red when below a threshold value and green when above it. When I use "value" in "ttk.Progressbar" along with "ttk.Style" I can change the colour of the bar. However the value being displayed should change as the signal strength changes and when I use "variable" instead it remains grey. The code which highlights my problem is shown below. The argument "root" is from another class which creates a frame so I can position this progress bar on a grid with other labels. The first example does change colour.
from tkinter import *
from tkinter import tkk
class Progressbar():
def __init__(self, root):
self.s = ttk.Style()
self.s.theme_use("clam")
self.s.configure("colour.Horizontal.TProgressbar", foreground="green", background="green")
self.pb = ttk.Progressbar(root, orient = "horizontal", length = 140, mode = "determinate", value = 20, maximum = 100, style = "colour.Horizontal.TProgressbar").grid(row=3, column=1)
This next example displays the value but does not change colour like the previous code. This is the problem I am having. I call the function in another file and input a float number. This works as expected and if I input "50.0", for example, it displays the value on the progress bar but the colour remains grey unlike last time. The only difference between the two blocks of code is that in the second one I use "variable" instead of "value" and set the value using a variable class.
from tkinter import *
from tkinter import tkk
class Progressbar():
def __init__(self, root):
self.s = ttk.Style()
self.s.theme_use("clam")
self.s.configure("colour.Horizontal.TProgressbar", foreground="green", background="green")
self.progbarval = DoubleVar()
self.pb = ttk.Progressbar(root, orient = "horizontal", length = 140, mode = "determinate", variable = self.progbarval, maximum = 100, style = "colour.Horizontal.TProgressbar").grid(row=3, column=1)
def set_progbarval(self, val1):
self.progbarval.set(val1)
I expected both pieces of code to behave the same way but they did not. Please could someone explain why this method for changing the colour of the progress bar does not work and what possible solutions there are for my problem. Any help would be appreciated, thanks.
For some reason, when you put them on the same window it doesn't work but separately try using this line (it worked for me):
self.s.configure("colour.Horizontal.TProgressbar", foreground="green", background="green", theme = "clam")
rather than:
self.s.theme_use("clam")
self.s.configure("colour.Horizontal.TProgressbar", foreground="green", background="green")
I realized I forgot to put root.update() in my main code so it wasn't updating. The progress bar changed colour after I did this. Please can a moderator delete this post on behalf of me as it was just an error on my part.

Creating a wizard in Tkinter

I'm creating a Wizard in Tkinter. Almost each of the steps shoud I have the same footer with the button for navigation and cancelling. How can I achieve this? Should I create a Frame? And in general, should all the steps be created as different frames?
The answer to this question is not much different than Switch between two frames in tkinter. The only significant difference is that you want a permanent set of buttons on the bottom, but there's nothing special to do there -- just create a frame with some buttons as a sibling to the widget that holds the individual pages (or steps).
I recommend creating a separate class for each wizard step which inherits from Frame. Then it's just a matter of removing the frame for the current step and showing the frame for the next step.
For example, a step might look something like this (using python 3 syntax):
class Step1(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
header = tk.Label(self, text="This is step 1", bd=2, relief="groove")
header.pack(side="top", fill="x")
<other widgets go here>
Other steps would be conceptually identical: a frame with a bunch of widgets.
Your main program or you Wizard class would either instantiate each step as needed, or instantiate them all ahead of time. Then, you could write a method that takes a step number as a parameter and adjust the UI accordingly.
For example:
class Wizard(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.current_step = None
self.steps = [Step1(self), Step2(self), Step3(self)]
self.button_frame = tk.Frame(self, bd=1, relief="raised")
self.content_frame = tk.Frame(self)
self.back_button = tk.Button(self.button_frame, text="<< Back", command=self.back)
self.next_button = tk.Button(self.button_frame, text="Next >>", command=self.next)
self.finish_button = tk.Button(self.button_frame, text="Finish", command=self.finish)
self.button_frame.pack(side="bottom", fill="x")
self.content_frame.pack(side="top", fill="both", expand=True)
self.show_step(0)
def show_step(self, step):
if self.current_step is not None:
# remove current step
current_step = self.steps[self.current_step]
current_step.pack_forget()
self.current_step = step
new_step = self.steps[step]
new_step.pack(fill="both", expand=True)
if step == 0:
# first step
self.back_button.pack_forget()
self.next_button.pack(side="right")
self.finish_button.pack_forget()
elif step == len(self.steps)-1:
# last step
self.back_button.pack(side="left")
self.next_button.pack_forget()
self.finish_button.pack(side="right")
else:
# all other steps
self.back_button.pack(side="left")
self.next_button.pack(side="right")
self.finish_button.pack_forget()
The definition of the functions next, back, and finish is very straight-forward: just call self.show_step(x) where x is the number of the step that should be shown. For example, next might look like this:
def next(self):
self.show_step(self.current_step + 1)
I recommend using one main window with the buttons and put the rest of the widgets in different labelframes that appear and disappear upon execution of different functions by the buttons

Tkinter: Integers as labels overwrite

I am creating labels in a for loop that display integers every time I fire an event (a mouse click) on my application. The problem is that old labels don't get erased and the new ones come on top of them causing a big mess.
Here is the working code that you can try out:
import numpy as np
import Tkinter as tk
class Plot(object):
def __init__(self, win):
self.win = win
self.bu1 = tk.Button(win,text='Load',command=self.populate,fg='red').grid(row=0,column=0)
self.listbox = tk.Listbox(win, height=5, width=5)
self.listbox.grid(row=1,column=0)#, rowspan=10, columnspan=2)
self.listbox.bind("<Button-1>", self.print_area)
def populate(self):
"""Populate listbox and labels"""
self.time = [1,2,3]
self.samples = ['a','b','c']
for item in self.time:
self.listbox.insert(tk.END,item)
for i,v in enumerate(self.samples):
tk.Label(self.win, text=v).grid(row=2+i,column=0,sticky=tk.W)
self.lbl_areas = []
for i in range(0, len(self.samples)):
self.lbl=tk.IntVar()
self.lbl.set(0)
self.lbl_areas.append(tk.Label(self.win,textvariable=self.lbl).grid(row=2+i,column=1,sticky=tk.W))
def print_area(self, event):
"""Prints the values"""
widget = event.widget
selection=widget.curselection()
value = widget.get(selection[0])
#Here is the dictionary that maps time with values
self.d = {1:[('a',33464.1),('b',43.5),('c',64.3)],
2:[('a',5.1),('b',3457575.5),('c',25.3)],
3:[('a',12.1),('b',13.5),('c',15373.3)]}
lbl_val = []
for i in range(0, len(self.samples)):
lbl_val.append(self.d[value][i][1])
for i in range(0, len(self.samples)):
self.lbl=tk.IntVar()
self.lbl.set(lbl_val[i])
tk.Label(self.win,textvariable=self.lbl).grid(row=2+i,column=1,sticky=tk.W)
def main():
root = tk.Tk()
app = Plot(root)
tk.mainloop()
if __name__ == '__main__':
main()
If You try to run this code and click on LOAD you will see the numbers appearing in the listbox and labels a,b,c with values set to zero at the beginning. If you click on the number in the listbox the values (mapped into the dictionary d) will appear but you will see the overwrite problem. How can I fix that?
How can I overcome this problem? Thank you
Don't create new labels. Create the labels once and then update them on mouse clicks using the configure method of the labels.
OR, before creating new labels delete the old labels.If you design your app so that all of these temporary labels are in a single frame you can delete and recreate the frame, and all of the labels in the frame will automatically get deleted. In either case (destroying the frame or destroying the individual labels) you would call the destroy method on the widget you want to destroy.

I need to slow down a loop in a python tkinter app

I am having a problem with a fairly simple app.
It performs properly, but I would like it to perform a little slower.
The idea is to randomly generate a name from a list, display it, then remove it fromthe list every time a button is clicked.
To make it a little more interesting, I want the program to display several names before
picking the last one. I use a simple for loop for this. However, the code executes so quickly, the only name that winds up displaying is the last one.
using time.sleep() merely delays the display of the last name. no other names are shown.
here is my code:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from tkinter import *
import random
import time
class Application(Frame):
def __init__(self, master):
""" Initialize the frame. """
super(Application, self).__init__(master)
self.grid()
self.name_list = ["Thorin","Tyler","Jose","Bryson","Joe"]
self.create_widget()
def create_widget(self):
self.lbl = Label(self)
self.lbl["text"] = "Click to spin"
self.lbl["font"] = ("Arial", 24)
self.lbl.grid()
self.bttn = Button(self)
self.bttn["text"]= "Spin"
self.bttn["command"] = self.spin
self.bttn.grid()
def spin(self):
if self.name_list:
for i in range(5):
index = random.randrange(len(self.name_list))
self.lbl["text"] = self.name_list[index]
self.lbl.grid()
self.name_list.pop(index)
else:
self.lbl["text"] = "No more names"
self.lbl.grid()
def main():
root = Tk()
root.title("Click Counter")
root.geometry("600x600")
app = Application(root)
root.mainloop()
if __name__ == '__main__':
main()
This is a pretty common class of problems related to GUI programming. The heart of the issue is the window drawing manager. As long as your function is executing, the drawing manager is frozen; updating the label's text will have no apparent effect until your function ends. So if you have a for loop with a sleep(1) command inside, all it will do is freeze everything for five seconds before updating with your final value when the function finally ends.
The solution is to use the after method, which tells Tkinter to call the specified function at some point in the future. Unlike sleep, this gives the drawing manager the breathing room it requires to update your window.
One possible way to do this is to register six events with after: five for the intermediate name label updates, and one for the final name change and pop.
def spin(self):
def change_name():
index = random.randrange(len(self.name_list))
self.lbl["text"] = self.name_list[index]
self.lbl.grid()
def finish_spinning():
index = random.randrange(len(self.name_list))
self.lbl["text"] = self.name_list[index]
self.lbl.grid()
self.name_list.pop(index)
if self.name_list:
name_changes = 5
for i in range(name_changes):
self.after(100*i, change_name)
self.after(100*name_changes, finish_spinning)
else:
self.lbl["text"] = "No more names"
self.lbl.grid()
(disclaimer: this is only a simple example of how you might use after, and may not be suitable for actual use. In particular, it may behave badly if you press the "spin" button repeatedly while the names are already spinning. Also, the code duplication between change_name and finish_spinning is rather ugly)
The code as it is can show the same item twice since it chooses a new random number each time and so will choose the same number part of the time. Note that you do not pop until after the loop which means that each time you run the program you will have one less name which may or may not be what you want. You can use a copy of the list if you want to keep it the same size, and/or random.shuffle on the list and display the shuffled list in order. Also you only have to grid() the label once,
class Application():
def __init__(self, master):
""" Initialize the frame. """
self.master=master
self.fr=Frame(master)
self.fr.grid()
self.name_list = ["Thorin","Tyler","Jose","Bryson","Joe"]
self.ctr=0
self.create_widget()
def create_widget(self):
self.lbl = Label(self.master width=30)
self.lbl["text"] = "Click to spin"
self.lbl["font"] = ("Arial", 24)
self.lbl.grid()
self.bttn = Button(self.master)
self.bttn["text"]= "Spin"
self.bttn["command"] = self.spin
self.bttn.grid()
def change_label(self):
self.lbl["text"] = self.name_list[self.ctr]
self.ctr += 1
if self.ctr < 5:
self.master.after(1000, self.change_label)
else:
self.ctr=0
def spin(self):
if self.name_list and 0==self.ctr: # not already running
random.shuffle(self.name_list)
self.change_label()
else:
self.lbl["text"] = "No more names"
if __name__ == '__main__':
root = Tk()
root.title("Click Counter")
root.geometry("600x600")
app = Application(root)
root.mainloop()

Categories

Resources