Create a Main Loop with Tkinter

How do you run your own code alongside Tkinter's event loop?

Use the after method on the Tk object:

from tkinter import *

root = Tk()

def task():
print("hello")
root.after(2000, task) # reschedule event in 2 seconds

root.after(2000, task)
root.mainloop()

Here's the declaration and documentation for the after method:

def after(self, ms, func=None, *args):
"""Call function once after given time.

MS specifies the time in milliseconds. FUNC gives the
function which shall be called. Additional parameters
are given as parameters to the function call. Return
identifier to cancel scheduling with after_cancel."""

Create a main loop with tkinter?

Tkinter provide a powerfull tool for it and it is called after. It is intended as synchronous sleep command but can build a loop inside the mainloop by calling itself.

after , a built-in Tcl command, manages the scheduling of scripts for
future evaluation, and also functions as a synchronous sleep command.

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
showed_time = current_time #update the variable to compare it next time again
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:
showed_time = current_time
clock.configure(text=current_time)
global alarm #make sure the alarm is known
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.

Why threading isn't a good choice in coding frame work.

Tkinter understanding mainloop

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:

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

How exactly does tkinter's mainloop work?

Although trying to rewrite the tkinter loop seems troublesome, it seems rewriting the asyncio loop is quite easy, given tkinter's after function. The main gist of it is this:

"""Example integrating `tkinter`'s `mainloop` with `asyncio`."""
import asyncio
import tkinter as tk
from typing import Any, Awaitable, TypeVar

T = TypeVar("T")

class AsyncTk(tk.Tk):
"""
A Tk class that can run asyncio awaitables alongside the tkinter application.

Use `root.run_with_mainloop(awaitable)` instead of `root.mainloop()` as a way to run
coroutines alongside it. It functions similarly to using `asyncio.run(awaitable)`.

Alternatively use `await root.async_loop()` if you need to run this in an asynchronous
context. Because this doesn't run `root.mainloop()` directly, it may not behave exactly
the same as using `root.run_with_mainloop(awaitable)`.
"""
is_running: bool

def __init__(self, /, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.is_running = True

def __advance_loop(self, loop: asyncio.AbstractEventLoop, timeout, /) -> None:
"""Helper method for advancing the asyncio event loop."""
# Stop soon i.e. only advance the event loop a little bit.
loop.call_soon(loop.stop)
loop.run_forever()
# If tkinter is still running, repeat this method.
if self.is_running:
self.after(timeout, self.__advance_loop, loop, timeout)

async def async_loop(self, /) -> None:
"""
An asynchronous variant of `root.mainloop()`.

Because this doesn't run `root.mainloop()` directly, it may not behave exactly
the same as using `root.run_with_mainloop(awaitable)`.
"""
# For threading.
self.tk.willdispatch()
# Run initial update.
self.update()
# Run until `self.destroy()` is called.
while self.is_running:
# Let other code run.
# Uses a non-zero sleep time because tkinter should be expected to be slow.
# This decreases the busy wait time.
await asyncio.sleep(tk._tkinter.getbusywaitinterval() / 10_000)
# Run one event.
self.tk.dooneevent(tk._tkinter.DONT_WAIT)

def run_with_mainloop(self, awaitable: Awaitable[T], /, *, timeout: float = 0.001) -> T:
"""
Run an awaitable alongside the tkinter application.

Similar to using `asyncio.run(awaitable)`.

Use `root.run_with_mainloop(awaitable, timeout=...)` to
customize the frequency the asyncio event loop is updated.
"""
if not isinstance(awaitable, Awaitable):
raise TypeError(f"awaitable must be an Awaitable, got {awaitable!r}")
elif not isinstance(timeout, (float, int)):
raise TypeError(f"timeout must be a float or integer, got {timeout!r}")
# Start a new event loop with the awaitable in it.
loop = asyncio.new_event_loop()
task = loop.create_task(awaitable)
# Use tkinter's `.after` to run the asyncio event loop.
self.after(0, self.__advance_loop, loop, max(1, int(timeout * 1000)))
# Run tkinter, which periodically checks
self.mainloop()
# After tkinter is done, wait until `asyncio` is done.
try:
return loop.run_until_complete(task)
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()

def destroy(self, /) -> None:
super().destroy()
self.is_running = False

The example application may be fixed up like this:

import asyncio
from random import randrange
import tkinter as tk

def deg_color(deg, d_per_tick, color):
"""Helper function for updating the degree and color."""
deg += d_per_tick
if 360 <= deg:
deg %= 360
color = f"#{randrange(256):02x}{randrange(256):02x}{randrange(256):02x}"
return deg, color

async def rotator(root, interval, d_per_tick):
"""
An example custom method for running code asynchronously
instead of using `tkinter.Tk.after`.

NOTE: Code that can use `tkinter.Tk.after` is likely
preferable, but this may not fit all use-cases and
may sometimes require more complicated code.
"""
canvas = tk.Canvas(root, height=600, width=600)
canvas.pack()
deg = 0
color = 'black'
arc = canvas.create_arc(
100,
100,
500,
500,
style=tk.CHORD,
start=0,
extent=deg,
fill=color,
)
while root.is_running:
deg, color = deg_color(deg, d_per_tick, color)
canvas.itemconfigure(arc, extent=deg, fill=color)
await asyncio.sleep(interval)

def main():
root = AsyncTk()
root.run_with_mainloop(rotator(root, 1/60, 2))

if __name__ == "__main__":
main()

How can I make a button on tkinter run a function that loops?

The tkinter method called after() is what you want to use here.

Here is an example of it in action and I have changed the button to toggle a tracking variable to simulate starting and stopping the loop.

The below code will first check if the loop is being accessed by the button and then toggle on or off basically. Then there is a loop built in with the after method to continue the loop until you press the button again.

import tkinter as tk

def loop(toggle=False):
global tracking_var
if toggle:
if tracking_var:
tracking_var = False
else:
tracking_var = True

if tracking_var:
print("loop")
root.after(1000, loop)

root = tk.Tk()
tracking_var = False
tk.Button(root, text="run", command=lambda: loop(True)).pack()

root.mainloop()


Related Topics



Leave a reply



Submit