Correct way to implement a custom popup tkinter dialog box Correct way to implement a custom popup tkinter dialog box tkinter tkinter

Correct way to implement a custom popup tkinter dialog box


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 tkinterclass 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 tkinterimport mboxroot = tkinter.Tk()Mbox = mbox.MboxMbox.root = rootD = {'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 tkinterclass 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)


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 tkclass 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()


I used Honest Abe's 2nd part of the code titled:

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

as template and made some modifications. I needed a combobox instead of entry, so I also implemented it. If you need something else, it should be fairly easy to modify.

Following are the changes

  • Acts as a child
  • Modal to the parent
  • Centered on top of the parent
  • Not resizable
  • Combobox instead of entry
  • Click cross (X) to close the dialog

Removed

  • frame, timer, clipboard

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 tkinterimport tkinter.ttk as ttkclass MessageBox(object):    def __init__(self, msg, b1, b2, parent, cbo, cboList):        root = self.root = tkinter.Toplevel(parent)        root.title('Choose')        root.geometry('100x100')        root.resizable(False, False)        root.grab_set() # modal        self.msg = str(msg)        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)        if cbo: message.pack(padx=8, pady=8)        else: message.pack(padx=8, pady=20)        # if entry=True create and set focus        if cbo:            self.cbo = ttk.Combobox(frm_1, state="readonly", justify="center", values= cboList)            self.cbo.pack()            self.cbo.focus_set()            self.cbo.current(0)        # 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        if cbo: btn_1.pack(side='left', padx=5)        else: btn_1.pack(side='left', padx=10)        if not cbo: btn_1.focus_set()        btn_2 = tkinter.Button(frm_2, width=8, text=b2)        btn_2['command'] = self.b2_action        if cbo: btn_2.pack(side='left', padx=5)        else: btn_2.pack(side='left', padx=10)        # 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()        root.geometry("210x110+%d+%d" % (parent.winfo_rootx()+7,                                         parent.winfo_rooty()+70))        root.protocol("WM_DELETE_WINDOW", self.close_mod)        # a trick to activate the window (on windows 7)        root.deiconify()    def b1_action(self, event=None):        try: x = self.cbo.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()    def close_mod(self):        # top right corner cross click: return value ;`x`;        # we need to send it a value, otherwise there will be an exception when closing parent window        self.returning = ";`x`;"        self.root.quit()

It should be quick and easy to use. Here's an example:

from mbox import MessageBoxfrom tkinter import *root = Tk()def mbox(msg, b1, b2, parent, cbo=False, cboList=[]):    msgbox = MessageBox(msg, b1, b2, parent, cbo, cboList)    msgbox.root.mainloop()    msgbox.root.destroy()    return msgbox.returningprompt = {}# it will only show 2 buttons & 1 label if (cbo and cboList) aren't provided# click on 'x' will return ;`x`;prompt['answer'] = mbox('Do you want to go?', ('Go', 'go'), ('Cancel', 'cancel'), root)ans = prompt['answer']print(ans)if ans == 'go':    # do stuff    passelse:    # do stuff    passallowedItems = ['phone','laptop','battery']prompt['answer'] = mbox('Select product to take', ('Take', 'take'), ('Cancel', 'cancel'), root, cbo=True, cboList=allowedItems)ans = prompt['answer']print(ans)if (ans == 'phone'):    # do stuff    passelif (ans == 'laptop'):    # do stuff    passelse:    # do stuff    pass