tkinter wont refresh widget - python

I want to write a program that has only a button, and after pressing that, program will start making 3 labels and then change the color of each one every 1 second only once.
It looks very simple and I wrote the following code :
import tkinter as tk
from time import sleep
def function():
mylist=list()
for i in range(3):
new_label=tk.Label(window,text='* * *',bg='yellow')
new_label.pack()
mylist.append(new_label)
print('First state finished')
sleep(1)
for label in mylist:
label.config(bg='red')
print('one label changed')
sleep(1)
window = tk.Tk()
window.geometry('300x300')
btn=tk.Button(window,text='start',command=function)
btn.pack()
tk.mainloop()
First the app is look like this (that is OK):
Second its look like this (its not OK because its print on the terminal but didn't update the lable) :
Third its look like this (at the end the app must be look like this and its OK) :
But I need to see the changes in the moment and use sleep for that reason.
Thank you All.

I would recommend to use .after(delay, callback) method of the tkinter to set the colour.
Hope this is what you want.
import tkinter as tk
def start():
global mylist
mylist = list()
for i in range(3):
new_label = tk.Label(window, text='* * *', bg='yellow')
new_label.pack()
mylist.append(new_label)
delay = 1000 # delay in seconds
for label in mylist:
# Additional delay so that next color change
# is scheduled after previous label color change
delay += 1000
schedule_color_change(delay, label)
def schedule_color_change(delay, label):
print("schedule color change for:", label)
label.after(delay, set_color, label)
def set_color(label):
print("setting color of:", label)
label.config(bg="red")
window = tk.Tk()
window.geometry('300x300')
btn = tk.Button(window, text='start', command=start)
btn.pack()
tk.mainloop()

Problem
The problem is your sleep(1), because it's a function that suspends the execution of the current thread for a set number of seconds, so it's like there is a stop to the whole script
Solution
The solution is to instantiate Thread with a target function, call start(), and let it start working. So you have to use timer which is included in the threading, then a timer from the threading module (import threading)
Inside the first "for" loop, remove your sleep(1) and write for example Time_Start_Here = threading.Timer (2, function_2) and then of course Time_Start_Here.start() to start.
start_time=threading.Timer(1,function_2)
start_time.start()
Instead you have to remove the second "for" loop and write what's inside ... inside the new function that will be called. Next you need to create the function
def function_2():
for label in mylist:
label.config(bg='red')
label.pack()
print('one label changed')

As Meritor guided me, I followed the after method and wrote the following recursive code without sleep :
import tkinter as tk
def recursive(i, listt):
lbl = listt[i]
if i >= 0:
lbl.config(bg='orange')
i -= 1
lbl.after(500, recursive, i, listt)
def function():
mylist = list()
for i in range(3):
new_label = tk.Label(window, text='* * *', bg='yellow')
new_label.pack()
mylist.append(new_label)
print('all label created')
# 2 is length of list minus 1
recursive(2, mylist)
window = tk.Tk()
window.geometry('300x300')
tk.Button(window, text='start', command=function).pack()
tk.mainloop()
Most likely my code is not optimized because it uses recursive and if you know anything better please tell me

Related

How do I delay a line of code in tkinter without it crashing? [duplicate]

Hey I am new to python and am using tkinter for my gui. I am having trouble using the "after" method.
The goal is to make a random letter appear every 5 seconds.
Here is my code:
import random
import time
from tkinter import *
root = Tk()
w = Label(root, text="GAME")
w.pack()
frame = Frame(root, width=300, height=300)
frame.pack()
L1 = Label(root, text="User Name")
L1.pack(side=LEFT)
E1 = Entry(root, bd =5)
E1.pack(side=LEFT)
tiles_letter = ['a', 'b', 'c', 'd', 'e']
while len(tiles_letter) > 0:
rand = random.choice(tiles_letter)
tile_frame = Label(frame, text=rand)
tile_frame.pack()
frame.after(500)
tiles_letter.remove(rand) # remove that tile from list of tiles
root.mainloop()
can someone please help me --- the problem is definitely frame.after(500):
i'm not sure if it is correct to use "frame" and I don't know what which argument follows the 500.
Thanks
You need to give a function to be called after the time delay as the second argument to after:
after(delay_ms, callback=None, *args)
Registers an alarm callback that is called after a given time.
So what you really want to do is this:
tiles_letter = ['a', 'b', 'c', 'd', 'e']
def add_letter():
rand = random.choice(tiles_letter)
tile_frame = Label(frame, text=rand)
tile_frame.pack()
root.after(500, add_letter)
tiles_letter.remove(rand) # remove that tile from list of tiles
root.after(0, add_letter) # add_letter will run as soon as the mainloop starts.
root.mainloop()
You also need to schedule the function to be called again by repeating the call to after inside the callback function, since after only executes the given function once. This is also noted in the documentation:
The callback is only called once for each call to this method. To keep
calling the callback, you need to reregister the callback inside
itself
Note that your example will throw an exception as soon as you've exhausted all the entries in tiles_letter, so you need to change your logic to handle that case whichever way you want. The simplest thing would be to add a check at the beginning of add_letter to make sure the list isn't empty, and just return if it is:
def add_letter():
if not tiles_letter:
return
rand = random.choice(tiles_letter)
tile_frame = Label(frame, text=rand)
tile_frame.pack()
root.after(500, add_letter)
tiles_letter.remove(rand) # remove that tile from list of tiles
Live-Demo: repl.it
I believe, the 500ms run in the background, while the rest of the code continues to execute and empties the list.
Then after 500ms nothing happens, as no function-call is implemented in the after-callup (same as frame.after(500, function=None))
after is used to delay execution of the program or to execute a command in background sometime in the future. But you can build a loop inside the mainloop by calling itself.
import tkinter as tk #import tkinter
import datetime #import datetime for our clock
def tick(): #function to update the clock
showed_time = clock['text'] #current showed time
current_time = datetime.datetime.now().strftime("%H:%M:%S") #real time
if showed_time != current_time: #if the showed time is not the real time
clock.configure(text=current_time) #update the label with the current time
clock.after(1000, tick) #call yourself in 1000ms (1sec.) again to update the clock
return None
root=tk.Tk()
clock = tk.Label(root)
clock.pack()
tick()
root.mainloop()
In the above script we had built a digital clock and get in touch with the after method. The after method is nothing but an interval and on the end of that interval we want that something happen.
To learn more about this basic widget method [click]
after(delay_ms, callback=None, args)
This method registers a callback function that will be called after a
given number of milliseconds. Tkinter only guarantees that the
callback will not be called earlier than that; if the system is busy,
the actual delay may be much longer.
import tkinter as tk
import datetime
def tick():
showed_time = clock['text']
current_time = datetime.datetime.now().strftime("%H:%M:%S")
if showed_time != current_time:
clock.configure(text=current_time)
global alarm #make sure the alarm is reachable
alarm = clock.after(1000, tick)#assign the alarm to a variable
return None
def stop():
stop.after_cancel(alarm) #cancel alarm
root=tk.Tk()
clock = tk.Label(root)
clock.pack()
stop = tk.Button(root, text='Stop it!', command=stop)
stop.pack()
tick()
root.mainloop()
Here we have the same code but with the ability to cancel our loop with the after_cancel method of tkinter. You dont need to global the alarm inside a class. self.alarm = self.clock.after(...) works fine.
after_cancel(id)
Cancels an alarm callback.
id
Alarm identifier.

How to add a delay between 2 text messages being displayed in Tkinter Python?

So my aim is to use a single function to show a text message upon a button click. Then there should be a delay and then another text message should be displayed.
The game is a dice game that should show 'Rolling...' upon a button click. And then after a while, it should display a random number.
I tried both .sleep() and .after() and both of them resulted in my program not showing the before delay text. Here's my code:
# Imports
import tkinter as tk
from random import randrange
import time
# Global variables
# SIDES is a constant
SIDES = 12
# Functions
def func():
display["text"] = "Rolling..."
window.after(2000)
display["text"] = str(randrange(SIDES) + 1)
# Main program loop
window = tk.Tk()
display = tk.Label(window, text="Press the button \nto roll the dice.", width=20, height=3)
button = tk.Button(window, text="Roll", command=func)
display.pack()
button.pack(pady=10)
window.mainloop()
Any help would be much appreciated!
Try:
window.after(2000, lambda: display.config(text=randrange(SIDES) + 1))
instead of the:
window.after(2000)
display["text"] = str(randrange(SIDES) + 1)
The problem is that when you sleep in the function, the tkinter main loop is interrupted and the screen isn't updated. (window.after() is just a gloified sleep here). The correct solution is to pass a callback to after, which will make it immediately return and call the callback later:
def func():
display["text"] = "Rolling..."
window.after(2000, lambda: display.__setitem__("text", str(randrange(SIDES) + 1)))
(Note that the call to __setitem__ is a direct one-liner lambda translation. This is not good design.)

Updating tkinter label text

I made a little script that takes some values from a web page and displays them as tkinter labels. The values are updated every 5 minutes. How can I make the labels update every time the values change?
url = 'https://myurl.something'
def check():
sleep(300)
value1 = str(requests.get(url))
root = Tk()
label1 = label(root, text=value1)
label1.grid()
while True:
check()
root.mainloop()
I know that the sleep loop could never work, but that's how I did this before adding a GUI
Here is some basic code to get you started. I took out the requests stuff, as it is not really relevant to your problem.
The key things are
The StringVar, which allows you to update your text.
The use of after, to shift your periodic task away from the gui
thread. No need for sleep.
code:
from tkinter import Tk, Label, StringVar
def check(val):
# do your request.get here
sv.set("dummy text %d" % val)
root.after(1000, lambda: check(val + 1))
root = Tk()
sv = StringVar()
sv.set("waiting...")
label1 = Label(root, textvariable=sv)
label1.grid()
root.after(1000, lambda: check(1))
root.mainloop()
Given this code, hopefully you can fit your http request back in and it will do what you want.

How to update labels in tkinter dynamically?

I've created this code that updates a label every second to indicate that something is loading (run the code to see what I mean). I'm using the threading module with tkinter but I feel like there must be a more efficient way to do this.
Here is my code:
from tkinter import *
from time import sleep
import threading
root = Tk()
new_var = StringVar()
new_var.set('Loading')
def change_text():
array = [".", "..", "...", ""]
while True:
for num in range(4):
sleep(1)
new_var.set(f"Loading{array[num]}")
root.update_idletasks()
l = Label(root, textvariable = new_var)
l.pack()
Loading_animation = threading.Thread(target=change_text)
Loading_animation.start()
root.mainloop()
Also, if there isn't a better way to do this how do I prevent the error that I keep receiving whenever I close the root window?
Thank you!
Here is a simpler method that doesn't involve threading.
Keep a counter and every second call the function. In the function simply set the text to each item in the list by the counter as an index.
Update: To answer your question in the comments.
This will not get stuck in some loop that stops us from reaching the mainloop() because this code only adds a command to be run on the event list at a regular interval of 1 second. What is actually happening is the after() method will add a new even to run no sooner than 1 second (1000 milliseconds). Because Tkinter is event-driven Tkinter will handle each even in the list as it comes after every mainloop() cycle.
import tkinter as tk
root = tk.Tk()
counter = 0
def change_text():
global counter
my_list = [".", "..", "...", ""]
if counter != 3:
l.config(text="Loading{}".format(my_list[counter]))
counter += 1
root.after(1000, change_text)
else:
l.config(text="Loading{}".format(my_list[counter]))
counter = 0
root.after(1000, change_text)
l = tk.Label(root, text = "")
l.pack()
change_text()
root.mainloop()
Here is the same answer as #Mike-SMT, but using the cycle function to make it a lot neater.
import tkinter as tk
from itertools import cycle
root = tk.Tk()
my_list = cycle([".", "..", "...", ""])
def change_text():
l.config(text="Loading{}".format(next(my_list)))
root.after(1000, change_text)
l = tk.Label(root)
l.pack()
change_text()
root.mainloop()

Holding Down Button and window updating issue

When I keep holding down Ctrl+Up, I need that the label to update on intervals of 1000 ms. (If I hold Ctrl+Up down for 5,2 seconds, the commands should run 5 times.)
The after method does not seem to work on this. It also acts weird, as if it records how many times I pressed the key and keeps looping that even after Ctrl+Up is unpressed.
from Tkinter import *
root = Tk()
def start(event):
global x
x = x+1
x_var.set(x)
root.after(1000, lambda: start(event))
x=1
x_var=IntVar()
x_var.set(x)
r = Label(root, textvariable=x_var)
r.pack()
root.bind('<Control-Up>', start)
root.mainloop()
Here is a way to run a command every 1000 ms while holding Control + Up:
Store and update the current pressed statuses of Control and Up.
Have an .after() loop which calls itself every 1000 ms, in which you run the desired command if both Control and Up are currently pressed.
Code
import Tkinter as tk
root = tk.Tk()
x = 1
x_var = tk.IntVar()
x_var.set(x)
r = tk.Label(root, textvariable=x_var)
r.pack()
isPressed = {"Control_L": False, "Up": False}
def update_key_status(key, value):
global isPressed
isPressed[key] = value
# Make the Press/Release events of both keys update the "isPressed" dictionary
for key in ["Up", "Control_L"]:
root.bind('<KeyPress-{}>'.format(key),
lambda evt, key=key: update_key_status(key, True))
root.bind('<KeyRelease-{}>'.format(key),
lambda evt, key=key: update_key_status(key, False))
def increment_x():
global x, x_var
x += 1
x_var.set(x)
def start():
if (isPressed["Control_L"] and isPressed["Up"]):
increment_x()
root.after(1000, start)
start()
root.mainloop()
The after method does not seem to work on this.
No, it is working as your program asks it to do.
It also acts weird, as if it records how many times I pressed the key
and keeps looping that even after Ctrl+Up is unpressed.
It is not that weird. Throw a glance to a given documentation and you will read:
The callback is only called once for each call to this method. To keep
calling the callback, you need to reregister the callback inside
itself
The bold text is what you exactly did in the start() function. So the logic behavior with your current code is that, once you press Ctrl + Up, that callback will wait 1000 milliseconds to be executed, and after that it will continue running forever (well, until you end the mainloop() event)
So to make your program increment the label's content only once after each press on the keys you specified, you need to avoid the design written in bold text above. A solution then could consist in creating a specific incrementalist function like this:
def increment_x():
global x, x_var
x += 1
x_var.set(x)
Then use this function as a callback within your start() function:
def start(event):
root.after(1000, increment_x)
So your code becomes:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from Tkinter import *
root = Tk()
def start(event):
root.after(1000, increment_x)
def increment_x():
global x, x_var
x += 1
x_var.set(x)
x=1
x_var=IntVar()
x_var.set(x)
r = Label(root, textvariable=x_var)
r.pack()
root.bind('<Control-Up>', start)
root.mainloop()
P.S. Please follow PEP8

Categories