Tkinter: использование пользовательских событий с меню в Windows

#python #tkinter #menu #tcl

Вопрос:

Использование Python 3.9.4

В интересах свободной связи я пытаюсь реализовать главное меню в Tkinter, которое генерирует пользовательские события, а не напрямую вызывает функцию обратного вызова. Пример сценария показывает основной подход:

 import tkinter as tk


root = tk.Tk()
root.geometry('300x200')
label = tk.Label(text='Test')
label.grid()
menu = tk.Menu(root)

root.configure(menu=menu)

submenu = tk.Menu(menu)
menu.add_cascade(menu=submenu, label='Change Text')
submenu.add_command(
    label='Foo',
    command=lambda: menu.event_generate('<<Foo>>')
)
submenu.add_command(
    label='Bar',
    command=lambda: menu.event_generate('<<Bar>>')
)

def setfoo(*_):
    label.configure(text='Foo')

def setbar(*_):
    label.configure(text='Bar')

menu.bind('<<Foo>>', setfoo)
menu.bind('<<Bar>>', setbar)

root.mainloop()
 

Этот подход работает в Linux, но в Windows привязки не работают. Они кажутся привязанными к событию, но при выборе пункта меню ничего не происходит.

Я предполагаю, что это связано с различием между реализациями меню в Windows и Linux, но есть ли правильный способ сделать это или обходной путь?

ПРАВКА: Чтобы помочь всем лучше понять, почему я хочу делать определенные вещи, вот объектно-ориентированная версия, которая более точно соответствует тому, как я использую Tkinter:

 import tkinter as tk

class MainMenu(tk.Menu):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        submenu = tk.Menu(self)
        submenu.add_command(
            label="foo",
            command=lambda: self.event_generate('<<Foo>>')
        )
        submenu.add_command(
            label="bar",
            command=lambda: self.event_generate('<<Bar>>')
        )
        self.add_cascade(menu=submenu, label='Test')


class Application(tk.Tk):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        menu = MainMenu(self)
        self.configure(menu=menu)

        menu.bind('<<Foo>>', print)
        menu.bind('<<Bar>>', print)

if __name__ == '__main__':
    app = Application()
    app.mainloop()
 

Обратите внимание, что MainMenu класс будет определен в отдельном файле, поэтому app глобальный для него недоступен. В этом случае я мог бы использовать MainMenu.master для получения корневого окна, но это нарушает связь, поскольку предполагает, что корневое окно является родительским для этого класса. Это предположение может нарушиться (скажем, главное окно помещается в меню гамбургера или добавляется в другое TopLevel , не являющееся основным приложением).

Комментарии:

1. Для упрощения кода вы можете просто иметь menu.bind("<<Foo>>", print) и menu.event_generate("<<Foo>>") . Оператор печати никогда не вызывается. Это интересная проблема.

2. как бы это ни было интересно, если вы используете root.event_generate() , а затем root.bind() это работает, также используете label.event_generate() , а затем label.bind() работает, так что это может быть как-то связано с Menu (может быть, я не знаю)

3. Привязка к root , возможно, должна быть обходным путем, но это как бы убивает идею слабой связи. Я полагаю, что это не так уж плохо, так как корневое окно должно существовать где-то в приложении.

4. Обычно вы генерируете события в некоторых других виджетах, таких как корневое окно или окно с фокусом. Создание их в меню кажется странным выбором. Есть ли конкретная причина, по которой вы решили создать их в меню?

5. В меню будет ссылка на родителя, и от родителя вы можете получить корневое окно FWIW.

Ответ №1:

Меню управляются операционной системой в OSX и Windows, и tkinter очень слабо контролирует их поведение. Я сомневаюсь, что отправка событий в меню на любой из этих платформ будет работать.

Я бы сказал, что отправка событий в меню является анти-шаблоном. Если события не связаны напрямую с меню, вы должны отправлять события либо в окно верхнего уровня, связанное с меню, корневое окно, либо в окно, на котором находится фокус клавиатуры, в зависимости от того, что представляет событие.

Поскольку вы передаете родительский виджет в качестве позиционного аргумента, вы можете использовать его для получения окна верхнего уровня, связанного с меню.

Например:

 class MainMenu(tk.Menu):

    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)

        top = parent.winfo_toplevel()
        submenu.add_command(
            label="foo",
            command=lambda: top.event_generate('<<Foo>>')
        )
        ...
 

Комментарии:

1. По моему опыту, одна и та же проблема с видимостью затрагивает все виджеты tkinter. Единственное, что видимость меню не обновляется, когда оно отображается как часть окна tkinter.

2. @TheLizzard: Я не понимаю, о чем ты говоришь. Мой ответ не имеет ничего общего с видимостью. В этом конкретном случае, если top он не виден, можно было бы выбрать пункт из меню, так как он тоже был бы невидим.

3. Я говорю о вашей ссылке на то, что tkinter не имеет большого контроля над поведением меню. Я говорю, что из того, что я мог бы проверить, все виджеты ведут себя так, когда вы не звоните .pack / .grid / .place на них.

4. Брайан, спасибо за ответ. Я не думаю, что есть лучший ответ на то, как это сделать, но я заинтригован тем, как вы сформулировали свой ответ. Я думаю, что моя ментальная модель событий немного отличается (возможно, ошибочна?), исходя из большей части модели паба или сигналов и слотов. Ваша формулировка, по-видимому, предполагает, что вы рассматриваете event_generate отправку событий в меню, в то время как я рассматриваю это как отправку событий из меню, поэтому для меня имеет смысл генерировать события в объекте меню. Моя ментальная модель неверна?

5. @AlanMoore: да, я думаю, что ваша ментальная модель обратная. События отправляются в виджеты. Подумайте о том, чтобы нажать на кнопку. Физическая кнопка вызывает создание события на самой кнопке. Привязка событий к функциям, по сути, гласит: «когда виджет X получает событие Y, вызовите функцию Z».

Ответ №2:

Я думаю, что понял это, хотя и не уверен на 100%.

Посмотрите на этот код:

 import tkinter as tk

def trying_to_reach(event):
    print("!")

root = tk.Tk()

text = tk.Text(root)
text.pack()

text.bind("<<Foo>>", trying_to_reach)
root.after(100, text.event_generate, "<<Foo>>")

root.mainloop()
 

через 100 мс после запуска кода ! на экране появляется сообщение, но я комментирую text.pack() , на экране ничего не отображается. Кроме того , если я переключусь root.after(100, text.event_generate, "<<Foo>>") на text.event_generate("<<Foo>>") , на экране ничего не отобразится. Это доказывает теорию @BryanOakley о том, что это как-то связано с видимостью виджета. Это означает, что проблема, скорее всего, кроется где-то в tcl tcl вызовах функций.

Решением было бы иметь фиктивный виджет или, как предложил @Matiiss, просто использовать root.event_generate и root.bind .

Комментарии:

1. Это не имеет ничего общего с pack самим по себе, хотя потенциально может иметь отношение к тому, действительно ли виджет виден или нет. В Windows меню являются собственными виджетами Windows, которые не управляются Tkinter.

2. @BryanOakley Я знаю, что меню управляются окнами (вот почему я не могу изменить их фон :D). Я не думаю, что проблема в ткинтере. Я делаю ставку на tcl или на то, что tcl вызывает функции. Кроме того , если я изменюсь root.after(100, text.event_generate, "<<Foo>>") text.event_generate("<<Foo>>") , это не сработает. Это доказывает вашу теорию видимости.

3. Вы правы, что проблема не в tkinter. Проблема в том, как tcl/tk реализует свои меню в Windows.