Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to open a menu programmatically in python tkinter?

Tags:

python

tkinter

I have a graphical user interface with a menubar. I would like to be able to open those menus programmatically as if a user had clicked on them.

My first guess was invoke but that has no visible effect. I know that I can open the menu using tk_popup but I can not figure out the coordinates. The return value of the yposition function does not look helpful. Strangely I can not even get the width of the menubar - it's always 1.

I know that I can bind a menubutton to a key event with underline and that I could probably create such an event programmatically but I really wouldn't want to do that.

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

Thanks in advance.

EDIT: I am assuming you, Jonathan, are referring to mb.menu_file.invoke(0). That works if I set tearoff to True, yes, but that is not what I am looking for. Because that opens the menu in a separate window somewhere (in my case in the top left corner of the screen - far away from the window) and it must be closed with an explicit click on the close button in the top right of the window.

Even with tearoff=True mb.invoke(mb.menu_file) has still no effect (other than printing "invoke(1)").

I have done some research on postcascade and it sounds exactly like what I am looking for. Except that - as you have pointed out already - it does not work. The tcl documentation says on that matter: "if pathName is not posted, the command has no effect except to unpost any currently posted submenu". And indeed if I replace m.config(menu=mb) by mb.update(); mb.post(m.winfo_rootx(), m.winfo_rooty()) it works. (I am using here post instead of tk_popup because in this case it is supposed to stay open.) It's still not perfect because I can not control the submenu with the keyboard; but, anyway, I want a menubar, not a posted menu.

Do you know what drawbacks it would have to "fake" the menu? I did not consider doing that because effbot states "Since this widget uses native code where possible, you shouldn’t try to fake menus using buttons and other Tkinter widgets." (On the other hand this article also suggests to open menus with post, which I have learned from this answer to not be the best way - tk_popup is better.) That solution would certainly provide the desired flexibility. One drawback I am seeing currently is that when moving the mouse cursor to the next menu that menu does not open. But it should be possible to handle that. Are there more details that I need to consider?

Regarding the why: I would like the user to be able to fully customize the keyboard shortcuts. Therefore I need a function which I can bind to the event which the user chooses.

Another usecase might be implementing a help functionality which does not only tell the user where to find a command but opens the correct menu and selects that command. Which would make it a lot faster for the user to find it than searching for the right menu himself.

like image 516
jakun Avatar asked Apr 11 '17 20:04

jakun


1 Answers

The invoke command is equivalent to tearoff. If you allowed tearoff you would see that work.

The command you are looking for is 'postcascade'. There is no tkinter binding to this command, and if you call it manually (root.tk.eval(str(mb)+' postcascade 1') nothing happens, which is probably why there is no binding.

I tried many other things but I could not get this to work.

However, tk has a Menubutton widget too, and that responds to the <<Invoke>> event. So (if you really really want this functionality) you can make your own menubar:

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

Once the menu is opened with the hotkey, it can be navigated with the arrow keys, the selected option can be run with the enter key, or closed with the escape key.

like image 79
Novel Avatar answered Nov 08 '22 06:11

Novel