-1

Is there a definitive attribute of Tk that I can check to see if either the mainloop has stopped running or if the root window has been destroyed?

The minimal code below shows a problem resulting from Tk’s apparent failure to propagate python exceptions. To see the problem in action, click on the root window button, “Start The Child Window Dialog”. Next, close the root window using its close window button (Red X).

import sys
import tkinter as tk

class ProgramIsEnding(Exception):
    pass


class UnrecognizedButtonException(Exception):
    pass


class MainWindow(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('Root Window')
        button = tk.Button(text='Start The Child Window Dialog')
        button.configure(command=self.run_dialog)
        button.grid()
        self.protocol('WM_DELETE_WINDOW', self.delete_window_callback)

    def delete_window_callback(self):
        self.destroy()
        print('Root has been destroyed')
        raise ProgramIsEnding

    def run_dialog(self):
        try:
            button = YesNoDialog(self)()
        except ProgramIsEnding:
            print('Doing end of program stuff.')
            return

        print(f"Button returned is '{button}'")
        if button == 'yes':
            print("'Yes' button clicked")
        elif button == 'no':
            print("'No' button clicked")
        else:
            msg = f"button '{button}'"
            raise UnrecognizedButtonException(msg)


class YesNoDialog:
    window: tk.Toplevel = None
    button_clicked = None

    def __init__(self, parent):
        self.parent = parent

    def __call__(self):
        self.create_window()
        return self.button_clicked

    def create_window(self):
        self.window = tk.Toplevel(self.parent)
        yes = tk.Button(self.window, text='Yes', command=self.yes_command)
        yes.pack(side='left')
        no = tk.Button(self.window, text='No', command=self.no_command)
        no.pack(side='left')
        self.window.wait_window()

    def yes_command(self):
        self.button_clicked = 'yes'
        self.window.destroy()

    def no_command(self):
        self.button_clicked = 'no'
        self.window.destroy()

def main():
    tkroot = MainWindow()
    tkroot.mainloop()


if __name__ == '__main__':
    sys.exit(main())

If the code was working as intended it would terminate correctly having caught the exception, “ProgramIsEnding”. Instead the program terminates with an unhandled “UnrecognizedButtonException”. The full error message follows. Note that the “ProgramIsEnding” exception has been reported via stdout even though it was not available to the try/except handler after control has been handed back to python from Tk.

Root has been destroyed
Exception in Tkinter callback
Traceback (most recent call last):
  File "[…]/python3.7/tkinter/__init__.py", line 1702, in __call__
    return self.func(*args)
  File "[…]/wmdeletedemo.py", line 25, in delete_window_callback
     raise ProgramIsEnding
ProgramIsEnding
Exception in Tkinter callback
Traceback (most recent call last):
  File "[…]/python3.7/tkinter/__init__.py", line 1702, in __call__
    return self.func(*args)
  File "[…]/wmdeletedemo.py", line 41, in run_dialog
    raise UnrecognizedButtonException(msg)
UnrecognizedButtonException: button 'None'
Button returned is 'None'

An obvious workaround is to check if the button value is None and, if so, return. For me, though, good practice suggests that I should check the primary event and neither rely on its secondary effects nor on setting flags.

So, is there some other attribute of Tk or tkinter which records the ending of mainloop or if the root window has been destroyed?

lemi57ssss
  • 1,287
  • 4
  • 17
  • 36
  • It is simple enough. Just `destroy` whatever the root window is. If the tk instance is started by a class that inherits from `tk()` then you can simply do `self.destroy()` or you need to destroy the tk instance like `root.destory()`. This will insure the instance is closed. If you need to really check if GUI is closed then you can simple put a print statement after the `mainloop()`. – Mike - SMT Sep 10 '18 at 12:15
  • Related: https://stackoverflow.com/q/20039096/ – fhdrsdg Sep 10 '18 at 12:20
  • @Goyo. Thank you. Clarified in post. – lemi57ssss Sep 11 '18 at 11:21
  • After Tk is finished the child window should have been already destroyed, I don't see how it could be attempting to interact with anything. Care to write a [mcve]? Your description does not make much sense to me. – Stop harming Monica Sep 11 '18 at 11:27
  • @Goyo. All windows including the child window have been destroyed. The python code controlling the child window – which was invoked from Tk via the menu – is still running: It has a 'while True' loop waiting for user input. I did consider a 'Minimal, Complete and Verifiable' example but it turned out to have lots of complicated moving parts. I thought it would be easier to simply request a definitive attribute of Tk if it exists. I’ve added an example of how my possibly unreliable solution would be implemented. This appears to work.. – lemi57ssss Sep 11 '18 at 12:13
  • 1
    @lemi57ssss Checking for the existence of the root window is either unnecesary (if the code is blocking and it was invoked from the root you know the root exists) or brittle (the root could exist when you check and not exist when you start to process buttons and validate fields). – Stop harming Monica Sep 11 '18 at 13:16
  • @Goyo. Yes. Agreed. Would you like to add that as an answer, possibly with a mention of using the 'destroy' event with a handler? – lemi57ssss Sep 11 '18 at 16:13
  • @lemi57ssss As I said I can't make much sense of your question other that you want a way to tell whether the root window has been destroyed. But you already know how to do it and I can't think of any better ways. – Stop harming Monica Sep 11 '18 at 17:19
  • @Goyo. Minimal, Complete, and Verifiable example as requested. – lemi57ssss Sep 13 '18 at 12:54
  • @lemi57ssss I didn't find a way to catch the exception that way. Apparently tkinter makes it very hard if not impossible. But I don't understand your objections. Your exception is a secondary effect of the primary event too. AFAIK tkinter doesn't record that in an attribute but you can do it by yourself. Why would be OK for tkinter to do it but not for you? It is at least debatable that raising an exception to signal that a widget has been destroyed is good practice. And it is as unreliable as any other method, for the same reasons. Well, it would be if you were able to make it work. – Stop harming Monica Sep 13 '18 at 20:30
  • @Goyo. Your criticism is accurate and agreed. Realizing that fact and worrying at the consequences led me to the answer which I have placed below. Thank you. – lemi57ssss Sep 15 '18 at 11:23

2 Answers2

0

You can set any variable to true inside your delete callback method. Then you can simply check for that variable at the right places. Something like this.

def _delete_window_callback(self):
    """Carry out actions needed when main window is closed."""

    # Save the current geometry
    geometry = self.winfo_geometry()
    config.config_dynamic.set(self.config_section, self.config_geometry, geometry)

    destroyed = true

    # Destroy all widgets and end mainloop.
    self.destroy()

destroyed = false
if destroyed:
    break
Rachit Tayal
  • 1,190
  • 14
  • 20
0

The short answer is that Tk does not set an attribute. Tkinter handles this by offering a <Destroy> event for every widget.

The corrected code below has these three features:

  • Binding tkinter’s <Destroy> event to the child window’s handler (destroy_callback) insulates the caller and the programmer from any need to understand the various things that might lead to Tk closing the child window. The programmer is thus freed to concentrate on dealing with the effects of the closure.

  • A TkClosedWindow exception is raised when YesNoDialog.__call__ exits. This can be handled at any appropriate place in the call chain for MainWindow.run_dialog. If any code which is written later fails to handle the exception the program will fail-fast with an explicit error message.

  • The event handler sets a flag to indicate that YesNoDialog.window has been closed. A python exception cannot be raised inside this handler because Tk/Tcl does not propagate python exceptions.

.

import sys
import tkinter as tk


class TkClosedWindow(Exception):
    pass


class UnrecognizedButtonException(Exception):
    pass


class MainWindow(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('Root Window')
        button = tk.Button(text='Start The Child Window Dialog')
        button.configure(command=self.run_dialog)
        button.grid()

    def run_dialog(self):
        try:
            button = YesNoDialog(self)()
        except TkClosedWindow:
            print('Doing end of program stuff.')
            return

        print(f"Button returned is '{button}'")
        if button == 'yes':
            print("Yes button clicked")
        elif button == 'no':
            print("No button clicked")
        else:
            msg = f"button '{button}'"
            raise UnrecognizedButtonException(msg)


class YesNoDialog:
    window: tk.Toplevel = None
    button_clicked = None
    closed_by_tk = False

    def __init__(self, parent):
        self.parent = parent

    def __call__(self):
        self.create_window()
        if self.closed_by_tk:
            raise TkClosedWindow
        else:
            return self.button_clicked

    def create_window(self):
        self.window = tk.Toplevel(self.parent)
        self.window.bind('<Destroy>', self.destroy_callback)
        yes = tk.Button(self.window, text='Yes', command=self.yes_command)
        yes.pack(side='left')
        no = tk.Button(self.window, text='No', command=self.no_command)
        no.pack(side='left')
        self.window.wait_window()

    def yes_command(self):
        self.button_clicked = 'yes'
        self.window.destroy()

    def no_command(self):
        self.button_clicked = 'no'
        self.window.destroy()

    def destroy_callback(self, *args):
        self.closed_by_tk = True

def main():
    tkroot = MainWindow()
    tkroot.mainloop()


if __name__ == '__main__':
    sys.exit(main()) 
lemi57ssss
  • 1,287
  • 4
  • 17
  • 36