Use asyncio and Tkinter (or another GUI lib) together without freezing the GUI Use asyncio and Tkinter (or another GUI lib) together without freezing the GUI tkinter tkinter

Use asyncio and Tkinter (or another GUI lib) together without freezing the GUI


Trying to run both event loops at the same time is a dubious proposition. However, since root.mainloop simply calls root.update repeatedly, one can simulate mainloop by calling update repeatedly as an asyncio task. Here is a test program that does so. I presume adding asyncio tasks to the tkinter tasks would work. I checked that it still runs with 3.7.0a2.

"""Proof of concept: integrate tkinter, asyncio and async iterator.Terry Jan Reedy, 2016 July 25"""import asynciofrom random import randrange as rrimport tkinter as tkclass App(tk.Tk):    def __init__(self, loop, interval=1/120):        super().__init__()        self.loop = loop        self.protocol("WM_DELETE_WINDOW", self.close)        self.tasks = []        self.tasks.append(loop.create_task(self.rotator(1/60, 2)))        self.tasks.append(loop.create_task(self.updater(interval)))    async def rotator(self, interval, d_per_tick):        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 await asyncio.sleep(interval, True):            deg, color = deg_color(deg, d_per_tick, color)            canvas.itemconfigure(arc, extent=deg, fill=color)    async def updater(self, interval):        while True:            self.update()            await asyncio.sleep(interval)    def close(self):        for task in self.tasks:            task.cancel()        self.loop.stop()        self.destroy()def deg_color(deg, d_per_tick, color):    deg += d_per_tick    if 360 <= deg:        deg %= 360        color = '#%02x%02x%02x' % (rr(0, 256), rr(0, 256), rr(0, 256))    return deg, colorloop = asyncio.get_event_loop()app = App(loop)loop.run_forever()loop.close()

Both the tk update overhead and time resolution increase as the interval is decreased. For gui updates, as opposed to animations, 20 per second may be enough.

I recently succeeded in running async def coroutines containing tkinter calls and awaits with mainloop. The prototype uses asyncio Tasks and Futures, but I don't know if adding normal asyncio tasks would work. If one wants to run asyncio and tkinter tasks together, I think running tk update with an asyncio loop is a better idea.

EDIT: A least as used above, exception without async def coroutines kill the coroutine but are somewhere caught and discarded. Silent error are pretty obnoxious.


In a slight modification to your code, I created the asyncio event_loop in the main thread and passed it as an argument to the asyncio thread. Now Tkinter won't freeze while the urls are fetched.

from tkinter import *from tkinter import messageboximport asyncioimport threadingimport randomdef _asyncio_thread(async_loop):    async_loop.run_until_complete(do_urls())def do_tasks(async_loop):    """ Button-Event-Handler starting the asyncio part. """    threading.Thread(target=_asyncio_thread, args=(async_loop,)).start()    async def one_url(url):    """ One task. """    sec = random.randint(1, 8)    await asyncio.sleep(sec)    return 'url: {}\tsec: {}'.format(url, sec)async def do_urls():    """ Creating and starting 10 tasks. """    tasks = [one_url(url) for url in range(10)]    completed, pending = await asyncio.wait(tasks)    results = [task.result() for task in completed]    print('\n'.join(results))def do_freezed():    messagebox.showinfo(message='Tkinter is reacting.')def main(async_loop):    root = Tk()    Button(master=root, text='Asyncio Tasks', command= lambda:do_tasks(async_loop)).pack()    Button(master=root, text='Freezed???', command=do_freezed).pack()    root.mainloop()if __name__ == '__main__':    async_loop = asyncio.get_event_loop()    main(async_loop)


I'm a bit late to the party but if you are not targeting Windows you can use aiotkinter to achieve what you want. I modified your code to show you how to use this package:

from tkinter import *from tkinter import messageboximport asyncioimport randomimport aiotkinterdef do_freezed():    """ Button-Event-Handler to see if a button on GUI works. """    messagebox.showinfo(message='Tkinter is reacting.')def do_tasks():    task = asyncio.ensure_future(do_urls())    task.add_done_callback(tasks_done)def tasks_done(task):    messagebox.showinfo(message='Tasks done.')async def one_url(url):    """ One task. """    sec = random.randint(1, 15)    await asyncio.sleep(sec)    return 'url: {}\tsec: {}'.format(url, sec)async def do_urls():    """ Creating and starting 10 tasks. """    tasks = [        one_url(url)        for url in range(10)    ]    completed, pending = await asyncio.wait(tasks)    results = [task.result() for task in completed]    print('\n'.join(results))if __name__ == '__main__':    asyncio.set_event_loop_policy(aiotkinter.TkinterEventLoopPolicy())    loop = asyncio.get_event_loop()    root = Tk()    buttonT = Button(master=root, text='Asyncio Tasks', command=do_tasks)    buttonT.pack()    buttonX = Button(master=root, text='Freezed???', command=do_freezed)    buttonX.pack()    loop.run_forever()