Last active
August 18, 2025 16:44
-
-
Save James-E-A/22438a80a6cb6998dc9d7f29113d5321 to your computer and use it in GitHub Desktop.
Python tkinter async mainloop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import asyncio, tkinter | |
| import ctypes | |
| import math | |
| import threading | |
| from types import coroutine | |
| __all__ = ["ATk"] | |
| if tkinter.TclVersion == 8.6: | |
| try: | |
| # Windows | |
| _tcl = ctypes.CDLL("tcl86t") | |
| _tk = ctypes.CDLL("tk86t") | |
| except FileNotFoundError: | |
| # Mac & Linux | |
| _tcl = ctypes.CDLL("libtcl8.6") # FIXME: actually test on Mac and/or Linux | |
| _tk = ctypes.CDLL("libtk8.6") # FIXME: actually test on Mac and/or Linux | |
| elif tkinter.TclVersion == 9.0: | |
| # FIXME: Tcl/Tk 9.0 added much better asynchronous APIs | |
| try: | |
| # Windows | |
| _tcl = ctypes.CDLL("tcl90t") | |
| _tk = ctypes.CDLL("tk90t") | |
| except FileNotFoundError: | |
| # Mac & Linux | |
| _tcl = ctypes.CDLL("libtcl9.0") # FIXME: actually test on Mac and/or Linux | |
| _tk = ctypes.CDLL("libtk9.0") # FIXME: actually test on Mac and/or Linux | |
| else: | |
| raise NotImplementedError(f"Tcl/Tk {tkinter.TclVersion} not supported yet") | |
| Tcl_WaitForEvent = _tcl["Tcl_WaitForEvent"] | |
| Tk_GetNumMainWindows = _tk["Tk_GetNumMainWindows"] | |
| class Tcl_Time(ctypes.Structure): | |
| _fields_ = [ | |
| ("sec", ctypes.c_long), | |
| ("usec", ctypes.c_int), | |
| ] | |
| @classmethod | |
| def fromseconds(cls, seconds): | |
| usec = int(math.ceil(seconds * 1_000_000)) | |
| sec, usec = divmod(usec, 1_000_000) | |
| assert 0 <= sec <= 2_147_483_647 | |
| assert 0 <= usec < 1_000_000 | |
| return cls(sec=sec, usec=usec) | |
| class ATk(tkinter.Tk): | |
| @coroutine | |
| def mainloop_async(self, *, _tcl8_MaxBlockTime=None): | |
| # https://www.tcl-lang.org/man/tcl8.6/TclLib/Notifier.htm#:~:text=event%20sources%20that%20need%20to%20poll%20for%20events,force%20the%20external%20event%20loop%20to%20call%20Tcl | |
| if _tcl8_MaxBlockTime is None: | |
| # WARNING: INFINITE BLOCK TIME HANGS NON-TK EVENTS WHENEVER USER IS IDLE | |
| timeout_ = ctypes.c_void_p(None) | |
| else: | |
| # WARNING: NONZERO BLOCK TIME STUTTERS NON-TK EVENTS WHENEVER USER IS IDLE | |
| # WARNING: ZERO BLOCK TIME WASTES A LOT OF CPU WITH AGGRESSIVE BUSY-POLL | |
| timeout = Tcl_Time.fromseconds(_tcl8_MaxBlockTime) | |
| timeout_ = ctypes.pointer(timeout) | |
| # https://github.com/python/cpython/blob/v3.14.0rc2/Modules/_tkinter.c#L2866-L2877 | |
| # https://github.com/tcltk/tk/blob/core-8-6-14/generic/tkEvent.c#L2108 | |
| # https://github.com/tcltk/tcl/blob/core-8-6-14/generic/tclNotify.c#L946 | |
| self_update = self.update | |
| result = Tcl_WaitForEvent(ctypes.pointer(Tcl_Time.fromseconds(0))) | |
| while result != -1 and Tk_GetNumMainWindows() > 0: | |
| if result > 0: | |
| self_update() | |
| yield | |
| result = Tcl_WaitForEvent(timeout_) | |
| def mainloop(self, coro=None): | |
| if coro is None: | |
| super().mainloop() | |
| return | |
| raise NotImplementedError("buddy coroutine support not implemented yet. If using Python 3.11+, consider manually combining ATk.mainloop_async with asyncio.TaskGroup") | |
| async def main(): | |
| # demo h/t https://github.com/python/cpython/blob/v3.14.0rc1/Lib/tkinter/__init__.py#L4972 | |
| import tkinter.ttk as ttk | |
| root = ATk() | |
| text = "This is Tcl/Tk %s" % root.globalgetvar('tk_patchLevel') | |
| text += "\nThis should be a cedilla: \u00e7" | |
| label = ttk.Label(root, text=text) | |
| label.pack() | |
| test = ttk.Button(root, text="Click me!", | |
| command=lambda root=root: root.test.configure( | |
| text="[%s]" % root.test['text'])) | |
| test.pack() | |
| root.test = test | |
| quit = ttk.Button(root, text="QUIT", command=root.destroy) | |
| quit.pack() | |
| root.iconify() | |
| root.update() | |
| root.deiconify() | |
| await root.mainloop_async() # <- WOW! | |
| if __name__ == '__main__': | |
| asyncio.run(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment