0

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?

Victor Domingos
  • 1,003
  • 1
  • 18
  • 40
  • Can you elaborate what is the problem? I cannot reproduce your error on a windows computer. – Dashadower Apr 19 '17 at 14:43
  • On the Mac I am currently using, if I run this code and the click the button more than once to open two or more secondary windows, and if I click on one of the windows in the backgroung (I mean for instance the first secondary window), the keyboard shortcut does not work as expected. I get another window (the last opened one) closing. Then the first secondary window does not close unless I quit the whole app. – Victor Domingos Apr 19 '17 at 14:48

3 Answers3

2

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()
Mia
  • 2,466
  • 22
  • 38
  • It solves the issue. I really need to take a deeper look into how this works :) – Victor Domingos Apr 19 '17 at 15:14
  • In your answer, the close event callback has been discarded.but if I understand correctly, I can now safely move the `self.newDetailsWindow[self.windows_count].destroy` statement into that method (adding the parentheses of course) and replace the `wm_protocol` call with a call to my method? – Victor Domingos Apr 20 '17 at 03:27
  • Also, it seems that the dictionary is growing but never gets its items removed. Maybe we should be doing like @Dashadower suggested, removing the references and adjusting the window count accordingly in the window close method. Or does the `destroy()` method ensure also that cleanup in the dictionary itself? – Victor Domingos Apr 20 '17 at 03:37
  • The thing is, if you are not planning to use `lambda` function, you don't even need a `dict` to store variables, as long as you passed the right `function` object to `wm_protocol` – Mia Apr 20 '17 at 11:15
  • In your original version, if you write `self.newDetailsWindow.wm_protocol("WM_DELETE_WINDOW",self.close_detail_window)`, it still works. But the windows conting may be wrong. However, if you want to bind any key to the window, you have to use something to store all the `Toplevel`s. By the way, `Toplevel` doesn't need a master. – Mia Apr 20 '17 at 11:18
  • Do you mean I can write `self.newDetailsWindow[self.windows_count]=tk.Toplevel()`? I recognize I don't understand very well the cases we need or not a master. I have seen this kind of code, so I assumed it was a pattern. But I am probably wrong, or picked that up in a wrong way. I would appreciate any further correction. Don't we need a master as a reference to be able to manipulate widgets in other windows, for instance? I mean, things like adding a button in one window that opens or closes another window, according to its current state. Or changing a label in another window. – Victor Domingos Apr 20 '17 at 12:07
  • `Toplevel` windows have a master. Like all other widgets, if you don't supply one, tkinter will use the root window as the master. The only widget that does not have a master is the root window. – Bryan Oakley Nov 08 '18 at 17:53
1

It looks like using self.newDetailsWindow to make a new toplevel garbage collects existing toplevels. I have added a list class variable in App which is a list of the Toplevels.

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.windows = [] #This is a list of the created windows instances.

    def create_detail_window(self, *event, number=None):
        newDetailsWindow = tk.Toplevel(self.master)
        self.windows.append(newDetailsWindow)

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


    def close_detail_window(self, window):
        """ will test for some condition before closing, save if necessary and
        then call destroy()
        """
        self.windows.remove(window)
        window.destroy() # destroy the specific instance in self.windows
Dashadower
  • 632
  • 1
  • 6
  • 20
0

As mentioned in the comments, in your case, the following code still works.

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))
        self.btn.pack()
        self.windows_count=0

    def create_detail_window(self, *event):
        self.windows_count+=1
        newDetailsWindow=tk.Toplevel()
        newDetailsWindow.geometry('900x600+80+130')
        newDetailsWindow.title(f'Detail: {self.windows_count}')
        newDetailsWindow.wm_protocol("WM_DELETE_WINDOW", newDetailsWindow.destroy)
        self.detail_window = detailWindow(newDetailsWindow, self.windows_count)
        newDetailsWindow.focus()


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()
Mia
  • 2,466
  • 22
  • 38