Search code examples
pythonooptkinterwindow-management

Why does "wm_protocol" break normal window management in Python3/tkinter?


I am testing tkinter window management for a somewhat large Python 3.6 project and there is one thing I don't seem to be able to get right or even understand quite well. In the following code, windows are opened and closed as expected (I mean, by clicking the red 'x' button or by pressing Command-W in OS X). But when I try to ad a callback for the secondary window closing event, things get messy. If I have more than one secondary window, for instance, the keyboard shortcut or even the button does not always close the active window. Any idea about what is wrong here?

Here is my current test code:

#!/usr/bin/env python3.6
# encoding: utf-8

import tkinter as tk
import tkinter.font
from tkinter import ttk


class baseApp(ttk.Frame):
    """
    Parent classe for main app window (will include some aditional methods and properties).
    """
    def __init__(self, master, *args, **kwargs):
        super().__init__(master, *args, **kwargs)
        self.master = master
        self.mainframe = ttk.Frame(master)
        self.mainframe.pack()


class App(baseApp):
    """ Base class for the main application window """
    def __init__(self, master, *args, **kwargs):
        super().__init__(master, *args, **kwargs)
        self.master = master
        self.lbl_text = ttk.Label(self.mainframe, text="This is the Main Window")
        self.lbl_text.pack()
        self.btn = ttk.Button(self.mainframe, text="Open Second window",
                              command=lambda: self.create_detail_window(self, number=0))
        self.btn.pack()

    def create_detail_window(self, *event, number=None):
        self.newDetailsWindow = tk.Toplevel(self.master)
        self.newDetailsWindow.geometry('900x600+80+130')
        self.newDetailsWindow.title(f'Detail: {number}')
        self.newDetailsWindow.wm_protocol("WM_DELETE_WINDOW", lambda: self.close_detail_window()) # This line breaks window management!...
        self.detail_window = detailWindow(self.newDetailsWindow, 0)
        self.newDetailsWindow.focus()

    def close_detail_window(self, *event):
        """ will test for some condition before closing, save if necessary and
            then call destroy()
        """
        self.newDetailsWindow.destroy() # Shouldn't this be enough to close the secondary window?...


class detailWindow(ttk.Frame):
    """ Base class for secondary windows """
    def __init__(self, master, rep_num, *args,**kwargs):
        super().__init__(master,*args,**kwargs)
        self.num_rep = rep_num
        self.master.minsize(900, 600)
        self.master.maxsize(900, 600)
        print(f"Showing details about nr. {self.num_rep}")
        self.mainframe = ttk.Frame(master)
        self.mainframe.pack()

        self.lbl_text = ttk.Label(self.mainframe,
                                  text=f"Showing details about nr. {self.num_rep}")
        self.lbl_text.pack()


if __name__ == "__main__":
    root = tk.Tk()
    janela_principal = App(root)
    root.title('Main Window')
    root.bind_all("<Mod2-q>", exit)
    root.mainloop()

It seems that when I de-comment the line self.newDetailsWindow.wm_protocol("WM_DELETE_WINDOW", lambda: self.close_detail_window()) the window management gets broken. Shouldn't the line self.newDetailsWindow.destroy() be enough to simply close the secondary window?... Am I doing anything wrong in the way I am instantiating the objects?


Solution

  • I did several adjustments to your code. It should work by now. Basically, your method app.create_detail_window reassigned the attribute self.newDetailWindow every time you call it, and that's why the 'x' button will be sent to the wrong window. I used a dict to store all the Toplevels you created

    #!/usr/bin/env python3.6
    # encoding: utf-8
    
    import tkinter as tk
    import tkinter.font
    from tkinter import ttk
    
    
    class baseApp(ttk.Frame):
        """
        Parent classe for main app window (will include some aditional methods and properties).
        """
        def __init__(self, master, *args, **kwargs):
            super().__init__(master, *args, **kwargs)
            self.master = master
            self.mainframe = ttk.Frame(master)
            self.mainframe.pack()
    
    
    class App(baseApp):
        """ Base class for the main application window """
        def __init__(self, master, *args, **kwargs):
            super().__init__(master, *args, **kwargs)
            self.master = master
            self.lbl_text = ttk.Label(self.mainframe, text="This is the Main Window")
            self.lbl_text.pack()
            self.btn = ttk.Button(self.mainframe, text="Open Second window",
                                  command=lambda: self.create_detail_window(self, number=0))
            self.btn.pack()
            self.newDetailsWindow = {}
            self.windows_count=0
    
        def create_detail_window(self, *event, number=None):
            self.windows_count+=1
            self.newDetailsWindow[self.windows_count]=tk.Toplevel(self.master)
            self.newDetailsWindow[self.windows_count].geometry('900x600+80+130')
            self.newDetailsWindow[self.windows_count].title(f'Detail: {self.windows_count}')
    
            self.newDetailsWindow[self.windows_count].wm_protocol("WM_DELETE_WINDOW", self.newDetailsWindow[self.windows_count].destroy)
            #self.newDetailsWindow[self.windows_count].bind("Command-w", lambda event: self.newDetailsWindow[-1].destroy())
    
            self.detail_window = detailWindow(self.newDetailsWindow[self.windows_count], self.windows_count)
            self.newDetailsWindow[self.windows_count].focus()
            print(self.newDetailsWindow)
    
        def close_detail_window(self, *event):
            """ will test for some condition before closing, save if necessary and
                then call destroy()
            """
            pass
            #self.newDetailsWindow.destroy() # Shouldn't this be enough to close the secondary window?...
    
    
    class detailWindow(ttk.Frame):
        """ Base class for secondary windows """
        def __init__(self, master, rep_num, *args,**kwargs):
            super().__init__(master,*args,**kwargs)
            self.num_rep = rep_num
            self.master.minsize(900, 600)
            self.master.maxsize(900, 600)
            print(f"Showing details about nr. {self.num_rep}")
            self.mainframe = ttk.Frame(master)
            self.mainframe.pack()
    
            self.lbl_text = ttk.Label(self.mainframe,
                                      text=f"Showing details about nr. {self.num_rep}")
            self.lbl_text.pack()
    
    
    if __name__ == "__main__":
        root = tk.Tk()
        janela_principal = App(root)
        root.title('Main Window')
        root.bind_all("<Mod2-q>", exit)
        root.mainloop()