Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki
Отображение панелей с вкладками с помощью Notebook
Класс ttk.Notebook
— еще один новый виджет из модуля ttk
. Он позволяет добавлять разные виды отображения приложения в одном окне, предлагая после этого выбрать желаемый с помощью клика по соответствующей вкладке.
Панели с вкладками — это удобный вариант повторного использования графического интерфейса для тех ситуаций, когда содержимое нескольких областей не должно отображаться одновременно.
Следующее приложение показывает список дел, разбитый по категориям. В этом примере данные доступны только для чтения (для упрощения):
Создаем ttk.Notebook
с фиксированными размерами, и затем проходимся по словарю с заранее определенными данными. Он выступит источником вкладок и названий для каждой области:
import tkinter as tk
import tkinter.ttk as ttk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Ttk Notebook")
todos = {
"Дом": ["Постирать", "Сходить за продуктами"],
"Работа": ["Установить Python", "Учить Tkinter", "Разобрать почту"],
"Отпуск": ["Отдых!"]
}
self.notebook = ttk.Notebook(self, width=250, height=100, padding=10)
for key, value in todos.items():
frame = ttk.Frame(self.notebook)
self.notebook.add(frame, text=key, underline=0,
sticky=tk.NE + tk.SW)
for text in value:
ttk.Label(frame, text=text).pack(anchor=tk.W)
self.label = ttk.Label(self)
self.notebook.pack()
self.label.pack(anchor=tk.W)
self.notebook.enable_traversal()
self.notebook.bind("<<NotebookTabChanged>>", self.select_tab)
def select_tab(self, event):
tab_id = self.notebook.select()
tab_name = self.notebook.tab(tab_id, "text")
text = "Ваш текущий выбор: {}".format(tab_name)
self.label.config(text=text)
if __name__ == "__main__":
app = App()
app.mainloop()
При клике по вкладке метка в нижней части экрана обновляет содержимое, показывая название текущей вкладки.
Как работает этот виджет
Виджет ttk.Notebook
создается с фиксированными шириной, высотой и внешними отступами.
Каждый ключ из словаря todos
используется в качестве названия вкладки, а список значений добавляется в виде меток в ttk.Frame
, который представляет собой область окна:
self.notebook = ttk.Notebook(self, width=250, height=100, padding=10)
for key, value in todos.items():
frame = ttk.Frame(self.notebook)
self.notebook.add(frame, text=key,
underline=0, sticky=tk.NE+tk.SW)
for text in value:
ttk.Label(frame, text=text).pack(anchor=tk.W)
После этого у виджета ttk.Notebook
вызывается метод enable_traversal()
. Это позволяет пользователям переключаться между вкладками с помощью Ctrl + Shift + Tab и Ctrl + Tab соответственно.
Благодаря этому также можно переключиться на определенную вкладку, зажав Alt и подчеркнутый символ: Alt + H для вкладки Home, Alt + W — для Work, а Alt + V — для Vacation.
Виртуальное событие "<<NotebookTabChanged>>"
генерируется автоматически при изменении выбора. Оно связывается с методом select_tab()
. Стоит отметить, что это событие автоматически срабатывает при добавлении вкладки в ttk.Notebook
:
self.notebook.pack()
self.label.pack(anchor=tk.W)
self.notebook.enable_traversal()
self.notebook.bind("<<NotebookTabChanged>>", self.select_tab)
При упаковке элементов необязательно размещать дочерние элементы ttk.Notebook
, поскольку это делается автоматически с помощью вызова geometry manager:
def select_tab(self, event):
tab_id = self.notebook.select()
tab_name = self.notebook.tab(tab_id, "text")
self.label.config(text=f"Ваш текущий выбор: {tab_name}")
Если нужно получить текущий дочерний элемент ttk.Notebook
, то для этого не нужно использовать дополнительные структуры данных для маппинга индекса вкладки и окна виджета.
Метод nametowidget()
доступен для всех классов виджетов, так что с его помощью можно легко получить объект виджета, соответствующий определенному имени:
def select_tab(self, event):
tab_id = self.notebook.select()
frame = self.nametowidget(tab_id)
# ...
Применение стилей Ttk
У тематических виджетов есть отдельный API для изменения внешнего вида. Прямо задавать параметры нельзя, потому что они определены в классе ttk.Style
.
В этом разделе разберем, как изменять виджеты и добавлять им стили.
Для добавления дополнительных настроек нужен объект ttk.Style
, который предоставляет следующие методы:
configure(style, opts)
— меняет внешний видopts
дляstyle
виджета. Именно здесь задаются такие параметры, как фон, отступы и анимации.map(style, query)
— меняет динамический видstyle
виджета. Аргументquery
— аргумент-ключевое слово, где каждый ключ отвечает за параметр стиля, а значение — список кортежей в виде(state, value)
. Это значит, что значение каждого параметра определяется его текущим состоянием.
Например, отметим следующие примеры для двух ситуаций:
import tkinter as tk
import tkinter.ttk as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Tk themed widgets")
style = ttk.Style(self)
style.configure("TLabel", padding=10)
style.map("TButton",
foreground=[("pressed", "grey"), ("active", "white")],
background=[("pressed", "white"), ("active", "grey")]
)
# ...
Теперь каждый ttk.Label
отображается с внутренним отступом 10, а у ttk.Button
динамические стили: серая заливка с белым фоном, когда состояние кнопки — pressed
и белая заливка с серым фоном — когда active.
Как работают стили
Создавать ttk.Style
довольно просто. Нужно лишь создать экземпляр с родительским виджетом в качестве первого параметра.
После этого можно задать настройки стиля для виджетов с помощью символа T в верхнем регистре и названия виджета: Tbutton
для ttk.Button
, Tlabel
для ttk.Label
и так далее. Однако есть и исключения, поэтому рекомендуется сверяться с помощью интерпретатора Python, вызывая winfo_class()
для экземпляра виджета.
Также можно добавить префикс, чтобы указать, что этот стиль должен быть не по умолчанию, а явно задаваться для определенных виджетов:
style.configure("My.TLabel", padding=10)
# ...
label = ttk.Label(master, text="Какой-то текст", style="My.TLabel")
Создание виджета выбора даты
Если нужно позволить пользователям выбирать дату в приложении, то можно попробовать оставить текстовую подсказку, которая бы побудила их написать строку в формате даты. Еще одно решение — добавить несколько числовых полей для ввода дня, месяца и года, но в этом случае понадобятся несколько правил валидации.
В отличие от других фреймворков для создания графических интерфейсов, в Tkinter нет класса для этих целей, но можно воспользоваться знаниями тематических виджетов для создания виджета календаря.
В этом материале пошагово разберем процесс создания виджета выбора даты с помощью виджетов Ttk:
Это рефакторинг решения https://svn.python.org/projects/sandbox/trunk/t tk-gsoc/samples/ttkcalendar.py, который не требует внешних зависимостей.
Помимо модулей tkinter также нужны модули calendar и datetime из стандартной библиотеки. Это поможет моделировать данные виджета и взаимодействовать с ними.
Заголовок виджета показывает стрелки для перемещения между месяцами. Их внешний вид зависит от текущих выбранных стилей Tk. Тело виджета состоит из таблицы ttk.Treeview
с экземпляром Canvas
, который подсвечивает ячейку выбранной даты:
import calendar
import datetime
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkfont
from itertools import zip_longest
class TtkCalendar(ttk.Frame):
def __init__(self, master=None, **kw):
now = datetime.datetime.now()
fwday = kw.pop('firstweekday', calendar.MONDAY)
year = kw.pop('year', now.year)
month = kw.pop('month', now.month)
sel_bg = kw.pop('selectbackground', '#ecffc4')
sel_fg = kw.pop('selectforeground', '#05640e')
super().__init__(master, **kw)
self.selected = None
self.date = datetime.date(year, month, 1)
self.cal = calendar.TextCalendar(fwday)
self.font = tkfont.Font(self)
self.header = self.create_header()
self.table = self.create_table()
self.canvas = self.create_canvas(sel_bg, sel_fg)
self.build_calendar()
# ...
def main():
root = tk.Tk()
root.title('Календарь Tkinter')
ttkcal = TtkCalendar(firstweekday=calendar.SUNDAY)
ttkcal.pack(expand=True, fill=tk.BOTH)
root.mainloop()
if __name__ == '__main__':
main()
Полный код в файле lesson_23/creating_widget.py на Gitlab.
Как создается виджет
Этот класс TtkCalendar
можно кастомизировать, передавая параметры в виде аргументов-ключевых слов. Их можно получать при инициализации, указав кое-какие значения по умолчанию, например, текущие месяц и год:
def __init__(self, master=None, **kw):
now = datetime.datetime.now()
fwday = kw.pop('firstweekday', calendar.MONDAY)
year = kw.pop('year', now.year)
month = kw.pop('month', now.month)
sel_bg = kw.pop('selectbackground', '#ecffc4')
sel_fg = kw.pop('selectforeground', '#05640e')
super().__init__(master, **kw)
После этого задаются атрибуты для хранения информации:
selected
— хранит значение выбранной даты.date
— дата, представляющая текущий месяц, который показывается на календаре.calendar
— григорианский календарь с информацией о неделях и названиями месяцев.
Визуальные элементы виджета внутри создаются с помощью методов create_header()
и create_table()
, речь о которых пойдет дальше.
Также используется экземпляр tkfont.Font
для определения размера шрифта.
После инициализации этих атрибутов визуальные элементы календаря выравниваются с помощью вызова метода build_calendar()
:
self.selected = None
self.date = datetime.date(year, month, 1)
self.cal = calendar.TextCalendar(fwday)
self.font = tkfont.Font(self)
self.header = self.create_header()
self.table = self.create_table()
self.canvas = self.create_canvas(sel_bg, sel_fg)
self.build_calendar()
Метод create_header()
использует ttk.Style
для отображения стрелок, которые нужны для переключения между месяцами. Он возвращает метку названия текущего месяца:
def create_header(self):
left_arrow = {'children': [('Button.leftarrow', None)]}
right_arrow = {'children': [('Button.rightarrow', None)]}
style = ttk.Style(self)
style.layout('L.TButton', [('Button.focus', left_arrow)])
style.layout('R.TButton', [('Button.focus', right_arrow)])
hframe = ttk.Frame(self)
btn_left = ttk.Button(hframe, style='L.TButton',
command=lambda: self.move_month(-1))
btn_right = ttk.Button(hframe, style='R.TButton',
command=lambda: self.move_month(1))
label = ttk.Label(hframe, width=15, anchor='center')
hframe.pack(pady=5, anchor=tk.CENTER)
btn_left.grid(row=0, column=0)
label.grid(row=0, column=1, padx=12)
btn_right.grid(row=0, column=2)
return label
Колбек move_month()
скрывает текущий выбор, выделенный с помощью поля полотна и добавляет параметр offset
текущему месяцу, чтобы задать атрибут date
с предыдущим или следующим месяцем. После этого календарь снова перерисовывается, показывая уже дни нового месяца:
def move_month(self, offset):
self.canvas.place_forget()
month = self.date.month - 1 + offset
year = self.date.year + month // 12
month = month % 12 + 1
self.date = datetime.date(year, month, 1)
self.build_calendar()
Тело календаря создается в create_table()
с помощью виджета ttk.Treeview
, который показывает каждую неделю текущего месяца в одной строке:
def create_table(self):
cols = self.cal.formatweekheader(3).split()
table = ttk.Treeview(self, show='', selectmode='none',
height=7, columns=cols)
table.bind('<Map>', self.minsize)
table.pack(expand=1, fill=tk.BOTH)
table.tag_configure('header', background='grey90')
table.insert('', tk.END, values=cols, tag='header')
for _ in range(6):
table.insert('', tk.END)
width = max(map(self.font.measure, cols))
for col in cols:
table.column(col, width=width, minwidth=width, anchor=tk.E)
return table
Полотно, подсвечивающее выбор, создается с помощью метода create_canvas()
. Поскольку оно выравнивает размер в зависимости от размеров выбранного элемента, то оно же скрывается при изменении размеров окна:
def create_canvas(self, bg, fg):
canvas = tk.Canvas(self.table, background=bg,
borderwidth=0, highlightthickness=0)
canvas.text = canvas.create_text(0, 0, fill=fg, anchor=tk.W)
handler = lambda _: canvas.place_forget()
canvas.bind('<ButtonPress-1>', handler)
self.table.bind('<Configure>', handler)
self.table.bind('<ButtonPress-1>', self.pressed)
return canvas
Календарь строится за счет перебора недель и позиций элементов таблицы ttk.Treeview
. С помощью функции zip_longest()
из модуля itertools
перебираем коллекцию, оставляя на месте недостающих дней пустые строки.
Это поведение нужно для первой и последней недель месяца, ведь именно там часто можно найти пустые слоты:
def build_calendar(self):
year, month = self.date.year, self.date.month
month_name = self.cal.formatmonthname(year, month, 0)
month_weeks = self.cal.monthdayscalendar(year, month)
self.header.config(text=month_name.title())
items = self.table.get_children()[1:]
for week, item in zip_longest(month_weeks, items):
week = week if week else []
fmt_week = ['%02d' % day if day else '' for day in week]
self.table.item(item, values=fmt_week)
При клике по элементу таблицы обработчик события pressed()
задает выделение и меняет полотно для выделения выбора:
def pressed(self, event):
x, y, widget = event.x, event.y, event.widget
item = widget.identify_row(y)
column = widget.identify_column(x)
items = self.table.get_children()[1:]
if not column or not item in items:
# клик на заголовок или за пределами столбцов
return
index = int(column[1]) - 1
values = widget.item(item)['values']
text = values[index] if len(values) else None
bbox = widget.bbox(item, column)
if bbox and text:
self.selected = '%02d' % text
self.show_selection(bbox)
Метод show_selection()
размещает полотно в пределах выбранного элемента, так что текст помещается внутри:
def show_selection(self, bbox):
canvas, text = self.canvas, self.selected
x, y, width, height = bbox
textw = self.font.measure(text)
canvas.configure(width=width, height=height)
canvas.coords(canvas.text, width - textw, height / 2 - 1)
canvas.itemconfigure(canvas.text, text=text)
canvas.place(x=x, y=y)
Наконец, параметр selection
позволяет получить выбранную дату в виде объекта datetime.date
. Он не используется в примере, но нужен для работы API в классе TtkCalendar
:
@property
def selection(self):
if self.selected:
year, month = self.date.year, self.date.month
return datetime.date(year, month, int(self.selected))