blob: 538bbfc318d704419884abdfe251872cf075ad33 [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)
Serhiy Storchaka3bb3fb32021-04-25 13:07:58 +030043
44 _setup_dialog(self.root)
45
Guido van Rossumc4570481998-03-20 20:45:49 +000046 self.message = Message(self.root, text=text, aspect=400)
47 self.message.pack(expand=1, fill=BOTH)
48 self.frame = Frame(self.root)
49 self.frame.pack()
50 self.num = default
51 self.cancel = cancel
52 self.default = default
53 self.root.bind('<Return>', self.return_event)
54 for num in range(len(buttons)):
55 s = buttons[num]
56 b = Button(self.frame, text=s,
57 command=(lambda self=self, num=num: self.done(num)))
58 if num == default:
59 b.config(relief=RIDGE, borderwidth=8)
60 b.pack(side=LEFT, fill=BOTH, expand=1)
61 self.root.protocol('WM_DELETE_WINDOW', self.wm_delete_window)
Serhiy Storchakac6c43b22020-12-24 20:26:28 +020062 self.root.transient(master)
63 _place_window(self.root, master)
Guido van Rossum990e6191996-06-17 17:14:46 +000064
65 def go(self):
Martin v. Löwisb217cd82004-08-03 18:36:25 +000066 self.root.wait_visibility()
Guido van Rossumc4570481998-03-20 20:45:49 +000067 self.root.grab_set()
68 self.root.mainloop()
69 self.root.destroy()
70 return self.num
Guido van Rossum990e6191996-06-17 17:14:46 +000071
72 def return_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +000073 if self.default is None:
74 self.root.bell()
75 else:
76 self.done(self.default)
Guido van Rossum990e6191996-06-17 17:14:46 +000077
78 def wm_delete_window(self):
Guido van Rossumc4570481998-03-20 20:45:49 +000079 if self.cancel is None:
80 self.root.bell()
81 else:
82 self.done(self.cancel)
Guido van Rossum990e6191996-06-17 17:14:46 +000083
84 def done(self, num):
Guido van Rossumc4570481998-03-20 20:45:49 +000085 self.num = num
86 self.root.quit()
Guido van Rossum990e6191996-06-17 17:14:46 +000087
88
Georg Brandl2e081362008-05-17 19:04:04 +000089class Dialog(Toplevel):
90
91 '''Class to open dialogs.
92
93 This class is intended as a base class for custom dialogs
94 '''
95
96 def __init__(self, parent, title = None):
Georg Brandl2e081362008-05-17 19:04:04 +000097 '''Initialize a dialog.
98
99 Arguments:
100
101 parent -- a parent window (the application window)
102
103 title -- the dialog title
104 '''
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200105 master = parent
Serhiy Storchakabb70b2a2020-12-25 17:04:26 +0200106 if master is None:
Serhiy Storchaka675c97e2020-12-25 20:19:20 +0200107 master = _get_temp_root()
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200108
109 Toplevel.__init__(self, master)
Georg Brandl2e081362008-05-17 19:04:04 +0000110
Guilherme Polof45b4cc2009-03-07 02:19:14 +0000111 self.withdraw() # remain invisible for now
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200112 # If the parent is not viewable, don't
Georg Brandl2e081362008-05-17 19:04:04 +0000113 # make the child transient, or else it
114 # would be opened withdrawn
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200115 if parent is not None and parent.winfo_viewable():
Georg Brandl2e081362008-05-17 19:04:04 +0000116 self.transient(parent)
117
118 if title:
119 self.title(title)
120
Serhiy Storchaka3bb3fb32021-04-25 13:07:58 +0300121 _setup_dialog(self)
122
Georg Brandl2e081362008-05-17 19:04:04 +0000123 self.parent = parent
124
125 self.result = None
126
127 body = Frame(self)
128 self.initial_focus = self.body(body)
129 body.pack(padx=5, pady=5)
130
131 self.buttonbox()
132
Serhiy Storchakabb70b2a2020-12-25 17:04:26 +0200133 if self.initial_focus is None:
Georg Brandl2e081362008-05-17 19:04:04 +0000134 self.initial_focus = self
135
136 self.protocol("WM_DELETE_WINDOW", self.cancel)
137
Serhiy Storchakac6c43b22020-12-24 20:26:28 +0200138 _place_window(self, parent)
Guilherme Polof45b4cc2009-03-07 02:19:14 +0000139
Georg Brandl2e081362008-05-17 19:04:04 +0000140 self.initial_focus.focus_set()
141
Guilherme Polof45b4cc2009-03-07 02:19:14 +0000142 # wait for window to appear on screen before calling grab_set
143 self.wait_visibility()
144 self.grab_set()
Georg Brandl2e081362008-05-17 19:04:04 +0000145 self.wait_window(self)
146
147 def destroy(self):
148 '''Destroy the window'''
149 self.initial_focus = None
150 Toplevel.destroy(self)
Serhiy Storchaka675c97e2020-12-25 20:19:20 +0200151 _destroy_temp_root(self.master)
Georg Brandl2e081362008-05-17 19:04:04 +0000152
153 #
154 # construction hooks
155
156 def body(self, master):
157 '''create dialog body.
158
159 return widget that should have initial focus.
160 This method should be overridden, and is called
161 by the __init__ method.
162 '''
163 pass
164
165 def buttonbox(self):
166 '''add standard button box.
167
168 override if you do not want the standard buttons
169 '''
170
171 box = Frame(self)
172
173 w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
174 w.pack(side=LEFT, padx=5, pady=5)
175 w = Button(box, text="Cancel", width=10, command=self.cancel)
176 w.pack(side=LEFT, padx=5, pady=5)
177
178 self.bind("<Return>", self.ok)
179 self.bind("<Escape>", self.cancel)
180
181 box.pack()
182
183 #
184 # standard button semantics
185
186 def ok(self, event=None):
187
188 if not self.validate():
189 self.initial_focus.focus_set() # put focus back
190 return
191
192 self.withdraw()
193 self.update_idletasks()
194
195 try:
196 self.apply()
197 finally:
198 self.cancel()
199
200 def cancel(self, event=None):
201
202 # put focus back to the parent window
203 if self.parent is not None:
204 self.parent.focus_set()
205 self.destroy()
206
207 #
208 # command hooks
209
210 def validate(self):
211 '''validate the data
212
213 This method is called automatically to validate the data before the
214 dialog is destroyed. By default, it always validates OK.
215 '''
216
217 return 1 # override
218
219 def apply(self):
220 '''process the data
221
222 This method is called automatically to process the data, *after*
223 the dialog is destroyed. By default, it does nothing.
224 '''
225
226 pass # override
227
228
Serhiy Storchakac6c43b22020-12-24 20:26:28 +0200229# Place a toplevel window at the center of parent or screen
230# It is a Python implementation of ::tk::PlaceWindow.
231def _place_window(w, parent=None):
232 w.wm_withdraw() # Remain invisible while we figure out the geometry
233 w.update_idletasks() # Actualize geometry information
234
235 minwidth = w.winfo_reqwidth()
236 minheight = w.winfo_reqheight()
237 maxwidth = w.winfo_vrootwidth()
238 maxheight = w.winfo_vrootheight()
239 if parent is not None and parent.winfo_ismapped():
240 x = parent.winfo_rootx() + (parent.winfo_width() - minwidth) // 2
241 y = parent.winfo_rooty() + (parent.winfo_height() - minheight) // 2
242 vrootx = w.winfo_vrootx()
243 vrooty = w.winfo_vrooty()
244 x = min(x, vrootx + maxwidth - minwidth)
245 x = max(x, vrootx)
246 y = min(y, vrooty + maxheight - minheight)
247 y = max(y, vrooty)
248 if w._windowingsystem == 'aqua':
249 # Avoid the native menu bar which sits on top of everything.
250 y = max(y, 22)
251 else:
252 x = (w.winfo_screenwidth() - minwidth) // 2
253 y = (w.winfo_screenheight() - minheight) // 2
254
255 w.wm_maxsize(maxwidth, maxheight)
256 w.wm_geometry('+%d+%d' % (x, y))
257 w.wm_deiconify() # Become visible at the desired location
258
259
Serhiy Storchaka3bb3fb32021-04-25 13:07:58 +0300260def _setup_dialog(w):
261 if w._windowingsystem == "aqua":
262 w.tk.call("::tk::unsupported::MacWindowStyle", "style",
263 w, "moveableModal", "")
264 elif w._windowingsystem == "x11":
265 w.wm_attributes("-type", "dialog")
266
Georg Brandl2e081362008-05-17 19:04:04 +0000267# --------------------------------------------------------------------
268# convenience dialogues
269
270class _QueryDialog(Dialog):
271
272 def __init__(self, title, prompt,
273 initialvalue=None,
274 minvalue = None, maxvalue = None,
275 parent = None):
276
Georg Brandl2e081362008-05-17 19:04:04 +0000277 self.prompt = prompt
278 self.minvalue = minvalue
279 self.maxvalue = maxvalue
280
281 self.initialvalue = initialvalue
282
283 Dialog.__init__(self, parent, title)
284
285 def destroy(self):
286 self.entry = None
287 Dialog.destroy(self)
288
289 def body(self, master):
290
291 w = Label(master, text=self.prompt, justify=LEFT)
292 w.grid(row=0, padx=5, sticky=W)
293
294 self.entry = Entry(master, name="entry")
295 self.entry.grid(row=1, padx=5, sticky=W+E)
296
Andrew Svetlov1fb0e3f2012-07-30 19:59:53 +0300297 if self.initialvalue is not None:
Georg Brandl2e081362008-05-17 19:04:04 +0000298 self.entry.insert(0, self.initialvalue)
299 self.entry.select_range(0, END)
300
301 return self.entry
302
303 def validate(self):
304 try:
305 result = self.getresult()
306 except ValueError:
307 messagebox.showwarning(
308 "Illegal value",
309 self.errormessage + "\nPlease try again",
310 parent = self
311 )
312 return 0
313
314 if self.minvalue is not None and result < self.minvalue:
315 messagebox.showwarning(
316 "Too small",
317 "The allowed minimum value is %s. "
318 "Please try again." % self.minvalue,
319 parent = self
320 )
321 return 0
322
323 if self.maxvalue is not None and result > self.maxvalue:
324 messagebox.showwarning(
325 "Too large",
326 "The allowed maximum value is %s. "
327 "Please try again." % self.maxvalue,
328 parent = self
329 )
330 return 0
331
332 self.result = result
333
334 return 1
335
336
337class _QueryInteger(_QueryDialog):
338 errormessage = "Not an integer."
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300339
Georg Brandl2e081362008-05-17 19:04:04 +0000340 def getresult(self):
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300341 return self.getint(self.entry.get())
Georg Brandl2e081362008-05-17 19:04:04 +0000342
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300343
Georg Brandl2e081362008-05-17 19:04:04 +0000344def askinteger(title, prompt, **kw):
345 '''get an integer from the user
346
347 Arguments:
348
349 title -- the dialog title
350 prompt -- the label text
351 **kw -- see SimpleDialog class
352
353 Return value is an integer
354 '''
355 d = _QueryInteger(title, prompt, **kw)
356 return d.result
357
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300358
Georg Brandl2e081362008-05-17 19:04:04 +0000359class _QueryFloat(_QueryDialog):
360 errormessage = "Not a floating point value."
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300361
Georg Brandl2e081362008-05-17 19:04:04 +0000362 def getresult(self):
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300363 return self.getdouble(self.entry.get())
Georg Brandl2e081362008-05-17 19:04:04 +0000364
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300365
Georg Brandl2e081362008-05-17 19:04:04 +0000366def askfloat(title, prompt, **kw):
367 '''get a float from the user
368
369 Arguments:
370
371 title -- the dialog title
372 prompt -- the label text
373 **kw -- see SimpleDialog class
374
375 Return value is a float
376 '''
377 d = _QueryFloat(title, prompt, **kw)
378 return d.result
379
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300380
Georg Brandl2e081362008-05-17 19:04:04 +0000381class _QueryString(_QueryDialog):
382 def __init__(self, *args, **kw):
383 if "show" in kw:
384 self.__show = kw["show"]
385 del kw["show"]
386 else:
387 self.__show = None
388 _QueryDialog.__init__(self, *args, **kw)
389
390 def body(self, master):
391 entry = _QueryDialog.body(self, master)
392 if self.__show is not None:
393 entry.configure(show=self.__show)
394 return entry
395
396 def getresult(self):
397 return self.entry.get()
398
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300399
Georg Brandl2e081362008-05-17 19:04:04 +0000400def askstring(title, prompt, **kw):
401 '''get a string from the user
402
403 Arguments:
404
405 title -- the dialog title
406 prompt -- the label text
407 **kw -- see SimpleDialog class
408
409 Return value is a string
410 '''
411 d = _QueryString(title, prompt, **kw)
412 return d.result
413
414
Martin v. Löwisa3837a02004-03-22 21:49:47 +0000415if __name__ == '__main__':
416
417 def test():
418 root = Tk()
419 def doit(root=root):
420 d = SimpleDialog(root,
Guido van Rossumc4570481998-03-20 20:45:49 +0000421 text="This is a test dialog. "
422 "Would this have been an actual dialog, "
423 "the buttons below would have been glowing "
424 "in soft pink light.\n"
425 "Do you believe this?",
426 buttons=["Yes", "No", "Cancel"],
427 default=0,
428 cancel=2,
429 title="Test Dialog")
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000430 print(d.go())
Georg Brandl2e081362008-05-17 19:04:04 +0000431 print(askinteger("Spam", "Egg count", initialvalue=12*12))
432 print(askfloat("Spam", "Egg weight\n(in tons)", minvalue=1,
433 maxvalue=100))
434 print(askstring("Spam", "Egg label"))
Martin v. Löwisa3837a02004-03-22 21:49:47 +0000435 t = Button(root, text='Test', command=doit)
436 t.pack()
437 q = Button(root, text='Quit', command=t.quit)
438 q.pack()
439 t.mainloop()
Guido van Rossum990e6191996-06-17 17:14:46 +0000440
Guido van Rossum990e6191996-06-17 17:14:46 +0000441 test()