blob: a66fbd6cb9885beca55571d295a2c24c77591435 [file] [log] [blame]
Georg Brandl2e081362008-05-17 19:04:04 +00001#
2# An Introduction to Tkinter
3#
4# Copyright (c) 1997 by Fredrik Lundh
5#
6# This copyright applies to Dialog, askinteger, askfloat and asktring
7#
8# fredrik@pythonware.com
9# http://www.pythonware.com
10#
11"""This modules handles dialog boxes.
Guido van Rossum990e6191996-06-17 17:14:46 +000012
Georg Brandl2e081362008-05-17 19:04:04 +000013It contains the following public symbols:
14
15SimpleDialog -- A simple but flexible modal dialog box
16
17Dialog -- a base class for dialogs
18
19askinteger -- get an integer from the user
20
21askfloat -- get a float from the user
22
23askstring -- get a string from the user
24"""
Guido van Rossum990e6191996-06-17 17:14:46 +000025
Georg Brandl14fc4272008-05-17 18:39:55 +000026from tkinter import *
Serhiy Storchaka675c97e2020-12-25 20:19:20 +020027from tkinter import _get_temp_root, _destroy_temp_root
28from tkinter import messagebox
Guido van Rossum990e6191996-06-17 17:14:46 +000029
Serhiy Storchakadc0d5712018-10-12 19:01:00 +030030
Guido van Rossum990e6191996-06-17 17:14:46 +000031class SimpleDialog:
32
33 def __init__(self, master,
Guido van Rossumc4570481998-03-20 20:45:49 +000034 text='', buttons=[], default=None, cancel=None,
35 title=None, class_=None):
36 if class_:
37 self.root = Toplevel(master, class_=class_)
38 else:
39 self.root = Toplevel(master)
40 if title:
41 self.root.title(title)
42 self.root.iconname(title)
43 self.message = Message(self.root, text=text, aspect=400)
44 self.message.pack(expand=1, fill=BOTH)
45 self.frame = Frame(self.root)
46 self.frame.pack()
47 self.num = default
48 self.cancel = cancel
49 self.default = default
50 self.root.bind('<Return>', self.return_event)
51 for num in range(len(buttons)):
52 s = buttons[num]
53 b = Button(self.frame, text=s,
54 command=(lambda self=self, num=num: self.done(num)))
55 if num == default:
56 b.config(relief=RIDGE, borderwidth=8)
57 b.pack(side=LEFT, fill=BOTH, expand=1)
58 self.root.protocol('WM_DELETE_WINDOW', self.wm_delete_window)
Serhiy Storchakac6c43b22020-12-24 20:26:28 +020059 self.root.transient(master)
60 _place_window(self.root, master)
Guido van Rossum990e6191996-06-17 17:14:46 +000061
62 def go(self):
Martin v. Löwisb217cd82004-08-03 18:36:25 +000063 self.root.wait_visibility()
Guido van Rossumc4570481998-03-20 20:45:49 +000064 self.root.grab_set()
65 self.root.mainloop()
66 self.root.destroy()
67 return self.num
Guido van Rossum990e6191996-06-17 17:14:46 +000068
69 def return_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +000070 if self.default is None:
71 self.root.bell()
72 else:
73 self.done(self.default)
Guido van Rossum990e6191996-06-17 17:14:46 +000074
75 def wm_delete_window(self):
Guido van Rossumc4570481998-03-20 20:45:49 +000076 if self.cancel is None:
77 self.root.bell()
78 else:
79 self.done(self.cancel)
Guido van Rossum990e6191996-06-17 17:14:46 +000080
81 def done(self, num):
Guido van Rossumc4570481998-03-20 20:45:49 +000082 self.num = num
83 self.root.quit()
Guido van Rossum990e6191996-06-17 17:14:46 +000084
85
Georg Brandl2e081362008-05-17 19:04:04 +000086class Dialog(Toplevel):
87
88 '''Class to open dialogs.
89
90 This class is intended as a base class for custom dialogs
91 '''
92
93 def __init__(self, parent, title = None):
Georg Brandl2e081362008-05-17 19:04:04 +000094 '''Initialize a dialog.
95
96 Arguments:
97
98 parent -- a parent window (the application window)
99
100 title -- the dialog title
101 '''
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200102 master = parent
Serhiy Storchakabb70b2a2020-12-25 17:04:26 +0200103 if master is None:
Serhiy Storchaka675c97e2020-12-25 20:19:20 +0200104 master = _get_temp_root()
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200105
106 Toplevel.__init__(self, master)
Georg Brandl2e081362008-05-17 19:04:04 +0000107
Guilherme Polof45b4cc2009-03-07 02:19:14 +0000108 self.withdraw() # remain invisible for now
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200109 # If the parent is not viewable, don't
Georg Brandl2e081362008-05-17 19:04:04 +0000110 # make the child transient, or else it
111 # would be opened withdrawn
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200112 if parent is not None and parent.winfo_viewable():
Georg Brandl2e081362008-05-17 19:04:04 +0000113 self.transient(parent)
114
115 if title:
116 self.title(title)
117
118 self.parent = parent
119
120 self.result = None
121
122 body = Frame(self)
123 self.initial_focus = self.body(body)
124 body.pack(padx=5, pady=5)
125
126 self.buttonbox()
127
Serhiy Storchakabb70b2a2020-12-25 17:04:26 +0200128 if self.initial_focus is None:
Georg Brandl2e081362008-05-17 19:04:04 +0000129 self.initial_focus = self
130
131 self.protocol("WM_DELETE_WINDOW", self.cancel)
132
Serhiy Storchakac6c43b22020-12-24 20:26:28 +0200133 _place_window(self, parent)
Guilherme Polof45b4cc2009-03-07 02:19:14 +0000134
Georg Brandl2e081362008-05-17 19:04:04 +0000135 self.initial_focus.focus_set()
136
Guilherme Polof45b4cc2009-03-07 02:19:14 +0000137 # wait for window to appear on screen before calling grab_set
138 self.wait_visibility()
139 self.grab_set()
Georg Brandl2e081362008-05-17 19:04:04 +0000140 self.wait_window(self)
141
142 def destroy(self):
143 '''Destroy the window'''
144 self.initial_focus = None
145 Toplevel.destroy(self)
Serhiy Storchaka675c97e2020-12-25 20:19:20 +0200146 _destroy_temp_root(self.master)
Georg Brandl2e081362008-05-17 19:04:04 +0000147
148 #
149 # construction hooks
150
151 def body(self, master):
152 '''create dialog body.
153
154 return widget that should have initial focus.
155 This method should be overridden, and is called
156 by the __init__ method.
157 '''
158 pass
159
160 def buttonbox(self):
161 '''add standard button box.
162
163 override if you do not want the standard buttons
164 '''
165
166 box = Frame(self)
167
168 w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
169 w.pack(side=LEFT, padx=5, pady=5)
170 w = Button(box, text="Cancel", width=10, command=self.cancel)
171 w.pack(side=LEFT, padx=5, pady=5)
172
173 self.bind("<Return>", self.ok)
174 self.bind("<Escape>", self.cancel)
175
176 box.pack()
177
178 #
179 # standard button semantics
180
181 def ok(self, event=None):
182
183 if not self.validate():
184 self.initial_focus.focus_set() # put focus back
185 return
186
187 self.withdraw()
188 self.update_idletasks()
189
190 try:
191 self.apply()
192 finally:
193 self.cancel()
194
195 def cancel(self, event=None):
196
197 # put focus back to the parent window
198 if self.parent is not None:
199 self.parent.focus_set()
200 self.destroy()
201
202 #
203 # command hooks
204
205 def validate(self):
206 '''validate the data
207
208 This method is called automatically to validate the data before the
209 dialog is destroyed. By default, it always validates OK.
210 '''
211
212 return 1 # override
213
214 def apply(self):
215 '''process the data
216
217 This method is called automatically to process the data, *after*
218 the dialog is destroyed. By default, it does nothing.
219 '''
220
221 pass # override
222
223
Serhiy Storchakac6c43b22020-12-24 20:26:28 +0200224# Place a toplevel window at the center of parent or screen
225# It is a Python implementation of ::tk::PlaceWindow.
226def _place_window(w, parent=None):
227 w.wm_withdraw() # Remain invisible while we figure out the geometry
228 w.update_idletasks() # Actualize geometry information
229
230 minwidth = w.winfo_reqwidth()
231 minheight = w.winfo_reqheight()
232 maxwidth = w.winfo_vrootwidth()
233 maxheight = w.winfo_vrootheight()
234 if parent is not None and parent.winfo_ismapped():
235 x = parent.winfo_rootx() + (parent.winfo_width() - minwidth) // 2
236 y = parent.winfo_rooty() + (parent.winfo_height() - minheight) // 2
237 vrootx = w.winfo_vrootx()
238 vrooty = w.winfo_vrooty()
239 x = min(x, vrootx + maxwidth - minwidth)
240 x = max(x, vrootx)
241 y = min(y, vrooty + maxheight - minheight)
242 y = max(y, vrooty)
243 if w._windowingsystem == 'aqua':
244 # Avoid the native menu bar which sits on top of everything.
245 y = max(y, 22)
246 else:
247 x = (w.winfo_screenwidth() - minwidth) // 2
248 y = (w.winfo_screenheight() - minheight) // 2
249
250 w.wm_maxsize(maxwidth, maxheight)
251 w.wm_geometry('+%d+%d' % (x, y))
252 w.wm_deiconify() # Become visible at the desired location
253
254
Georg Brandl2e081362008-05-17 19:04:04 +0000255# --------------------------------------------------------------------
256# convenience dialogues
257
258class _QueryDialog(Dialog):
259
260 def __init__(self, title, prompt,
261 initialvalue=None,
262 minvalue = None, maxvalue = None,
263 parent = None):
264
Georg Brandl2e081362008-05-17 19:04:04 +0000265 self.prompt = prompt
266 self.minvalue = minvalue
267 self.maxvalue = maxvalue
268
269 self.initialvalue = initialvalue
270
271 Dialog.__init__(self, parent, title)
272
273 def destroy(self):
274 self.entry = None
275 Dialog.destroy(self)
276
277 def body(self, master):
278
279 w = Label(master, text=self.prompt, justify=LEFT)
280 w.grid(row=0, padx=5, sticky=W)
281
282 self.entry = Entry(master, name="entry")
283 self.entry.grid(row=1, padx=5, sticky=W+E)
284
Andrew Svetlov1fb0e3f2012-07-30 19:59:53 +0300285 if self.initialvalue is not None:
Georg Brandl2e081362008-05-17 19:04:04 +0000286 self.entry.insert(0, self.initialvalue)
287 self.entry.select_range(0, END)
288
289 return self.entry
290
291 def validate(self):
292 try:
293 result = self.getresult()
294 except ValueError:
295 messagebox.showwarning(
296 "Illegal value",
297 self.errormessage + "\nPlease try again",
298 parent = self
299 )
300 return 0
301
302 if self.minvalue is not None and result < self.minvalue:
303 messagebox.showwarning(
304 "Too small",
305 "The allowed minimum value is %s. "
306 "Please try again." % self.minvalue,
307 parent = self
308 )
309 return 0
310
311 if self.maxvalue is not None and result > self.maxvalue:
312 messagebox.showwarning(
313 "Too large",
314 "The allowed maximum value is %s. "
315 "Please try again." % self.maxvalue,
316 parent = self
317 )
318 return 0
319
320 self.result = result
321
322 return 1
323
324
325class _QueryInteger(_QueryDialog):
326 errormessage = "Not an integer."
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300327
Georg Brandl2e081362008-05-17 19:04:04 +0000328 def getresult(self):
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300329 return self.getint(self.entry.get())
Georg Brandl2e081362008-05-17 19:04:04 +0000330
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300331
Georg Brandl2e081362008-05-17 19:04:04 +0000332def askinteger(title, prompt, **kw):
333 '''get an integer from the user
334
335 Arguments:
336
337 title -- the dialog title
338 prompt -- the label text
339 **kw -- see SimpleDialog class
340
341 Return value is an integer
342 '''
343 d = _QueryInteger(title, prompt, **kw)
344 return d.result
345
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300346
Georg Brandl2e081362008-05-17 19:04:04 +0000347class _QueryFloat(_QueryDialog):
348 errormessage = "Not a floating point value."
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300349
Georg Brandl2e081362008-05-17 19:04:04 +0000350 def getresult(self):
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300351 return self.getdouble(self.entry.get())
Georg Brandl2e081362008-05-17 19:04:04 +0000352
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300353
Georg Brandl2e081362008-05-17 19:04:04 +0000354def askfloat(title, prompt, **kw):
355 '''get a float from the user
356
357 Arguments:
358
359 title -- the dialog title
360 prompt -- the label text
361 **kw -- see SimpleDialog class
362
363 Return value is a float
364 '''
365 d = _QueryFloat(title, prompt, **kw)
366 return d.result
367
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300368
Georg Brandl2e081362008-05-17 19:04:04 +0000369class _QueryString(_QueryDialog):
370 def __init__(self, *args, **kw):
371 if "show" in kw:
372 self.__show = kw["show"]
373 del kw["show"]
374 else:
375 self.__show = None
376 _QueryDialog.__init__(self, *args, **kw)
377
378 def body(self, master):
379 entry = _QueryDialog.body(self, master)
380 if self.__show is not None:
381 entry.configure(show=self.__show)
382 return entry
383
384 def getresult(self):
385 return self.entry.get()
386
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300387
Georg Brandl2e081362008-05-17 19:04:04 +0000388def askstring(title, prompt, **kw):
389 '''get a string from the user
390
391 Arguments:
392
393 title -- the dialog title
394 prompt -- the label text
395 **kw -- see SimpleDialog class
396
397 Return value is a string
398 '''
399 d = _QueryString(title, prompt, **kw)
400 return d.result
401
402
Martin v. Löwisa3837a02004-03-22 21:49:47 +0000403if __name__ == '__main__':
404
405 def test():
406 root = Tk()
407 def doit(root=root):
408 d = SimpleDialog(root,
Guido van Rossumc4570481998-03-20 20:45:49 +0000409 text="This is a test dialog. "
410 "Would this have been an actual dialog, "
411 "the buttons below would have been glowing "
412 "in soft pink light.\n"
413 "Do you believe this?",
414 buttons=["Yes", "No", "Cancel"],
415 default=0,
416 cancel=2,
417 title="Test Dialog")
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000418 print(d.go())
Georg Brandl2e081362008-05-17 19:04:04 +0000419 print(askinteger("Spam", "Egg count", initialvalue=12*12))
420 print(askfloat("Spam", "Egg weight\n(in tons)", minvalue=1,
421 maxvalue=100))
422 print(askstring("Spam", "Egg label"))
Martin v. Löwisa3837a02004-03-22 21:49:47 +0000423 t = Button(root, text='Test', command=doit)
424 t.pack()
425 q = Button(root, text='Quit', command=t.quit)
426 q.pack()
427 t.mainloop()
Guido van Rossum990e6191996-06-17 17:14:46 +0000428
Guido van Rossum990e6191996-06-17 17:14:46 +0000429 test()