Tkinter understanding mainloop - python

Till now, I used to end my Tkinter programs with: tk.mainloop(), or nothing would show up! See example:
from Tkinter import *
import random
import time
tk = Tk()
tk.title = "Game"
tk.resizable(0,0)
tk.wm_attributes("-topmost", 1)
canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()
class Ball:
def __init__(self, canvas, color):
self.canvas = canvas
self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
self.canvas.move(self.id, 245, 100)
def draw(self):
pass
ball = Ball(canvas, "red")
tk.mainloop()
However, when tried the next step in this program (making the ball move by time), the book am reading from, says to do the following. So I changed the draw function to:
def draw(self):
self.canvas.move(self.id, 0, -1)
and add the following code to my program:
while 1:
ball.draw()
tk.update_idletasks()
tk.update()
time.sleep(0.01)
But I noticed that adding this block of code, made the use of tk.mainloop() useless, since everything would show up even without it!!!
At this moment I should mention that my book never talks about tk.mainloop() (maybe because it uses Python 3) but I learned about it searching the web since my programs didn't work by copying book's code!
So I tried doing the following that would not work!!!
while 1:
ball.draw()
tk.mainloop()
time.sleep(0.01)
What's going on? What does tk.mainloop()? What does tk.update_idletasks() and tk.update() do and how that differs from tk.mainloop()? Should I use the above loop?tk.mainloop()? or both in my programs?

tk.mainloop() blocks. It means that execution of your Python commands halts there. You can see that by writing:
while 1:
ball.draw()
tk.mainloop()
print("hello") #NEW CODE
time.sleep(0.01)
You will never see the output from the print statement. Because there is no loop, the ball doesn't move.
On the other hand, the methods update_idletasks() and update() here:
while True:
ball.draw()
tk.update_idletasks()
tk.update()
...do not block; after those methods finish, execution will continue, so the while loop will execute over and over, which makes the ball move.
An infinite loop containing the method calls update_idletasks() and update() can act as a substitute for calling tk.mainloop(). Note that the whole while loop can be said to block just like tk.mainloop() because nothing after the while loop will execute.
However, tk.mainloop() is not a substitute for just the lines:
tk.update_idletasks()
tk.update()
Rather, tk.mainloop() is a substitute for the whole while loop:
while True:
tk.update_idletasks()
tk.update()
Response to comment:
Here is what the tcl docs say:
Update idletasks
This subcommand of update flushes all currently-scheduled idle events
from Tcl's event queue. Idle events are used to postpone processing
until “there is nothing else to do”, with the typical use case for
them being Tk's redrawing and geometry recalculations. By postponing
these until Tk is idle, expensive redraw operations are not done until
everything from a cluster of events (e.g., button release, change of
current window, etc.) are processed at the script level. This makes Tk
seem much faster, but if you're in the middle of doing some long
running processing, it can also mean that no idle events are processed
for a long time. By calling update idletasks, redraws due to internal
changes of state are processed immediately. (Redraws due to system
events, e.g., being deiconified by the user, need a full update to be
processed.)
APN As described in Update considered harmful, use of update to handle
redraws not handled by update idletasks has many issues. Joe English
in a comp.lang.tcl posting describes an alternative:
So update_idletasks() causes some subset of events to be processed that update() causes to be processed.
From the update docs:
update ?idletasks?
The update command is used to bring the application “up to date” by
entering the Tcl event loop repeatedly until all pending events
(including idle callbacks) have been processed.
If the idletasks keyword is specified as an argument to the command,
then no new events or errors are processed; only idle callbacks are
invoked. This causes operations that are normally deferred, such as
display updates and window layout calculations, to be performed
immediately.
KBK (12 February 2000) -- My personal opinion is that the [update]
command is not one of the best practices, and a programmer is well
advised to avoid it. I have seldom if ever seen a use of [update] that
could not be more effectively programmed by another means, generally
appropriate use of event callbacks. By the way, this caution applies
to all the Tcl commands (vwait and tkwait are the other common
culprits) that enter the event loop recursively, with the exception of
using a single [vwait] at global level to launch the event loop inside
a shell that doesn't launch it automatically.
The commonest purposes for which I've seen [update] recommended are:
Keeping the GUI alive while some long-running calculation is
executing. See Countdown program for an alternative. 2) Waiting for a window to be configured before doing things like
geometry management on it. The alternative is to bind on events such
as that notify the process of a window's geometry. See
Centering a window for an alternative.
What's wrong with update? There are several answers. First, it tends
to complicate the code of the surrounding GUI. If you work the
exercises in the Countdown program, you'll get a feel for how much
easier it can be when each event is processed on its own callback.
Second, it's a source of insidious bugs. The general problem is that
executing [update] has nearly unconstrained side effects; on return
from [update], a script can easily discover that the rug has been
pulled out from under it. There's further discussion of this
phenomenon over at Update considered harmful.
.....
Is there any chance I can make my program work without the while loop?
Yes, but things get a little tricky. You might think something like the following would work:
class Ball:
def __init__(self, canvas, color):
self.canvas = canvas
self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
self.canvas.move(self.id, 245, 100)
def draw(self):
while True:
self.canvas.move(self.id, 0, -1)
ball = Ball(canvas, "red")
ball.draw()
tk.mainloop()
The problem is that ball.draw() will cause execution to enter an infinite loop in the draw() method, so tk.mainloop() will never execute, and your widgets will never display. In gui programming, infinite loops have to be avoided at all costs in order to keep the widgets responsive to user input, e.g. mouse clicks.
So, the question is: how do you execute something over and over again without actually creating an infinite loop? Tkinter has an answer for that problem: a widget's after() method:
from Tkinter import *
import random
import time
tk = Tk()
tk.title = "Game"
tk.resizable(0,0)
tk.wm_attributes("-topmost", 1)
canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()
class Ball:
def __init__(self, canvas, color):
self.canvas = canvas
self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
self.canvas.move(self.id, 245, 100)
def draw(self):
self.canvas.move(self.id, 0, -1)
self.canvas.after(1, self.draw) #(time_delay, method_to_execute)
ball = Ball(canvas, "red")
ball.draw() #Changed per Bryan Oakley's comment
tk.mainloop()
The after() method doesn't block (it actually creates another thread of execution), so execution continues on in your python program after after() is called, which means tk.mainloop() executes next, so your widgets get configured and displayed. The after() method also allows your widgets to remain responsive to other user input. Try running the following program, and then click your mouse on different spots on the canvas:
from Tkinter import *
import random
import time
root = Tk()
root.title = "Game"
root.resizable(0,0)
root.wm_attributes("-topmost", 1)
canvas = Canvas(root, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()
class Ball:
def __init__(self, canvas, color):
self.canvas = canvas
self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
self.canvas.move(self.id, 245, 100)
self.canvas.bind("<Button-1>", self.canvas_onclick)
self.text_id = self.canvas.create_text(300, 200, anchor='se')
self.canvas.itemconfig(self.text_id, text='hello')
def canvas_onclick(self, event):
self.canvas.itemconfig(
self.text_id,
text="You clicked at ({}, {})".format(event.x, event.y)
)
def draw(self):
self.canvas.move(self.id, 0, -1)
self.canvas.after(50, self.draw)
ball = Ball(canvas, "red")
ball.draw() #Changed per Bryan Oakley's comment.
root.mainloop()

while 1:
root.update()
... is (very!) roughly similar to:
root.mainloop()
The difference is, mainloop is the correct way to code and the infinite loop is subtly incorrect. I suspect, though, that the vast majority of the time, either will work. It's just that mainloop is a much cleaner solution. After all, calling mainloop is essentially this under the covers:
while the_window_has_not_been_destroyed():
wait_until_the_event_queue_is_not_empty()
event = event_queue.pop()
event.handle()
... which, as you can see, isn't much different than your own while loop. So, why create your own infinite loop when tkinter already has one you can use?
Put in the simplest terms possible: always call mainloop as the last logical line of code in your program. That's how Tkinter was designed to be used.

I'm using an MVC / MVA design pattern, with multiple types of "views". One type is a "GuiView", which is a Tk window. I pass a view reference to my window object which does things like link buttons back to view functions (which the adapter / controller class also calls).
In order to do that, the view object constructor needed to be completed prior to creating the window object. After creating and displaying the window, I wanted to do some initial tasks with the view automatically. At first I tried doing them post mainloop(), but that didn't work because mainloop() blocked!
As such, I created the window object and used tk.update() to draw it. Then, I kicked off my initial tasks, and finally started the mainloop.
import Tkinter as tk
class Window(tk.Frame):
def __init__(self, master=None, view=None ):
tk.Frame.__init__( self, master )
self.view_ = view
""" Setup window linking it to the view... """
class GuiView( MyViewSuperClass ):
def open( self ):
self.tkRoot_ = tk.Tk()
self.window_ = Window( master=None, view=self )
self.window_.pack()
self.refresh()
self.onOpen()
self.tkRoot_.mainloop()
def onOpen( self ):
""" Do some initial tasks... """
def refresh( self ):
self.tkRoot_.update()

Related

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)

Python 3.x Proper way to make gameloop in tkinter? [duplicate]

I have a simple code to visualise some data using tkinter. A button click is bound to the function that redraws the next "frame" of data. However, I'd like to have the option to redraw automatically with a certain frequency. I'm very green when it comes to GUI programming (I don't have to do a lot for this code), so most of my tkinter knowledge comes from following and modifying examples. I guess I can use root.after to achieve this, but I'm not quite sure I understand how from other codes. The basic structure of my program is as follows:
# class for simulation data
# --------------------------------
def Visualisation:
def __init__(self, args):
# sets up the object
def update_canvas(self, Event):
# draws the next frame
canvas.delete(ALL)
# draw some stuff
canvas.create_........
# gui section
# ---------------------------------------
# initialise the visualisation object
vis = Visualisation(s, canvasWidth, canvasHeight)
# Tkinter initialisation
root = Tk()
canvas = Canvas(root, width = canvasWidth, height = canvasHeight)
# set mouse click to advance the simulation
canvas.grid(column=0, row=0, sticky=(N, W, E, S))
canvas.bind('<Button-1>', vis.update_canvas)
# run the main loop
root.mainloop()
Apologies for asking a question which I'm sure has an obvious and simple answer. Many thanks.
The basic pattern for doing animation or periodic tasks with Tkinter is to write a function that draws a single frame or performs a single task. Then, use something like this to call it at regular intervals:
def animate(self):
self.draw_one_frame()
self.after(100, self.animate)
Once you call this function once, it will continue to draw frames at a rate of ten per second -- once every 100 milliseconds. You can modify the code to check for a flag if you want to be able to stop the animation once it has started. For example:
def animate(self):
if not self.should_stop:
self.draw_one_frame()
self.after(100, self.animate)
You would then have a button that, when clicked, sets self.should_stop to False
I just wanted to add Bryan's answer. I don't have enough rep to comment.
Another idea would be to use self.after_cancel() to stop the animation.
So...
def animate(self):
self.draw_one_frame()
self.stop_id = self.after(100, self.animate)
def cancel(self):
self.after_cancel(self.stop_id)

Stopping a Tkinter window without closing it

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)

Changing Canvas Fill inside Application

I'm trying to spawn a thread, which periodically changes the color of my oval canvas, but somehow this codes does nothing:
from Tkinter import *
class Application(Frame):
def laufer(self):
self.canvas1.configure(fill='black')
def bt_start(self):
t = Thread(target=self.laufer)
t.start()
def createWidgets(self):
self.canvas1 = Canvas(self, width=50, height=50)
self.canvas1.create_oval(10, 10, 50, 50, outline="black",
fill="green", width=1)
self.canvas1.grid(row=3, column=0)
[bt_start...]
Which is the correct way to change the fill color?
To answer the question about how to change the color of the item, you must use the itemconfigure method, giving it the id of the object you want to configure, or a tag that represents zero or more objects to configure.
For example:
self.id = self.canvas1.create_oval(..., fill="green")
...
self.canvas1.itemconfigure(self.id, fill="black")
There are bigger problems, however. You cannot interact with the canvas from any thread other than the main thread. It might work, but there's a good chance your app will eventually crash.
There are two solutions: the preferred one is, don't use threads. They add complexity, and a lot of times you just don't need them. In the type of program used in the question, threads are completely unnecessary.
If, however, you really do need threads for your program, the generally accepted solution is to create a thread-safe queue, have your worker threads post some kind of data to the queue, and then have your main thread poll the queue on a regular schedule to act upon the data.

simple animation using tkinter

I have a simple code to visualise some data using tkinter. A button click is bound to the function that redraws the next "frame" of data. However, I'd like to have the option to redraw automatically with a certain frequency. I'm very green when it comes to GUI programming (I don't have to do a lot for this code), so most of my tkinter knowledge comes from following and modifying examples. I guess I can use root.after to achieve this, but I'm not quite sure I understand how from other codes. The basic structure of my program is as follows:
# class for simulation data
# --------------------------------
def Visualisation:
def __init__(self, args):
# sets up the object
def update_canvas(self, Event):
# draws the next frame
canvas.delete(ALL)
# draw some stuff
canvas.create_........
# gui section
# ---------------------------------------
# initialise the visualisation object
vis = Visualisation(s, canvasWidth, canvasHeight)
# Tkinter initialisation
root = Tk()
canvas = Canvas(root, width = canvasWidth, height = canvasHeight)
# set mouse click to advance the simulation
canvas.grid(column=0, row=0, sticky=(N, W, E, S))
canvas.bind('<Button-1>', vis.update_canvas)
# run the main loop
root.mainloop()
Apologies for asking a question which I'm sure has an obvious and simple answer. Many thanks.
The basic pattern for doing animation or periodic tasks with Tkinter is to write a function that draws a single frame or performs a single task. Then, use something like this to call it at regular intervals:
def animate(self):
self.draw_one_frame()
self.after(100, self.animate)
Once you call this function once, it will continue to draw frames at a rate of ten per second -- once every 100 milliseconds. You can modify the code to check for a flag if you want to be able to stop the animation once it has started. For example:
def animate(self):
if not self.should_stop:
self.draw_one_frame()
self.after(100, self.animate)
You would then have a button that, when clicked, sets self.should_stop to False
I just wanted to add Bryan's answer. I don't have enough rep to comment.
Another idea would be to use self.after_cancel() to stop the animation.
So...
def animate(self):
self.draw_one_frame()
self.stop_id = self.after(100, self.animate)
def cancel(self):
self.after_cancel(self.stop_id)

Categories