Search code examples
pythontkinterlistboxwindowpython-2.x

Can't close tkinter root window after modalPane closed


I've cloned a class called ListBoxChoice found on the web found (adding some needed features) below:

from Tkinter import *

class ListBoxChoice(object):
    def __init__(self, master=None, title=None, message=None,\
                 list=[]):
    self.master = master
    self.value = None
    self.list = list[:]

    self.modalPane = Toplevel(self.master)

    self.modalPane.transient(self.master)
    self.modalPane.grab_set()

    self.modalPane.bind("<Return>", self._choose)
    self.modalPane.bind("<Escape>", self._cancel)

    if title:
        self.modalPane.title(title)

    if message:
        Label(self.modalPane, text=message).pack(padx=5, pady=5)

    listFrame = Frame(self.modalPane)
    listFrame.pack(side=TOP, padx=5, pady=5)

    scrollBar = Scrollbar(listFrame)
    scrollBar.pack(side=RIGHT, fill=Y)

    # get the largest value of the 'list' to set the width
    widthOfList = 0
    for k in list:
        if len(str(k)) > widthOfList:
            widthOfList = len(str(k))

    # now pad some space to back of the widthOfList
    widthOfList = widthOfList + 2

    self.listBox = Listbox(listFrame, selectmode=SINGLE,\
                   width=widthOfList)

    self.listBox.pack(side=LEFT, fill=Y)
    scrollBar.config(command=self.listBox.yview)

    self.listBox.config(yscrollcommand=scrollBar.set)
    self.list.sort()

    for item in self.list:
        self.listBox.insert(END, item)

    buttonFrame = Frame(self.modalPane)
    buttonFrame.pack(side=BOTTOM)

    chooseButton = Button(buttonFrame, text="Choose",\
                   command=self._choose)
    chooseButton.pack()

    cancelButton = Button(buttonFrame, text="Cancel",\
                   command=self._cancel)
    cancelButton.pack(side=RIGHT)

    def _choose(self, event=None):
        try:
            firstIndex = self.listBox.curselection()[0]
            self.value = self.list[int(firstIndex)]
        except IndexError:
            self.value = None
        self.modalPane.destroy()

    def _cancel(self, event=None):
        self.modalPane.destroy()

    def returnValue(self):
        self.master.wait_window(self.modalPane)
        return self.value

if __name__ == '__main__':
    import random
    root = Tk()

    returnValue = True
    list = [random.randint(1,100) for x in range(50)]
    while returnValue:
        returnValue = ListBoxChoice(root, "Number Picking",\
                     "Pick one of these crazy random numbers",\
                     list).returnValue()
        print returnValue

Now this example says to do something like this:
results = ListBoxChoice(root, list=listOfItems).returnValue().

What I'm trying to do is provide a list of values from which the user selects a single value. The window should close before I use the results from the selected value. Here is that code:

from tkinter import Tk, Label
form ListBoxChoice import ListBoxChoice
...
eventList = ["20190120","20190127","20190203"]
root = Tk()
root.withdraw() # This causes the ListBoxChoice object not to appear
selectValue = ListBoxChoice(root, title="Event",\
              message="Pick Event", list=eventList).returnValue()
root.wait_window() # Modal Pane/window closes but not the root
print("selectValue:", selectValue)

A root window is placed behind the modalPane (Toplevel). I have to close that window before the calling process continues. So there is a block in place.

I've tried to put a sleep(1.01) command above but had no impact. How do I get the ListBoxChoice to close once the selection has been made before my print statement of the selectValue? For it is at that point I want to use the results to plot data.

If I don't use root.wait_winow(), it is only when the plot is closed (end of the process) that the ListBoxChoice box close as well.

Suggestions?


Solution

  • Slightly updated

    Here's a version of the ListBoxChoice class which I think works the way you desire. I've updated my previous answer slightly so the class is now defined in a separate module named listboxchoice.py. This didn't change anything I could see when I tested—it other words it still seems to work—but I wanted to more closely simulate the way you said you're using it the comments.

    It still uses wait_window() because doing so is required to give tkinter's mandatory event-processing-loop the opportunity to run (since mainloop() isn't called anywhere). There's some good background material in the article Dialog Windows about programming tkiner dialogs you might find useful. The added root.withdraw() call eliminates the issue of not being able to close it because it's not there. This is fine since there's no need to have the empty window being displayed anyway.

    test_lbc.py

    import random
    try:
        import Tkinter as tk  # Python 2
    except ModuleNotFoundError:
        import tkinter as tk  # Python 3
    from listboxchoice import ListBoxChoice
    
    
    root = tk.Tk()
    root.withdraw()  # Hide root window.
    
    values = [random.randint(1, 100) for _ in range(50)]
    choice = None
    
    while choice is None:
        choice = ListBoxChoice(root, "Number Picking",
                               "Pick one of these crazy random numbers",
                               values).returnValue()
    print('choice: {}'.format(choice))
    

    listboxchoice.py

    """ ListBoxChoice widget to display a list of values and allow user to
        choose one of them.
    """
    try:
        import Tkinter as tk  # Python 2
    except ModuleNotFoundError:
        import tkinter as tk  # Python 3
    
    
    class ListBoxChoice(object):
        def __init__(self, master=None, title=None, message=None, values=None):
            self.master = master
            self.value = None
            if values is None:  # Avoid use of mutable default argument value.
                raise RuntimeError('No values argument provided.')
            self.values = values[:]  # Create copy.
    
            self.modalPane = tk.Toplevel(self.master, takefocus=True)
            self.modalPane.bind("<Return>", self._choose)
            self.modalPane.bind("<Escape>", self._cancel)
    
            if title:
                self.modalPane.title(title)
    
            if message:
                tk.Label(self.modalPane, text=message).pack(padx=5, pady=5)
    
            listFrame = tk.Frame(self.modalPane)
            listFrame.pack(side=tk.TOP, padx=5, pady=5)
    
            scrollBar = tk.Scrollbar(listFrame)
            scrollBar.pack(side=tk.RIGHT, fill=tk.Y)
    
            # Get length the largest value in 'values'.
            widthOfList = max(len(str(value)) for value in values)
            widthOfList += 2  # Add some padding.
    
            self.listBox = tk.Listbox(listFrame, selectmode=tk.SINGLE, width=widthOfList)
    
            self.listBox.pack(side=tk.LEFT, fill=tk.Y)
            scrollBar.config(command=self.listBox.yview)
    
            self.listBox.config(yscrollcommand=scrollBar.set)
            self.values.sort()
    
            for item in self.values:
                self.listBox.insert(tk.END, item)
    
            buttonFrame = tk.Frame(self.modalPane)
            buttonFrame.pack(side=tk.BOTTOM)
    
            chooseButton = tk.Button(buttonFrame, text="Choose", command=self._choose)
            chooseButton.pack()
    
            cancelButton = tk.Button(buttonFrame, text="Cancel", command=self._cancel)
            cancelButton.pack(side=tk.RIGHT)
    
        def _choose(self, event=None):
            try:
                firstIndex = self.listBox.curselection()[0]
                self.value = self.values[int(firstIndex)]
            except IndexError:
                self.value = None
            self.modalPane.destroy()
    
        def _cancel(self, event=None):
            self.modalPane.destroy()
    
        def returnValue(self):
            self.master.wait_window(self.modalPane)
            return self.value