#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.