Michael Madsen
Michael Madsen

Reputation: 55009

Hiding Tkinter root window while showing modal window

I have a TKinter-based application that attempts to manage settings for a game. Users may have multiple versions of this game installed, and in that case, I need to ask the user which installation they want to manage on startup. That part works well enough on its own; the selection dialog pops up after the main window is constructed, and runs modally.

However, due to differences between game versions, it would be useful if I could adapt the interface slightly for those cases. That, however, means I can't really build the main window until I know which installation I'm working on, so it's going to be blank until the user makes a choice.

I would like to hide the root window while this dialog is being shown, but calling withdraw on the root window simply causes the modal dialog to not be shown either - Python just ends up hanging with no CPU usage, and I can't figure out how to get around the problem without having to resort to a non-modal window (and a significantly different control flow, which I'd like to avoid).

Sample code exhibiting the problem and general code structure (Python 2.7):

from Tkinter import *
from ttk import *

class TkGui(object):
    def __init__(self):
        self.root = root = Tk()
        self.root.withdraw()
        selector = FolderSelection(self.root, ('foo', 'bar'))
        self.root.deiconify()
        print(selector.result)

class ChildWindow(object): #Base class
    def __init__(self, parent, title):
        top = self.top = Toplevel(parent)
        self.parent = parent
        top.title(title)
        f = Frame(top)
        self.create_controls(f)
        f.pack(fill=BOTH, expand=Y)

    def create_controls(self, container):
        pass

    def make_modal(self, on_cancel):
        self.top.transient(self.parent)
        self.top.wait_visibility() # Python will hang here...
        self.top.grab_set()
        self.top.focus_set()
        self.top.protocol("WM_DELETE_WINDOW", on_cancel)
        self.top.wait_window(self.top) # ...or here, if wait_visibility is removed

class FolderSelection(ChildWindow):
    def __init__(self, parent, folders):
        self.parent = parent
        self.listvar = Variable(parent)
        self.folderlist = None
        super(FolderSelection, self).__init__(parent, 'Select folder')
        self.result = ''
        self.listvar.set(folders)
        self.make_modal(self.cancel)

    def create_controls(self, container):
        f = Frame(container)
        Label(
            f, text='Please select the folder '
            'you would like to use.').grid(column=0, row=0)
        self.folderlist = Listbox(
            f, listvariable=self.listvar, activestyle='dotbox')
        self.folderlist.grid(column=0, row=1, sticky="nsew")
        Button(
            f, text='OK', command=self.ok
            ).grid(column=0, row=2, sticky="s")
        self.folderlist.bind("<Double-1>", lambda e: self.ok())
        f.pack(fill=BOTH, expand=Y)

    def ok(self):
        if len(self.folderlist.curselection()) != 0:
            self.result = self.folderlist.get(self.folderlist.curselection()[0])
            self.top.protocol('WM_DELETE_WINDOW', None)
            self.top.destroy()

    def cancel(self):
        self.top.destroy()

TkGui()

Upvotes: 2

Views: 4375

Answers (1)

NorthCat
NorthCat

Reputation: 9937

It seems that in your case there is no difference, modal window or not. In fact, you just need to use the wait_window() method. According the docs:

In many situations, it is more practical to handle dialogs in a synchronous fashion; create the dialog, display it, wait for the user to close the dialog, and then resume execution of your application. The wait_window method is exactly what we need; it enters a local event loop, and doesn’t return until the given window is destroyed (either via the destroy method, or explicitly via the window manager).

Consider follow example with nonmodal window:

from Tkinter import *

root = Tk()

def go():
   wdw = Toplevel()
   wdw.geometry('+400+400')
   e = Entry(wdw)
   e.pack()
   e.focus_set()
   #wdw.transient(root)
   #wdw.grab_set()
   root.wait_window(wdw)
   print 'done!'

Button(root, text='Go', command=go).pack()
Button(root, text='Quit', command=root.destroy).pack()

root.mainloop()

When you click Go button, nonmodal dialog will appear, but code wil stop execution and the string done! will be displayed only after you close the dialog window.

If this is the behavior that you want, then here is your example in a modified form (I modified __init__ in TkGui and make_modal method, also added mainloop()):

from Tkinter import *
from ttk import *

class TkGui(object):
    def __init__(self):
        self.root = root = Tk()
        self.root.withdraw()
        selector = FolderSelection(self.root, ('foo', 'bar'))
        self.root.deiconify()
        print(selector.result)

class ChildWindow(object): #Base class
    def __init__(self, parent, title):
        top = self.top = Toplevel(parent)
        self.parent = parent
        top.title(title)
        f = Frame(top)
        self.create_controls(f)
        f.pack(fill=BOTH, expand=Y)

    def create_controls(self, container):
        pass

    def make_modal(self, on_cancel):
        #self.top.transient(self.parent)
        #self.top.wait_visibility() # Python will hang here...
        #self.top.grab_set()
        self.top.focus_set()
        self.top.protocol("WM_DELETE_WINDOW", on_cancel)
        self.top.wait_window(self.top) # ...or here, if wait_visibility is removed

class FolderSelection(ChildWindow):
    def __init__(self, parent, folders):
        self.parent = parent
        self.listvar = Variable(parent)
        self.folderlist = None
        super(FolderSelection, self).__init__(parent, 'Select folder')
        self.result = ''
        self.listvar.set(folders)
        self.make_modal(self.cancel)

    def create_controls(self, container):
        f = Frame(container)
        Label(
            f, text='Please select the folder '
            'you would like to use.').grid(column=0, row=0)
        self.folderlist = Listbox(
            f, listvariable=self.listvar, activestyle='dotbox')
        self.folderlist.grid(column=0, row=1, sticky="nsew")
        Button(
            f, text='OK', command=self.ok
            ).grid(column=0, row=2, sticky="s")
        self.folderlist.bind("<Double-1>", lambda e: self.ok())
        f.pack(fill=BOTH, expand=Y)

    def ok(self):
        if len(self.folderlist.curselection()) != 0:
            self.result = self.folderlist.get(self.folderlist.curselection()[0])
            self.top.protocol('WM_DELETE_WINDOW', None)
            self.top.destroy()

    def cancel(self):
        self.top.destroy()

TkGui()
mainloop()

The code stops on line selector = FolderSelection(self.root, ('foo', 'bar')) and then continue after you close the dialog.

Upvotes: 2

Related Questions