Tkinter:在 Windows 上使用带有菜单的自定义事件

Tkinter: using custom events with a menu on Windows

使用 Python 3.9.4

为了松耦合,我试图在 Tkinter 中实现一个主菜单,它生成自定义事件而不是直接调用回调函数。示例脚本显示了基本方法:

import tkinter as tk


root = tk.Tk()
root.geometry('300x200')
label = tk.Label(text='Test')
label.grid()
menu = tk.Menu(root)

root.configure(menu=menu)

submenu = tk.Menu(menu)
menu.add_cascade(menu=submenu, label='Change Text')
submenu.add_command(
    label='Foo',
    command=lambda: menu.event_generate('<<Foo>>')
)
submenu.add_command(
    label='Bar',
    command=lambda: menu.event_generate('<<Bar>>')
)

def setfoo(*_):
    label.configure(text='Foo')

def setbar(*_):
    label.configure(text='Bar')

menu.bind('<<Foo>>', setfoo)
menu.bind('<<Bar>>', setbar)

root.mainloop()

此方法在 Linux 中有效,但在 Windows 中绑定无效。它们似乎绑定到事件,但在选择菜单项时没有任何反应。

我假设这与 Windows 和 Linux 上的菜单实现之间的差异有关,但是否有正确的方法或解决方法?

编辑:为了帮助每个人更好地理解为什么我想做某些事情,这里有一个面向对象的版本,它更符合我使用 Tkinter 的方式:

import tkinter as tk

class MainMenu(tk.Menu):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        submenu = tk.Menu(self)
        submenu.add_command(
            label="foo",
            command=lambda: self.event_generate('<<Foo>>')
        )
        submenu.add_command(
            label="bar",
            command=lambda: self.event_generate('<<Bar>>')
        )
        self.add_cascade(menu=submenu, label='Test')


class Application(tk.Tk):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        menu = MainMenu(self)
        self.configure(menu=menu)

        menu.bind('<<Foo>>', print)
        menu.bind('<<Bar>>', print)

if __name__ == '__main__':
    app = Application()
    app.mainloop()

请注意,MainMenu class 将在单独的文件中定义,因此 app 全局变量对它不可用。在这种情况下,我可以使用 MainMenu.master 来获取根 window,但这打破了松耦合,因为它假定根 window 是此 class 的父级。这个假设可能会被打破(比如 mainwindow 被放入汉堡菜单,或者添加到另一个不是主应用程序的 TopLevel)。

我想我明白了,但不是 100% 确定。

看这段代码:

import tkinter as tk

def trying_to_reach(event):
    print("!")

root = tk.Tk()

text = tk.Text(root)
text.pack()

text.bind("<<Foo>>", trying_to_reach)
root.after(100, text.event_generate, "<<Foo>>")

root.mainloop()

100 毫秒后代码是 运行,! 出现在屏幕上,但我评论 text.pack(),屏幕上没有显示任何内容。另外,如果我将 root.after(100, text.event_generate, "<<Foo>>") 更改为 text.event_generate("<<Foo>>"),屏幕上不会显示任何内容。这证明了@BryanOakley 的理论,即它与小部件的可见性有关。这意味着问题很可能出在 tcl/ 函数 tcl 调用中的某处。

解决方案是使用一个虚拟小部件,或者按照@Matiiss 的建议只使用 root.event_generateroot.bind

菜单由 OS X 和 Windows 上的 OS 管理,tkinter 几乎无法控制它们的行为。我怀疑将事件发送到这两个平台上的菜单是否可行。

我会说向菜单发送事件是一种反模式。除非事件与菜单直接相关,否则您应该将事件发送到与菜单关联的顶级 window、根 window 或具有键盘焦点,取决于事件代表什么。

由于您将父小部件作为位置参数传递,因此您可以使用它来获取与菜单关联的顶层 window。

例如:

class MainMenu(tk.Menu):

    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)

        top = parent.winfo_toplevel()
        submenu.add_command(
            label="foo",
            command=lambda: top.event_generate('<<Foo>>')
        )
        ...