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_generate
和 root.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>>')
)
...
使用 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_generate
和 root.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>>')
)
...