Stopping a Tkinter window without closing it - python

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)

Related

While loop not working properly in tkinter window

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

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 I perform an action after mainloop() has been called in Tkinter?

I want to make a GUI command line using the Text widget. For debugging purposes, I am trying to print whatever the user types into the separate GUI window to the system terminal. I know that it is frowned upon to mix GUI and Text Based commands into the same script, but I am just debugging, so forgive me 😉
Here is my code:
from Tkinter import *
main = Tk()
console = Text(main)
console.pack()
main.mainloop()
while True:
text = console.get("1.0", "end-1c")
print(text)
My current issue is that when the mainloop starts, (of course) the while loop doesn't. If I were to move the while loop in front of the mainloop call, it would never call mainloop. I really want it to continuously check for new text.
Is there a way to like "pause" the mainloop, or just carry out the command, maybe on a new thread or something?
I want to avoid using main.after(), but if that is the only way, then so be it. ¯\(°_o)/¯
I recommend using main.after(), as it's the canonical way to do things like this in Tkinter. The following will also ensure that it only tries to print every second, instead of as fast as the console can handle it (as the while loop in your code would do if it worked).
def print_console():
print(console.get("1.0", "end-1c"))
main.after(1000, print_console)
print_console()
main.mainloop()
You can also bind widgets to "Modified"
from Tkinter import *
class TextModified():
def __init__(self):
root = Tk()
self.txt = Text(root)
self.txt.pack()
self.txt.focus_set()
self.txt.bind('<<Modified>>', self.changed)
Button(text='Exit', command=root.quit).pack()
root.mainloop()
def changed(self, value=None):
flag = self.txt.edit_modified()
if flag: # prevent from getting called twice
print "changed called", self.txt.get("1.0", "end-1c")
## reset so this will be called on the next change
self.txt.edit_modified(False)
TM=TextModified()

Categories