Search code examples
pythonuser-interfacetkintercontextmenu

tkinter app adding a right click context menu?


I have a python-tkinter gui app that I've been trying to find some way to add in some functionality. I was hoping there would be a way to right-click on an item in the app's listbox area and bring up a context menu. Is tkinter able to accomplish this? Would I be better off looking into gtk or some other gui-toolkit?


Solution

  • You would create a Menu instance and write a function that calls
    its post() or tk_popup() method.

    The tkinter documentation doesn't currently have any information about tk_popup().
    Read the Tk documentation for a description, or the source:

    library/menu.tcl in the Tcl/Tk source:

    ::tk_popup --
    This procedure pops up a menu and sets things up for traversing
    the menu and its submenus.
    
    Arguments:
    menu  - Name of the menu to be popped up.
    x, y  - Root coordinates at which to pop up the menu.  
    entry - Index of a menu entry to center over (x,y).  
            If omitted or specified as {}, then menu's  
            upper-left corner goes at (x,y).  
    

    tkinter/__init__.py in the Python source:

    def tk_popup(self, x, y, entry=""):
        """Post the menu at position X,Y with entry ENTRY."""
        self.tk.call('tk_popup', self._w, x, y, entry)
    

    You associate your context menu invoking function with right-click via:
    the_widget_clicked_on.bind("<Button-3>", your_function).

    However, the number associated with right-click is not the same on every platform.

    library/tk.tcl in the Tcl/Tk source:

    On Darwin/Aqua, buttons from left to right are 1,3,2.  
    On Darwin/X11 with recent XQuartz as the X server, they are 1,2,3; 
    other X servers may differ.
    

    Here is an example I wrote that adds a context menu to a Listbox:

    import tkinter # Tkinter -> tkinter in Python 3
    
    class FancyListbox(tkinter.Listbox):
    
        def __init__(self, parent, *args, **kwargs):
            tkinter.Listbox.__init__(self, parent, *args, **kwargs)
    
            self.popup_menu = tkinter.Menu(self, tearoff=0)
            self.popup_menu.add_command(label="Delete",
                                        command=self.delete_selected)
            self.popup_menu.add_command(label="Select All",
                                        command=self.select_all)
    
            self.bind("<Button-3>", self.popup) # Button-2 on Aqua
    
        def popup(self, event):
            try:
                self.popup_menu.tk_popup(event.x_root, event.y_root, 0)
            finally:
                self.popup_menu.grab_release()
    
        def delete_selected(self):
            for i in self.curselection()[::-1]:
                self.delete(i)
    
        def select_all(self):
            self.selection_set(0, 'end')
    
    
    root = tkinter.Tk()
    flb = FancyListbox(root, selectmode='multiple')
    for n in range(10):
        flb.insert('end', n)
    flb.pack()
    root.mainloop()
    

    The use of grab_release() was observed in an example on effbot.
    Its effect might not be the same on all systems.