Programmatically press the `X` button on the toobar?

Viewed 405

I know I can intercept pressing the X button with protocol("WM_DELETE_WINDOW", do_something) however I am having a hard time figuring out how to activate this button or at least the protocol that triggers when this button is pressed.

Here is the situation. I have 2 classes. My main Tk class and my Menu class. When I am setting up the command to close the program with an exit button from the menu I want this button to do exactly the same thing as the X button on the Tk class.

Now I know I could simply call the controller that was passed to the menu class and then call the method I built to handle the close event however I am trying to build this menu class in such a way that I do not need to do this from the menu class. This will allow me to use the menu class on any app I build with little to no editing.

I have not been able to find a post or some documentation that tells me how I can programmatically activate the "WM_DELETE_WINDOW" protocol.

Here is an image if it is unclear what I want. Simply I want the exit button to do exactly what the X button does.

enter image description here

Main class:

import tkinter as tk
import PIP_MENU


class PIP(tk.Tk):
    def __init__(self):
        super().__init__()
        PIP_MENU.start(self)
        self.protocol("WM_DELETE_WINDOW", self.handle_close)

    def handle_close(self):
        print("Closing")
        self.quit()

if __name__ == '__main__':
    PIP().mainloop()

Menu class on separate .py file:

import tkinter as tk

class Menu(tk.Menu):
    def __init__(self, controller):
        super().__init__()
        self.controller = controller
        controller.config(menu=self)
        file_menu = tk.Menu(self, tearoff=0)
        self.add_cascade(label="File", menu=file_menu)
        file_menu.add_command(label="Exit", command=self.handle_exit)

    def handle_exit(self):
        # What can I do here that will be handled by
        # protocol "WM_DELETE_WINDOW" of the main class?
        # All I can find is destroy() and quit()
        # But both of these do not get handled by "WM_DELETE_WINDOW".

def start(controller):
    Menu(controller)
3 Answers

I have not been able to find a post or some documentation that tells me how I can programmatically active the "WM_DELETE_WINDOW" protocol.

You can't. By definition, the WM_DELETE_WINDOW protocol comes from the window manager.

Catching the protocol handler is designed to give you an opportunity to override its behavior. It is not designed to be a way to trigger some code no matter how the application is destroyed. If you want to run some code when the window is destroyed, whether that is by the user clicking the control on the window frame or through some other way, the correct way to do that is to bind to the <Destroy> event on the root window.

You have to be careful, in that any binding on the root window will be triggered for every widget. Therefore, your binding should only run when event.widget is the same as the root window.

The following example illustrates the technique. There is a method handle_close which is called whenever the window is destroyed. Whether you close the window by clicking on the control on the window frame, or whether you click on the "Close me!" button, the code still runs.

import tkinter as tk


class Example(tk.Tk):
    def __init__(self):
        super().__init__()
        self.bind("<Destroy>", self.handle_close)

        button = tk.Button(self, text="Close me!", command=self.destroy)
        button.pack()

    def handle_close(self, event):
        if event.widget == self:
            print("Closing")
        self.quit()

example = Example()
example.mainloop()

I don't believe there's a method that invokes a specific protocol, since protocol seems to be a specific event watch. Here's a snippet from the module's class Tk:

class Tk(Misc, Wm):
    """Toplevel widget of Tk which represents mostly the main window
    of an application. It has an associated Tcl interpreter."""

    def _loadtk(self):

    ...

    self.protocol("WM_DELETE_WINDOW", self.destroy)

As you can see, by default the module itself sets the protocol to destroy(). The protocol() method only seeks to replace the default function (at the absence of a function, it just removes the function):

def wm_protocol(self, name=None, func=None):
    """Bind function FUNC to command NAME for this widget.
    Return the function bound to NAME if None is given. NAME could be
    e.g. "WM_SAVE_YOURSELF" or "WM_DELETE_WINDOW"."""
    if callable(func):
        command = self._register(func)
    else:
        command = func
    return self.tk.call(
        'wm', 'protocol', self._w, name, command)
protocol = wm_protocol

but to achieve what you want you should be able to reference back to the same handling method with this:

def handle_exit(self):
    self.controller.handle_close()

Of course, this is not as versatile since you must explicitly know the handler in your main window.

Thought I have accepted Bryan's answer I did manage to come to a workaround I think is fine here.

If I pass the method that is being used to deal with window closing to my menu class and then check if something has been passed I can then decide on weather or not to use the exit method I made or self.controller.destroy() with an if statement.

Here is my solution.

Main file:

import tkinter as tk
from tkinter import messagebox
import PIP_MENU


class PIP(tk.Tk):
    def __init__(self):
        super().__init__()
        PIP_MENU.start(self, self.handle_close)
        self.protocol("WM_DELETE_WINDOW", self.handle_close)

    def handle_close(self):
        x = messagebox.askquestion("DERP", "Do you want to close without saving?")
        if x == "yes":
            self.destroy()

if __name__ == '__main__':
    PIP().mainloop()

Menu file:

import tkinter as tk

class Menu(tk.Menu):
    def __init__(self, controller, exit_handler=None):
        super().__init__()
        self.controller = controller
        self.exit_handler = exit_handler
        controller.config(menu=self)
        file_menu = tk.Menu(self, tearoff=0)
        self.add_cascade(label="File", menu=file_menu)
        file_menu.add_command(label="Exit", command=self.handle_exit)

    def handle_exit(self):
        if self.exit_handler != None:
            self.exit_handler()
        else:
            self.controller.quit()


def start(controller, exit_handler=None):
    return Menu(controller, exit_handler)
Related