How to Refresh Tkinter Display on a Cycle with No Lag - python

I'm trying to create a simple program to monitor data that displays in a Tkinter window and refreshes itself every second. The method I'm currently using creates an ever-increasing lag in the refresh cycle as the program runs.
This is a super simplified representation of the approach in the form of a counter. While the cycle begins at the intended rate of one second per loop, even after just a few minutes the cycle-time becomes noticeably slower. Any suggestions to eliminate this lag accumulation would be greatly appreciated. Thanks so much!
from tkinter import *
i=0
root = Tk()
root.title("Simple Clock")
root.configure(background="black")
def Cycle():
global i
Label(root, text="------------------------", bg="black", fg="black").grid(row=0, column=0, sticky=W)
Label(root, text = i, bg="black", fg="gray").grid(row=0, column=0, sticky=W)
i += 1
root.after(1000,Cycle)
root.after(1000,Cycle)
root.mainloop()

Stop creating new objects with each call. Instead, only update the parts that change. For the code above it would be updating the 2nd Label's text:
from tkinter import *
def Cycle():
global i
labels[1]['text'] = i
i += 1
root.after(1000,Cycle)
i=0
root = Tk()
root.title("Simple Clock")
root.configure(background="black")
labels = list()
labels.append(Label(root, text="------------------------", bg="black", fg="black"))
labels.append(Label(root, bg="black", fg="gray"))
for j in range(len(labels)):
labels[j].grid(row=0, column=0, sticky=W)
root.after(1000,Cycle)
root.mainloop()

Related

Python Tkinter count-down GUI crashing when i start the Count-Down with a button. (app not answering)

I am making a count-down GUI with Tkinter in Python 3.10 and I'm making a slider that sets the minutes, 2 labels that display the minutes and seconds and a button to start the timer.
The problem is that it crashes when I click the start timer button, The strange thing is that it doesn't give me any error messages it just doesn't respond/crashes the GUI.
I'm using functions with long loops and I think the problem is with the loops.
Both the slider and Button calls functions
I'm a beginner so I know that the code isn't so good but I'm trying my best. Please Help Me :)
Python Code:
from time import sleep
from tkinter import *
root = Tk()
root.geometry("300x400")
root.title("Countdown Timer")
time = 0
time_2 = 0
running = False
def clear_field():
clock_text_min.delete(1.0, "end")
clock_text_sec.delete(1.0, "end")
min_slider.set(30)
def before_start_timer(time):
global time_2
time_2 = time
time = int(time)
time_3 = str(time)
counter = 0
while (not running) and (counter < 10):
clock_text_min.config(text=time_3)
counter += 1
def start_timer(time):
global running
running = True
time = int(time)
while time >= 0:
clock_text_min.config(text=time)
sec_time = 60
for _ in range(60):
clock_text_sec.config(text=sec_time)
sleep(1)
sec_time -= 1
time -= 1
clock_text_min = Label(root, text="0", font=("Arial", 25))
clock_text_min.pack(padx=20, pady=20)
clock_text_sec = Label(root, text="0", font=("Arial", 25))
clock_text_sec.pack(padx=20, pady=20)
min_slider = Scale(root, from_=1, to=60, orient=HORIZONTAL, command=before_start_timer)
min_slider.set(30)
min_slider.pack(padx=20, pady=20)
start_button = Button(root, text="Start Countdown", font=("Arial", 15), command=lambda: start_timer(time_2))
start_button.pack(padx=20, pady=20)
root.mainloop()
Its a very difficult problem so i haven't tried anything yet because i dont understand the problem, but as i said i think the problem is in the loops.
I was expecting it to start coundting down from the set number of minutes and when its finished just stop at 0 on both min and sec but it just crashed when i clicked start countdown.
Please help me, it would be really helpfull because this is my second GUI i have ever made without any help :)
code is fixed/question ended
Hi everyone, thanks for all the help, my code is now working by using the after function and the root.update_idletasks() function. thanks
As JRiggles mentioned sleep and tkinter do not go well. This is because sleep() blocks the main loop of tkinter.
The most common way to solve this issue is to use after() to manage these kind of loops.
Here is a paired down version of your code that uses a combination of after() and a refactored function.
import tkinter as tk
root = tk.Tk()
root.geometry("300x200")
root.title("Countdown Timer")
def start_timer(time):
if time >= 0:
clock_text_sec.config(text=time)
time -= 1
# you can supply a variable after the function call with after.
root.after(1000, start_timer, time)
# you can also use a lambda to do the same thing
# root.after(1000, lambda: start_timer(time))
clock_text_sec = tk.Label(root, text="0", font=("Arial", 25))
clock_text_sec.pack(padx=20, pady=20)
tk.Button(root, text="Start Countdown", font=("Arial", 15), command=lambda: start_timer(60)).pack(padx=20, pady=20)
root.mainloop()

How to update tkinter texbox in Python during program runtime

I am writing a Python GUI program using tkinter to read data from an instrument based on a list of setpoints and to show the results of each reading in a tkinter scrolled textbox as the program is running, once the user clicks the "Start" button.
However, my program only shows the results at the end of all loops, rather than update the textbox after each loop. In actual usage, the program might run for many tens of minutes due to the large number of test points.
I tried using the .after method, to allow the GUI to update, but the program does not update the textbox during each loop.
How can I modify my code so that the tkinter textbox in the GUI is updated during each loop?
Here is my simplified code using random results as a sample instrument reading:
import tkinter as tk
import tkinter.scrolledtext as tkscrolled
from tkinter import ttk
def start_measurement():
import random
test_points = [tp for tp in range(1,5)]
for setpoint in test_points:
data_reading = random.normalvariate(setpoint,0.05/2)
data_results.insert(tk.INSERT,"Setpoint = {:.3f}, Reading = {:.3f}\n".format(setpoint, data_reading))
data_results.after(1000)
data_results.insert(tk.INSERT,"\nDone\n")
root = tk.Tk()
# Create main frame for entire program: mainframe
mainframe = ttk.Frame(root, padding="10 10 10 10", style='bgstyle.TFrame')
mainframe.grid(row=0, column=0, sticky=(tk.N, tk.W, tk.E, tk.S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
# Create Data Output Text Box with Vertical Scrollbar
data_label = ttk.Label(mainframe, text="Results")
data_label.grid(row=1, column=0, padx=(30,30), pady=(20,0), sticky=(tk.S,tk.W,tk.E))
data_results = tkscrolled.ScrolledText(mainframe, width=100, height=20)
data_results.grid(row=2, column=0, padx=(30,30), pady=(0,20), sticky=(tk.N,tk.W,tk.E))
start_button = ttk.Button(mainframe, width=15, text="Start", command=lambda:start_measurement())
start_button.grid(row=0, column=0, padx=(30,30), pady=(20,0), sticky=(tk.N, tk.W))
root.mainloop()
data_results.after(1000) behaves like time.sleep(1). So the updates are not shown until tkinter mainloop() takes back the control after the function exits.
You should use .after() like below:
def start_measurement(setpoint=1):
import random
if setpoint < 5:
start_button.config(state=tk.DISABLED) # prevent multiple executions
data_reading = random.normalvariate(setpoint, 0.05/2)
data_results.insert(tk.INSERT, "Setpoint = {:.3f}, Reading = {:.3f}\n".format(setpoint, data_reading))
# execute this function one second later with updated setpoint
data_results.after(1000, start_measurement, setpoint+1)
else:
data_results.insert(tk.INSERT, "\nDone\n")
start_button.config(state=tk.NORMAL) # enable for next execution
You can use root.after like this -
data_results.after(1000,lambda:data_results.insert(tk.INSERT,"\nDone\n"))
So, after approximately 1 second it inserts Done. And you should keep this out of the for loop -
def start_measurement():
test_points = [tp for tp in range(1,5)]
for setpoint in test_points:
data_reading = random.normalvariate(setpoint,0.05/2)
data_results.insert(tk.INSERT,"Setpoint = {:.3f}, Reading = {:.3f}\n".format(setpoint, data_reading))
data_results.after(1000,lambda:data_results.insert(tk.INSERT,"\nDone\n"))
Change your code as follows.
def start_measurement():
import random
test_points = [tp for tp in range(1,5)]
for setpoint in test_points:
data_reading = random.normalvariate(setpoint,0.05/2)
data_results.insert(tk.INSERT,"Setpoint = {:.3f}, Reading = {:.3f}\n".format(setpoint, data_reading))
data_results.update()
data_results.after(1000)
data_results.insert(tk.INSERT,"\nDone\n")
Add data_results.update() to your code.This updates your textbox every second.After all iteration You get 'Done' displayed.

Why isn't the label animation working till the last value of the loop?

I am new to python and I have been learning tkinter recently. So I thought with myself that using the grid_forget() function I can remove a widget and redefine it. I thought of this animation that changes the padding of a label so it would create space (kind of like moving the label but not exactly). However, the animation does not work at all. The program freezes until the label reaches the last value of the padding. How can I fix this? Or is there a better way to animate a label moving in the screen?
Here is my code:
from tkinter import *
import time
root = Tk()
lbl = Label(root, text='------')
lbl.grid(row=0, column=0)
def animation():
padding = 0
while padding < 31:
lbl.grid_forget()
padding += 1
lbl.grid(row=0, column=0, padx=padding)
time.sleep(0.2)
# alternative: root.after(200, lambda: lbl.grid(row=0, column=0, padx=padding))
btn = Button(root, text='Animate', command=animation)
btn.grid(row=1, column=1)
root.mainloop()
You need to update the screen for changes to be shown.
Here is a working version using the .update() method:
from tkinter import *
import time
root = Tk()
lbl = Label(root, text='------')
lbl.grid(row=0, column=0)
def animation():
padding = 0
while padding < 31:
lbl.grid_forget()
padding += 1
lbl.grid(row=0, column=0, padx=padding)
root.update()
time.sleep(0.2)
# alternative: root.after(200, lambda: lbl.grid(row=0, column=0, padx=padding))
btn = Button(root, text='Animate', command=animation)
btn.grid(row=1, column=1)
root.mainloop()
Here is a way I also use to animate stuff on the screen, I am not able to understand what you were trying to achieve with your code snippet above, I tried making some changes to it but I feel this way is much better and let's you get more control of your window.
This uses the widely used Canvas widget in the tkinter library.
The Canvas is a general purpose widget, You can use it for a lot of things. Visit the hyper link for more clarity
Here is a short example of how you would create text on the screen.
from tkinter import *
root = Tk()
root.title("My animation")
c = Canvas(root)
x = 20
y = 20 #Instead of using row and column, you simply use x and y co-ordinates
#We will use these co-ordinates to show where the text is in the starting
my_text = c.create_text(x,y,text = '-----')
c.pack()
# This is all you need to create this text on your screen!
root.mainloop()
The idea is that you put your canvas up on your window , and then place whatever you want on it.
There are a lot more attributes that you can add to make your text look even better. Here is an in-depth tutorial on it.
Now that we have made your text widget, It is now time to move it around. Let us move it to 90,20 From our initial position which is 20,20
Here is how we will do it. If we simply move to text object to 90,90, We won't see any animations, it will just directly have it there. So what we will do is first create it at 21,20. Then 22,20. And so on...
We do this really fast till we reach 90,20
This looks like we are moving the text
from tkinter import *
import time
root = Tk()
root.title("My animation")
c = Canvas(root)
x = 20
y = 20 #Instead of using row and column, you simply use x and y co-ordinates
#We will use these co-ordinates to show where the text is in the starting
my_text = c.create_text(x,y,text = 'weee')
c.pack()
def animation():
y = 0.1
x = 0
for _ in range(1000):
c.move(my_text,x,y)
root.update()
anlabel = Button(root,text = 'Animate!',command = animation).pack()
root.mainloop()
This is not only applicable to text, but everything (like other images)that is there on the canvas. The canvas also has Events which will let you use mouse-clicks and other keys on the computer too.
I have made some changes from the previous code, But it is executable and you can try it for yourself to see how it works. increasing the value in time.sleep() makes the animation slower, the lesser the value, the faster.
Are you sure you aren't trying to do something more like the below example? Animating the padding on one of your widgets is going to screw up the rest of your display.
from tkinter import *
import time
root = Tk()
lbl = Label(root, text='')
lbl.grid(row=0, column=0)
def animation(step=12):
step = 12 if step < 0 else step
lbl['text'] = ' ------ '[step:step+6]
root.after(200, lambda: animation(step-1))
Button(root, text='Animate', command=animation).grid(row=1, column=0, sticky='w')
root.mainloop()

How to update the ttk progress bar on tkinter

I've got a program that does some simple webscraping. I'm working on giving it to my coworkers to use, but they are unfamiliar with code so I'd like to make a GUI. I've started using tkinter, and I'm currently working on making a progress bar showing where the program is at (it can take up to a couple hours to run). My problem is that I can't seem to get the progress bar to update, and all the online sources use Tkinter, which is an old version. Here is my code:
I've tried updating progressBar['value'] to whatever number I want, but that hasn't been working.
from tkinter import *
from tkinter import ttk
import time
def clicked(progressBar): # runs a couple functions and updates the progress bar when the button is clicked
num = 0
for item in range(5):
# functions go here
num += 10
progressBar['value'] = num
time.sleep(2)
window = Tk()
window.title("Welcome to my app")
window.geometry('600x400')
progressBar = ttk.Progressbar(window, orient='horizontal', length=300, mode='determinate', maximum=100, value=0)
progressBar.grid(columnspan=3, row=2, sticky=(W, E))
btn = Button(window, text="Click me!", command=clicked(progressBar))
btn.grid(column=1, row=1)
window.mainloop()
The tkinter window doesn't open up until 10 seconds after I run the program, and it has the progress bar already at 50% full. I'd like for the bar to slowly increment up, AFTER the button has been clicked. Any advice would be helpful! Thank you!
There are two problems with the code:
command=clicked(progressBar) indeed calls the function promptly.So simply use command=clicked. There is no need to pass progressBar as the argument since the function gets it from global scope.
while the function clicked() is running, GUI freezes. After 5*2sec, progressBar updates to 5*10 abruptly. To update the widgets in a loop, use update_idletastk method:
import tkinter as tk
from tkinter import ttk
import time
def clicked():
num = 0
for item in range(5):
num += 10
progressBar['value'] = num
window.update_idletasks()
time.sleep(2)
window = tk.Tk()
progressBar = ttk.Progressbar(window, orient='horizontal', length=300, mode='determinate', maximum=100, value=0)
progressBar.grid(columnspan=3, row=2, sticky=(tk.W, tk.E))
btn = tk.Button(window, text="Click me!", command=clicked)
btn.grid(column=1, row=1)
window.mainloop()

Trouble incrementing variables in Python 3.4.3 Tkinter with labels and buttons

So I have started making a survival game, but ran into a problem pretty fast. I wanted to make a button that should will go and collect a certain time, (as in shrubs), and when hit, the amount of shrub shown on the screen will increment by 15. But every time I try to make it, the amount of shrub goes from 0 to 15, which is good, but then it won't go any higher. Here is the code I have currently:
import tkinter as tk
root = tk.Tk() # have root be the main window
root.geometry("550x300") # size for window
root.title("SURVIVE") # window title
shrubcount = 0
def collectshrub():
global shrubcount
shrubcount += 15
shrub.config(text=str(shrubcount))
def shrub():
shrub = tk.Label(root, text='Shrub: {}'.format(shrubcount),
font=("Times New Roman", 16)).place(x=0, y=0)
def shrubbutton():
shrubbutton = tk.Button(root, command=collectshrub, text="Collect shrub",
font=('Times New Roman', 16)).place(x=0, y=200)
shrub() # call shrub to be shown
root.mainloop() # start the root window
Any help would be nice! Thanks tons
Getting these errors
shrub.config(text=str(shrub count))
AttributeError: 'NoneType' object has no attribute 'config'
In your current code, you defined shrub in one function and tried to use it in the other when you've only assigned it locally. The solution is to remove the shrub() and shrubbutton()function completely like so:
import tkinter as tk
root = tk.Tk()
root.geometry("550x300")
root.title("SURVIVE")
shrubcount = 0
def collectshrub():
global shrubcount
shrubcount += 15
shrub.config(text="Shrub: {" + str(shrubcount) + "}")
if shrubcount >= 30:
print(text="You collected 30 sticks")
craftbutton.pack()
def craftbed():
#Do something
shrub = tk.Label(root, text="Shrub: {" + str(shrubcount) + "}", font=("Times New Roman", 16))
shrub.place(x=0, y=0)
shrubbutton = tk.Button(root, command=collectshrub, text="Collect shrub", font=('Times New Roman', 16))
shrubbutton.place(x=0, y=200)
craftbutton = tk.Button(root, text="Craft bed", comma=craftbed)
root.mainloop()
ALSO
After seeing the error you got at the bottom of your question, your variable shrubcount was named shrub count in the function. Small things like this can completely alter the way your code works.
Inkblot has the right idea.
Your shrub function sets the number of shrubs at 0.
Clicking "Collect Shrub" calls forces shrub to output shrubcount but at no time is the shrubcount variable updated.
A cleaner version of your code, that does what you want may look like this:
import ttk
from Tkinter import *
class Gui(object):
root = Tk()
root.title("Survive")
shrubs = IntVar()
def __init__(self):
frame = ttk.Frame(self.root, padding="3 3 12 12")
frame.grid(column=0, row=0, sticky=(N, W, E, S))
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
ttk.Label(frame, text="Shrubs:").grid(column=0, row=0, sticky=W)
ttk.Entry(frame, textvariable=self.shrubs, width=80).grid(column=0, row=2, columnspan=4, sticky=W)
ttk.Button(frame, command=self.add_shrubs, text="Get Shrubs").grid(column=6, row=3, sticky=W)
def add_shrubs(self):
your_shrubs = self.shrubs.get()
self.shrubs.set(your_shrubs + 15)
go = Gui()
go.root.mainloop()
Notice that all add_shrub does is increment shrubcount by 15. Displaying the number of shrubs is handled by the shrubs label object.

Categories