'How to create a modal dialog in tkinter?

I have a MFC application which runs some embedded Python scripts. I am trying to make one of the dialogs this embedded script creates modal, but I am not having much success.

Can anyone point me the way to make a modal dialog? Do I need to use a windows functions for this or only Tk or Python functions are enough?

For what I googled looks like the following combination of functions should do the magic, but they dont seem to work the way I was expecting:

focus_set()

grab_set()

transient(parent)


Solution 1:[1]

grab_set is the proper mechanism for making a window "application modal". That is, it takes all input from all other windows in the same application (ie: other Tkinter windows in the same process), but it allows you to interact with other applications.

If you want your dialog to be globally modal, use grab_set_global. This will take over all keyboard and mouse input for the entire system. You must be extremely careful when using this because you can easily lock yourself out of your computer if you have have a bug that prevents your app from releasing the grab.

When I have the need to do this, during development I'll try to write a bulletproof failsafe such as a timer that will release the grab after a fixed amount of time.

Solution 2:[2]

In one of my projects I used the Tcl window manager attribute '-disabled' onto the parent window, that called a (modal) toplevel dialog window.

Don't know which windows you show with your MFC application are created or used with Tcl stuff, but if your parent window is Tk based you could do this:

In Python simply call onto the parent window inside the creation method of your toplevel window:

MyParentWindow.wm_attributes("-disabled", True)

After you got what you want with your modal window don't forget to use a callback function inside your modal window, to enable inputs on your parent window again! (otherwise you won't be able to interact with your parent window again!):

MyParentWindow.wm_attributes("-disabled", False)

A Tkinter (Tcl Version 8.6) Python example (tested on Windows 10 64bit):

# Python 3+
import tkinter as tk
from tkinter import ttk

class SampleApp(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        self.minsize(300, 100)
        self.button = ttk.Button(self, text="Call toplevel!", command=self.Create_Toplevel)
        self.button.pack(side="top")

    def Create_Toplevel(self):

        # THE CLUE
        self.wm_attributes("-disabled", True)

        # Creating the toplevel dialog
        self.toplevel_dialog = tk.Toplevel(self)
        self.toplevel_dialog.minsize(300, 100)

        # Tell the window manager, this is the child widget.
        # Interesting, if you want to let the child window 
        # flash if user clicks onto parent
        self.toplevel_dialog.transient(self)



        # This is watching the window manager close button
        # and uses the same callback function as the other buttons
        # (you can use which ever you want, BUT REMEMBER TO ENABLE
        # THE PARENT WINDOW AGAIN)
        self.toplevel_dialog.protocol("WM_DELETE_WINDOW", self.Close_Toplevel)



        self.toplevel_dialog_label = ttk.Label(self.toplevel_dialog, text='Do you want to enable my parent window again?')
        self.toplevel_dialog_label.pack(side='top')

        self.toplevel_dialog_yes_button = ttk.Button(self.toplevel_dialog, text='Yes', command=self.Close_Toplevel)
        self.toplevel_dialog_yes_button.pack(side='left', fill='x', expand=True)

        self.toplevel_dialog_no_button = ttk.Button(self.toplevel_dialog, text='No')
        self.toplevel_dialog_no_button.pack(side='right', fill='x', expand=True)

    def Close_Toplevel(self):

        # IMPORTANT!
        self.wm_attributes("-disabled", False) # IMPORTANT!

        self.toplevel_dialog.destroy()

        # Possibly not needed, used to focus parent window again
        self.deiconify() 


if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()

For more information about Tcl window manager attributes, just take a look at the Tcl documentation: https://wiki.tcl.tk/9457

Solution 3:[3]

Below is a minimal working example that runs on Mac 10.15.17, Python 3.8.2.

When run, it creates a main window with a single button. When that's clicked a "modal dialog" appears, and the main window is visible but disabled until the modal dialog is closed.

from tkinter import *

class simpledialog(object):
    def __init__(self, parent):
        # The "return value" of the dialog,
        # entered by user in self.entry Entry box.
        self.data = None

        self.root=Toplevel(parent)
        self.entry = Entry(self.root)
        self.entry.pack()
        self.ok_btn = Button(self.root, text="ok", command=self.ok)
        self.ok_btn.pack()

        # Modal window.
        # Wait for visibility or grab_set doesn't seem to work.
        self.root.wait_visibility()   # <<< NOTE
        self.root.grab_set()          # <<< NOTE
        self.root.transient(parent)   # <<< NOTE

        self.parent = parent

    def ok(self):
        self.data = self.entry.get()
        self.root.grab_release()      # <<< NOTE
        self.root.destroy()

class MainWindow:
    def __init__(self, window):
        window.geometry('600x400')
        Button(window, text='popup', command=self.popup).pack()
        Button(window, text='quit', command=self.closeme).pack()
        self.window = window
        window.bind('<Key>', self.handle_key)

    def handle_key(self, event):
        k = event.keysym
        print(f"got k: {k}")

    def popup(self):
        d = simpledialog(self.window)
        print('opened login window, about to wait')
        self.window.wait_window(d.root)   # <<< NOTE
        print('end wait_window, back in MainWindow code')
        print(f'got data: {d.data}')

    def closeme(self):
        self.window.destroy()

root = Tk()
app = MainWindow(root)
root.mainloop()
print('exit main loop')

The lines marked with # <<< NOTE all seemed to be necessary.

Cheers! jz

Edit 1: added the keyboard event handler handle_key to main, to show that the bind is still in effect correctly before and after the popup is opened.

Edit 2: you might also want to block a regular window close, otherwise the grab_release() isn't called ... if so, add self.root.protocol('WM_DELETE_WINDOW', self.ok) to the __init__ of the popup. For Mac users, there are some potential issues with that, see How to intercept WM_DELETE_WINDOW on OSX using Tkinter. :-( Alas. Software: a world where everything is just a little bit broken.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Bryan Oakley
Solution 2
Solution 3