Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to implement a custom popup tkinter dialog box

I just started learning how to create a custom pop up dialog box; and as it turns out, the tkinter messagebox is really easy to use, but it also does not do too much. Here is my attempt to create a dialog box that will take input and then store that in the username.

My question is what is the recommended style to implement this? As Bryan Oakley suggested in this comment.

I would advise against using a global variable. Instead of having the dialog destroy itself, have it destroy only the actual widget but leave the object alive. Then, call something like inputDialog.get_string() and then del inputDialog from your main logic.

Maybe using the global variable to return my string is not the best idea, but why? And what is the suggested way? I get confused because I don't know how to trigger the getstring once the window is destroyed, and... the line about destroying the actual widget, I am not sure if he is referring to TopLevel.

The reason I ask is because I want the pop up box to be destroyed after I press the submit button; because after all, I want it to resume back to the main program, update something, etc. What should the button method send do in this case? Because the idea in this particular example is to allow the user to do it over and over, if he desires.

import tkinter as tk  class MyDialog:     def __init__(self, parent):         top = self.top = tk.Toplevel(parent)         self.myLabel = tk.Label(top, text='Enter your username below')         self.myLabel.pack()          self.myEntryBox = tk.Entry(top)         self.myEntryBox.pack()          self.mySubmitButton = tk.Button(top, text='Submit', command=self.send)         self.mySubmitButton.pack()      def send(self):         global username         username = self.myEntryBox.get()         self.top.destroy()  def onClick():     inputDialog = MyDialog(root)     root.wait_window(inputDialog.top)     print('Username: ', username)  username = 'Empty' root = tk.Tk() mainLabel = tk.Label(root, text='Example for pop up input box') mainLabel.pack()  mainButton = tk.Button(root, text='Click me', command=onClick) mainButton.pack()  root.mainloop() 
like image 858
George Avatar asked Apr 07 '12 19:04

George


People also ask

How do you make a pop up box in Tkinter?

Popup window in Tkinter can be created by defining the Toplevel(win) window. A Toplevel window manages to create a child window along with the parent window. It always opens above all the other windows defined in any application.


2 Answers

Using the global statement is unnecessary in the two scenarios that come to mind.

  1. you want to code a dialog box that can be imported to use with a main GUI
  2. you want to code a dialog box that can be imported to use without a main GUI

code a dialog box that can be imported to use with a main GUI


Avoiding the global statement can be accomplished by passing a dictionary & key when you create an instance of a dialog box. The dictionary & key can then be associated with the button's command, by using lambda. That creates an anonymous function that will execute your function call (with args) when the button is pressed.

You can avoid the need to pass the parent every time you create an instance of the dialog box by binding the parent to a class attribute (root in this example).

You can save the following as mbox.py in your_python_folder\Lib\site-packages or in the same folder as your main GUI's file.

import tkinter  class Mbox(object):      root = None      def __init__(self, msg, dict_key=None):         """         msg = <str> the message to be displayed         dict_key = <sequence> (dictionary, key) to associate with user input         (providing a sequence for dict_key creates an entry for user input)         """         tki = tkinter         self.top = tki.Toplevel(Mbox.root)          frm = tki.Frame(self.top, borderwidth=4, relief='ridge')         frm.pack(fill='both', expand=True)          label = tki.Label(frm, text=msg)         label.pack(padx=4, pady=4)          caller_wants_an_entry = dict_key is not None          if caller_wants_an_entry:             self.entry = tki.Entry(frm)             self.entry.pack(pady=4)              b_submit = tki.Button(frm, text='Submit')             b_submit['command'] = lambda: self.entry_to_dict(dict_key)             b_submit.pack()          b_cancel = tki.Button(frm, text='Cancel')         b_cancel['command'] = self.top.destroy         b_cancel.pack(padx=4, pady=4)      def entry_to_dict(self, dict_key):         data = self.entry.get()         if data:             d, key = dict_key             d[key] = data             self.top.destroy() 

You can see examples that subclass TopLevel and tkSimpleDialog (tkinter.simpledialog in py3) at effbot.

It's worth noting that ttk widgets are interchangeable with the tkinter widgets in this example.

To accurately center the dialog box read → this.

Example of use:

import tkinter import mbox  root = tkinter.Tk()  Mbox = mbox.Mbox Mbox.root = root  D = {'user':'Bob'}  b_login = tkinter.Button(root, text='Log in') b_login['command'] = lambda: Mbox('Name?', (D, 'user')) b_login.pack()  b_loggedin = tkinter.Button(root, text='Current User') b_loggedin['command'] = lambda: Mbox(D['user']) b_loggedin.pack()  root.mainloop() 

code a dialog box that can be imported to use without a main GUI


Create a module containing a dialog box class (MessageBox here). Also, include a function that creates an instance of that class, and finally returns the value of the button pressed (or data from an Entry widget).

Here is a complete module that you can customize with the help of these references: NMTech & Effbot.
Save the following code as mbox.py in your_python_folder\Lib\site-packages

import tkinter  class MessageBox(object):      def __init__(self, msg, b1, b2, frame, t, entry):          root = self.root = tkinter.Tk()         root.title('Message')         self.msg = str(msg)         # ctrl+c to copy self.msg         root.bind('<Control-c>', func=self.to_clip)         # remove the outer frame if frame=False         if not frame: root.overrideredirect(True)         # default values for the buttons to return         self.b1_return = True         self.b2_return = False         # if b1 or b2 is a tuple unpack into the button text & return value         if isinstance(b1, tuple): b1, self.b1_return = b1         if isinstance(b2, tuple): b2, self.b2_return = b2         # main frame         frm_1 = tkinter.Frame(root)         frm_1.pack(ipadx=2, ipady=2)         # the message         message = tkinter.Label(frm_1, text=self.msg)         message.pack(padx=8, pady=8)         # if entry=True create and set focus         if entry:             self.entry = tkinter.Entry(frm_1)             self.entry.pack()             self.entry.focus_set()         # button frame         frm_2 = tkinter.Frame(frm_1)         frm_2.pack(padx=4, pady=4)         # buttons         btn_1 = tkinter.Button(frm_2, width=8, text=b1)         btn_1['command'] = self.b1_action         btn_1.pack(side='left')         if not entry: btn_1.focus_set()         btn_2 = tkinter.Button(frm_2, width=8, text=b2)         btn_2['command'] = self.b2_action         btn_2.pack(side='left')         # the enter button will trigger the focused button's action         btn_1.bind('<KeyPress-Return>', func=self.b1_action)         btn_2.bind('<KeyPress-Return>', func=self.b2_action)         # roughly center the box on screen         # for accuracy see: https://stackoverflow.com/a/10018670/1217270         root.update_idletasks()         xp = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)         yp = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)         geom = (root.winfo_width(), root.winfo_height(), xp, yp)         root.geometry('{0}x{1}+{2}+{3}'.format(*geom))         # call self.close_mod when the close button is pressed         root.protocol("WM_DELETE_WINDOW", self.close_mod)         # a trick to activate the window (on windows 7)         root.deiconify()         # if t is specified: call time_out after t seconds         if t: root.after(int(t*1000), func=self.time_out)      def b1_action(self, event=None):         try: x = self.entry.get()         except AttributeError:             self.returning = self.b1_return             self.root.quit()         else:             if x:                 self.returning = x                 self.root.quit()      def b2_action(self, event=None):         self.returning = self.b2_return         self.root.quit()      # remove this function and the call to protocol     # then the close button will act normally     def close_mod(self):         pass      def time_out(self):         try: x = self.entry.get()         except AttributeError: self.returning = None         else: self.returning = x         finally: self.root.quit()      def to_clip(self, event=None):         self.root.clipboard_clear()         self.root.clipboard_append(self.msg) 

and:

def mbox(msg, b1='OK', b2='Cancel', frame=True, t=False, entry=False):     """Create an instance of MessageBox, and get data back from the user.     msg = string to be displayed     b1 = text for left button, or a tuple (<text for button>, <to return on press>)     b2 = text for right button, or a tuple (<text for button>, <to return on press>)     frame = include a standard outerframe: True or False     t = time in seconds (int or float) until the msgbox automatically closes     entry = include an entry widget that will have its contents returned: True or False     """     msgbox = MessageBox(msg, b1, b2, frame, t, entry)     msgbox.root.mainloop()     # the function pauses here until the mainloop is quit     msgbox.root.destroy()     return msgbox.returning 

After mbox creates an instance of MessageBox it starts the mainloop,
which effectively stops the function there until the mainloop is exited via root.quit().
The mbox function can then access msgbox.returning, and return its value.

Example:

user = {} mbox('starting in 1 second...', t=1) user['name'] = mbox('name?', entry=True) if user['name']:     user['sex'] = mbox('male or female?', ('male', 'm'), ('female', 'f'))     mbox(user, frame=False) 
like image 54
Honest Abe Avatar answered Sep 22 '22 17:09

Honest Abe


Since the object inputDialog is not destroyed, I was able to access the object attribute. I added the return string as an attribute:

import tkinter as tk  class MyDialog:      def __init__(self, parent):         top = self.top = tk.Toplevel(parent)         self.myLabel = tk.Label(top, text='Enter your username below')         self.myLabel.pack()         self.myEntryBox = tk.Entry(top)         self.myEntryBox.pack()         self.mySubmitButton = tk.Button(top, text='Submit', command=self.send)         self.mySubmitButton.pack()      def send(self):         self.username = self.myEntryBox.get()         self.top.destroy()  def onClick():     inputDialog = MyDialog(root)     root.wait_window(inputDialog.top)     print('Username: ', inputDialog.username)  root = tk.Tk() mainLabel = tk.Label(root, text='Example for pop up input box') mainLabel.pack()  mainButton = tk.Button(root, text='Click me', command=onClick) mainButton.pack()  root.mainloop() 
like image 21
ashwinjv Avatar answered Sep 22 '22 17:09

ashwinjv