1

In the following code, when I press the button to add some secondary windows, and then try to close a window by using "Command-w" it does not always close the active window. But if I disable the menu creation by commenting the line self.gerar_menu(), windows are opened and closed as expected (I mean, by clicking the red 'x' button or by pressing Command-W in OS X). 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.gerar_menu()  # This line breaks "Command-w" functionality

        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 gerar_menu(self):
        """ generate the application menu """
        self.menu = tk.Menu(root)
        root.config(menu=self.menu)

        self.fileMenu = tk.Menu(self.menu)
        self.menu.add_cascade(label="File", menu=self.fileMenu)
        self.fileMenu.add_command(label="New Document", command=None, accelerator="Command+n")


    def create_detail_window(self, *event, number=None):
        self.windows_count += 1
        self.newDetailsWindow[self.windows_count]=tk.Toplevel()
        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)


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()
Victor Domingos
  • 1,003
  • 1
  • 18
  • 40
  • Cannot reproduce. I don't know what mod2 is in tkinter, but neither ctrl nor alt nor shift nor super/windows + Q do anything. But if I change the hotkey to something that works, it works with and without the menu. (Where "works" means "it throws SystemExit, but the Window stays open until python exits".) – Aran-Fey Apr 21 '17 at 10:15
  • I am running this in macOS, the mod2 represents here the Command key in Apple keyboards. Also, notice that the issue happens when trying to close a secondary window. I have edited my question to better reflect that. – Victor Domingos Apr 21 '17 at 10:17
  • Why are you using `wm_protocol` here, if all you have it do is destroy the window? Also, you are making a binding to a specific window that may delete some other window. Is there a reason for that, or are you intending for the binding to delete the window that processes the event? – Bryan Oakley Apr 21 '17 at 12:53
  • Finally, your event is incorrect. `Command-w` is not a proper event. You should be using ``. – Bryan Oakley Apr 21 '17 at 12:53
  • @BryanOakley This is a small version of the application for testing purposes. I want to be able later to call a function whenever the user presses command-n and or pushes the cross button – Victor Domingos Apr 21 '17 at 13:07
  • @BryanOakley How can I make a binding that does not affect other windows? – Victor Domingos Apr 21 '17 at 13:09
  • @BryanOakley my intention is to let the user delete a window, I mean, the current active window by pressing the keyboard shortcut or the button. – Victor Domingos Apr 21 '17 at 13:10
  • Then have your binding delete the window that gets the binding. You are passed an event object (which you are ignoring) which will tell you which window received the event. – Bryan Oakley Apr 21 '17 at 13:12

1 Answers1

0

If you want to create a binding for closing a window, you need the function to act upon the actual window that received the event. Your code is always deleting the last window that was opened no matter which window received the event.

The first step is to bind to a function rather than using lambda. While lambda has its uses, binding to a named function is much easier to debug and maintain.

Once the function is called, the event object can tell you which window got the event via the widget attribute of the event object. Given this window, you can get the toplevel window that this window is in (or itself, if it's a toplevel window) via the winfo_toplevel command.

For example:

window = tk.Toplevel(...)
...
window.bind("<Command-w>", self.destroy_window)
...
def destroy_window(self, event):
    window = event.widget.winfo_toplevel()
    window.destroy()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Ok, a step closer to get this issue solved. But I am having trouble inserting this in my sample code. I still need to keep the list of `Toplevel()` windows references, right? – Victor Domingos Apr 21 '17 at 14:43
  • @VictorDomingos: I don't know if you do or don't. You don't for this piece of code to work. – Bryan Oakley Apr 21 '17 at 15:54
  • Well, let me put a little more context here. I am starting what is for me a long tkinter project. Basically, it will have a main app window, some Toplevels that can be opened opened and closed by pressing a button in the main window or by a menu command, for instance. Those Toplevel windows will have forms that may communicate with other forms in the main window, for instance adding the recently created contact information to another form that is being edited. Then, I'll have some simpler windows that show some information, like a single database record, with less interaction with the rest. – Victor Domingos Apr 21 '17 at 16:00
  • I could provide a link to a github project with source code, but maybe it's out of the scope of StackOverflow (and probably I would be abusing your kind and patient helpfulness). – Victor Domingos Apr 21 '17 at 16:02
  • I have been trying to separate the code related to each main kind of window, but maybe it's a better choice to have all the bindings done in the main window class, since the bindings propagate all over to the root. It gets a little more complicated than what I expected but I will try that way. Indeed, I was not being aware that I could use the event object. I guess that will make things a lot easier. – Victor Domingos Apr 24 '17 at 07:21
  • @VictorDomingos: I don't quite understand what you wrote. If you add a binding to a specific widget such as a toplevel, it absolutely will _not_ "propagate all over to the root". – Bryan Oakley Apr 24 '17 at 11:26
  • I have read this in a book about tkinter: "Most keyboard and mouse events occur at the operating system level. It propagates hierarchically upwards from the source of the event until it finds a window that has the corresponding binding. The event propagation does not stop there. It propagates itself upwards, looking for other bindings from other widgets, until it reaches the root window. If it does reach the root window and no bindings are discovered by it, the event is disregarded". I recognize I don't fully understand when it propagates or not. – Victor Domingos Apr 24 '17 at 11:42
  • @VictorDomingos: Ok, yes, that is a correct description. I was more concerned about your use of "all over". Once the event is handled by your widget, it may also be handled by the root widget (or more correctly, handled by other bind tags, one of which is the root window). However, if you don't have any bindings to the same event at the root level, nothing will happen. A more thorough description of how events are processed is here: http://stackoverflow.com/a/11542200/7432. The description is for a keyboard event, but mouse events are the same. – Bryan Oakley Apr 24 '17 at 12:23
  • Would you say that for window management related like in this case, it is better to bind the keyboard shortcuts to the root and then let a function or method catch the event object and see what was the active window and then act accordingly? – Victor Domingos Apr 24 '17 at 14:02
  • @VictorDomingos: bind to root if you want the binding to fire no matter what window or widget has focus. Bind to individual widgets if you want only that widget to handle the binding. – Bryan Oakley Apr 24 '17 at 14:12
  • In the case of Toplevel(), if I bind to it will it fire anytime that specific window if the active one or can be some cases where that does not happen? Also, is there anything specific to Mac with regard to the menu keyboard shortcut bindings? I have the feeling it is not the first time I have the menu bindings interfering with other bindings. – Victor Domingos Apr 24 '17 at 14:17