首页 > 解决方案 > Tkinter:在 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该类将在单独的文件中定义,因此app全局对它不可用。在这种情况下,我可以MainMenu.master用来获取根窗口,但这会破坏松散耦合,因为它假定根窗口是此类的父级。这个假设可能会被打破(比如主窗口被放在汉堡菜单中,或者被添加到另一个TopLevel不是主应用程序的菜单中)。

标签: pythontkintermenutcl

解决方案


菜单由 OSX 和 Windows 上的操作系统管理,tkinter 几乎无法控制它们的行为。我怀疑将事件发送到其中任何一个平台上的菜单都会起作用。

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

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

例如:

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

推荐阅读