How do I keep updating a tkinter canvas, like in a while ( True ): loop?
I know that you can do after( 1000 , refresh_function );, but how do I make the loop repeat forever?
Practical example: a program that draws a line with fixed length under an angle, and the angle is constantly increasing (so the line is rotating / spinning).
I think I have taken a look at all relevant questions here, but this may still be a duplicate, and if it is, I am sorry.
A while True: loop in incompatible with using .mainloop(). You make a function repeat by having it re-schedule itself before it exits. There are several examples in other answers, such as making something glide across a canvas. Here is another that illustrates the idea.
import tkinter as tk
root = tk.Tk()
text = tk.StringVar(root)
label = tk.Label(root, textvariable=text)
label.pack()
def add_a():
text.set(text.get()+'a')
root.after(500, add_a) # <== re-schedule add_a
root.after(500, add_a) # <== start the repeating process
root.mainloop()
Perhaps class threading.Timer could help you
def f():
# write our code for repainting canvas
# call f() again in 60 seconds
threading.Timer(60, f).start()
# start calling f now and then every 60 sec
f()
To update a TKinter window (with canvas etc.), you'll need root.mainloop(), which equals in:
while 1:
root.update()
Related
I am currently having an issue where I am trying to run a while loop inside a tkinter window. It waits until the while loop finishes for it to actual show the window. What I want to happen is for the window to come up, and then the while loop will begin. After research I found that it is something to do with the root.mainloop() function, but I am not sure how to change it.
#creates new window
root = Tk()
#makes backdrop picture of map
C = Canvas(root, bg="black", height=780, width=1347)
C.grid(row=0,column=0)
filename = PhotoImage(file = "map4.png")
background_label = Label(root, image=filename)
background_label.place(x=0, y=0, relwidth=1, relheight=1)
totalDeaths = 0
totalCases = 0
totalPopulation = 15000
dayCount = 0
#loops until total deaths = population
while totalDeaths < totalPopulation:
totalDeaths += 1
time.sleep(1)
root.mainloop()
Don't use sleep with tkinter. Instead, use method called after:
import tkinter as tk
class App:
def __init__(self):
self.root = tk.Tk()
self.total_deaths = 0
self.label = tk.Label(text='')
self.label.pack()
self.update_deaths()
self.root.mainloop()
def update_deaths(self):
self.total_deaths += 1
self.label.configure(text=f'Total deaths: {self.total_deaths}')
self.root.after(1000, self.update_deaths)
App()
Output:
The while loop is working exactly as designed. You have in effect asked it to sleep for 15,000 seconds so that's what it is doing. While it is sleeping, tkinter is unable to refresh the display or process events of any type.
When creating GUIs, a rule of thumb is that you should never have a large while loop in the same thread as the GUI. The GUI already has an infinite loop running all the time.
So, the first step is to move the code which is inside your loop to a function. In a UI it's often good to separate the UI code from non-UI code, so in this case I recommend two functions: one to do the calculation and one to update the display. This will make it easier to replace either the UI or the method of calculation without having to rewrite the whole program. It will also make it easier to test the calculation function with a unit test.
So, let's start with a function that calculates the deaths. Based on comments to other answers, it appears that you have a complicated formula for doing that but simply incrementing the total is good enough for a simulation.
def update_deaths():
global totalDeaths
totalDeaths += 1
Next, you need a way to display those deaths. The code you posted doesn't have any way to do that, so this solution requires the addition of a Label to show current deaths. How you do it for real is up to you, but the following code illustrates the general principle:
death_label = Label(...)
...
def update_display():
global totalDeaths
death_label.configure(text=f"Total Deaths: {totalDeaths}")
The third piece of the puzzle is the code to simulate your while loop. Its job is to update the deaths and then update the display every second until the entire population has died.
We do that with the after method which can schedule a function to be run in the future. By using after rather than sleep, it allows mainloop to be able to continue to process events such as button clicks, key presses, requests to update the display, etc.
def simulate_deaths():
global totalDeaths
global totalPopulation
update_deaths()
update_display()
if totalDeaths < totalPopulation:
root.after(1000, simulate_deaths)
If you call this function once at the start of your program, it will continue to be called once a second until the condition is met.
The after method returns an identifier, which you can use to cancel the function before its next iteration. If you save that in a global variable (or instance variable if you're using classes), you can stop the simulation by calling after_cancel with that identifier.
For example:
def simulate_deaths():
global after_id
...
after_id = root.after(1000, simulate_deahts)
def stop_simulation():
root.after_cancel(after_id)
I believe your while loop is never ending since totalDeaths will always be smaller than totalPopulation
You could have something like this:
while totalDeaths < totalPopulation:
if somebodyDies:
totalDeaths+=1
else:
continue
I've been learning python for a month now and run into my first brick wall. I have a large art viewer GUI program and at one point want to put an image on screen with a countdown counter-approx every 5 secs. I thought of a code such as the one below The problem is that this uses update and all my reading says that update is bad (starts a new event loop (?)) and that I should use update_idletasks. when I replace update with update_idletasks in the code below the countdown button is not visible until it reaches single figures, update superficially works fine. But also the q bound key calls the subroutine but has no effect
from tkinter import *
import sys
import time
root = Tk()
def q_key(event):
sys.exit()
frame=Frame(root, padx=100, pady=100, bd=10, relief=FLAT)
frame.pack()
button=Button(frame,relief="flat",bg="grey",fg="white",font="-size 18",text="60")
button.pack()
root.bind("q",q_key)
for x in range(30, -1, -5) :
button.configure(text=str(x))
button.update()
print(x)
button.after(5000)
root.mainloop()
In this case you don't need update nor update_idletasks. You also don't need the loop, because tkinter is already running in a loop: mainloop.
Instead, move the body of the loop to a function, and call the function via after. What happens is that you do whatever work you want to do, and then schedule your function to run again after a delay. Since your function exits, tkinter returns to the event loop and is able to process events as normal. When the delay is up, tkinter calls your function and the whole process starts over again.
It looks something like this:
def show(x):
button.configure(text=x)
if x > 0:
button.after(5000, show, x-5)
show(30)
I would like to understand why this code:
import time
for i in range(1,11):
print(i)
time.sleep(1)
shows (as it should!) numbers from 1 to 10, each every 1 second, while this code:
from tkinter import *
import time
root = Tk()
for i in range(1,11):
Label(root, text = i).grid(row=0, column=i-1, padx=5, pady =5)
time.sleep(1)
root.mainloop()
waits for 10 seconds, and then displays a window with the 10 numbers (instead of adding them one by one).
I am aware this is a silly question, but I really can't understand! Many Thanks! Alessandro
Most GUI's work differently to what you expect.
They work in an asynchronous way, which means, that you setup your windows and start an event loop.
This event loop will display all widgets, labels, etc, that you set up before calling the event loop and wait for any events (GUI events like mouse or keyboard events, timer events and perhaps network events).
When any event is encountered code associated to that event will be called and this code can request to change the GUI (show or hide elements, change labels or attributes of graphical widgets) However the change to the GUI will only be performed when you give control back to the event loop (when the code handling an event finished)
In your given code you change a label in a for loop with sleep statements, but only after the for loop is finished your main loop is being called and this is the moment, where the final state of your GUI will be displayed.
So what you encounter is a know issue for almost all GUI / asynhronous kind of applications.
You have to rewrite your code such, that you start a timer event, and when the timer event fires a function will set a label and increase the counter by 1. And if the counter is not 11 it will restart another timer
This is because the time.sleep function is before the root.mainloop function.
root.mainloop is what causes the window to appear on-screen and start doing things. Instead, I'd recommend using window.after, as that tells the window to run a function after some time when it's on-screen.
Here's an example of a modification you could make (it's not that good but it works):
from tkinter import *
import time
root = Tk()
progress = 0
end = 10
def update_progress():
global progress
progress += 1
Label(root, text = progress).grid(row=0, column=progress-1, padx=5, pady =5)
if progress < end: root.after(1000,update_progress) # Tell the window to call this function in 1000ms (1 second)
root.after(0,update_progress) # Tell the window to run the update_progress function 0ms after now.
root.mainloop()
I'd recommend looking at gelonida's answer for an explanation of why your original code didn't work, and what you need to keep in mind when programming with GUIs in the future.
I was racking my brain a bit trying to figure out why a tkinter window would only appear after I had stopped my script. Turns out, it won't appear if the delay time in my root.after (that is within my infinite fruity loop) was set to 0. Setting it to 1 or higher caused it to work correctly. Is this a bug or am I missing something important about how .after works? I'm running this with Python 2.7 in Anaconda on mac OS.
import time
import Tkinter as tk
import random
root = tk.Tk()
root.title("random numbers")
root.geometry("220x220+5+5")
frame = tk.Frame(root, width=210, height=210)
frame.pack()
luckynumber = tk.IntVar()
label1 = tk.Label(frame, text="random number").pack(side=tk.LEFT)
display1 = tk.Label(frame, textvariable=luckynumber)
display1.pack( side=tk.LEFT )
def askrandy():
randy = random.randrange(0, 100, 1)
luckynumber.set(randy)
def fruityloop():
time.sleep(.5)
askrandy()
root.after(1, fruityloop)
root.after(0, fruityloop)
root.mainloop()
Second question: this code doesn't run very smoothly. Seeing as it's quite simple, I assumed it would be pretty solid. But I find that it takes a couple seconds to get started and moving the window around causes it to stutter as well. Would this work better with my main loop run as a class?
This is normal behavior.
Tkinter maintains a queue of work to be done when it goes idle. This is the "idle" queue.
When you call after, the function you supply is added to this queue. When the main event loop (or a call to after_idle) processes the queue, it looks for items on the queue that should be run based on the current time and the time that the item should be run. All items that are due to be run are run before processing of the queue stops.
If one of those adds an item to the queue with a value of zero it will be run since its time is due. If that item itself adds an item to the queue, then you take one item off of the queue and immediately put one one so the queue will never become empty. If the queue never becomes empty, tkinter isn't able to process other types of events.
The reason that the program seems slow and jerky is because of the call to sleep. When you call sleep, tkinter does exactly that: it sleeps. It cannot process any events, even events that simply refresh the window. If you want askrandy to be called once every half second, you should simply call after with a value of 500, rather than call it with a value of zero and then sleep for half a second.
Whether the main window is a class or not will not affect your program all all. You simply need to stop using sleep, and provide sane values to after. If you are trying to show a simple animation, a value of 30 is about as small as you need to go.
This is how it should looks without sleep(). But I don't know if it can help. It works fast on Linux.
If you run code in IDLE then you may have problem because it uses Tkinter to display windows and runs own mainloop() but Tkinter should run only one mainloop(). You can try directly in console python script.py.
import Tkinter as tk
import random
# --- functions ---
def fruityloop():
randy = random.randrange(0, 100, 1)
luckynumber.set(randy)
# run again after 500ms = 0.5s
root.after(500, fruityloop)
# --- main ---
root = tk.Tk()
root.title("random numbers")
root.geometry("220x220+5+5")
luckynumber = tk.IntVar()
frame = tk.Frame(root, width=210, height=210)
frame.pack()
label = tk.Label(frame, text="random number")
label.pack(side=tk.LEFT)
display = tk.Label(frame, textvariable=luckynumber)
display.pack(side=tk.LEFT)
# run first time
fruityloop()
root.mainloop()
I have written a python script that shows an animation of the evolution of a path-finding algorithm and i am wondering how to stop the main loop once it has reached a certain condition.
my basic idea (without any of my actual code) is:
import Tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=800, height=800, background="black")
canvas.pack()
def initialise():
<some code to initialise everything on the canvas>
def move():
<some code for move()>
root.after(1000,move)
root.mainloop()
i would like to be able to test a condition in the function move() that allows me to stop the Tkinter main loop but still leave the window open so you can see it, and then be able to do other things after (not with the window tho, it doesnt matter if that is unchangeable, just so long as it is visible until the user closes the window)
so basically similar to this:
while true: # this represents the Tkinter mainloop
<do something>
if <final condition>:
break
<some other operations on data> # this happens after mainloop stops
You can't stop mainloop without destroying the window. That's the fundamental nature of Tkinter.
What you can do instead is simply stop the animation. You can do that exactly like you propose: add a flag or final condition in your move method.
def move():
if some_condition:
return
...
root.after(1000, move)