Skip to content

Commit 675c97e

Browse files
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 <[email protected]>
1 parent 586f3db commit 675c97e

File tree

7 files changed

+144
-24
lines changed

7 files changed

+144
-24
lines changed

Lib/tkinter/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,31 @@ def _get_default_root(what=None):
300300
return _default_root
301301

302302

303+
def _get_temp_root():
304+
global _support_default_root
305+
if not _support_default_root:
306+
raise RuntimeError("No master specified and tkinter is "
307+
"configured to not support default root")
308+
root = _default_root
309+
if root is None:
310+
assert _support_default_root
311+
_support_default_root = False
312+
root = Tk()
313+
_support_default_root = True
314+
assert _default_root is None
315+
root.withdraw()
316+
root._temporary = True
317+
return root
318+
319+
320+
def _destroy_temp_root(master):
321+
if getattr(master, '_temporary', False):
322+
try:
323+
master.destroy()
324+
except TclError:
325+
pass
326+
327+
303328
def _tkerror(err):
304329
"""Internal function."""
305330
pass

Lib/tkinter/commondialog.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
__all__ = ["Dialog"]
1212

13-
from tkinter import Frame
13+
from tkinter import Frame, _get_temp_root, _destroy_temp_root
1414

1515

1616
class Dialog:
@@ -37,22 +37,17 @@ def show(self, **options):
3737

3838
self._fixoptions()
3939

40-
# we need a dummy widget to properly process the options
41-
# (at least as long as we use Tkinter 1.63)
42-
w = Frame(self.master)
43-
40+
master = self.master
41+
if master is None:
42+
master = _get_temp_root()
4443
try:
45-
46-
s = w.tk.call(self.command, *w._options(self.options))
47-
48-
s = self._fixresult(w, s)
49-
44+
self._test_callback(master) # The function below is replaced for some tests.
45+
s = master.tk.call(self.command, *master._options(self.options))
46+
s = self._fixresult(master, s)
5047
finally:
51-
52-
try:
53-
# get rid of the widget
54-
w.destroy()
55-
except:
56-
pass
48+
_destroy_temp_root(master)
5749

5850
return s
51+
52+
def _test_callback(self, master):
53+
pass

Lib/tkinter/simpledialog.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"""
2525

2626
from tkinter import *
27-
from tkinter import messagebox, _get_default_root
27+
from tkinter import _get_temp_root, _destroy_temp_root
28+
from tkinter import messagebox
2829

2930

3031
class SimpleDialog:
@@ -100,7 +101,7 @@ def __init__(self, parent, title = None):
100101
'''
101102
master = parent
102103
if master is None:
103-
master = _get_default_root('create dialog window')
104+
master = _get_temp_root()
104105

105106
Toplevel.__init__(self, master)
106107

@@ -142,6 +143,7 @@ def destroy(self):
142143
'''Destroy the window'''
143144
self.initial_focus = None
144145
Toplevel.destroy(self)
146+
_destroy_temp_root(self.master)
145147

146148
#
147149
# construction hooks
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import unittest
2+
import tkinter
3+
from test.support import requires, run_unittest, swap_attr
4+
from tkinter.test.support import AbstractDefaultRootTest
5+
from tkinter.commondialog import Dialog
6+
from tkinter.colorchooser import askcolor
7+
8+
requires('gui')
9+
10+
11+
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
12+
13+
def test_askcolor(self):
14+
def test_callback(dialog, master):
15+
nonlocal ismapped
16+
master.update()
17+
ismapped = master.winfo_ismapped()
18+
raise ZeroDivisionError
19+
20+
with swap_attr(Dialog, '_test_callback', test_callback):
21+
ismapped = None
22+
self.assertRaises(ZeroDivisionError, askcolor)
23+
#askcolor()
24+
self.assertEqual(ismapped, False)
25+
26+
root = tkinter.Tk()
27+
ismapped = None
28+
self.assertRaises(ZeroDivisionError, askcolor)
29+
self.assertEqual(ismapped, True)
30+
root.destroy()
31+
32+
tkinter.NoDefaultRoot()
33+
self.assertRaises(RuntimeError, askcolor)
34+
35+
36+
tests_gui = (DefaultRootTest,)
37+
38+
if __name__ == "__main__":
39+
run_unittest(*tests_gui)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import unittest
2+
import tkinter
3+
from test.support import requires, run_unittest, swap_attr
4+
from tkinter.test.support import AbstractDefaultRootTest
5+
from tkinter.commondialog import Dialog
6+
from tkinter.messagebox import showinfo
7+
8+
requires('gui')
9+
10+
11+
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
12+
13+
def test_showinfo(self):
14+
def test_callback(dialog, master):
15+
nonlocal ismapped
16+
master.update()
17+
ismapped = master.winfo_ismapped()
18+
raise ZeroDivisionError
19+
20+
with swap_attr(Dialog, '_test_callback', test_callback):
21+
ismapped = None
22+
self.assertRaises(ZeroDivisionError, showinfo, "Spam", "Egg Information")
23+
self.assertEqual(ismapped, False)
24+
25+
root = tkinter.Tk()
26+
ismapped = None
27+
self.assertRaises(ZeroDivisionError, showinfo, "Spam", "Egg Information")
28+
self.assertEqual(ismapped, True)
29+
root.destroy()
30+
31+
tkinter.NoDefaultRoot()
32+
self.assertRaises(RuntimeError, showinfo, "Spam", "Egg Information")
33+
34+
35+
tests_gui = (DefaultRootTest,)
36+
37+
if __name__ == "__main__":
38+
run_unittest(*tests_gui)

Lib/tkinter/test/test_tkinter/test_simpledialog.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,25 @@
1010
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
1111

1212
def test_askinteger(self):
13-
self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
14-
root = tkinter.Tk()
15-
with swap_attr(Dialog, 'wait_window', lambda self, w: w.destroy()):
13+
@staticmethod
14+
def mock_wait_window(w):
15+
nonlocal ismapped
16+
ismapped = w.master.winfo_ismapped()
17+
w.destroy()
18+
19+
with swap_attr(Dialog, 'wait_window', mock_wait_window):
20+
ismapped = None
21+
askinteger("Go To Line", "Line number")
22+
self.assertEqual(ismapped, False)
23+
24+
root = tkinter.Tk()
25+
ismapped = None
1626
askinteger("Go To Line", "Line number")
17-
root.destroy()
18-
tkinter.NoDefaultRoot()
19-
self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
27+
self.assertEqual(ismapped, True)
28+
root.destroy()
29+
30+
tkinter.NoDefaultRoot()
31+
self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
2032

2133

2234
tests_gui = (DefaultRootTest,)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
When simple query dialogs (:mod:`tkinter.simpledialog`), message boxes
2+
(:mod:`tkinter.messagebox`) or color choose dialog
3+
(:mod:`tkinter.colorchooser`) are created without arguments *master* and
4+
*parent*, and the default root window is not yet created, and
5+
:func:`~tkinter.NoDefaultRoot` was not called, a new temporal
6+
hidden root window will be created automatically. It will not be set as the
7+
default root window and will be destroyed right after closing the dialog
8+
window. It will help to use these simple dialog windows in programs which
9+
do not need other GUI.

0 commit comments

Comments
 (0)