diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 8c4678b4b23c77..f0c61a13ec3f5e 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -1536,7 +1536,7 @@ Base and mixin classes :meth:`waitvar` is an alias of :meth:`!wait_variable`. - .. method:: wait_window(window=None) + .. method:: wait_window(window=None, *, timeout=None) Wait until *window* is destroyed, continuing to process events in the meantime. @@ -1544,6 +1544,13 @@ Base and mixin classes This is typically used to wait for the user to finish interacting with a dialog box. + If *timeout* is given, it is the maximum time to wait in seconds. + Return ``True`` if the widget was destroyed, or ``False`` if the timeout elapsed before that. + Without a *timeout* the call blocks until the widget is destroyed and always returns ``True``. + + .. versionchanged:: next + Added the *timeout* parameter and the return value. + .. method:: wait_visibility(window=None) Wait until the visibility state of *window* changes, for example when it diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 8407d0df325619..a8f12506960664 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -371,6 +371,11 @@ tkinter ttk version, and accepts mappings of button options as *buttons* entries. (Contributed by Serhiy Storchaka in :gh:`59396`.) +* The :meth:`!tkinter.Misc.wait_window` method now accepts an optional + keyword-only *timeout* argument that bounds how long it waits, instead of + blocking indefinitely. + (Contributed by Serhiy Storchaka in :gh:`100617`.) + xml --- diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index e6221f7089704d..1ed61223c75545 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -3,6 +3,7 @@ import platform import sys import textwrap +import time import unittest import weakref import tkinter @@ -608,9 +609,25 @@ def test_wait_variable(self): def test_wait_window(self): top = tkinter.Toplevel(self.root) self.root.after(1, top.destroy) - self.root.wait_window(top) # Returns once the window is destroyed. + # Returns once the window is destroyed. + self.assertIs(self.root.wait_window(top), True) self.assertFalse(top.winfo_exists()) + def test_wait_window_timeout(self): + # The window is destroyed before the timeout elapses. + top = tkinter.Toplevel(self.root) + self.root.after(1, top.destroy) + self.assertIs( + self.root.wait_window(top, timeout=support.SHORT_TIMEOUT), True) + self.assertFalse(top.winfo_exists()) + # The window outlives the timeout: give up instead of blocking. + top = tkinter.Toplevel(self.root) + start = time.monotonic() + self.assertIs(self.root.wait_window(top, timeout=0.2), False) + self.assertGreaterEqual(time.monotonic() - start, 0.2) + self.assertTrue(top.winfo_exists()) + top.destroy() + def test_tk_focusFollowsMouse(self): self.root.tk_focusFollowsMouse() # No exception. diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 83d11a7695ffbe..c04c5d9328d7dc 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -826,13 +826,43 @@ def wait_variable(self, name): self.tk.call('tkwait', 'variable', name) waitvar = wait_variable # XXX b/w compat - def wait_window(self, window=None): + def wait_window(self, window=None, *, timeout=None): """Wait until a WIDGET is destroyed. - If no parameter is given self is used.""" + If no parameter is given self is used. + + If timeout is given, it specifies the maximum time to wait in + seconds. Return True if the widget was destroyed, or False if the + timeout elapsed before that. Without a timeout the call blocks + until the widget is destroyed and always returns True.""" if window is None: window = self - self.tk.call('tkwait', 'window', window._w) + if timeout is None: + self.tk.call('tkwait', 'window', window._w) + return True + + if not window.winfo_exists(): + return True + timed_out = [] + # A one-shot timer guarantees that dooneevent() wakes up at the + # deadline even if no other event arrives. + timer = self.after(max(int(timeout * 1000), 1), + lambda: timed_out.append(True)) + try: + while not timed_out: + self.tk.dooneevent(_tkinter.ALL_EVENTS) + if not window.winfo_exists(): + return True + return False + except TclError: + # the widget or application was torn down + return True + finally: + # Cleanup may fail if the application was torn down. + try: + self.after_cancel(timer) + except TclError: + pass def wait_visibility(self, window=None): """Wait until the visibility of a WIDGET changes diff --git a/Misc/NEWS.d/next/Library/2026-06-29-11-40-32.gh-issue-100617.Wm4kQz.rst b/Misc/NEWS.d/next/Library/2026-06-29-11-40-32.gh-issue-100617.Wm4kQz.rst new file mode 100644 index 00000000000000..223ccc92285001 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-29-11-40-32.gh-issue-100617.Wm4kQz.rst @@ -0,0 +1,3 @@ +Add an optional *timeout* parameter to :meth:`tkinter.Misc.wait_window`. +If the widget is not destroyed within the given time, the call returns +``False`` instead of blocking indefinitely.