Correct Way to Implement a Custom Popup Tkinter Dialog Box

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

How to create a custom messagebox using Tkinter in Python with changing message and changing button state

Try developing your own dialog. Example:

import Tkinter as tk

class CustomDialog(tk.Toplevel):
def __init__(self, title, message, command1=self.ok, command2=self.ok, command3=self.ok, buttontext1="button1", buttontext2="button2", buttontext3="button3"):
self.base = tk.Toplevel()
self.base.title(title)
self.label = tk.Label(self.base, text=message)
self.label.pack()
self.label.grid(row=0, column=0, columnspan=3, sticky=N)
self.button1 = tk.Button(self.base, text=buttontext1, command=command1)
self.button1.pack()
self.button1.grid(row=1, column=0, sticky=N)
self.button2 = tk.Button(self.base, text=buttontext2, command=command2)
self.button2.pack()
self.button2.grid(row=1, column=1, sticky=N)
self.button3 = tk.Button(self.base, text=buttontext3, command=command3)
self.button3.pack()
self.button3.grid(row=1, column=2, sticky=N)
def ok(self, event=None):
self.destroy()
def baseconfig(self, option, value):
self.base[option] = value
def labelconfig(self, option, value):
self.label[option] = value
def buttonconfig(self, number, option, value):
exec "self.button%s[option] = value" % str(number)

def customDialog(title, message, command1=self.ok, command2=self.ok, command3=self.ok, buttontext1="button1", buttontext2="button2", buttontext3="button3", button1ret=None, button2ret=None, button3ret=None):
def _button1press():
command1()
return button1ret
def _button2press():
command2()
return button2ret
def _button3press():
command3()
return button3ret
dialog = CustomDialog(title, message, _button1press, _button2press, _button3press, buttontext1, buttontext2, buttontext3)

And to disable the first button, call self.buttonconfig(1, state, DISABLED) (1 refers to the number of the button). To enable it, use self.buttonconfig(1, state, NORMAL).

How to open a tkinter dialog box and use the result later in the program

That way:

import tkinter.simpledialog
result = tkinter.simpledialog.askstring('Input', 'Enter a string') # title and description

Note - to use tkinter's dialogs (the only ones available) you must activate it from within a tkinter application.

A hack would be to call this line twice (first one crashes - handle it - and opens a window), then the second call has a parent window:

>>> import tkinter.simpledialog
>>> result = tkinter.simpledialog.askstring('Input', 'Enter a string') # title and description
Traceback (most recent call last):
...
AttributeError: 'NoneType' object has no attribute 'winfo_viewable'
>>> result = tkinter.simpledialog.askstring('Input', 'Enter a string') # title and description
>>> result
'hello'


Related Topics



Leave a reply



Submit