如何为不同的Tkinter [sub-]菜单执行不同的回调?

时间:2019-04-18 21:16:24

标签: python-2.7 tkinter callback menu submenu

我有一个带有主菜单栏的Tkinter GUI,使用Tkinter的Menu小部件。我想在发布子菜单(通过.add_cascade()从其级联的另一个菜单项)之前执行代码,以便在显示之前可以动态更改其内容。我使用Menu的postcommand参数来完成这项工作,但是我注意到使用它的效率很低;单击任何一个子菜单时,将调用 all 子菜单的后命令回调,而不仅仅是创建具有回调的特定子菜单。即使没有创建任何子菜单,即使单击没有菜单项的菜单栏,也会执行所有回调。

“菜单”模块及其后命令自变量是否具有这种预期行为?我不明白为什么在为下拉菜单创建单独的Menu实例后仍然会发生这种情况。

我已经尝试挂钩到Tk.Menu的本机方法,但是当仅单击菜单栏项以调出级联Menu时,都不会调用它们。即使.add_cascade()接受一个'command'参数,它也仅在不包括.add_cascade()'menu'参数或它是lambda表达式的情况下才调用该对象提供的可调用对象(两者均不导致子菜单在您单击该项目时显示)。 (您可以在下面的test()函数中看到它。)

这是一个显示此行为的简单应用程序:

import Tkinter as Tk
import time


def test(): print 'test'

class firstMenu( Tk.Menu ):

    def __init__( self, parent, tearoff=False ):
        Tk.Menu.__init__( self, parent, tearoff=tearoff, postcommand=self.repopulate )

    def repopulate( self ):
        print 'repopulating firstMenu'
        time.sleep( 2 ) # Represents some thinking/processing

        # Clear all current population
        self.delete( 0, 'last' )

        # Add the new menu items
        self.add_command( label='Option 1.1' )
        self.add_command( label='Option 1.2' )


class secondMenu( Tk.Menu ):

    def __init__( self, parent, tearoff=False ):
        Tk.Menu.__init__( self, parent, tearoff=tearoff, postcommand=self.repopulate )

    def repopulate( self ):
        print 'repopulating secondMenu'
        time.sleep( 2 ) # Represents some thinking/processing

        # Clear all current population
        self.delete( 0, 'last' )

        # Add the new menu items
        self.add_command( label='Option 2.1' )
        self.add_command( label='Option 2.2' )


class Gui( object ):

    def __init__( self ): # Create the TopLevel window
        root = Tk.Tk()
        root.withdraw() # Keep the GUI minimized until it is fully generated
        root.title( 'Menu Test' )

        # Create the GUI's main program menus
        menubar = Tk.Menu( root )
        menubar.add_cascade( label='File', menu=firstMenu( menubar ), command=test )
        menubar.add_cascade( label='Settings', menu=secondMenu( menubar ) )
        root.config( menu=menubar )

        root.deiconify() # Brings the GUI to the foreground now that rendering is complete

        # Start the GUI's mainloop
        root.mainloop()
        root.quit()


if __name__ == '__main__': Gui()

如果单击菜单栏上的任意位置,则会同时调用两个后命令回调。当您单击相应的菜单项时,我只需要(并且期望)它们中的一个被调用。

我不确定是否相关,但是我还在其他小部件上使用了与上下文菜单相同的菜单项。因此,他们的.post()方法还需要能够在显示菜单之前触发相同的回调。

如果有任何见解,请先谢谢。

1 个答案:

答案 0 :(得分:0)

这是一个非常棘手的问题,但是我终于找到了解决方案。经过大量搜索,无数次失败的实验以及更多搜索之后,我遇到了虚拟事件<<MenuSelect>>和以下关键代码行:print tk.call(event.widget, "index", "active"),由Michael O'Donnell {{3}指出}。

尝试使用此功能的第一个怪异部分是,event.widget在这种情况下不是小部件对象的实例,而是tcl / tk路径名字符串,例如'。#37759048L'。 (这似乎是Tkinter中的一个错误,因为正如我所测试的,甚至其他虚拟事件-TreeviewSelect和NotebookTabChanged-都包含了实际的小部件实例,正如预期的那样。)不管怎样,print tk.call(event.widget, "index", "active")可以使用tcl / tk字符串命令;返回当前活动菜单项的索引,该索引很大。

使用MenuSelect事件带来的第二个问题是,正常遍历菜单时会多次调用它。单击菜单项两次,将鼠标移至相邻菜单项,或将鼠标移至子菜单,然后返回主菜单项,也将两次调用。离开菜单也可以。但这可以通过在Menu类中添加一个标志并在事件处理程序中添加一些逻辑来很好地解决。这是完整的解决方案:

import Tkinter as Tk
import time


class 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 += 2


class 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 += 2


class 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 = False


if __name__ == '__main__': Gui()

最终结果是,仅当实际上要显示该特定菜单时,才通过.repopulate()调用每个菜单的代码以生成其内容。在离开整个主菜单并重新打开之前,不会再次调用该方法。也可以通过键盘导航。