bpo-42721: Improve using simple dialogs without root window (GH-23897)

When simple query dialogs (tkinter.simpledialog), message boxes
(tkinter.messagebox) or color choose dialog (tkinter.colorchooser)
are created without arguments master and parent, and the default
root window is not yet created, a new temporary hidden root window
will be created automatically. It will not be set as the default root
window and will be destroyed right after closing the dialog window.
It will help to use these simple dialog windows in programs which do
not need other GUI.

Previously, message boxes and color chooser created the blank root
window and left it after closing the dialog window, and query dialogs
just raised an exception.

Co-authored-by: Terry Jan Reedy <tjreedy@udel.edu>
This commit is contained in:
Serhiy Storchaka 2020-12-25 20:19:20 +02:00 committed by GitHub
parent 586f3dbe15
commit 675c97eb6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 144 additions and 24 deletions

View File

@ -300,6 +300,31 @@ def _get_default_root(what=None):
return _default_root return _default_root
def _get_temp_root():
global _support_default_root
if not _support_default_root:
raise RuntimeError("No master specified and tkinter is "
"configured to not support default root")
root = _default_root
if root is None:
assert _support_default_root
_support_default_root = False
root = Tk()
_support_default_root = True
assert _default_root is None
root.withdraw()
root._temporary = True
return root
def _destroy_temp_root(master):
if getattr(master, '_temporary', False):
try:
master.destroy()
except TclError:
pass
def _tkerror(err): def _tkerror(err):
"""Internal function.""" """Internal function."""
pass pass

View File

@ -10,7 +10,7 @@
__all__ = ["Dialog"] __all__ = ["Dialog"]
from tkinter import Frame from tkinter import Frame, _get_temp_root, _destroy_temp_root
class Dialog: class Dialog:
@ -37,22 +37,17 @@ class Dialog:
self._fixoptions() self._fixoptions()
# we need a dummy widget to properly process the options master = self.master
# (at least as long as we use Tkinter 1.63) if master is None:
w = Frame(self.master) master = _get_temp_root()
try: try:
self._test_callback(master) # The function below is replaced for some tests.
s = w.tk.call(self.command, *w._options(self.options)) s = master.tk.call(self.command, *master._options(self.options))
s = self._fixresult(master, s)
s = self._fixresult(w, s)
finally: finally:
_destroy_temp_root(master)
try:
# get rid of the widget
w.destroy()
except:
pass
return s return s
def _test_callback(self, master):
pass

View File

@ -24,7 +24,8 @@ askstring -- get a string from the user
""" """
from tkinter import * from tkinter import *
from tkinter import messagebox, _get_default_root from tkinter import _get_temp_root, _destroy_temp_root
from tkinter import messagebox
class SimpleDialog: class SimpleDialog:
@ -100,7 +101,7 @@ class Dialog(Toplevel):
''' '''
master = parent master = parent
if master is None: if master is None:
master = _get_default_root('create dialog window') master = _get_temp_root()
Toplevel.__init__(self, master) Toplevel.__init__(self, master)
@ -142,6 +143,7 @@ class Dialog(Toplevel):
'''Destroy the window''' '''Destroy the window'''
self.initial_focus = None self.initial_focus = None
Toplevel.destroy(self) Toplevel.destroy(self)
_destroy_temp_root(self.master)
# #
# construction hooks # construction hooks

View File

@ -0,0 +1,39 @@
import unittest
import tkinter
from test.support import requires, run_unittest, swap_attr
from tkinter.test.support import AbstractDefaultRootTest
from tkinter.commondialog import Dialog
from tkinter.colorchooser import askcolor
requires('gui')
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
def test_askcolor(self):
def test_callback(dialog, master):
nonlocal ismapped
master.update()
ismapped = master.winfo_ismapped()
raise ZeroDivisionError
with swap_attr(Dialog, '_test_callback', test_callback):
ismapped = None
self.assertRaises(ZeroDivisionError, askcolor)
#askcolor()
self.assertEqual(ismapped, False)
root = tkinter.Tk()
ismapped = None
self.assertRaises(ZeroDivisionError, askcolor)
self.assertEqual(ismapped, True)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, askcolor)
tests_gui = (DefaultRootTest,)
if __name__ == "__main__":
run_unittest(*tests_gui)

View File

@ -0,0 +1,38 @@
import unittest
import tkinter
from test.support import requires, run_unittest, swap_attr
from tkinter.test.support import AbstractDefaultRootTest
from tkinter.commondialog import Dialog
from tkinter.messagebox import showinfo
requires('gui')
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
def test_showinfo(self):
def test_callback(dialog, master):
nonlocal ismapped
master.update()
ismapped = master.winfo_ismapped()
raise ZeroDivisionError
with swap_attr(Dialog, '_test_callback', test_callback):
ismapped = None
self.assertRaises(ZeroDivisionError, showinfo, "Spam", "Egg Information")
self.assertEqual(ismapped, False)
root = tkinter.Tk()
ismapped = None
self.assertRaises(ZeroDivisionError, showinfo, "Spam", "Egg Information")
self.assertEqual(ismapped, True)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, showinfo, "Spam", "Egg Information")
tests_gui = (DefaultRootTest,)
if __name__ == "__main__":
run_unittest(*tests_gui)

View File

@ -10,13 +10,25 @@ requires('gui')
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
def test_askinteger(self): def test_askinteger(self):
self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number") @staticmethod
root = tkinter.Tk() def mock_wait_window(w):
with swap_attr(Dialog, 'wait_window', lambda self, w: w.destroy()): nonlocal ismapped
ismapped = w.master.winfo_ismapped()
w.destroy()
with swap_attr(Dialog, 'wait_window', mock_wait_window):
ismapped = None
askinteger("Go To Line", "Line number") askinteger("Go To Line", "Line number")
root.destroy() self.assertEqual(ismapped, False)
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number") root = tkinter.Tk()
ismapped = None
askinteger("Go To Line", "Line number")
self.assertEqual(ismapped, True)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
tests_gui = (DefaultRootTest,) tests_gui = (DefaultRootTest,)

View File

@ -0,0 +1,9 @@
When simple query dialogs (:mod:`tkinter.simpledialog`), message boxes
(:mod:`tkinter.messagebox`) or color choose dialog
(:mod:`tkinter.colorchooser`) are created without arguments *master* and
*parent*, and the default root window is not yet created, and
:func:`~tkinter.NoDefaultRoot` was not called, a new temporal
hidden root window will be created automatically. It will not be set as the
default root window and will be destroyed right after closing the dialog
window. It will help to use these simple dialog windows in programs which
do not need other GUI.