Как я могу выполнить разные обратные вызовы для разных меню Tkinter [sub]? - PullRequest
1 голос
/ 19 апреля 2019

У меня есть графический интерфейс пользователя Tkinter с главной строкой меню, использующий виджет Tkinter Menu. Я хочу выполнить код перед отправкой подменю (другой элемент меню каскадно из него через .add_cascade()), чтобы я мог динамически изменять его содержимое, прежде чем оно будет показано. У меня это работает с использованием аргумента postcommand Menu, но я заметил огромную неэффективность его использования; обратный вызов postcommand для всех подменю вызывается при нажатии на любого подменю , а не только на конкретное подменю, которое было создано для получения обратного вызова. Даже нажатие на строку меню, где нет пунктов меню, также выполняет все обратные вызовы, даже если подменю не создаются.

Это ожидаемое поведение от модуля Menu и его аргумента после команды? Я не понимаю, почему это все еще происходит после создания отдельных экземпляров Меню для выпадающих меню.

Я пытался подключиться к собственным методам Tk.Menu, но ни один из них не вызывается при простом нажатии на элементы меню, чтобы вызвать каскадное Меню. И даже если .add_cascade () принимает аргумент 'command', он вызывает только вызываемую функцию, обеспечиваемую тем, что .add_cascade () 'menu' аргумент 'menu' не включен или если это лямбда-выражение (оба из которых не приводят к подменю) отображается при нажатии на элемент). (Вы можете увидеть это, используя функцию 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 голосов
/ 20 апреля 2019

Это была действительно сложная проблема, но я наконец нашел решение.После долгих поисков, многочисленных неудачных экспериментов и новых поисков я наткнулся на виртуальное событие <<MenuSelect>> и эту ключевую строку кода: print tk.call(event.widget, "index", "active"), на которую указал Майкл О'Доннелл, здесь .

Первая странная вещь в попытке использовать это, это то, что event.widget в данном случае не является экземпляром объекта виджета, это строка имени пути tcl / tk, например, '. # 37759048L'.(Похоже, что это ошибка в Tkinter, так как даже другие виртуальные события, которые я тестировал -TreeviewSelect и NotebookTabChanged, включают в себя фактические экземпляры виджета, как и ожидалось.) В любом случае, строка tcl / tk может использоваться командой print tk.call(event.widget, "index", "active");это возвращает индекс активного в данный момент пункта меню, который огромен.

Вторая проблема, возникающая при использовании события 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() вызывается только один раз, и только если это конкретное меню действительно будет отображаться.Метод не вызывается снова, пока все главное меню не будет оставлено и повторно открыто.Работает и при навигации по клавиатуре.

...