How to Run Your Own Code Alongside Tkinter'S Event Loop

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."""

Tkinter managing my event loops alongside my mainloop

You already have an infinite loop running, so you shouldn't be trying to add another one. Instead, you can use the after method to cause a function to be repeatedly called every so often. In your case, you can replace this:

def updateStock(self):
while True:
labelName = str(s) + "Label"
stockPrice = get_quote(s)
self.labelName = Label(self.myContainer1, text = s.upper() + ": " + str(stockPrice))
self.labelName.pack()
time.sleep(10)

... with this:

def updateStock(self):
labelName = str(s) + "Label"
stockPrice = get_quote()
self.labelName = Label(self.myContainer1, text = s.upper() + ": " + str(stockPrice))
self.labelName.pack()
self.after(10000, self.updateStock)

This will get a quote, add a label, then arrange for itself to be called again in 10 seconds (10,000 ms).

However, I doubt that you want to create a new label every 10 seconds, do you? Eventually the window will fill up with labels. Instead, you can create a label once, then update the label in each iteration. For example, create self.label once in the init, then in the loop you can do:

self.labelName.configure(text=s.upper() + ": " + str(stockPrice))

How can I actually execute a 'while True' loop code while there is a 'mainloop()' in the code?

You should try to avoid this, but you can use a recursion loop.

make a function called "myloop" and repeat it, like so, and then call it just before mainloop:

def myloop():
<code>
root.after(1, myloop) #repeats the function again 1 1000th of a second after it finishes


#----- down by where you're calling mainloop-----


root.after(1, myloop)
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()


Related Topics



Leave a reply



Submit