blob: b882d47c961bdbf889d67273e12b5969a86a639c [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 Storchaka3d569fd2020-12-19 12:17:08 +020027from tkinter import messagebox, _get_default_root
Guido van Rossum990e6191996-06-17 17:14:46 +000028
Serhiy Storchakadc0d5712018-10-12 19:01:00 +030029
Guido van Rossum990e6191996-06-17 17:14:46 +000030class SimpleDialog:
31
32 def __init__(self, master,
Guido van Rossumc4570481998-03-20 20:45:49 +000033 text='', buttons=[], default=None, cancel=None,
34 title=None, class_=None):
35 if class_:
36 self.root = Toplevel(master, class_=class_)
37 else:
38 self.root = Toplevel(master)
39 if title:
40 self.root.title(title)
41 self.root.iconname(title)
42 self.message = Message(self.root, text=text, aspect=400)
43 self.message.pack(expand=1, fill=BOTH)
44 self.frame = Frame(self.root)
45 self.frame.pack()
46 self.num = default
47 self.cancel = cancel
48 self.default = default
49 self.root.bind('<Return>', self.return_event)
50 for num in range(len(buttons)):
51 s = buttons[num]
52 b = Button(self.frame, text=s,
53 command=(lambda self=self, num=num: self.done(num)))
54 if num == default:
55 b.config(relief=RIDGE, borderwidth=8)
56 b.pack(side=LEFT, fill=BOTH, expand=1)
57 self.root.protocol('WM_DELETE_WINDOW', self.wm_delete_window)
58 self._set_transient(master)
Guido van Rossum17f2b2d1996-07-21 02:20:06 +000059
60 def _set_transient(self, master, relx=0.5, rely=0.3):
Guido van Rossumc4570481998-03-20 20:45:49 +000061 widget = self.root
62 widget.withdraw() # Remain invisible while we figure out the geometry
63 widget.transient(master)
64 widget.update_idletasks() # Actualize geometry information
65 if master.winfo_ismapped():
66 m_width = master.winfo_width()
67 m_height = master.winfo_height()
68 m_x = master.winfo_rootx()
69 m_y = master.winfo_rooty()
70 else:
71 m_width = master.winfo_screenwidth()
72 m_height = master.winfo_screenheight()
73 m_x = m_y = 0
74 w_width = widget.winfo_reqwidth()
75 w_height = widget.winfo_reqheight()
76 x = m_x + (m_width - w_width) * relx
77 y = m_y + (m_height - w_height) * rely
78 if x+w_width > master.winfo_screenwidth():
79 x = master.winfo_screenwidth() - w_width
80 elif x < 0:
81 x = 0
82 if y+w_height > master.winfo_screenheight():
83 y = master.winfo_screenheight() - w_height
84 elif y < 0:
85 y = 0
86 widget.geometry("+%d+%d" % (x, y))
87 widget.deiconify() # Become visible at the desired location
Guido van Rossum990e6191996-06-17 17:14:46 +000088
89 def go(self):
Martin v. Löwisb217cd82004-08-03 18:36:25 +000090 self.root.wait_visibility()
Guido van Rossumc4570481998-03-20 20:45:49 +000091 self.root.grab_set()
92 self.root.mainloop()
93 self.root.destroy()
94 return self.num
Guido van Rossum990e6191996-06-17 17:14:46 +000095
96 def return_event(self, event):
Guido van Rossumc4570481998-03-20 20:45:49 +000097 if self.default is None:
98 self.root.bell()
99 else:
100 self.done(self.default)
Guido van Rossum990e6191996-06-17 17:14:46 +0000101
102 def wm_delete_window(self):
Guido van Rossumc4570481998-03-20 20:45:49 +0000103 if self.cancel is None:
104 self.root.bell()
105 else:
106 self.done(self.cancel)
Guido van Rossum990e6191996-06-17 17:14:46 +0000107
108 def done(self, num):
Guido van Rossumc4570481998-03-20 20:45:49 +0000109 self.num = num
110 self.root.quit()
Guido van Rossum990e6191996-06-17 17:14:46 +0000111
112
Georg Brandl2e081362008-05-17 19:04:04 +0000113class Dialog(Toplevel):
114
115 '''Class to open dialogs.
116
117 This class is intended as a base class for custom dialogs
118 '''
119
120 def __init__(self, parent, title = None):
Georg Brandl2e081362008-05-17 19:04:04 +0000121 '''Initialize a dialog.
122
123 Arguments:
124
125 parent -- a parent window (the application window)
126
127 title -- the dialog title
128 '''
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200129 master = parent
130 if not master:
131 master = _get_default_root('create dialog window')
132
133 Toplevel.__init__(self, master)
Georg Brandl2e081362008-05-17 19:04:04 +0000134
Guilherme Polof45b4cc2009-03-07 02:19:14 +0000135 self.withdraw() # remain invisible for now
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200136 # If the parent is not viewable, don't
Georg Brandl2e081362008-05-17 19:04:04 +0000137 # make the child transient, or else it
138 # would be opened withdrawn
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200139 if parent is not None and parent.winfo_viewable():
Georg Brandl2e081362008-05-17 19:04:04 +0000140 self.transient(parent)
141
142 if title:
143 self.title(title)
144
145 self.parent = parent
146
147 self.result = None
148
149 body = Frame(self)
150 self.initial_focus = self.body(body)
151 body.pack(padx=5, pady=5)
152
153 self.buttonbox()
154
Georg Brandl2e081362008-05-17 19:04:04 +0000155 if not self.initial_focus:
156 self.initial_focus = self
157
158 self.protocol("WM_DELETE_WINDOW", self.cancel)
159
Serhiy Storchaka3d569fd2020-12-19 12:17:08 +0200160 if parent is not None:
Georg Brandl2e081362008-05-17 19:04:04 +0000161 self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
162 parent.winfo_rooty()+50))
163
Guilherme Polof45b4cc2009-03-07 02:19:14 +0000164 self.deiconify() # become visible now
165
Georg Brandl2e081362008-05-17 19:04:04 +0000166 self.initial_focus.focus_set()
167
Guilherme Polof45b4cc2009-03-07 02:19:14 +0000168 # wait for window to appear on screen before calling grab_set
169 self.wait_visibility()
170 self.grab_set()
Georg Brandl2e081362008-05-17 19:04:04 +0000171 self.wait_window(self)
172
173 def destroy(self):
174 '''Destroy the window'''
175 self.initial_focus = None
176 Toplevel.destroy(self)
177
178 #
179 # construction hooks
180
181 def body(self, master):
182 '''create dialog body.
183
184 return widget that should have initial focus.
185 This method should be overridden, and is called
186 by the __init__ method.
187 '''
188 pass
189
190 def buttonbox(self):
191 '''add standard button box.
192
193 override if you do not want the standard buttons
194 '''
195
196 box = Frame(self)
197
198 w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
199 w.pack(side=LEFT, padx=5, pady=5)
200 w = Button(box, text="Cancel", width=10, command=self.cancel)
201 w.pack(side=LEFT, padx=5, pady=5)
202
203 self.bind("<Return>", self.ok)
204 self.bind("<Escape>", self.cancel)
205
206 box.pack()
207
208 #
209 # standard button semantics
210
211 def ok(self, event=None):
212
213 if not self.validate():
214 self.initial_focus.focus_set() # put focus back
215 return
216
217 self.withdraw()
218 self.update_idletasks()
219
220 try:
221 self.apply()
222 finally:
223 self.cancel()
224
225 def cancel(self, event=None):
226
227 # put focus back to the parent window
228 if self.parent is not None:
229 self.parent.focus_set()
230 self.destroy()
231
232 #
233 # command hooks
234
235 def validate(self):
236 '''validate the data
237
238 This method is called automatically to validate the data before the
239 dialog is destroyed. By default, it always validates OK.
240 '''
241
242 return 1 # override
243
244 def apply(self):
245 '''process the data
246
247 This method is called automatically to process the data, *after*
248 the dialog is destroyed. By default, it does nothing.
249 '''
250
251 pass # override
252
253
254# --------------------------------------------------------------------
255# convenience dialogues
256
257class _QueryDialog(Dialog):
258
259 def __init__(self, title, prompt,
260 initialvalue=None,
261 minvalue = None, maxvalue = None,
262 parent = None):
263
Georg Brandl2e081362008-05-17 19:04:04 +0000264 self.prompt = prompt
265 self.minvalue = minvalue
266 self.maxvalue = maxvalue
267
268 self.initialvalue = initialvalue
269
270 Dialog.__init__(self, parent, title)
271
272 def destroy(self):
273 self.entry = None
274 Dialog.destroy(self)
275
276 def body(self, master):
277
278 w = Label(master, text=self.prompt, justify=LEFT)
279 w.grid(row=0, padx=5, sticky=W)
280
281 self.entry = Entry(master, name="entry")
282 self.entry.grid(row=1, padx=5, sticky=W+E)
283
Andrew Svetlov1fb0e3f2012-07-30 19:59:53 +0300284 if self.initialvalue is not None:
Georg Brandl2e081362008-05-17 19:04:04 +0000285 self.entry.insert(0, self.initialvalue)
286 self.entry.select_range(0, END)
287
288 return self.entry
289
290 def validate(self):
291 try:
292 result = self.getresult()
293 except ValueError:
294 messagebox.showwarning(
295 "Illegal value",
296 self.errormessage + "\nPlease try again",
297 parent = self
298 )
299 return 0
300
301 if self.minvalue is not None and result < self.minvalue:
302 messagebox.showwarning(
303 "Too small",
304 "The allowed minimum value is %s. "
305 "Please try again." % self.minvalue,
306 parent = self
307 )
308 return 0
309
310 if self.maxvalue is not None and result > self.maxvalue:
311 messagebox.showwarning(
312 "Too large",
313 "The allowed maximum value is %s. "
314 "Please try again." % self.maxvalue,
315 parent = self
316 )
317 return 0
318
319 self.result = result
320
321 return 1
322
323
324class _QueryInteger(_QueryDialog):
325 errormessage = "Not an integer."
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300326
Georg Brandl2e081362008-05-17 19:04:04 +0000327 def getresult(self):
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300328 return self.getint(self.entry.get())
Georg Brandl2e081362008-05-17 19:04:04 +0000329
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300330
Georg Brandl2e081362008-05-17 19:04:04 +0000331def askinteger(title, prompt, **kw):
332 '''get an integer from the user
333
334 Arguments:
335
336 title -- the dialog title
337 prompt -- the label text
338 **kw -- see SimpleDialog class
339
340 Return value is an integer
341 '''
342 d = _QueryInteger(title, prompt, **kw)
343 return d.result
344
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300345
Georg Brandl2e081362008-05-17 19:04:04 +0000346class _QueryFloat(_QueryDialog):
347 errormessage = "Not a floating point value."
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300348
Georg Brandl2e081362008-05-17 19:04:04 +0000349 def getresult(self):
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300350 return self.getdouble(self.entry.get())
Georg Brandl2e081362008-05-17 19:04:04 +0000351
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300352
Georg Brandl2e081362008-05-17 19:04:04 +0000353def askfloat(title, prompt, **kw):
354 '''get a float from the user
355
356 Arguments:
357
358 title -- the dialog title
359 prompt -- the label text
360 **kw -- see SimpleDialog class
361
362 Return value is a float
363 '''
364 d = _QueryFloat(title, prompt, **kw)
365 return d.result
366
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300367
Georg Brandl2e081362008-05-17 19:04:04 +0000368class _QueryString(_QueryDialog):
369 def __init__(self, *args, **kw):
370 if "show" in kw:
371 self.__show = kw["show"]
372 del kw["show"]
373 else:
374 self.__show = None
375 _QueryDialog.__init__(self, *args, **kw)
376
377 def body(self, master):
378 entry = _QueryDialog.body(self, master)
379 if self.__show is not None:
380 entry.configure(show=self.__show)
381 return entry
382
383 def getresult(self):
384 return self.entry.get()
385
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300386
Georg Brandl2e081362008-05-17 19:04:04 +0000387def askstring(title, prompt, **kw):
388 '''get a string from the user
389
390 Arguments:
391
392 title -- the dialog title
393 prompt -- the label text
394 **kw -- see SimpleDialog class
395
396 Return value is a string
397 '''
398 d = _QueryString(title, prompt, **kw)
399 return d.result
400
401
Martin v. Löwisa3837a02004-03-22 21:49:47 +0000402if __name__ == '__main__':
403
404 def test():
405 root = Tk()
406 def doit(root=root):
407 d = SimpleDialog(root,
Guido van Rossumc4570481998-03-20 20:45:49 +0000408 text="This is a test dialog. "
409 "Would this have been an actual dialog, "
410 "the buttons below would have been glowing "
411 "in soft pink light.\n"
412 "Do you believe this?",
413 buttons=["Yes", "No", "Cancel"],
414 default=0,
415 cancel=2,
416 title="Test Dialog")
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000417 print(d.go())
Georg Brandl2e081362008-05-17 19:04:04 +0000418 print(askinteger("Spam", "Egg count", initialvalue=12*12))
419 print(askfloat("Spam", "Egg weight\n(in tons)", minvalue=1,
420 maxvalue=100))
421 print(askstring("Spam", "Egg label"))
Martin v. Löwisa3837a02004-03-22 21:49:47 +0000422 t = Button(root, text='Test', command=doit)
423 t.pack()
424 q = Button(root, text='Quit', command=t.quit)
425 q.pack()
426 t.mainloop()
Guido van Rossum990e6191996-06-17 17:14:46 +0000427
Guido van Rossum990e6191996-06-17 17:14:46 +0000428 test()