Search code examples
pythonpython-3.xtkinterdialog

Python/tkinter: Pressing OK in dialog destroys the info in it


I'm using Python/tkinter for a GUI and I found a strange behaviour.

I'm building a very small dialog for selecting the language for my app. Then when closing it, I find that my 'result' variable in the dialog is gone.

Here's the snippet:

import tkinter as tk
from tkinter.simpledialog import Dialog
from tkinter import ttk, LEFT, ACTIVE
from tkinter.ttk import Button, Frame

class LanguageDialog(Dialog):
    lang_dict = {
        'italiano': 'it',
        'español': 'es',
        'english': 'en',
        'galego': 'gl',
    }

    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        self.selected_language = tk.StringVar()
        self.result = 'en'  # default value

    def _on_cmb_change(self, event):
        """
            Keeps updated the result variable with the code I want in the end
        """
        print(self.lang_dict[self.selected_language.get()])
        self.result = self.lang_dict[self.selected_language.get()]

    def body(self, master):
        self.selected_language = tk.StringVar()
        ops = tuple(self.lang_dict.keys())
        cmb_lang = ttk.Combobox(self, values=ops, state='readonly',
                                textvariable=self.selected_language)
        cmb_lang.pack(side=tk.TOP)

        cmb_lang.bind('<<ComboboxSelected>>', self._on_cmb_change)

    def buttonbox(self):
        """add standard button box.

        override if you do not want the standard buttons
        """

        box = Frame(self)

        w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
        w.pack(side=LEFT, padx=5, pady=5)

        self.bind("<Return>", self.ok)

        box.pack()

if __name__ == '__main__':
    root = tk.Tk()
    d = LanguageDialog(root)
    print(f'After the dialog, {d.result}')
    root.mainloop()

I believe I have some misconception on how to use the dialog. While debugging I saw the dialog triggers some destroy() method, but it seems like it is probably coming again to the initializer and executing the

self.result = 'en'

line, even though I didn't create or invoke the dialog again through LanguageDialog().

I searched the web for similar examples and found that they are using the dialog in the same way, for example, here


Solution

  • If you look into the source code of simpledialog.Dialog class, you will find that wait_window() is executed at the end of Dialog.__init__() which tries to make the window like a modal dialog.

    So super().__init__(...) inside LanguageDialog.__init__() will not return until the dialog is closed. When the dialog is closed, self.result is reset to 'en'.

    You should move the line, self.result = 'en' into the beginning of body() (just like self.selected_language) and then remove the __init__() function.

    Below is the modified code:

    import tkinter as tk
    from tkinter.simpledialog import Dialog
    from tkinter import ttk, LEFT, ACTIVE
    from tkinter.ttk import Button, Frame
    
    class LanguageDialog(Dialog):
        lang_dict = {
            'italiano': 'it',
            'español': 'es',
            'english': 'en',
            'galego': 'gl',
        }
    
        def _on_cmb_change(self, event):
            """
                Keeps updated the result variable with the code I want in the end
            """
            print(self.lang_dict[self.selected_language.get()])
            self.result = self.lang_dict[self.selected_language.get()]
    
        def body(self, master):
            self.selected_language = tk.StringVar()
            # initial self.result to 'en' here
            self.result = 'en'  # default value
    
            ops = tuple(self.lang_dict.keys())
            cmb_lang = ttk.Combobox(self, values=ops, state='readonly',
                                    textvariable=self.selected_language)
            cmb_lang.pack(side=tk.TOP)
    
            cmb_lang.bind('<<ComboboxSelected>>', self._on_cmb_change)
    
        def buttonbox(self):
            """add standard button box.
    
            override if you do not want the standard buttons
            """
    
            box = Frame(self)
    
            w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE)
            w.pack(side=LEFT, padx=5, pady=5)
    
            self.bind("<Return>", self.ok)
    
            box.pack()
    
    if __name__ == '__main__':
        root = tk.Tk()
        root.withdraw() # hide the root window
        d = LanguageDialog(root)
        print(f'After the dialog, {d.result}')
        #root.mainloop() # don't need to call mainloop()