如何在Python的Tkinter中以编程方式打开菜单?

6
我有一个带有菜单栏的图形用户界面。我想要能够以编程方式打开这些菜单,就像用户单击它们一样。
我的第一个猜测是使用 "invoke",但这没有任何可见效果。 我知道可以使用 "tk_popup" 打开菜单,但我无法弄清楚坐标。 "yposition" 函数的返回值看起来并不有用。 奇怪的是,我甚至无法获取菜单栏的宽度 - 它始终为 1。
我知道可以使用 "underline" 将 menubutton 绑定到键事件,并且我可能可以通过编程方式创建这样的事件,但我真的不想这样做。
import Tkinter as tk

class MenuBar(tk.Menu):
     def __init__(self, root):
         tk.Menu.__init__(self, root)
         self.root = root
         self.menu_file = tk.Menu(m, tearoff=False)
         self.menu_file.label = 'File'
         self.menu_file.add_command(label='save')
         self.menu_file.add_command(label='open')

         self.menu_edit = tk.Menu(m, tearoff=False)
         self.menu_edit.label = 'Edit'
         self.menu_edit.add_command(label='add')
         self.menu_edit.add_command(label='remove')

         self.menus = (
             self.menu_file,
             self.menu_edit,
         )
         for menu in self.menus:
             self.add_cascade(label=menu.label, menu=menu, underline=0)

     def invoke(self, menu):
         if menu in self.menus:
             index = self.index(menu.label)
         else:
             index = menu
         print("invoke({!r})".format(index))
         tk.Menu.invoke(self, index)

     def open_menu(self, menu):
         x = self.root.winfo_rootx()
         y = self.root.winfo_rooty()
         print("yposition: {}".format(self.yposition(self.index(menu.label))))
         print("mb.width : {}".format(self.winfo_width()))
         print("mb.geometry: {}".format(self.winfo_geometry()))
         print("tk_popup({x},{y})".format(x=x, y=y))
         menu.tk_popup(x,y)
         pass

m = tk.Tk()
mb = MenuBar(m)
m.config(menu=mb)
m.update()
m.bind('f', lambda e: mb.invoke(mb.menu_file))
m.bind('e', lambda e: mb.invoke(mb.menu_edit))
m.bind('<Control-f>', lambda e: mb.open_menu(mb.menu_file))
m.bind('<Control-e>', lambda e: mb.open_menu(mb.menu_edit))
m.mainloop()

感谢您提前的帮助。
编辑: 我假设您,乔纳森,是指mb.menu_file.invoke(0)。如果我将tearoff设置为True,那么它可以工作,但这不是我想要的。因为它会在某个地方打开菜单(在我的情况下是屏幕左上角 - 远离窗口),必须通过在窗口右上角的关闭按钮上进行明确的点击来关闭它。
即使设置了tearoff=True,mb.invoke(mb.menu_file)仍然没有效果(除了打印“invoke(1)”)。

我对postcascade进行了一些研究,它听起来正是我需要的东西。但正如您已经指出的那样,它不起作用。tcl文档对此有所说明:“如果未发布路径名,则该命令除了取消任何当前发布的子菜单外没有任何效果。”事实上,如果我将m.config(menu=mb)替换为mb.update(); mb.post(m.winfo_rootx(), m.winfo_rooty()),它就可以工作了。(在这种情况下,我使用post而不是tk_popup,因为它应该保持打开状态。)它仍然不完美,因为我无法使用键盘控制子菜单;但是,无论如何,我想要的是一个菜单栏,而不是一个发布的菜单。

您知道“伪造”菜单会有什么缺点吗?我没有考虑过这样做,因为effbot指出:“由于此小部件尽可能使用本机代码,您不应尝试使用按钮和其他Tkinter小部件来伪造菜单。” (另一方面,这篇文章还建议使用post打开菜单,但我从this answer中学到,这并不是最好的方法-tk_popup更好。)该解决方案肯定会提供所需的灵活性。我目前看到的一个缺点是,将鼠标光标移动到下一个菜单时,该菜单不会打开。但应该可以处理。还有其他需要考虑的细节吗?
关于为什么:我希望用户能够完全自定义键盘快捷方式。因此,我需要一个可以绑定到用户选择的事件的函数。
另一个用例可能是实现帮助功能,该功能不仅告诉用户在哪里找到命令,而且还打开正确的菜单并选择该命令。这将使用户比自己搜索正确的菜单快得多。
1个回答

4
invoke命令相当于tearoff。如果你允许tearoff,你会看到它的作用。
你需要的命令是'postcascade'。Tkinter没有绑定这个命令,如果你手动调用它(root.tk.eval(str(mb)+' postcascade 1'),什么也不会发生,这可能就是为什么没有绑定的原因。
我尝试了很多其他方法,但我无法让它工作。
然而,tk还有一个Menubutton小部件,它响应<<Invoke>>事件。所以(如果你真的非常希望拥有这样的功能),你可以自己制作菜单栏:
import Tkinter as tk
import ttk

def log(command):
    print 'running {} command'.format(command)

class MenuBar(tk.Frame):
    def __init__(self, master=None):
        tk.Frame.__init__(self, master, bd=1, relief=tk.RAISED)

        file_btn = tk.Menubutton(self, text='File')
        menu_file = tk.Menu(file_btn, tearoff=False)
        menu_file.add_command(label='save', command=lambda: log('save'))
        menu_file.add_command(label='open', command=lambda: log('open'))
        file_btn.config(menu=menu_file)
        file_btn.pack(side=tk.LEFT)
        master.bind('f', lambda e: file_btn.event_generate('<<Invoke>>'))

        edit_btn = tk.Menubutton(self, text='Edit')
        menu_edit = tk.Menu(edit_btn, tearoff=False)
        menu_edit.add_command(label='add', command=lambda: log('add'))
        menu_edit.add_command(label='remove', command=lambda: log('remove'))
        edit_btn.config(menu=menu_edit)
        edit_btn.pack(side=tk.LEFT)
        master.bind('e', lambda e: edit_btn.event_generate('<<Invoke>>'))

m = tk.Tk()
m.geometry('300x300')
mb = MenuBar(m)
mb.pack(side=tk.TOP, fill=tk.X)
m.mainloop()

一旦使用热键打开菜单,可以使用箭头键进行导航,选定的选项可以使用回车键运行,或使用Esc键关闭。


我认为无法完全做到你想要的,这就是为什么我给了你一个虚假的方法。据我所知,唯一的缺点是它看起来更丑(因为它是tkinter而不是操作系统绘制),并且你必须自己布局。但它可以做到你想要的,包括键盘控制。 - Novel
为了获得不同的外观,您可以使用 ttk.Menubutton - Novel
当我将mb.edit_btn.event_generate('<<Invoke>>')(在类下更改为self.edit_btn)放在log下时,它可以正常工作一次,然后变得有缺陷,即使运行了打印语句也无法重复。 - Nae

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