While loop not working properly in tkinter window - python

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

Related

Alternative To Time.Sleep() for pausing a function

I created an example code because my original is too big and has private information(My own) in it.
While running a program from a Tkinter GUI, it runs the program but makes the GUI unresponsive because of time.sleep() blocking the GUI from updating.
I am trying to avoid using timers because it fires a different function after a duration instead of simply pausing the function and then continuing the same function.
Is there an alternative that does not block the GUI but still adds a delay inside of the function?
Example Code:
from tkinter import *
import time
wn = Tk()
wn.geometry("400x300")
MyLabel = Label(wn, text="This is a Status Bar")
MyLabel.pack()
def MyFunction():
Value = 1
while Value < 10:
print("Do something")
time.sleep(1) **# - here blocks everything outside of the function**
MyLabel.config(text=Value)
# A lot more code is under here so I cannot use a timer that fires a new function
Value = 1
MyButton = Button(wn, text="Run Program", command=MyFunction)
MyButton.pack()
wn.mainloop()
Edit: Thanks so much, you're answers were fast and helpful, I changed the code and added "wn.mainloop()" after the delay and replaced "time.sleep(1)" with wn.after(100, wn.after(10, MyLabel.config(text=Value))
here is the final code:
from tkinter import *
import time
wn = Tk()
wn.geometry("400x300")
MyLabel = Label(wn, text="This is a Status Bar")
MyLabel.pack()
def MyFunction():
Value = 0
while Value < 10:
print("Do something")
wn.after(10, MyLabel.config(text=Value))
Value += 1
wn.mainloop()
MyButton = Button(wn, text="Run Program", command=MyFunction)
MyButton.pack()
wn.mainloop()
The short answer is that you can use wn.after() to request a callback after a certain amount of time. That's how you handle it. You get a timer tick at a one-per-second rate, and you have enough state information to let you proceed to the next state, then you go back to the main loop.
Put another way, timers are exactly how you have to solve this problem.
Fundamentally, any callback function in Tkinter runs in the main GUI thread, and so the GUI thread will block until the function exits. Thus you cannot add a delay inside the function without causing the GUI thread to be delayed.
There are two ways to solve this. One would be to refactor your function into multiple pieces so that it can schedule the remaining work (in a separate function) via .after. This has the advantage of ensuring that all of your functions are running in the main thread, so you can perform GUI operations directly.
The other way is to run your function in a separate thread that is kicked off whenever your main callback is executed. This lets you keep all the logic inside the one function, but it can no longer perform GUI operations directly - instead, any GUI operations would have to go through an event queue that you manage from the main thread.
You can combine after() and wait_variable() to simulate time.sleep() without blocking tkinter from handling pending events and updates:
def tk_sleep(delay):
v = wn.IntVar()
# update variable "delay" ms later
wn.after(delay, v.set, 0)
# wait for update of variable
wn.wait_variable(v)
Using tk_sleep() in your while loop:
def MyFunction():
Value = 1
while Value < 10:
print("Do something")
tk_sleep(1000) # waits for one second
MyLabel.config(text=Value)
# A lot more code is under here so I cannot use a timer that fires a new function
Value += 1

Problems with update and update_idletasks

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)

Delete all from TkInter Canvas and put new items while in mainloop

The goal is to achieve different "screens" in TkInter and change between them. The easiest to imagine this is to think of a mobile app, where one clicks on the icon, for example "Add new", and new screen opens. The application has total 7 screens and it should be able to change screens according to user actions.
Setup is on Raspberry Pi with LCD+touchscreen attached. I am using tkinter in Python3. Canvas is used to show elements on the screen.
Since I am coming from embedded hardware world and have very little experience in Python, and generally high-level languages, I approached this with switch-case logic. In Python this is if-elif-elif...
I have tried various things:
Making global canvas object. Having a variable programState which determines which screen is currently shown. This obviously didn't work because it would just run once and get stuck at the mainloop below.
from tkinter import *
import time
root = Tk()
programState = 0
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
time.sleep(2)
canvas.delete(ALL) #delete all objects from canvas
programState = 1
elif(programState == 1):
....
....
....
root.mainloop()
Using root.after function but this failed and wouldn't show anything on the screen, it would only create canvas. I probably didn't use it at the right place.
Trying making another thread for changing screens, just to test threading option. It gets stuck at first image and never moves to second one.
from tkinter import *
from threading import Thread
from time import sleep
def threadFun():
while True:
backgroundImage = PhotoImage(file="image1.gif")
backgroundImage2 = PhotoImage(file="image2.gif")
canvas.create_image(0,0,image=backgroundImage, anchor=NW)
sleep(2)
canvas.delete(ALL)
canvas.create_image(0,0,image=backgroundImage2, anchor=NW)
root = Tk()
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
# daemon=True kills the thread when you close the GUI, otherwise it would continue to run and raise an error.
Thread(target=threadFun, daemon=True).start()
root.mainloop()
I expect this app could change screens using a special thread which would call a function which redraws elements on the canvas, but this has been failing so far. As much as I understand now, threads might be the best option. They are closest to my way of thinking with infinite loop (while True) and closest to my logic.
What are options here? How deleting whole screen and redrawing it (what I call making a new "screen") can be achieved?
Tkinter, like most GUI toolkits, is event driven. You simply need to create a function that deletes the old screen and creates the new, and then does this in response to an event (button click, timer, whatever).
Using your first canvas example
In your first example you want to automatically switch pages after two seconds. That can be done by using after to schedule a function to run after the timeout. Then it's just a matter of moving your redraw logic into a function.
For example:
def set_programState(new_state):
global programState
programState = new_state
refresh()
def refresh():
canvas.delete("all")
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
canvas.after(2000, set_programState, 1)
elif(programState == 1):
...
Using python objects
Arguably a better solution is to make each page be a class based off of a widget. Doing so makes it easy to add or remove everything at once by adding or removing that one widget (because destroying a widget also destroys all of its children)
Then it's just a matter of deleting the old object and instantiating the new. You can create a mapping of state number to class name if you like the state-driven concept, and use that mapping to determine which class to instantiate.
For example:
class ThisPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
class ThatPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
page_map = {0: ThisPage, 1: ThatPage}
current_page = None
...
def refresh():
global current_page
if current_page:
current_page.destroy()
new_page_class = page_map[programstate]
current_page = new_page_class()
current_page.pack(fill="both", expand=True)
The above code is somewhat ham-fisted, but hopefully it illustrates the basic technique.
Just like with the first example, you can call update() from any sort of event: a button click, a timer, or any other sort of event supported by tkinter. For example, to bind the escape key to always take you to the initial state you could do something like this:
def reset_state(event):
global programState
programState = 0
refresh()
root.bind("<Escape>", reset_state)

How to keep refreshing a canvas using Tkinter?

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()

How can threading be used to create multiple independent re-usable object instances?

I am building a GUI in Python and using threads to generate multiple countdown timers that work independently. I have a minimized module for testing which i am including bellow. I want the button to start the count down and then when its clicked again stop it, then reset so that it can be started on the next click. Also reset on its own once time runs out. The only problem is that after it stops, I cannot get it to restart. I get the "cannot start a thread twice" error. I have been trying to use a conditional loop to get the thread to exit its self but it hasn't been working. I would really appreciate some insight
There are actually two things that i want this program to be able to do.
1) run all the way through the timer then automatically reset so that it can be restarted
2) be stopped in the middle of a countdown, have it automatically reset so that it can be restarted
I think that solving these issues will be valuable for the community to see because it is an example of a real world solution to the issue that a lot of people talk about on the forum which is how to get around the no restarting threads.
__author__ = 'iKRUSTY'
'''
there are two things i would like this code to be able to do
1) run all the way through the timer and then reset so that it can be restarted
2) be stopped before completing and then reset so that it can be restarted
'''
from tkinter import *
import time
import os
import threading
#Variables
global FRYER_ONE_TIME_VAR # holds the value for changing label text to update timer
global BASKET_ONE_TARGET_TIME #this is a user input that will determine the length of the countdown
global timerOneStayAlive #this is the value that i am attempting to use so that the thread closes after it is set to false
timerOneStayAlive = FALSE #initializes to false because the the invert function changes it to true once the button is clicked
FRYER_ONE_TIME_VAR=" " #used to pass time between functiuons
#Font Profiles
SMALLEST_FONT = ("Verdana", 9)
SMALL_FONT = ("Verdana", 10)
LARGE_FONT = ("Verdana", 12)
LARGEST_FONT = ("Verdana", 18)
class timer():
global BASKET_ONE_TARGET_TIME
BASKET_ONE_TARGET_TIME = 5 #Just setting it manually for now
def __init__(self):
self.s = 0 #these values are used to convert from seconds to a minute:second format
self.m = 0 #these values are used to convert from seconds to a minute:second format
def SetTime(self, seconds):
self.seconds=seconds #this is a counter that will be used to calculate remaining time
def TimerReset(self):
self.seconds = BASKET_ONE_TARGET_TIME #resets counter to target time
def StartCountdown(self, FryerLabel): #takes a label as an argumet to tell it where to display the countdown
global timerOneStayAlive
print("StartCountdown Started!") #USED FOR TROUBLE SHOOTING
self.seconds = BASKET_ONE_TARGET_TIME #set start value for seconds counter
self.seconds=self.seconds+1 #makes the full time appear upon countdown start
while self.seconds > 0:
FRYER_ONE_TIME_VAR = self.CalculateTime() #Calculate time reduces the counter by one and reformats it to a minute:second format. returns a string
FryerLabel.config(text=FRYER_ONE_TIME_VAR) #Update Label with current value
print(self.seconds) #USED FOR TROUBLE SHOOTING
time.sleep(1)
# reset label with default time
if self.seconds == 0: #Reset once counter hits zero
print("resetting time") #USED FOR TROUBLE SHOOTING
self.seconds = BASKET_ONE_TARGET_TIME + 1
FRYER_ONE_TIME_VAR = self.CalculateTime()
FryerLabel.config(text=FRYER_ONE_TIME_VAR)
break
print("TimerStayAlive before invert: ", timerOneStayAlive) #USED FOR TROUBLE SHOOTING
timerOneStayAlive = invert(timerOneStayAlive) #inverts the value back to FALSE so that Ideally the loop breaks
# and the thread completes so that it can be called again
print("TimerStayAlive after invert: ", timerOneStayAlive) #USED FOR TROUBLE SHOOTING
print("end startcountdown") #USED FOR TROUBLE SHOOTING
def CalculateTime(self):
#print("CalculateTime has been called")
lessThanTen=0
self.seconds = self.seconds - 1
self.m, self.s = divmod(self.seconds, 60)
if self.s<10:
lessThanTen=1
#create time String Variables
colonVar=':'
minutesString = str(self.m)
secondsString = str(self.s)
#insert variables into string array
timeArray = []
timeArray.append(minutesString)
timeArray.append(colonVar)
if lessThanTen == 1:
timeArray.append("0")
timeArray.append(secondsString)
#prepare for output
self.timeString = ''.join(timeArray)
return self.timeString
def invert(boolean):
return not boolean
def BasketOneButtonClicked():
print("button clicked") #USED FOR TROUBLE SHOOTING
global timerOneStayAlive
timerOneStayAlive = invert(timerOneStayAlive) #Changes from FALSE to TRUE
if timerOneStayAlive == TRUE:
print("timerOneStayAlive: ", timerOneStayAlive) #USED FOR TROUBLE SHOOTING
basketOneThread.start()
updateButtonStatus() #changes text of button depending on whether timerOneStayAlive is TRUE or FALSE
else:
print("timerOneStayAlive: ", timerOneStayAlive) #USED FOR TROUBLE SHOOTING
return
def updateButtonStatus():
global timerOneStayAlive
if timerOneStayAlive == FALSE:
basketOneStartButton.config(text="Start")
if timerOneStayAlive == TRUE:
basketOneStartButton.config(text="Stop")
def BasketOneThreadComShell(): # I used this so that i can ideally call multiple functions with a single thread
'''
This is where i think the problem may be. this is what is called when the thread is initialized and theoretically
when this completes the thread should come to a halt so that when the button is reset, the thread can be called again
I think that the function is completing but for some reason the thread keeps on running.
'''
global timerOneStayAlive
print("ComShell Started") #USED FOR TROUBLE SHOOTING
while timerOneStayAlive:
basketOneTimer.StartCountdown(countdownLabelBasket1)
updateButtonStatus()
if timerOneStayAlive == FALSE: #redundant because while loop should do it. i just tried it because i couldnt get the process to end
break
print("Threadshell has ended") #USED FOR TROUBLE SHOOTING
return
print("after return check") #USED FOR TROUBLE SHOOTING
root = Tk()
'''
the following is all just building the GUI Stuff
'''
Container = Frame(root)
Container.grid(row=1, column=0, padx=10, pady=10)
countdownContainerBasket1 = Label(Container, width=10, height=5)
countdownContainerBasket1.grid(row=2, column=0, sticky=NSEW)
countdownLabelBasket1 = Label(countdownContainerBasket1, text="Basket 1", background="white", anchor=CENTER, width=10, height=6, font=LARGE_FONT, padx=20)
countdownLabelBasket1.pack()
basketOneTimer = timer()
basketOneTimer.SetTime(5)
basketOneStartButton = Button(Container, text="Start", font=LARGE_FONT, command=BasketOneButtonClicked)
basketOneStartButton.grid(row=3, column=0, padx=10, pady=10)
basketOneThread = threading.Thread(target=BasketOneThreadComShell) #this is where the thread is initialized. start() is called in BasketOneButtonClick
print(threading.active_count) #tried to use this to see if the thread was exiting but it didnt help
root.mainloop()
You'll probly want to give this a run just to see what the GUI looks like and what i mean when i say timer. Try letting it run all the way through and resetting to get an idea of what im talking about.
Let me know if there is any other info that would be helpful, and keep in mind this is my first week with python so im no expert. Thanks all
The python community is very lucky to have a style guide that is very well accepted https://www.python.org/dev/peps/pep-0008/. It is much harder for me to quickly understand the problems you are talking about due to your non pep8 naming and formatting.
Other threads cannot interact with your widgets, only the tkinter mainloop can do that. You can use the after method provided by tkinter to run a function after a given time period in the mainloop.
see this example
Mutli-threading python with Tkinter for an example!
To be clear threading with tkinter may work, but it is unreliable and you will get unexpected behavior that is hard to debug. Don't do it.
Suggestions
the tkinter after method would be called to start a function that indefinitely checks a queue for functions to run. It then schedules these functions to be run by tkinter using the after method
I would have a timer class with start, stop and pause methods.
When started it would count up/down and simply restart itself on completion. Each timer instance you create would also need a reference to a tkinter button.
to update the button you put a function on the queue that may look like
tkinter_queue.put(lambda: but.config(text=i))
now the function that checks the queue will update your button timers
Also as you are in your first week of python programming you can be sure that most problems you encounter will have answers here if not elsewhere. Please do your research before hand. Gui programming and threading are not the easiest topics so even more reason to do your research.

Categories