如何在tkinter中创建一个模态对话框?

11

我有一个运行一些嵌入的Python脚本的MFC应用程序。我试图使其中一个对话框成为模态对话框,但是我没有取得太大的成功。

有人能指点我如何制作模态对话框吗?我需要使用Windows函数还是仅需要使用Tk或Python函数?

根据我的搜索结果,以下函数组合似乎可以实现所需的效果,但它们似乎不像我期望的那样工作:

focus_set()

grab_set()

transient(parent)

w.grab_set()(或者更粗鲁的 w.grab_set_global() )应该就可以了。在这里搜索“模态”即可。 - martineau
@martineau 刚刚尝试了一下,但没有起作用。我仍然可以访问基础的 MFC 应用程序。还有其他想法吗? - Mac
4个回答

18

grab_set 是使窗口“应用程序模态”的正确机制。也就是说,它会接收同一进程中其他 Tkinter 窗口的所有输入,但允许你与其他应用程序交互。

如果您想要您的对话框成为全局模态,请使用 grab_set_global。这将接管整个系统的所有键盘和鼠标输入。使用时必须非常小心,因为如果您的应用程序存在无法释放占据的情况,您很容易被锁定在计算机上。

当我有这个需求时,在开发过程中,我会尝试编写一个弹性保险措施,例如一个计时器,该计时器将在固定时间后释放占据。


这对我不起作用(Python3,Tkinter 8.6,Debian)。非模态窗口中的点击事件似乎被缓存,并在grab_release()之后的事件中发生。 - buhtz

5
在我的一个项目中,我在父窗口上使用了Tcl窗口管理器属性“-disabled”,这调用了一个(模态)顶层对话框窗口。
不知道您的MFC应用程序显示哪些窗口是使用Tcl工具创建或使用的,但如果您的父窗口基于Tk,则可以这样做:
在Python中,只需在创建顶层窗口的方法内调用父窗口即可:
MyParentWindow.wm_attributes("-disabled", True)

在您使用模态窗口获得所需内容后,不要忘记在模态窗口内使用回调函数,以使父窗口上的输入再次可用!(否则,您将无法再与父窗口进行交互!):
MyParentWindow.wm_attributes("-disabled", False)

一个 Tkinter (Tcl 版本 8.6) 的 Python 示例(在 Windows 10 64 位系统上测试通过):

# 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()

如果想了解更多关于Tcl窗口管理器属性的信息,可以查看Tcl文档:https://wiki.tcl.tk/9457


3

以下是一个在 Mac 10.15.17,Python 3.8.2 上运行的简单示例。

运行后,它将创建一个带有一个按钮的主窗口。当单击该按钮时,会出现一个“模态对话框”,并且在模态对话框关闭之前,主窗口不可用但仍可见。

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')

被标记为# <<< NOTE的这些行似乎是必要的。

干杯! jz

编辑1: 在主函数中添加了键盘事件处理程序handle_key,以显示在打开弹出窗口之前和之后bind仍然有效。

编辑2: 您可能还想阻止常规窗口关闭,否则grab_release()不会被调用...如果是这样,请将self.root.protocol('WM_DELETE_WINDOW',self.ok)添加到弹出窗口的__init__中。 对于Mac用户,这可能存在一些潜在问题,请参见如何使用Tkinter拦截OSX上的WM_DELETE_WINDOW。 :-( 唉。 软件:一个世界,一切都有点出毛病。


0

你可以添加以下代码来创建一个模态窗口:

 window.wm_attributes("-topmost", True) #always on top
 window.grab_set_global()               # inputs only this window

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接