How can I execute different callbacks for different Tkinter [sub-] Menus? How can I execute different callbacks for different Tkinter [sub-] Menus? tkinter tkinter

How can I execute different callbacks for different Tkinter [sub-] Menus?


This was a really tricky problem, but I finally found a solution. After a lot of searching, numerous failed experiments, and more searching, I came across the virtual event <<MenuSelect>> and this pivotal line of code: print tk.call(event.widget, "index", "active"), pointed out by Michael O' Donnell, here.

The first weird part about trying to use this, is that event.widget isn't an instance of a widget object in this case, it's a tcl/tk path name string, e.g. '.#37759048L'. (This seems to be a bug in Tkinter, as even other virtual events I've tested -TreeviewSelect and NotebookTabChanged- include actual widget instances, as expected.) Regardless, the tcl/tk string can be used by the print tk.call(event.widget, "index", "active") command; that returns the index of the currently active menu item, which is huge.

The second issue that comes up with using the MenuSelect event is that it's called multiple times when traversing the menus normally. Clicking on a menu item calls it twice, and moving the mouse to a neighboring menu item, or moving the mouse to a submenu and then back to the main menu item, will also call it twice. Leaving the menu can as well. But this can be cleaned up nicely by adding a flag to the Menu classes and a little logic to the event handler. Here's the full solution:

import Tkinter as Tkimport timeclass firstMenu( Tk.Menu ):    def __init__( self, parent, tearoff=False ):        Tk.Menu.__init__( self, parent, tearoff=tearoff )        self.optionNum = 0 # Increments each time the menu is show, so we can see it update        self.open = False    def repopulate( self ):        print 'repopulating firstMenu'        # Clear all current population        self.delete( 0, 'last' )        # Add the new menu items        self.add_command( label='Option 1.' + str(self.optionNum+1) )        self.add_command( label='Option 1.' + str(self.optionNum+2) )        self.optionNum += 2class secondMenu( Tk.Menu ):    def __init__( self, parent, tearoff=False ):        Tk.Menu.__init__( self, parent, tearoff=tearoff )        self.optionNum = 0 # Increments each time the menu is show, so we can see it update        self.open = False    def repopulate( self ):        print 'repopulating secondMenu'        # Clear all current population        self.delete( 0, 'last' )        # Add the new menu items        self.add_command( label='Option 2.' + str(self.optionNum+1) )        self.add_command( label='Option 2.' + str(self.optionNum+2) )        self.optionNum += 2class Gui( object ):    def __init__( self ): # Create the TopLevel window        self.root = Tk.Tk()        self.root.withdraw() # Keep the GUI minimized until it is fully generated        self.root.title( 'Menu Tests' )        # Create the GUI's main program menus        self.menubar = Tk.Menu( self.root )        self.menubar.add_cascade( label='File', menu=firstMenu( self.menubar ) )        self.menubar.add_cascade( label='Settings', menu=secondMenu( self.menubar ) )        self.root.config( menu=self.menubar )        self.root.deiconify() # Brings the GUI to the foreground now that rendering is complete        # Add an event handler for activation of the main menus        self.menubar.bind( "<<MenuSelect>>", self.updateMainMenuOptions )        # Start the GUI's mainloop        self.root.mainloop()        self.root.quit()    def updateMainMenuOptions( self, event ):        activeMenuIndex = self.root.call( event.widget, "index", "active" ) # event.widget is a path string, not a widget instance        if isinstance( activeMenuIndex, int ):            activeMenu = self.menubar.winfo_children()[activeMenuIndex]            if not activeMenu.open:                # Repopulate the menu's contents                activeMenu.repopulate()                activeMenu.open = True        else: # The active menu index is 'none'; all menus are closed            for menuWidget in self.menubar.winfo_children():                menuWidget.open = Falseif __name__ == '__main__': Gui()

The end result is that each menu's code to generate its contents, via .repopulate(), is only called once, and only if that particular menu is actually going to be shown. The method isn't called again until the whole main menu is left and re-opened. Works when navigating via keyboard too.