blob: 802202560af529c1221e0e284685f23d07a2c092 [file] [log] [blame]
xf.li86118912025-03-19 20:07:27 -07001#
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.
12
13It 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"""
25
26from tkinter import *
27from tkinter import messagebox, _get_default_root
28
29
30class SimpleDialog:
31
32 def __init__(self, master,
33 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
43 _setup_dialog(self.root)
44
45 self.message = Message(self.root, text=text, aspect=400)
46 self.message.pack(expand=1, fill=BOTH)
47 self.frame = Frame(self.root)
48 self.frame.pack()
49 self.num = default
50 self.cancel = cancel
51 self.default = default
52 self.root.bind('<Return>', self.return_event)
53 for num in range(len(buttons)):
54 s = buttons[num]
55 b = Button(self.frame, text=s,
56 command=(lambda self=self, num=num: self.done(num)))
57 if num == default:
58 b.config(relief=RIDGE, borderwidth=8)
59 b.pack(side=LEFT, fill=BOTH, expand=1)
60 self.root.protocol('WM_DELETE_WINDOW', self.wm_delete_window)
61 self._set_transient(master)
62
63 def _set_transient(self, master, relx=0.5, rely=0.3):
64 widget = self.root
65 widget.withdraw() # Remain invisible while we figure out the geometry
66 widget.transient(master)
67 widget.update_idletasks() # Actualize geometry information
68 if master.winfo_ismapped():
69 m_width = master.winfo_width()
70 m_height = master.winfo_height()
71 m_x = master.winfo_rootx()
72 m_y = master.winfo_rooty()
73 else:
74 m_width = master.winfo_screenwidth()
75 m_height = master.winfo_screenheight()
76 m_x = m_y = 0
77 w_width = widget.winfo_reqwidth()
78 w_height = widget.winfo_reqheight()
79 x = m_x + (m_width - w_width) * relx
80 y = m_y + (m_height - w_height) * rely
81 if x+w_width > master.winfo_screenwidth():
82 x = master.winfo_screenwidth() - w_width
83 elif x < 0:
84 x = 0
85 if y+w_height > master.winfo_screenheight():
86 y = master.winfo_screenheight() - w_height
87 elif y < 0:
88 y = 0
89 widget.geometry("+%d+%d" % (x, y))
90 widget.deiconify() # Become visible at the desired location
91
92 def go(self):
93 self.root.wait_visibility()
94 self.root.grab_set()
95 self.root.mainloop()
96 self.root.destroy()
97 return self.num
98
99 def return_event(self, event):
100 if self.default is None:
101 self.root.bell()
102 else:
103 self.done(self.default)
104
105 def wm_delete_window(self):
106 if self.cancel is None:
107 self.root.bell()
108 else:
109 self.done(self.cancel)
110
111 def done(self, num):
112 self.num = num
113 self.root.quit()
114
115
116class Dialog(Toplevel):
117
118 '''Class to open dialogs.
119
120 This class is intended as a base class for custom dialogs
121 '''
122
123 def __init__(self, parent, title = None):
124 '''Initialize a dialog.
125
126 Arguments:
127
128 parent -- a parent window (the application window)
129
130 title -- the dialog title
131 '''
132 master = parent
133 if not master:
134 master = _get_default_root('create dialog window')
135
136 Toplevel.__init__(self, master)
137
138 self.withdraw() # remain invisible for now
139 # If the parent is not viewable, don't
140 # make the child transient, or else it
141 # would be opened withdrawn
142 if parent is not None and parent.winfo_viewable():
143 self.transient(parent)
144
145 if title:
146 self.title(title)
147
148 _setup_dialog(self)
149
150 self.parent = parent
151
152 self.result = None
153
154 body = Frame(self)
155 self.initial_focus = self.body(body)
156 body.pack(padx=5, pady=5)
157
158 self.buttonbox()
159
160 if not self.initial_focus:
161 self.initial_focus = self
162
163 self.protocol("WM_DELETE_WINDOW", self.cancel)
164
165 if parent is not None:
166 self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
167 parent.winfo_rooty()+50))
168
169 self.deiconify() # become visible now
170
171 self.initial_focus.focus_set()
172
173 # wait for window to appear on screen before calling grab_set
174 self.wait_visibility()
175 self.grab_set()
176 self.wait_window(self)
177
178 def destroy(self):
179 '''Destroy the window'''
180 self.initial_focus = None
181 Toplevel.destroy(self)
182
183 #
184 # construction hooks
185
186 def body(self, master):
187 '''create dialog body.
188
189 return widget that should have initial focus.
190 This method should be overridden, and is called
191 by the __init__ method.
192 '''
193 pass
194
195 def buttonbox(self):
196 '''add standard button box.
197
198 override if you do not want the standard buttons
199 '''
200
201 box = Frame(self)
202
203 w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
204 w.pack(side=LEFT, padx=5, pady=5)
205 w = Button(box, text="Cancel", width=10, command=self.cancel)
206 w.pack(side=LEFT, padx=5, pady=5)
207
208 self.bind("<Return>", self.ok)
209 self.bind("<Escape>", self.cancel)
210
211 box.pack()
212
213 #
214 # standard button semantics
215
216 def ok(self, event=None):
217
218 if not self.validate():
219 self.initial_focus.focus_set() # put focus back
220 return
221
222 self.withdraw()
223 self.update_idletasks()
224
225 try:
226 self.apply()
227 finally:
228 self.cancel()
229
230 def cancel(self, event=None):
231
232 # put focus back to the parent window
233 if self.parent is not None:
234 self.parent.focus_set()
235 self.destroy()
236
237 #
238 # command hooks
239
240 def validate(self):
241 '''validate the data
242
243 This method is called automatically to validate the data before the
244 dialog is destroyed. By default, it always validates OK.
245 '''
246
247 return 1 # override
248
249 def apply(self):
250 '''process the data
251
252 This method is called automatically to process the data, *after*
253 the dialog is destroyed. By default, it does nothing.
254 '''
255
256 pass # override
257
258
259def _setup_dialog(w):
260 if w._windowingsystem == "aqua":
261 w.tk.call("::tk::unsupported::MacWindowStyle", "style",
262 w, "moveableModal", "")
263 elif w._windowingsystem == "x11":
264 w.wm_attributes("-type", "dialog")
265
266# --------------------------------------------------------------------
267# convenience dialogues
268
269class _QueryDialog(Dialog):
270
271 def __init__(self, title, prompt,
272 initialvalue=None,
273 minvalue = None, maxvalue = None,
274 parent = None):
275
276 self.prompt = prompt
277 self.minvalue = minvalue
278 self.maxvalue = maxvalue
279
280 self.initialvalue = initialvalue
281
282 Dialog.__init__(self, parent, title)
283
284 def destroy(self):
285 self.entry = None
286 Dialog.destroy(self)
287
288 def body(self, master):
289
290 w = Label(master, text=self.prompt, justify=LEFT)
291 w.grid(row=0, padx=5, sticky=W)
292
293 self.entry = Entry(master, name="entry")
294 self.entry.grid(row=1, padx=5, sticky=W+E)
295
296 if self.initialvalue is not None:
297 self.entry.insert(0, self.initialvalue)
298 self.entry.select_range(0, END)
299
300 return self.entry
301
302 def validate(self):
303 try:
304 result = self.getresult()
305 except ValueError:
306 messagebox.showwarning(
307 "Illegal value",
308 self.errormessage + "\nPlease try again",
309 parent = self
310 )
311 return 0
312
313 if self.minvalue is not None and result < self.minvalue:
314 messagebox.showwarning(
315 "Too small",
316 "The allowed minimum value is %s. "
317 "Please try again." % self.minvalue,
318 parent = self
319 )
320 return 0
321
322 if self.maxvalue is not None and result > self.maxvalue:
323 messagebox.showwarning(
324 "Too large",
325 "The allowed maximum value is %s. "
326 "Please try again." % self.maxvalue,
327 parent = self
328 )
329 return 0
330
331 self.result = result
332
333 return 1
334
335
336class _QueryInteger(_QueryDialog):
337 errormessage = "Not an integer."
338
339 def getresult(self):
340 return self.getint(self.entry.get())
341
342
343def askinteger(title, prompt, **kw):
344 '''get an integer from the user
345
346 Arguments:
347
348 title -- the dialog title
349 prompt -- the label text
350 **kw -- see SimpleDialog class
351
352 Return value is an integer
353 '''
354 d = _QueryInteger(title, prompt, **kw)
355 return d.result
356
357
358class _QueryFloat(_QueryDialog):
359 errormessage = "Not a floating point value."
360
361 def getresult(self):
362 return self.getdouble(self.entry.get())
363
364
365def askfloat(title, prompt, **kw):
366 '''get a float from the user
367
368 Arguments:
369
370 title -- the dialog title
371 prompt -- the label text
372 **kw -- see SimpleDialog class
373
374 Return value is a float
375 '''
376 d = _QueryFloat(title, prompt, **kw)
377 return d.result
378
379
380class _QueryString(_QueryDialog):
381 def __init__(self, *args, **kw):
382 if "show" in kw:
383 self.__show = kw["show"]
384 del kw["show"]
385 else:
386 self.__show = None
387 _QueryDialog.__init__(self, *args, **kw)
388
389 def body(self, master):
390 entry = _QueryDialog.body(self, master)
391 if self.__show is not None:
392 entry.configure(show=self.__show)
393 return entry
394
395 def getresult(self):
396 return self.entry.get()
397
398
399def askstring(title, prompt, **kw):
400 '''get a string from the user
401
402 Arguments:
403
404 title -- the dialog title
405 prompt -- the label text
406 **kw -- see SimpleDialog class
407
408 Return value is a string
409 '''
410 d = _QueryString(title, prompt, **kw)
411 return d.result
412
413
414if __name__ == '__main__':
415
416 def test():
417 root = Tk()
418 def doit(root=root):
419 d = SimpleDialog(root,
420 text="This is a test dialog. "
421 "Would this have been an actual dialog, "
422 "the buttons below would have been glowing "
423 "in soft pink light.\n"
424 "Do you believe this?",
425 buttons=["Yes", "No", "Cancel"],
426 default=0,
427 cancel=2,
428 title="Test Dialog")
429 print(d.go())
430 print(askinteger("Spam", "Egg count", initialvalue=12*12))
431 print(askfloat("Spam", "Egg weight\n(in tons)", minvalue=1,
432 maxvalue=100))
433 print(askstring("Spam", "Egg label"))
434 t = Button(root, text='Test', command=doit)
435 t.pack()
436 q = Button(root, text='Quit', command=t.quit)
437 q.pack()
438 t.mainloop()
439
440 test()