Friday, January 22, 2016

python tkinter how to pop up a window from inside a class

It took a little bit of searching and pondering of the info that I had found to accomplish one of the most important functions in my GUI after I recoded it to properly use the Tkinter mainloop, which is how to pop up custom dialog windows when something happened on the main GUI. My GUI is all under a single class, which is a good way of doing it but not much like the super simple examples that are often found online for creating dialog windows. I needed dialogs that were more customized than the built in Message or OK dialogs. The answer was for each dialog launched from the main GUI to create an instance of the Tkinter.Toplevel object and have that be the root of the new dialog window. This essentially creates a new window frame, it's like the Tkinter.Tk object that you use to create your GUI at the initialization of the class. You then pack data entry and/or display widgets into that Toplevel object. All of the objects you put in this frame are still accessible from the main class as long as you store the pointers to them in the class; essentially this new window is just an extension of the original window. Declare the callbacks for each of the widgets in the dialog as part of the class; you can allow the callbacks to destroy the Toplevel object by its pointer in order to perform the function of dismissing the dialog. There's a magic function of the Toplevel object (.transient()) which allows you to define that it appears on top of the original GUI window, and then you can place it using geometry commands like you would your main window. The only trick is to keep all the coding event-driven; for a complex program that means employing the state machine type of coding where you build the dialog in one state and then let user interaction with the dialog widgets cause advancement to a new state.

Here's my example of how I did it; not necessarily the best way but fairly functional:

import Tkinter,tkMessageBox,ttk
class mygui(Tkinter.Tk):

def __init__(self, *args, **kwargs):
Tkinter.Tk.__init__(self, *args, **kwargs)

self.title('My GUI Main Window')

# Making a fancy frame here with a blue border just for fun
# Bottom layer is a frame with blue fill
frm_0 = Tkinter.Frame(self, bg="blue")

# This is the main frame that will be on top of the blue frame
f = Tkinter.Frame(frm_0)

# Putting some text in the frame just to have a frame.
# Actual GUI would have more stuff
self.mylabel = Tkinter.Label(f,text="This is a label ",justify="left")
self.mylabel.pack(side="left",padx=10,pady=10)

# Place the main frame inside the blue frame with some padding
# so that the blue of the lower layer shows all around it
f.pack(fill="both",padx=5,pady=5)
frm_0.pack()

# Place the bottom frame where I want it (in the center)
frm_0.place(relx=0.5,rely=0.5,anchor="center")

# Make this example gui the size of the packed controls,
# in the middle of the screen
self.update_idletasks()
width = frm_0.winfo_width()
height = frm_0.winfo_height()
x = self.winfo_screenwidth() // 2 - width // 2
y = self.winfo_screenheight() // 2 - height // 2
self.geometry('{}x{}+{}+{}'.format(width, height, x, y))

# Initialize the state machine to the first state
self.state = 0

# Initialize other variables in this class
self.mytext = ''
self.text_accepted = False
self.abort_selected = False

# Define all the widget callback functions for the class
def accept(self, event=None):
self.mytext = self.e.get()
self.text_accepted = True
self.top.destroy()

def close_top(self):
self.mytext = ''
self.abort_selected = True
self.top.destroy()

# Main executive loop. This is where the part of the program that
# actually does things would go
def executive(self):

if (self.state == 0):
# First state, for doing stuff before popping up the dialog
print "Doing stuff in first state."

# Set next state, schedule the next call of exeutive() and exit
# In this example there is only one state before dialog; could
# be any number of states before the dialog
self.state = 1
self.executive()

elif (self.state == 1):
# Draw dialog popup
self.top = Tkinter.Toplevel()
msg = 'Enter text and press accept'
self.top.title(msg)

frm1 = Tkinter.Frame(self.top)
self.e = ttk.Entry(frm1)
self.e.config(width=(len(msg)+20))
self.e.insert(0,"default text")
self.e.pack(side="left",padx=5,pady=5)
b = Tkinter.Button(frm1, text="Accept", command=self.accept)
b.pack(side='left',padx=5,pady=5)
frm1.pack()

self.top.protocol("WM_DELETE_WINDOW", self.close_top)

# Make sure this dialog stays on top of the root console
self.top.transient(self)

# Make this dialog positioned in the center of the screen
self.update_idletasks()
win_width = self.top.winfo_width()
win_height = self.top.winfo_height()
x = self.winfo_screenwidth() // 2 - win_width // 2
y = self.winfo_screenheight() // 2 - win_height // 2
self.top.geometry('{}x{}+{}+{}'.format(win_width, win_height, x, y))

# set state to next state and exit
self.state = 2
self.executive()

elif (self.state == 2):
# waiting for the accept button to be pushed.
if self.text_accepted:
self.state = 3
self.executive()
elif self.abort_selected:
self.state = 4
self.executive()
else:
self.after(100,self.executive)

elif (self.state == 3):
# Do something with the input (boring example).
print self.mytext
self.state = 4
self.executive()

elif (self.state == 4):
# Exit state. Call the built-in .quit() function to exit the GUI
# Note that you can still call a built-in MessageBox widget any time
tkMessageBox.showinfo(title="Example complete",message="Example complete. Click OK to

exit.")
self.quit()

# Note that this "start" function is basically redundant, however having
# a function with this name helps readability.
def start(self):
self.state = 0;
self.executive()

if __name__=='__main__':

# Main code for example

app = mygui()
app.start()
app.mainloop()
app.destroy()