I was interested in trying to merge Python's tkinter
with asyncio
, and after having read this answer I was largely successful. For reference, you can recreate the mainloop
as follows:
import asyncio
import tkinter as tk
class AsyncTk(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.is_running = True
async def async_loop(self):
"""Asynchronous equivalent of `Tk.mainloop`."""
while self.is_running:
self.update()
await asyncio.sleep(0)
def destroy(self):
super().destroy()
self.is_running = False
async def main():
root = AsyncTk()
await asyncio.gather(
root.async_loop(),
other_async_functions(),
)
However, this comment pointed out that in some cases, the GUI may freeze during an self.update()
call. One may instead use self.update_idletasks()
to prevent this, but since root.async_loop
is supposed to simulate the root.mainloop
, I fear never running some tasks may cause other problems.
I couldn't find the source code for how root.mainloop
works, though I did discover that replacing self.update()
with
self.tk.dooneevent(tk._tkinter.DONT_WAIT)
should produce more fine-grained concurrency by only doing one event instead of flushing all events (I'm assuming that's what it does, I couldn't find the documentation for this either but it seemed to work).
So my two questions are:
Is it fine to use just
self.update_idletasks()
only, and never run whatever elseself.update()
is supposed to run?How exactly does
root.mainloop()
work?
For some code that can be ran and experimented with:
"""Example integrating `tkinter`'s `mainloop` with `asyncio`."""
import asyncio
from random import randrange
from time import time
import tkinter as tk
class AsyncTk(tk.Tk):
"""
An asynchronous Tk class.
Use `await root.async_loop()` instead of `root.mainloop()`.
Schedule asynchronous tasks using `asyncio.create_task(...)`.
"""
is_running: bool
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.is_running = True
async def async_loop(self):
"""An asynchronous version of `root.mainloop()`."""
# For threaded calls.
self.tk.willdispatch()
# Run until `self.destroy` is called.
while self.is_running:
#self.update_idletasks() # NOTE: using `update_idletasks`
# prevents the window from freezing
# when you try to resize the window.
self.update()
#self.tk.dooneevent(tk._tkinter.DONT_WAIT)
await asyncio.sleep(0)
def destroy(self):
"""
Destroy this and all descendants widgets. This will
end the application of this Tcl interpreter.
"""
super().destroy()
# Mark the Tk as not running.
self.is_running = False
async def rotator(self, 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(self, 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 self.is_running:
deg, color = deg_color(deg, d_per_tick, color)
canvas.itemconfigure(arc, extent=deg, fill=color)
await asyncio.sleep(interval)
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 main():
root = AsyncTk()
await asyncio.gather(root.async_loop(), root.rotator(1/60, 2))
if __name__ == "__main__":
asyncio.run(main())