Кроме добавления виджетов, графический интерфейс должен также проектироваться с учетом их расположения. От того, какой макет будет составлен, зависит пользовательский опыт взаимодействия с определенной программой. Если правильно расположить кнопки и элементы окна, использование приложения станет интуитивно понятным. В свою очередь, пользователь может с легкостью разобраться в приложении, и не нужно тратить много времени на то, чтобы создавать подробные мануалы. Сегодня мы разберем много способов, с помощью которых осуществляется проектирование окна в Tkinter.
Выполнение группировки виджетов с фреймами
Для начала разберем значение слова «фрейм». Под ним подразумевается прямоугольная область окна, применяемая в макетах и содержащая виджеты. Каждый фрейм имеет следующие характеристики:
- Отступы.
- Рамки.
- Фон.
Фреймы используются для логической группировки элементов пользовательского интерфейса.
Еще одно преимущество фреймов – инкапсуляция. То есть, можно спрятать детали реализации дочерних элементов с помощью абстракции.
Предположим, нам надо создать такое приложение.
Чтобы реализовать эту задачу, необходимо сначала определить подкласс Frame, а потом туда добавить список с возможностью скроллинга. Реализуется эта задача с помощью этого кода.
import tkinter as tk
class ListFrame(tk.Frame):
def __init__(self, master, items=[]):
super().__init__(master)
self.list = tk.Listbox(self)
self.scroll = tk.Scrollbar(self, orient=tk.VERTICAL,
command=self.list.yview)
self.list.config(yscrollcommand=self.scroll.set)
self.list.insert(0, *items)
self.list.pack(side=tk.LEFT)
self.scroll.pack(side=tk.LEFT, fill=tk.Y)
def pop_selection(self):
index = self.list.curselection()
if index:
value = self.list.get(index)
self.list.delete(index)
return value
def insert_item(self, item):
self.list.insert(tk.END, item)
class App(tk.Tk):
def __init__(self):
super().__init__()
months = [«Январь», «Февраль», «Март», «Апрель»,
«Май», «Июнь», «Июль», «Август», «Сентябрь»,
«Октябрь», «Ноябрь», «Декабрь»]
self.frame_a = ListFrame(self, months)
self.frame_b = ListFrame(self)
self.btn_right = tk.Button(self, text=»>»,
command=self.move_right)
self.btn_left = tk.Button(self, text=»<«,
command=self.move_left)
self.frame_a.pack(side=tk.LEFT, padx=10, pady=10)
self.frame_b.pack(side=tk.RIGHT, padx=10, pady=10)
self.btn_right.pack(expand=True, ipadx=5)
self.btn_left.pack(expand=True, ipadx=5)
def move_right(self):
self.move(self.frame_a, self.frame_b)
def move_left(self):
self.move(self.frame_b, self.frame_a)
def move(self, frame_from, frame_to):
value = frame_from.pop_selection()
if value:
frame_to.insert_item(value)
if __name__ == «__main__»:
app = App()
app.mainloop()
Принцип группировки виджетов
Разберем описанный выше код. Класс ListFrame содержит два метода, с помощью которого осуществляется взаимодействие пользователя со списком.
Первый – это pop_selection. С его помощью можно получить выделенный элемент и удалить его.
Второй вставляет дополнительный элемент в конец списка – insert_item(). Совокупность этих методов используется для того, чтобы перемещать элементы между списками.
Кроме этого, возможно использовать особенности контейнеров родительского фрейма для верного размещения с правильными внутренними отступами. Данная задача реализуется с помощью этого кода.
# …
self.frame_a.pack(side=tk.LEFT, padx=10, pady=10)
self.frame_b.pack(side=tk.RIGHT, padx=10, pady=1
Фреймы дают возможность упростить управление геометрией макетов, а также возможность укладывать фрейм в основном окне с помощью pack() или использовать grid(), чтобы использовать geometry manager.
Тем не менее, запрещается смешивание этих менеджеров в одном контейнере, поскольку это приведет к нарушению работы приложения.
Geometry manager Pack
После того, как виджет будет создан, он не будет сразу отображаться на экране. Чтобы это случилось, необходимо использовать метод pack() для каждого из виджетов. Соответственно, необходимо использовать geometry manager.
Этот менеджер прекрасно подходит, чтобы создавать простые макеты. Например, если все элементы необходимо разместить рядом по осям x или y.
Допустим, необходимо создать такой макет.
Видим, что в структуру входит три строки, где последняя содержит три виджета, размещенных последовательно по оси x. В этом случае дополнительные фреймы не требуются.
Чтобы разделять прямоугольные области, мы будем использовать пять виджетов Label с разным фоном и текстом.
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
label_a = tk.Label(self, text=»Label A», bg=»yellow»)
label_b = tk.Label(self, text=»Label B», bg=»orange»)
label_c = tk.Label(self, text=»Label C», bg=»red»)
label_d = tk.Label(self, text=»Label D», bg=»green»)
label_e = tk.Label(self, text=»Label E», bg=»blue»)
opts = { ‘ipadx’: 10, ‘ipady’: 10, ‘fill’: tk.BOTH }
label_a.pack(side=tk.TOP, **opts)
label_b.pack(side=tk.TOP, **opts)
label_c.pack(side=tk.LEFT, **opts)
label_d.pack(side=tk.LEFT, **opts)
label_e.pack(side=tk.LEFT, **opts)
if __name__ == «__main__»:
app = App()
app.mainloop()
Также мы добавили параметры в словаре opts, что дает возможность более четко разграничивать области и понимать их размеры.
После этого у нас получился такой результат.
Принцип работы Pack
Для более точного понимания того, как работает Pack, необходимо разобрать механизм добавления виджетов в родительский контейнер. Обратите внимание на значение, которое передается параметру side. Он используется для того, чтобы определить расположение одного элемента фрейма относительно других.
Сперва мы добавляем две надписи сверху. Значение параметра side устанавливаем на tk.TOP.
После этого выполняем добавление дополнительных трех надписей, в которых выставляем значение параметра side на tk.LEFT. Как следствие, их расположение устанавливается рядом друг с другом.
Если в контейнер добавляется последний виджет, нет особой разницы, как определяется сторона элемента.
Чтобы избежать непредвиденных неприятностей при отладке приложения, рекомендуется размещать элементы в пределах фрейма так, чтобы они находились рядом друг с другом, не пересекаясь.
Geometry manager Grid
Из всех существующих Geometry manager, Grid – наиболее адаптивный. Он позволяет совсем по-другому посмотреть на понятие сетки, применяемой в разработке стиля пользовательского интерфейса. Под сеткой подразумевается таблица, где каждая ячейка служит пространством, в которое может быть помещен виджет.
Посмотрим, как работает этот Geometry Manager на таком примере.
Несмотря на нетипичную для таблицы структуру, его можно изобразить в виде таблицы с тремя столбцами и тремя колонками. Просто в этом примере виджеты в двух правых столбцах занимают две строчки, а элементы, расположенные в третьем сверху ряду, размещаются на трех колонках.
Аналогично прошлому примеру, мы будем добавлять 5 надписей с разным фоном, чтобы показать, как распределяются ячейки между собой.
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
label_a = tk.Label(self, text=»Label A», bg=»yellow»)
label_b = tk.Label(self, text=»Label B», bg=»orange»)
label_c = tk.Label(self, text=»Label C», bg=»red»)
label_d = tk.Label(self, text=»Label D», bg=»green»)
label_e = tk.Label(self, text=»Label E», bg=»blue»)
opts = { ‘ipadx’: 10, ‘ipady’: 10 , ‘sticky’: ‘nswe’ }
label_a.grid(row=0, column=0, **opts)
label_b.grid(row=1, column=0, **opts)
label_c.grid(row=0, column=1, rowspan=2, **opts)
label_d.grid(row=0, column=2, rowspan=2, **opts)
label_e.grid(row=2, column=0, columnspan=3, **opts)
if __name__ == «__main__»:
app = App()
app.mainloop()
Кроме этого, нами будет передаваться словарь с параметрами. С его помощью мы определяем внутренний отступ. За счет этого у нас и получится растянуть виджет по всему пространству ячейки.
Принцип работы Grid
Учитывая то, что индексация в Python начинается с нуля, метки с именем label_a и label_b располагаются в первом и втором ряду первого столбца соответственно.
Для того, чтобы метки label_c и label_d занимали две клетки в этой условной таблице, необходимо использовать параметр rowspan и передать ему значение 2. Если так сделать, соответствующие элементы будут занимать две ячейки. Точка отсчета задается параметрами row и column.
Пользователь может изменять порядок вызовов grid() для каждого элемента. И для этого не нужно даже изменять макет. Это существенное преимущество по сравнению с pack.
Geometry Manager Place
С помощью менеджера Place пользователь имеет возможность определить положение и размер элемента как относительно других, так и в абсолютном выражении. Он используется чаще всего и может работать даже со сложным позиционированием. Например, он применяется, если необходимо свободно расположить элемент или накрыть им другой виджет.
Чтобы продемонстрировать, что может Geometry Manager, приведем такой рисунок.
Приведем код, наглядно демонстрирующий, как он работает.
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
label_a = tk.Label(self, text=»Label A», bg=»yellow»)
label_b = tk.Label(self, text=»Label B», bg=»orange»)
label_c = tk.Label(self, text=»Label C», bg=»red»)
label_d = tk.Label(self, text=»Label D», bg=»green»)
label_e = tk.Label(self, text=»Label E», bg=»blue»)
label_a.place(relwidth=0.25, relheight=0.25)
label_b.place(x=100, anchor=tk.N,
width=100, height=50)
label_c.place(relx=0.5, rely=0.5, anchor=tk.CENTER,
relwidth=0.5, relheight=0.5)
label_d.place(in_=label_c, anchor=tk.N + tk.W,
x=2, y=2, relx=0.5, rely=0.5,
relwidth=0.5, relheight=0.5)
label_e.place(x=200, y=200, anchor=tk.S + tk.E,
relwidth=0.25, relheight=0.25)
if __name__ == «__main__»:
app = App()
app.mainloop()
Таким образом, нет ничего сложного в том, чтобы задавать позиционирование элементов относительно структуры окна. Это поможет сделать работу с программой более простой.