Создание меню / tkinter 10

Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Сложные графические интерфейсы обычно используют строки меню для организации действий и навигации по приложению. Этот подход также применяется для группировки тесно связанных операций. Например, в большинстве текстовых редакторов есть пункт «Файл».

Tkinter поддерживает такие меню, которые отображаются и ощущаются в соответствии с окружением конкретной операционной системы. Это позволяет не симулировать все с помощью фреймов, ведь из-за этого пропадет возможность пользоваться встроенными кроссплатформенными особенностями Tkinter.

Добавим меню в корневое окно со вложенным выпадающим меню. В Windows 10 это отображается следующим образом:

Создание строки меню

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


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
menu = tk.Menu(self)
file_menu = tk.Menu(menu, tearoff=0)

file_menu.add_command(label="Новый файл")
file_menu.add_command(label="Открыть")
file_menu.add_separator()
file_menu.add_command(label="Сохранить")
file_menu.add_command(label="Сохранить как...")

menu.add_cascade(label="Файл", menu=file_menu)
menu.add_command(label="О программе")
menu.add_command(label="Выйти", command=self.destroy)
self.config(menu=menu)

if __name__ == "__main__":
app = App()
app.mainloop()

Если запустить это скрипт, то вы увидите, что элемент Файл показывает дополнительное меню, а с помощью кнопки Выйти приложение можно закрыть.

Как работает создание верхнего меню

Сначала создаем экземпляр каждого меню, указывая родительский контейнер. Значение 1 у параметра tearoff указывает на то, что меню можно открепить с помощью пунктирной линии на границе. Это поведение не характерно для верхнего меню, но если его нужно отключить, то стоит задать значение 0 для этого параметра:


def __init__(self):
super().__init__()
menu = tk.Menu(self)
file_menu = tk.Menu(menu, tearoff=0)

Элементы меню организованы в том же порядке, в котором они добавляются с помощью методов: add_command, app_separator и add_cascade:


menu.add_cascade(label="Файл", menu=file_menu)
menu.add_command(label="О программе")
menu.add_command(label="Выйти", command=self.destroy)

Обычно add_command вызывается с параметром command, который является функцией обратного вызова, срабатывающей при нажатии. Аргументы ей не передаются — то же характерно и для виджета Button.

Для демонстрации добавим параметр элементу Выйти, который будет уничтожать экземпляр Tk и закрывать приложение.

Наконец, прикрепляем меню к основному окну с помощью вызова self.config(menu=menu). Стоит отметить, что у этого окна может быть только одна строка меню.

Использование переменных в меню

Помимо вызова команд и вложенных встроенных меню также можно подключать переменные Tkinter к элементам меню.

Добавим кнопку-флажок и три переключателя в подменю «Опции», разделив их между собой. Внутри будет две переменных Tkinter, которые сохранят выбранные значения так, что их можно будет получить в других методах приложения:

Создание меню / tkinter 10

Эти типы элементов добавляются с помощью методов add_checkbutton и add_radiobutton из класса виджета Menu. Как и в случае с обычными переключателями все связаны с одной переменной Tkinter, но имеют разные значения:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.checked = tk.BooleanVar()
self.checked.trace("w", self.mark_checked)
self.radio = tk.StringVar()
self.radio.set("1")
self.radio.trace("w", self.mark_radio)

menu = tk.Menu(self)
submenu = tk.Menu(menu, tearoff=0)

submenu.add_checkbutton(label="Checkbutton", onvalue=True,
offvalue=False, variable=self.checked)
submenu.add_separator()
submenu.add_radiobutton(label="Radio 1", value="1",
variable=self.radio)
submenu.add_radiobutton(label="Radio 2", value="2",
variable=self.radio)
submenu.add_radiobutton(label="Radio 3", value="3",
variable=self.radio)

menu.add_cascade(label="Опции", menu=submenu)
menu.add_command(label="Выход", command=self.destroy)
self.config(menu=menu)

def mark_checked(self, *args):
print(self.checked.get())

def mark_radio(self, *args):
print(self.radio.get())

if __name__ == "__main__":
app = App()
app.mainloop()

Будем отслеживать изменения переменной так, чтобы можно было выводить значения в консоли во время работы приложения.

Как работает создание переменных в меню

Для добавления булевой переменной элементу Checkbutton сначала нужно определить BooleanVar и затем создать элемент с помощью вызова add_checkbutton и параметра variable.

Стоит запомнить, что параметры onvalue и offvalue должны совпадать с типами переменных Tkinter как и в случае с виджетами RadioButton и Checkbutton:


self.checked = tk.BooleanVar()
self.checked.trace("w", self.mark_checked)
# ...
submenu.add_checkbutton(label="Checkbutton", onvalue=True,
offvalue=False, variable=self.checked)

Элементы Radiobutton создаются похожим образом с помощью метода add_radiobutton, и лишь один параметр value может быть задан для переменной Tkinter при нажатии на переключатель. Поскольку изначально в StringVar хранится пустая строка, зададим значение для первого переключателя, чтобы был отмечен как выбранный:


self.radio = tk.StringVar()
self.radio.set("1")
self.radio.trace("w", self.mark_radio)
# ...
submenu.add_radiobutton(label="Radio 1", value="1",
variable=self.radio)
submenu.add_radiobutton(label="Radio 2", value="2",
variable=self.radio)
submenu.add_radiobutton(label="Radio 3", value="3",
variable=self.radio)

Обе переменные отслеживают изменения с помощью методов mark_checked и mark_radio, которые просто выводят значения в консоль.

Отображение контекстных меню

Меню Tkinter не обязательно должны быть расположены в строке меню. Их можно размещать в любом месте. Такие меню называются контекстными, и обычно они отображаются при нажатии правой кнопкой по элементу.

Контекстные меню широко распространены в приложениях с графическим интерфейсом. Например, файловые менеджеры позволяют показывать все доступные операции для выбранного файла так, что для пользователей это будет ощущаться интуитивно.

Создадим контекстное меню для виджета Text, которое будет отображать некоторые распространенные действия в редакторах текста: Вырезать, Копировать, Вставить и Удалить:

Отображение контекстных меню

Вместо настройки экземпляра меню в качестве контейнера можно явно задать положение с помощью метода post.

Все команды в элементах меню вызывают метод, использующий текст для получения текущего выделения или позиции для вставки:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.menu = tk.Menu(self, tearoff=0)
self.menu.add_command(label="Вырезать", command=self.cut_text)
self.menu.add_command(label="Копировать", command=self.copy_text)
self.menu.add_command(label="Вставить", command=self.paste_text)
self.menu.add_command(label="Удалить", command=self.delete_text)

self.text = tk.Text(self, height=10, width=50)
self.text.bind("", self.show_popup)
self.text.pack()

def show_popup(self, event):
self.menu.post(event.x_root, event.y_root)

def cut_text(self):
self.copy_text()
self.delete_text()

def copy_text(self):
selection = self.text.tag_ranges(tk.SEL)
if selection:
self.clipboard_clear()
self.clipboard_append(self.text.get(*selection))

def paste_text(self):
self.text.insert(tk.INSERT, self.clipboard_get())

def delete_text(self):
selection = self.text.tag_ranges(tk.SEL)
if selection:
self.text.delete(*selection)

if __name__ == "__main__":
app = App()
app.mainloop()

Как работает контекстное меню

Связываем событие нажатия правой кнопки мыши с обработчиком show_popup для экземпляра текста. Оно будет выводить меню, чей левый верхний угол находится над положением, где был произведен клик. Каждый раз при срабатывании события заново отображается одно и то же меню:


def show_popup(self, event):
self.menu.post(event.x_root, event.y_root)

Следующие методы доступны для всех классов виджетов, что позволяет взаимодействовать с буфером обмена:

  • clipboard_clear() — очищает данные в буфере обмена
  • clipboard_append(string) — добавляет строку в буфер обмена
  • clipboard_get() — получает данные из буфера обмена

Метод обратного вызова для действия copy получает текущее выделение и добавляет его в буфер обмена:


def copy_text(self):
selection = self.text.tag_ranges(tk.SEL)
if selection:
self.clipboard_clear()
self.clipboard_append(self.text.get(*selection))

Действие action вставляет содержимое буфера на место курсора, которое определено индексом INSERT. Его нужно обернуть в блок try...except, поскольку вызов clipboard_get вызывает ошибку TclError, если буфер пуст:


def paste_text(self):
try:
self.text.insert(tk.INSERT, self.clipboard_get())
except tk.TclError:
pass

Действие delete не взаимодействует с буфером обмена, но удаляет содержимое текущего выделения:


def delete_text(self):
selection = self.text.tag_ranges(tk.SEL)
if selection:
self.text.delete(*selection)

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


Параметр postcommand позволяет настраивать меню каждый раз, когда оно отображается в методe post. Для демонстрации этого отключим элементы Вырезать, Копировать, Удалить в том случае, если в виджете Text нет выделения, а элемент Вставить — при отсутствии содержимого в буфере обмена.

По аналогии с другими функциями обратного вызова передаем ссылку на метод в класс для добавления параметра:

Затем проверяем существует ли диапазон SEL для определения того, должно ли состояние элементов быть ACTIVE или DISABLED. Это значение передается методу entryconfig, который принимает индекс элемента для настройки в качестве первого аргумента и список параметров для обновления. Элементы меню также начинаются с индекса 0:


def enable_selection(self):
state_selection = tk.ACTIVE if self.text.tag_ranges(tk.SEL) else tk.DISABLED
state_clipboard = tk.ACTIVE

try:
self.clipboard_get()
except tk.TclError:
state_clipboard = tk.DISABLED

self.menu.entryconfig(0, state=state_selection) # Вырезать
self.menu.entryconfig(1, state=state_selection) # Копировать
self.menu.entryconfig(2, state=state_clipboard) # Вставить
self.menu.entryconfig(3, state=state_selection) # Удалить

Например, все элементы должны быть серого цвета, если нет выделения или содержимого в буфере обмена:

Отображение контекстных меню

С помощью entryconfig также можно настроить другие параметры: метку, шрифт или фон. По ссылке https://www.tcl.tk/man/tcl8.6/TkCmd/menu.htm#M48 доступен весь список параметров.

Максим
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.
Мои контакты: Почта
admin@pythonru.comAlex Zabrodin2018-10-26OnlinePython, Programming, HTML, CSS, JavaScript