Tkinter — PythonRu https://pythonru.com Изучайте Python на русском: учебные руководства Python для разработчиков с разным уровнем знаний, рекомендации книг и курсов Python, новости, примеры кода, статьи и уроки Sat, 30 Jan 2021 12:08:50 +0000 ru-RU hourly 1 https://pythonru.com/wp-content/uploads/2018/11/cropped-pythonru-icon-32x32.png Tkinter — PythonRu https://pythonru.com 32 32 Создание собственных виджетов и Notebook / tkinter 23 https://pythonru.com/uroki/sozdanie-sobstvennyh-vidzhetov-i-notebook-tkinter-23 Sat, 09 Jan 2021 15:22:00 +0000 https://pythonru.com/?p=4347 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Отображение панелей с вкладками с помощью Notebook

Класс ttk.Notebook — еще один новый виджет из модуля ttk. Он позволяет добавлять разные виды отображения приложения в одном окне, предлагая после этого выбрать желаемый с помощью клика по соответствующей вкладке.

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

Следующее приложение показывает список дел, разбитый по категориям. В этом примере данные доступны только для чтения (для упрощения):

Отображение панелей с вкладками с помощью Notebook

Создаем 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))
]]>
Виджет Treeview / tkinter 22 https://pythonru.com/uroki/vidzhet-treeview-tkinter-22 Sat, 02 Jan 2021 10:42:00 +0000 https://pythonru.com/?p=4221 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

В этом материале рассмотрим класс ttk.Treeview, с помощью которого можно выводить информацию в иерархической или форме таблицы.

Каждый элемент, добавленный к классу ttk.Treeview разделяется на одну или несколько колонок. Первая может содержать текст и иконку, которые показывают, может ли элемент быть раскрыт, чтобы показать вложенные элементы. Оставшиеся колонки показывают значения для каждой строки.

Первая строка класса ttk.Treeview состоит из заголовков, которые определяют каждую колонку с помощью имени. Их можно скрыть.

С помощью ttk.Treeview создадим таблицу из списка контактов, которые хранятся в CSV-файле:

Использование виджета Treeview

Создадим виджет ttk.Treeview с тремя колонками, в каждой из которых будут поля каждого из контактов: имя, фамилия и адрес электронной почты.

Контакты загружаются из CSV-файла с помощью модуля csv, и после этого добавляется связывание для виртуального элемента <<TreeviewSelect>>, который генерируется при выборе одного или большего количества элементов:


import csv
import tkinter as tk
import tkinter.ttk as ttk

class App(tk.Tk):
def __init__(self, path):
super().__init__()
self.title("Ttk Treeview")

columns = ("#1", "#2", "#3")
self.tree = ttk.Treeview(self, show="headings", columns=columns)
self.tree.heading("#1", text="Фамилия")
self.tree.heading("#2", text="Имя")
self.tree.heading("#3", text="Почта")
ysb = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscroll=ysb.set)

with open("../lesson_13/contacts.csv", newline="") as f:
for contact in csv.reader(f):
self.tree.insert("", tk.END, values=contact)
self.tree.bind("<<TreeviewSelect>>", self.print_selection)

self.tree.grid(row=0, column=0)
ysb.grid(row=0, column=1, sticky=tk.N + tk.S)
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)

def print_selection(self, event):
for selection in self.tree.selection():
item = self.tree.item(selection)
last_name, first_name, email = item["values"][0:3]
text = "Выбор: {}, {} <{}>"
print(text.format(last_name, first_name, email))

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

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

Как работает виджет Treeview

Для создания ttk.Treeview с несколькими колонками нужно указать идентификатор каждой с помощью параметра columns. После этого можно настроить текст заголовка с помощью метода heading().

Используем идентификаторы #1, #2 и #3, поскольку первая колонка, включающая иконку раскрытия и текст, всегда генерируется с идентификатором #0.

Также параметру show передается значение «headings», чтобы обозначить, что нужно скрыть колонку #0, потому что вложенных элементов тут не будет.

Следующие значения являются валидными для параметра show:

  • tree — отображает колонку #0;
  • headings — отображает строку заголовка;
  • tree headings — отображает и колонку #0, и строку заголовка (является значением по умолчанию);
  • "" — не отображает ни колонку #0, ни строку заголовка.

После этого к виджету ttk.Treeview добавляется вертикальный скроллбар:


columns = ("#1", "#2", "#3")
self.tree = ttk.Treeview(self, show="headings", columns=columns)
self.tree.heading("#1", text="Фамилия")
self.tree.heading("#2", text="Имя")
self.tree.heading("#3", text="Почта")
ysb = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscroll=ysb.set)

Для загрузки контактов в таблицу файл нужно обработать с помощью функции render() из модуля CSV. В процессе строка, прочтенная на каждой итерации, добавляется к ttk.Treeview.

Это делается с помощью метода insert(), который получает родительский узел и положение для размещения элемента.

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

Также можно использовать другие аргументы-ключевые слова для метода insert(). Здесь используется параметр values, который принимает последовательность значений — они и отображаются в каждой колонке Treeview:


with open("../lesson_13/contacts.csv", newline="") as f:
for contact in csv.reader(f):
self.tree.insert("", tk.END, values=contact)
self.tree.bind("<<TreeviewSelect>>", self.print_selection)

<<TreeviewSelect>> — это виртуальное событие, которое генерируется при выборе одного или нескольких элементов из таблицы. В обработчике print_selection() получаем текущее выделение с помощью метода selection(), и для каждого результата выполняем следующие шаги:

  1. С помощью метода item() получаем словарь параметров и значений выбранного элемента.
  2. Получаем первые три значения словаря item, которые соответствуют фамилии, имени и адресу электронной почты контакта.
  3. Значения форматируются и выводятся в стандартный вывод:

def print_selection(self, event):
for selection in self.tree.selection():
item = self.tree.item(selection)
last_name, first_name, email = item["values"][0:3]
text = "Выбор: {}, {} <{}>"
print(text.format(last_name, first_name, email))

Это были базовые особенности класса ttk.Treeview, поскольку работа велась с обычной таблицей. Однако приложение можно расширить и с помощью более продвинутых особенностей этого класса.

Использование тегов в элементах Treeview

Тэги доступны для элементов ttk.Treeview, благодаря чему существует возможность связать последовательности события с конкретными строками таблицы Contacts.

Предположим, что есть необходимость открывать новое окно для добавления информации об электронной почте по двойному клику. Однако это должно работать только для записей, в которых поле email уже заполнено.

Это можно реализовать, добавляя тег с условием при вставке. После этого нужно вызывать tag_bind() на экземпляре виджета с последовательностью "<Double-Button-1>" — здесь можно просто сослаться на реализацию функции-обработчика send_email_to_contact() по имени:


columns = ("Фамилия", "Имя", "Почта")
tree = ttk.Treeview(self, show="headings", columns=columns)

for contact in csv.reader(f):
email = contact[2]
tags = ("dbl-click",) if email else ()
self.tree.insert("", tk.END, values=contact, tags=tags)

tree.tag_bind("dbl-click", "<Double-Button-1>", send_email_to_contact)

По аналогии с тем, что происходит при связывании событий с элементами Canvas, важно не забывать добавлять элементы с тегами к ttk.Treeview до вызова tag_bind(), потому что связывания добавляются только к существующим совпадающим элементам.

Заполнение вложенных элементов в Treeview

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

Это удобно для отображения результатов рекурсивных вызовов и нескольких уровней вложенных элементов. В этом материале рассмотрим сценарий работы с такой структурой.

Для демонстрации рекурсивного добавления элементов в виджет ttk.Treeview создадим базовый браузер файловой системы. Раскрываемые узлы будут представлять собой папки, а после раскрытия они будут показывать вложенные файлы и папки:

Заполнение вложенных элементов в Treeview

Дерево изначально будет заполняться с помощью метода populate_node(), который содержит записи текущей директории. Если запись сама является директорией, то она добавляет дочерний раскрываемый узел.

Когда такой узел раскрывается, он «лениво» загружает содержимое с помощью еще одного вызова populate_node(). В этот раз, вместо добавления элементов в качестве узлов верхнего уровня, они вкладываются внутрь открытого узла:


import os
import tkinter as tk
import tkinter.ttk as ttk

class App(tk.Tk):
def __init__(self, path):
super().__init__()
self.title("Ttk Treeview")

abspath = os.path.abspath(path)
self.nodes = {}
self.tree = ttk.Treeview(self)
self.tree.heading("#0", text=abspath, anchor=tk.W)
ysb = ttk.Scrollbar(self, orient=tk.VERTICAL,
command=self.tree.yview)
xsb = ttk.Scrollbar(self, orient=tk.HORIZONTAL,
command=self.tree.xview)
self.tree.configure(yscroll=ysb.set, xscroll=xsb.set)

self.tree.grid(row=0, column=0, sticky=tk.N + tk.S + tk.E + tk.W)
ysb.grid(row=0, column=1, sticky=tk.N + tk.S)
xsb.grid(row=1, column=0, sticky=tk.E + tk.W)
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)

self.tree.bind("<<TreeviewOpen>>", self.open_node)
self.populate_node("", abspath)

def populate_node(self, parent, abspath):
for entry in os.listdir(abspath):
entry_path = os.path.join(abspath, entry)
node = self.tree.insert(parent, tk.END, text=entry, open=False)
if os.path.isdir(entry_path):
self.nodes[node] = entry_path
self.tree.insert(node, tk.END)

def open_node(self, event):
item = self.tree.focus()
abspath = self.nodes.pop(item, False)
if abspath:
children = self.tree.get_children(item)
self.tree.delete(children)
self.populate_node(item, abspath)

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

Запуск предыдущего примера выведет иерархию файловой системы в зависимости от того, где запустить этот файл. Однако можно явно указать директорию с помощью аргумента path конструктора App.

Как работают выпадающие элементы

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

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


import os
import tkinter as tk
import tkinter.ttk as ttk

class App(tk.Tk):
def __init__(self, path):
# ...
abspath = os.path.abspath(path)
self.nodes = {}

Например, os.path.abspath(".") вернет абсолютную версию пути к папке, откуда был запущен скрипт. Этот подход лучше использования относительных путей, потому что он помогает не думать о возможных проблемах при работе с путями.

Дальше инициализируется экземпляр ttk.Treeview с вертикальным и горизонтальным скроллбарами. Параметр text иконки заголовка будет тем самым абсолютным путем:


self.tree = ttk.Treeview(self)
self.tree.heading("#0", text=abspath, anchor=tk.W)
ysb = ttk.Scrollbar(self, orient=tk.VERTICAL,
command=self.tree.yview)
xsb = ttk.Scrollbar(self, orient=tk.HORIZONTAL,
command=self.tree.xview)
self.tree.configure(yscroll=ysb.set, xscroll=xsb.set)

После этого виджеты размещаются с помощью geometry manager Grid. Экземпляр ttk.Treeview нужно сделать автоматически изменяемым горизонтально и вертикально.

После этого выполняется связывание виртуального события "<<TreeviewOpen>>", которое генерируется при открытии раскрываемого элемента в обработчике open_node(). populate_node() вызывается для загрузки записей конкретной директории:


self.tree.bind("<<TreeviewOpen>>", self.open_node)
self.populate_node("", abspath)

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

В методе populate_node() перечисляем названия записей директорий с помощью вызова os.listdir(). Для каждого названия после этого выполняем следующие действия:

  • Вычисляем абсолютный путь к записи. В UNIX-системах это делается за счет объединения строк слэшем, а в Windows используются обратные слэши. Благодаря os.path.join() с путями можно работать безопасно, не думая об особенностях платформ.
  • Вставляем строку entry как последний дочерний элемент конкретного узла parent. Всегда отмечаем их закрытыми, чтобы «лениво» загружать вложенные элементы только тогда, когда те потребуются.
  • Если абсолютный путь записи указывает на директорию, то добавляется связь для узла и пути в атрибут nodes, а также добавляется пустой дочерний элемент, позволяющий раскрывать его:

def populate_node(self, parent, abspath):
for entry in os.listdir(abspath):
entry_path = os.path.join(abspath, entry)
node = self.tree.insert(parent, tk.END, text=entry, open=False)
if os.path.isdir(entry_path):
self.nodes[node] = entry_path
self.tree.insert(node, tk.END)

При нажатии на такой элемент обработчик open_node() получает выбранный элемент с помощью вызова метода focus() экземпляра ttk.Treeview.

Идентификатор элемента используется для получения абсолютного пути, который был добавлен до этого в атрибут nodes. Чтобы ошибка KeyError не появлялась, если узел не существует в словаре, используем метод pop(), который возвращает второй параметр в качестве значения по умолчанию — False.

Если узел существует, очищаем «фейковый» элемент расширяемого узла. Вызов self.tree.get_children(item) возвращает идентификаторы дочерних элементов item. После этого они удаляются с помощью вызова self.tree.delete(children).

После очистки элемента добавляем реальные дочерние элементы с помощью метода populate_node() и item в качестве родителя:


def open_node(self, event):
item = self.tree.focus()
abspath = self.nodes.pop(item, False)
if abspath:
children = self.tree.get_children(item)
self.tree.delete(children)
self.populate_node(item, abspath)
]]>
Замена виджетов и Combobox / tkinter 21 https://pythonru.com/uroki/zamena-vidzhetov-i-combobox-tkinter-21 Sat, 26 Dec 2020 09:02:00 +0000 https://pythonru.com/?p=4206 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Тематические виджеты Tk — это отдельная коллекция виджетов Tk, которые выглядят и ощущаются как нативные, и могут быть кастомизированы с помощью API.

Эти классы определены в модуле tkinter.ttk. Помимо новых виджетов, таких как Treeview и Notebook, этот модуль предлагает альтернативную реализацию классических виджетов Tk: Button, Label и Frame.

В этом материале рассмотрим, как использовать эти классы, как их стилизовать и как применять новые классы виджетов.

Набор тематических виджетов Tk был представлен в Tk 8.5, что не должно быть проблемой, ведь установка Python как минимум версии 3.6 добавляет интерпретатор Tcl/Tk версии 8.6.

Убедиться в этом можно, набрав на любой платформе в командной строке python –mtkinter. После этого запустится следующая программа с указанием текущей версии Tcl/Tk:

python –mtkinter

Замена классов базовых виджетов

В качестве первого пункта знакомства с тематическими виджетами Tkinter посмотрим, как использовать уже знакомые (Button, Label, Entry и так далее), но взятые из другого модуля, сохраняя то же поведение в приложении.

Хотя это не даст аналогичных возможностей в плане настройки стилей, достаточно обратить внимание на визуальные отличия, которые привносят нативные дизайн и «ощущения» этих тематических виджетов.

На следующем скриншоте можно увидеть различия между тематическим виджетом и стандартным виджетом Tkinter:

Замена классов базовых виджетов

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

Стоит отметить, что это поведение сильно зависит от платформы. В этом конкретном случае тематический виджет повторяет внешний вид виджетов с Windows 10.

Чтобы начать использовать тематические виджеты нужны импортировать модуль tkinter.ttk и привычным образом использовать виджеты из него в приложении:


import tkinter as tk
import tkinter as ttk

class App(tk.Tk):
greetings = ("Привет", "Ciao", "Hola")

def __init__(self):
super().__init__()
self.title("Тематические виджеты Tk")

var = tk.StringVar()
var.set(self.greetings[0])
label_frame = ttk.LabelFrame(self, text="Выберите приветствие")
for greeting in self.greetings:
radio = ttk.Radiobutton(label_frame, text=greeting,
variable=var, value=greeting)
radio.pack()

frame = ttk.Frame(self)
label = ttk.Label(frame, text="Введите ваше имя")
entry = ttk.Entry(frame)

command = lambda: print("{}, {}!".format(var.get(), entry.get()))
button = ttk.Button(frame, text="Приветствовать", command=command)

label.grid(row=0, column=0, padx=5, pady=5)
entry.grid(row=0, column=1, padx=5, pady=5)
button.grid(row=1, column=0, columnspan=2, pady=5)

label_frame.pack(side=tk.LEFT, padx=10, pady=10)
frame.pack(side=tk.LEFT, padx=10, pady=10)

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

Если же в будущем потребуется переключиться к обычным виджетам Tkinter, то достаточно заменить все ttk. на tk..

Как работает замена виджетов

Чтобы начать использовать тематически виджеты нужно импортировать модуль tkinter.ttk с помощь синтаксиса import … as. Это позволит быстро отличать стандартные виджеты благодаря имени tk и тематические — с именем ttk:

Вы могли заметить, что для замены виджетов из модуля tkinter на аналогичные им из tkinter.ttk достаточно просто поменять алиас:


import tkinter as tk
import tkinter as ttk

# ...
entry_1 = tk.Entry(root)
entry_2 = ttk.Entry(root)

В этом примере мы делаем это с помощью виджетов ttk.Frame, ttk.Label, ttk.Entry, ttk.LabelFrame и ttk.Radiobutton. Эти классы принимают почти те же базовые параметры, что и эквиваленты из Tkinter. На самом деле, они являются их подклассами.

Тем не менее эта смена довольно простая, потому что в этом случае не переносятся никакие особенности стиля: например, foreground и background. В тематических виджетах эти ключевые слова используются в классе ttk.Style, речь о котором пойдет дальше.

Создание редактируемого выпадающего списка с Combobox

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

Эта функциональность комбинируется с классом ttk.Combobox, который выглядит и ощущается как нативные (для конкретной платформы) выпадающие списки.

Следующее приложение будет состоять из простого выпадающего списка с парой кнопок для подтверждения или сброса содержимого.

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

Создание выпадающего списка с Combobox

Это приложение создает экземпляр ttk.Combobox при инициализации, передавая заранее определенную последовательность значений, которые можно будет выбирать из списка:


import tkinter as tk
import tkinter.ttk as ttk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Ttk Combobox")
colors = ("Purple", "Yellow", "Red", "Blue")

self.label = ttk.Label(self, text="Пожалуйста, выберите цвет")
self.combo = ttk.Combobox(self, values=colors)
btn_submit = ttk.Button(self, text="Разместить",
command=self.display_color)
btn_clear = ttk.Button(self, text="Очистить",
command=self.clear_color)

self.combo.bind("<>", self.display_color)

self.label.pack(pady=10)
self.combo.pack(side=tk.LEFT, padx=10, pady=5)
btn_submit.pack(side=tk.TOP, padx=10, pady=5)
btn_clear.pack(padx=10, pady=5)

def display_color(self, *args):
color = self.combo.get()
print("Ваш выбор", color)

def clear_color(self):
self.combo.set("")

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

Как работает виджет Combobox

Традиционно виджет ttk.Combobox добавляется в приложение через передачу экземпляра Tk в качестве первого параметра в конструктор. Параметр values определяет список выбираемых вариантов, которые будут отображаться по клику.

Также выполняется связывание виртуального события <<ComboboxSelected>> при выборе одного из значений списка:


self.label = ttk.Label(self, text="Пожалуйста, выберите цвет")
self.combo = ttk.Combobox(self, values=colors)
btn_submit = ttk.Button(self, text="Разместить",
command=self.display_color)
btn_clear = ttk.Button(self, text="Очистить",
command=self.clear_color)

self.combo.bind("<<ComboboxSelected>>", self.display_color)

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

display_color() принимает переменный список аргументов с помощью синтаксиса *, что позволяет безопасно обрабатывать опциональные аргументы. Это происходит, потому что событие передается внутрь при вызове через связывание. Однако функция не получает параметры при вызове через колбек кнопки.

Внутри этого метода мы получаем текущее значение Combobox с помощью метода get() и выводим его:


def display_color(self, *args):
color = self.combo.get()
print("Ваш выбор", color)

Наконец, clear_color() очищает содержимое Combobox с помощью вызова set() с пустой строкой:

С помощью этих методов вы знаете, как взаимодействовать с выбранным значением в экземпляре Combobox.

Класс ttk.Combobox расширяет ttk.Entry, который, в свою очередь, расширяет класс Entry из модуля tkinter.

Это значит, что можно использовать уже рассмотренные методы из класса Entry, если потребуется:


combobox.insert(0, "Добавьте это в начало: ")

Вот этот код более понятен чем combobox.set("Добавьте это в начало: " + combobox.get()).

]]>
Canvas, рисование графики ч.3 / tkinter 20 https://pythonru.com/uroki/canvas-3-tkinter-20 Sun, 20 Dec 2020 12:54:37 +0000 https://pythonru.com/?p=4187 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Определение пересечений между элементами

В продолжение предыдущего материала о поиске ближайшего элемента стоит отметить, что существует также возможность определять, пересекается ли один прямоугольник с другим. Этого можно добиться благодаря тому, что все элементы заключены в прямоугольные контейнеры. А для определения пересечений используется метод find_overlapping() из класса Canvas.

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

Определение пересечений между элементами

Поскольку код во многом повторяет предыдущий, отметим лишь те части кода, которые отвечают за создание новых прямоугольников и вызов метода canvas.find_overlapping():


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Обнаружение пересечений между предметами")

self.canvas = tk.Canvas(self, bg="white")
self.canvas.pack()
self.update()
self.width = w = self.canvas.winfo_width()
self.height = h = self.canvas.winfo_height()

pos = (w / 2 - 15, h / 2 - 15, w / 2 + 15, h / 2 + 15)
self.item = self.canvas.create_rectangle(*pos, fill="blue")
positions = [(60, 60), (w - 60, 60), (60, h - 60), (w - 60, h - 60)]
for x, y in positions:
self.canvas.create_rectangle(x - 10, y - 10, x + 10, y + 10,
fill="green")

self.pressed_keys = {}
self.bind("<KeyPress>", self.key_press)
self.bind("<KeyRelease>", self.key_release)
self.process_movements()

def key_press(self, event):
self.pressed_keys[event.keysym] = True

def key_release(self, event):
self.pressed_keys.pop(event.keysym, None)

def process_movements(self):
all_items = self.canvas.find_all()
for item in filter(lambda i: i is not self.item, all_items):
self.canvas.itemconfig(item, fill="green")

x0, y0, x1, y1 = self.canvas.coords(self.item)
items = self.canvas.find_overlapping(x0, y0, x1, y1)
for item in filter(lambda i: i is not self.item, items):
self.canvas.itemconfig(item, fill="yellow")

off_x, off_y = 0, 0
speed = 3
if 'Right' in self.pressed_keys:
off_x += speed
if 'Left' in self.pressed_keys:
off_x -= speed
if 'Down' in self.pressed_keys:
off_y += speed
if 'Up' in self.pressed_keys:
off_y -= speed

pos_x = x0 + (x1 - x0) / 2 + off_x
pos_y = y0 + (y1 - y0) / 2 + off_y
if 0 <= pos_x <= self.width and 0 <= pos_y <= self.height:
self.canvas.move(self.item, off_x, off_y)

self.after(10, self.process_movements)

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

Как определяются пересечения

До пересечения цвет заполнения всех прямоугольников на полотне, кроме управляемого пользователем, будет зеленым. Идентификаторы этих элементов можно получить с помощью метода canvas.find_all():


def process_movements(self):
all_items = self.canvas.find_all()
for item in filter(lambda i: i is not self.item, all_items):
self.canvas.itemconfig(item, fill="green")

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


def process_movements(self):
# ... x0, y0, x1, y1 = self.canvas.coords(self.item)
items = self.canvas.find_overlapping(x0, y0, x1, y1)
for item in filter(lambda i: i is not self.item, items):
self.canvas.itemconfig(item, fill="yellow")

Метод продолжает выполнение, перемещая синий прямоугольник на заданный показатель сдвига, и планируя себя же снова с помощью process_movements().

Если нужно определить, когда движущийся элемент полностью перекрывает другой (а не частично), то стоит воспользоваться методом canvas.find_enclosed() вместо canvas.find_overlapping() с теми же параметрами.

Удаление элементов с полотна

Помимо добавления и изменения элементов полотна их также можно удалять с помощью метода delete() класса Canvas. Хотя в принципах его работы нет каких-либо особенностей, существуют кое-какие паттерны, которые будут рассмотрены дальше.

Стоит учитывать, что чем больше элементов на полотне, тем дольше Tkinter будет рендерить виджет. Таким образом важно удалять неиспользуемые для улучшения производительности.

В этом примере создадим приложение, которое случайным образом выбирает несколько кругов на полотне. Каждый кружок будет удаляться по клику. Одна кнопка в нижней части виджета сбрасывает состояние полотна, а вторая — удаляет все элементы.

Canvas, рисование графики ч.3 / tkinter 20

Чтобы случайным образом размещать элементы на полотне, будем генерировать координаты с помощью функции randint модуля random. Цвет элемента будет выбираться случайным образом с помощью вызова choice и определенного набора цветов.

После генерации элементы можно будет удалить с помощью обработчика on_click или кнопки Clearitems, которая, в свою очередь, вызывает функцию обратного вызова clear_all. Внутри этот метод вызывает canvas.delete() с нужными параметрами:


import random
import tkinter as tk

class App(tk.Tk):
colors = ("red", "yellow", "green", "blue", "orange")

def __init__(self):
super().__init__()
self.title("Удаление элементов холста")

self.canvas = tk.Canvas(self, bg="white")
frame = tk.Frame(self)
generate_btn = tk.Button(frame, text="Создавать элементы",
command=self.generate_items)
clear_btn = tk.Button(frame, text="Удалить элементы",
command=self.clear_items)

self.canvas.pack()
frame.pack(fill=tk.BOTH)
generate_btn.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
clear_btn.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

self.update()
self.width = self.canvas.winfo_width()
self.height = self.canvas.winfo_height()

self.canvas.bind("<Button-1>", self.on_click)
self.generate_items()

def on_click(self, event):
item = self.canvas.find_withtag(tk.CURRENT)
self.canvas.delete(item)

def generate_items(self):
self.clear_items()
for _ in range(10):
x = random.randint(0, self.width)
y = random.randint(0, self.height)
color = random.choice(self.colors)
self.canvas.create_oval(x, y, x + 20, y + 20, fill=color)

def clear_items(self):
self.canvas.delete(tk.ALL)

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

Как работает удаление элементов

Метод canvas.delete() принимает один аргумент, который может быть идентификатором элемента или тегом, и удаляет один или несколько соответствующих элементов (поскольку тег может быть использован несколько раз).

В обработчике on_click() можно увидеть пример удаления элемента по идентификатору:


def on_click(self, event):
item = self.canvas.find_withtag(tk.CURRENT)
self.canvas.delete(item)

Стоит также отметить, что если сейчас кликнуть по пустой точке, то canvas.find_withtag(tk.CURRENT) вернет None, но когда это значение будет передано в canvas.delete(), то ошибки не будет. Это объясняется тем, что параметр None не совпадает ни с одним идентификатором или тегом. Таким образом это валидный параметр, хоть в результате никакое действие и не выполняется.

В функции обратного вызова clear_items() можно найти другой пример удаления элементов. Здесь вместо передачи идентификатора элемента используется тег ALL, который соответствует всем элементам и удаляет их с полотна:


def clear_items(self):
self.canvas.delete(tk.ALL)

Можно обратить внимание на то, что тег ALL работает «из коробки», поэтому его не нужно добавлять каждому элементу полотна.

Связывание событий с элементами полотна

Вы уже знаете, как связывать события с виджетами, но то же самое можно делать и с элементами. Это помогает писать более специфичные и простые обработчики событий. Такой намного удобнее, чем связывать все события с экземпляром Canvas и потом определять, какое из них нужно в текущий момент.

Следующее приложение показывает, как реализовать функциональность drag and drop для элементов полотна. Это распространенная особенность, которая способна значительно упростить программы.

Создадим несколько элементов (прямоугольник и овал), которые можно будет перетаскивать с помощью мыши. Разная форма поможет заметить, как события кликов корректно применяются к элементам, даже когда они пересекаются между собой:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Drag and drop")

self.dnd_item = None
self.canvas = tk.Canvas(self, bg="white")
self.canvas.pack()

self.canvas.create_rectangle(30, 30, 60, 60, fill="green",
tags="draggable")
self.canvas.create_oval(120, 120, 150, 150, fill="red",
tags="draggable")

self.canvas.tag_bind("draggable", "<ButtonPress-1>",
self.button_press)
self.canvas.tag_bind("draggable", "<Button1-Motion>",
self.button_motion)

def button_press(self, event):
item = self.canvas.find_withtag(tk.CURRENT)
self.dnd_item = (item, event.x, event.y)

def button_motion(self, event):
x, y = event.x, event.y
item, x0, y0 = self.dnd_item
self.canvas.move(item, x - x0, y - y0)
self.dnd_item = (item, x, y)

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

Как работает drag and drop

Для связывания событий с элементами используется метод tag_bind() из класса Canvas. Это добавляет связывание для всех элементов, которые соответствуют конкретному элементу — тегу draggable в этом случае.

И хотя метод называется tag_bind() вместо тега в него можно передавать также идентификатор:


self.canvas.tag_bind("draggable", "<ButtonPress-1>",
self.button_press)
self.canvas.tag_bind("draggable", "<Button1-Motion>",
self.button_motion)

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

Метод button_press() — это обработчик, который запускается после нажатия на элемент. Традиционный паттерн для получения соответствующего элемента — вызов canvas.find_withtag(tk.CURRENT).

Идентификатор элемента, а также координаты x и y события click хранятся в поле dnd_item. Эти значения позже будут использованы для перемещения элемента в соответствии с движением мыши:


def button_press(self, event):
item = self.canvas.find_withtag(tk.CURRENT)
self.dnd_item = (item, event.x, event.y)

Метод button_motion() обрабатывает события движения мыши до тех пор, пока зажата основная кнопка.

Для определения дистанции, на которую должен быть перемещен элемент, нужно вычислить разницу текущей позиции с предыдущими координатами. Эти значения передаются в метод canvas.move() и снова сохраняются в поле dnd_item:


def button_motion(self, event):
x, y = event.x, event.y
item, x0, y0 = self.dnd_item
self.canvas.move(item, x - x0, y - y0)
self.dnd_item = (item, x, y)

Существуют вариации этой drag & drop функциональности, которые также задействуют обработчик последовательности <ButtonRelease-1>. Она сбрасывает текущий элемент.

Однако использовать его необязательно, потому что после того как это событие происходит, связывание <Button1-Motion> не запустится до очередного клика по элементу. Это также помогает избежать проверки того, не является ли None значением dnd_item в начале обработчика button_motion().

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

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


final_x, final_y = pos_x + off_x, pos_y + off_y
if 0 <= final_x <= canvas_width and 0 <= final_y <= canvas_height:
canvas.move(item, off_x, off_y)

Рендер полотна в файл PostScript

Класс Canvas нативно поддерживает сохранение содержимого с помощью языка PostScript и метода postscript(). Он сохраняет графическое представление элементов полотна (линий, прямоугольников, овалов и так далее), но не его виджетов или изображений.

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

Возьмем уже знакомый код и добавим в него кусок для вывода содержимого полотна в файл PostScript:


import tkinter as tk
from lesson_18.drawing import LineForm

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Базовый холст")

self.line_start = None
self.form = LineForm(self)
self.render_btn = tk.Button(self, text="Render canvas",
command=self.render_canvas)
self.canvas = tk.Canvas(self, bg="white")
self.canvas.bind("<Button-1>", self.draw)

self.form.grid(row=0, column=0, padx=10, pady=10)
self.render_btn.grid(row=1, column=0)
self.canvas.grid(row=0, column=1, rowspan=2)

def draw(self, event):
x, y = event.x, event.y
if not self.line_start:
self.line_start = (x, y)
else:
x_origin, y_origin = self.line_start
self.line_start = None
line = (x_origin, y_origin, x, y)
arrow = self.form.get_arrow()
color = self.form.get_color()
width = self.form.get_width()
self.canvas.create_line(*line, arrow=arrow,
fill=color, width=width)

def render_canvas(self):
self.canvas.postscript(file="output.ps", colormode="color")

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

Как работает рендеринг в .ps

Основное нововведение — это кнопка Render canvas с функцией обратного вызова render_canvas().

Она вызывает метод postscript() для экземпляра canvas с аргументами file и colormode. Эти параметры определяют путь к расположению файла, а также информацию о цвете. Вторым параметром может быть color для полностью цветного вывода, gray — для использования оттенков серого или mono — для конвертации цветов в черный и белый:


def render_canvas(self):
self.canvas.postscript(file="output.ps", colormode="color")

Все параметры, которые можно передать в postscript(), стоит искать в официальной документации Tk/Tcl по ссылке https://www.tcl.tk/m an/tcl8.6/TkCmd/canvas.htm#M61. Стоит напомнить, что PostScript — язык печати, поэтому большая часть его параметров касается настроек страницы.

Поскольку файлы PostScript не так популярны, как другие форматы файлов, возможно, возникнет необходимость конвертировать готовый файл во что-то более знакомое — например, PDF.

Для этого нужен сторонний софт, такой как, например, Ghostscript, который распространяется по лицензии GNU APGL. Интерпретатор и инструмент рендеринга можно вызвать из программы для автоматической конвертации результатов PostScript в PDF.

Установить программу можно с сайта https://w ww.ghostscript.com/download/gsdnld.html. Дальше нужно только добавить папки bin и lib из установки в переменную path операционной системы.

Затем остается изменить приложение Tkinter для вызова программы ps2pdf в качестве подпроцеса и удалить файл output.ps после завершения выполнения:


import os
import subprocess
import tkinter as tk

class App(tk.Tk):
# ...

def render_canvas(self):
output_filename = "output.ps"
self.canvas.postscript(file=output_filename, colormode="color")
process = subprocess.run(["ps2pdf", output_filename, "output.pdf"],
shell=True)
os.remove(output_filename)
]]>
Canvas, рисование графики ч.2 / tkinter 19 https://pythonru.com/uroki/canvas-risovanie-grafiki-ch-2-tkinter-19 Sat, 12 Dec 2020 08:28:00 +0000 https://pythonru.com/?p=4130 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Добавление геометрических фигур на полотно

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

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

Положение элементов определяется двумя кликами: первый указывает на верхний левый угол контейнера, в который будет заключен элемент, а второй – на правый нижний. Также по умолчанию задаются определенные параметры:

Добавление геометрических фигур на полотно

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

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


import tkinter as tk
from functools import partial

class App(tk.Tk):
shapes = ("прямоугольник", "овал", "дуга")
def __init__(self):
super().__init__()
self.title("Отрисовка стандартных элементов")

self.start = None
self.shape = None
self.canvas = tk.Canvas(self, bg="white")
frame = tk.Frame(self)
for shape in self.shapes:
btn = tk.Button(frame, text=shape.capitalize())
btn.config(command=partial(self.set_selection, btn, shape))
btn.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

self.canvas.bind("<Button-1>", self.draw_item)
self.canvas.pack()
frame.pack(fill=tk.BOTH)

def set_selection(self, widget, shape):
for w in widget.master.winfo_children():
w.config(relief=tk.RAISED)
widget.config(relief=tk.SUNKEN)
self.shape = shape

def draw_item(self, event):
x, y = event.x, event.y
if not self.start:
self.start = (x, y)
else:
x_origin, y_origin = self.start
self.start = None
bbox = (x_origin, y_origin, x, y)
if self.shape == "прямоугольник":
self.canvas.create_rectangle(*bbox, fill="blue",
activefill="yellow")
elif self.shape == "овал":
self.canvas.create_oval(*bbox, fill="red",
activefill="yellow")
elif self.shape == "дуга":
self.canvas.create_arc(*bbox, fill="green",
activefill="yellow")

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

Как работает рисование фигур

Для сохранения возможности динамически выбирать тип элемента создаются кнопки для каждого из представленных. Они создаются за счет перебора кортежа shapes.

Каждая функция обратного вызова определяется с помощью функции partial из модуля functools. Это позволяет заморозить экземпляр Button и текущую форму цикла в качестве аргументов функции обратного вызова для каждой кнопки:


for shape in self.shapes:
btn = tk.Button(frame, text=shape.capitalize())
btn.config(command=partial(self.set_selection, btn, shape))
btn.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

Функция обратного вызова set_section() помечает нажатую кнопку с помощью SUNKEN и сохраняет выбор в поле shape.

Остальные кнопки настраиваются со стандартным рельефом RAISED. Это делается с помощью перехода к родителю, который доступен в поле master текущего виджета. Из него и можно получить все дочерние виджеты, используя метод winfo_children():


def set_selection(self, widget, shape):
for w in widget.master.winfo_children():
w.config(relief=tk.RAISED)
widget.config(relief=tk.SUNKEN)
self.shape = shape

Обработчик draw_item() сохраняет координаты первого клика каждой пары событий, чтобы нарисовать элемент при повторном клике по полотну.

В зависимости от типа поля shape вызывается один из следующих методов для отображения соответствующего элемента:

  • canvas.create_rectangele(x0, y0, x1, y,1 **options) – рисует прямоугольник, чей левый верхний угол расположен по координатам (x0, y0), а правый нижний – (x1, y1).
  • canvas.create_oval(x0, y0, x1, y1, **options) – рисует эллипс, который вписывается в прямоугольник с координатами (x0, y0) и (x1, y1).
  • canvas.create_arc(x0, y0, x1, y1, **options) – рисует четверть эллипса, который поместится в прямоугольник с координатами (x0, y0) и (x1, y1).

Поиск элементов по их положению

Класс Canvas включает методы для получения идентификаторов элементов, которые находятся рядом с координатами полотна.

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

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

Поиск элементов по их положению

Для нахождения ближайшего к курсору элемента координаты мыши передаются методу canvas.find_closest(), который и определяют идентификатор ближайшего элемента.

Если в пределах полотна есть хотя бы один элемент, можно быть уверенным в том, что метод всегда вернет валидный идентификатор элемента:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Поиск предметов на canvas")

self.current = None
self.canvas = tk.Canvas(self, bg="white")
self.canvas.bind("<Motion>", self.mouse_motion)
self.canvas.pack()

self.update()
w = self.canvas.winfo_width()
h = self.canvas.winfo_height()
positions = [(60, 60), (w-60, 60), (60, h-60), (w-60, h-60)]
for x, y in positions:
self.canvas.create_rectangle(x-10, y-10, x+10, y+10,
fill="blue")

def mouse_motion(self, event):
self.canvas.itemconfig(self.current, fill="blue")
self.current = self.canvas.find_closest(event.x, event.y)
self.canvas.itemconfig(self.current, fill="yellow")

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

Как работает поиск элементов

При инициализации приложения создается полотно и определяется поле current для сохранения ссылки на текущий подсвеченный элемент. Также обрабатываются события "<Motion>" с помощью метода mouse_motion():


self.current = None
self.canvas = tk.Canvas(self, bg="white")
self.canvas.bind("<Motion>", self.mouse_motion)
self.canvas.pack()

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


self.update()
w = self.canvas.winfo_width()
h = self.canvas.winfo_height()
positions = [(60, 60), (w-60, 60), (60, h-60), (w-60, h-60)]
for x, y in positions:
self.canvas.create_rectangle(x-10, y-10, x+10, y+10,
fill="blue")

Обработчик mouse_motion() задает цвет текущего элемента обратно в синий и сохраняет идентификатор нового. Наконец, цвет fill этого элемента становится желтым:


def mouse_motion(self, event):
self.canvas.itemconfig(self.current, fill="blue")
self.current = self.canvas.find_closest(event.x, event.y)
self.canvas.itemconfig(self.current, fill="yellow")

Изначально при вызове mouse_motion() ошибок нет, а поле current равно None, поскольку это также валидное значение для параметра itemconfig. Просто в таком случае действия не выполняются.

Перемещение элементов в canvas

После размещения элементы полотна могут быть перемещены – для этого им не нужно задавать абсолютные координаты.

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

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

Чтобы элемент не ушел за пределы экрана, ограничим перемещение в пределах полотна:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Перемещение элементов холста")

self.canvas = tk.Canvas(self, bg="white")
self.canvas.pack()
self.update()
self.width = self.canvas.winfo_width()
self.height = self.canvas.winfo_height()

self.item = self.canvas.create_rectangle(30, 30, 60, 60,
fill="blue")
self.pressed_keys = {}
self.bind("<KeyPress>", self.key_press)
self.bind("<KeyRelease>", self.key_release)
self.process_movements()

def key_press(self, event):
self.pressed_keys[event.keysym] = True

def key_release(self, event):
self.pressed_keys.pop(event.keysym, None)

def process_movements(self):
off_x, off_y = 0, 0
speed = 3
if 'Right' in self.pressed_keys:
off_x += speed
if 'Left' in self.pressed_keys:
off_x -= speed
if 'Down' in self.pressed_keys:
off_y += speed
if 'Up' in self.pressed_keys:
off_y -= speed

x0, y0, x1, y1 = self.canvas.coords(self.item)
pos_x = x0 + (x1 - x0) / 2 + off_x
pos_y = y0 + (y1 - y0) / 2 + off_y
if 0 <= pos_x <= self.width and 0 <= pos_y <= self.height:
self.canvas.move(self.item, off_x, off_y)

self.after(10, self.process_movements)

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

Как работает перемещение элементов

Для обработки клавиш стрелок на клавиатуре свяжем "<KeyPress>" и "<KeyRelease>" с экземпляром приложения. Нажатые сейчас клавиши сохраняются в словарь pressed_keys:


def __init__(self):
# ...
self.pressed_keys = {}
self.bind("<KeyPress>", self.key_press)
self.bind("<KeyRelease>", self.key_release)

def key_press(self, event):
self.pressed_keys[event.keysym] = True

def key_release(self, event):
self.pressed_keys.pop(event.keysym, None)

Такой подход лучше отдельного связывания клавиш "<Up>", "<Down>", "<Right>" и "<Left>", потому что они вызывали бы каждый из обработчиков только при обработке событий клавиатуры. В результате элементы бы «перепрыгивали» с одного положения на другое, а не плавно перемещались.

Последний элемент инициализации экземпляра App – вызов process_movements(), который запускает обработку движения элемента полотна.

Этот метод вычисляет сдвиг по каждой из осей. В зависимости от содержания словаря pressed_keys, скорость speed добавляется или вычисляется из координат компонентов:


def process_movements(self):
off_x, off_y = 0, 0
speed = 3
if 'Right' in self.pressed_keys:
off_x += speed
if 'Left' in self.pressed_keys:
off_x -= speed
if 'Down' in self.pressed_keys:
off_y += speed
if 'Up' in self.pressed_keys:
off_y -= speed

После этого мы получаем положение текущего элемента с помощью вызова canvas.coords() и распаковки пары точек, которые формируют контейнер из четырех переменных.

Центр каждого элемента вычисляется за счет сложения x и y верхнего левого угла с половиной ширины и высоты. Результат, плюс сдвиг по каждой оси, соответствует финальному положению элемента после перемещения:


x0, y0, x1, y1 = self.canvas.coords(self.item)
pos_x = x0 + (x1 - x0) / 2 + off_x
pos_y = y0 + (y1 - y0) / 2 + off_y

После этого мы проверяем, находимся ли мы в пределах полотна. Для этого используем встроенную в Python поддержку связанных операторов сравнения:


if 0 <= pos_x <= self.width and 0 <= pos_y <= self.height:
self.canvas.move(self.item, off_x, off_y)

Наконец, этот метод планирует сам себя с задержкой в 10 миллисекунд с помощью вызова self.after(10, self.process_movements). Таким образом достигается эффект собственного основного цикла внутри реального цикла Tkinter.

Вас может заинтересовать, почему в этом примере использовался after(), а не after_idle() для планирования метода process_movements().

Это может казаться корректным подходом, поскольку других событий для обработки помимо перерисовки полотна и обработки событий клавиатуры нет, и нет необходимости добавлять задержку между вызовами process_movements(), если нет событий интерфейса в процессе ожидания.

Однако при использовании after_idle элементы бы перемещались со скоростью, зависящей от скорости компьютера. Это значит, что более быстрая система вызывала бы process_movements() больше раз за один и тот же промежуток времени.

С помощью минимальной фиксированной задержки есть возможность одинаково обрабатывать элементы на разных машинах.

]]>
Canvas, рисование графики ч.1 / tkinter 18 https://pythonru.com/uroki/canvas-risovanie-grafiki-ch-1-tkinter-18 Sat, 05 Dec 2020 08:40:00 +0000 https://pythonru.com/?p=4091 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

В предыдущих материалах основное внимание было уделено стандартному виджету Tkinter. Однако вне внимания остался виджет Canvas. Причина в том, что он предоставляет массу графических возможностей и заслуживает отдельного рассмотрения.

Canvas (полотно) — это прямоугольная область, в которой можно выводить не только текст или геометрические фигуры, такие как линии, прямоугольники или овалы, но также другие виджеты Tkinter. Все вложенные объекты называются элементами Canvas, и у каждого есть свой идентификатор, с помощью которого ими можно манипулировать еще до момента отображения.

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

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

Для рисования графических элементов на полотне, нужно обозначать их положение с помощью системы координат. Поскольку Canvas — это двумерная область, то точки будут обозначаться координатами горизонтальной и вертикальной осей — традиционными x и y соответственно.

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

Следующая программа содержит пустое полотно, а также метку, которая показывает положение курсора на нем. Можно перемещать курсор и видеть, в каком положении он находится. Это явно показывает, как изменяются координаты x и y в зависимости от положения курсора:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Базовый canvas")

self.canvas = tk.Canvas(self, bg="white")
self.label = tk.Label(self)
self.canvas.bind("", self.mouse_motion)

self.canvas.pack()
self.label.pack()

def mouse_motion(self, event):
x, y = event.x, event.y
text = "Позиция курсора: ({}, {})".format(x, y)
self.label.config(text=text)

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

Как работает система координат

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


def __init__(self):
# ...
self.canvas = tk.Canvas(self, bg="white")
self.label = tk.Label(self)
self.canvas.bind("", self.mouse_motion)

Следующий скриншот показывает точку, составленную из перпендикулярных проекций двух осей:

  • Координата x соответствует расстоянию по горизонтальной оси и увеличивается по мере движения слева направо;
  • Координата y соответствует расстоянию по вертикальной оси и увеличивается по мере движения снизу вверх;
Canvas, рисование графики ч.1 / tkinter 18

Можно обратить внимание на то, что эти координаты точно соответствуют атрибутам x и y экземпляра event, который был передан обработчику:


def mouse_motion(self, event):
x, y = event.x, event.y
text = "Позиция курсора: ({}, {})".format(x, y)
self.label.config(text=text)

Так происходит из-за того, что атрибуты рассчитываются относительно виджета, к которому прикреплено событие — в этом случае это последовательность <Motion>.

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

Аналогично если расположить элемент так, что его координаты будут лежать за пределами полотна, то часть его будет видна у правого и нижнего краев.

Рисование линий и стрелок

Одно из базовых действий, которое можно выполнить на полотне — рисование сегментов от одной точки к другой. Хотя есть другие способы рисовать многоугольники, метод create_line класса Canvas предлагает достаточное количество опций для понимания основ отображения элементов.

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

Также можно будет задавать определенные элементы внешнего вида, например, толщину и цвет:

Рисование линий и стрелок

Класс App будет отвечать за создание пустого полотна и обработку кликов мышью.

Информация о линии будет идти из класса LineForm. Такой подход с выделением компонента в отдельный класс позволит абстрагировать детали его реализации и сфокусироваться на работе с виджетом Canvas.

Говоря простым словами, мы пропускаем реализацию класса LineForm в следующем коде:


import tkinter as tk class LineForm(tk.LabelFrame):
# ...

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Базовый canvas")
self.line_start = None
self.form = LineForm(self)
self.canvas = tk.Canvas(self, bg="white")
self.canvas.bind("", self.draw)

self.form.pack(side=tk.LEFT, padx=10, pady=10)
self.canvas.pack(side=tk.LEFT)

def draw(self, event):
x, y = event.x, event.y
if not self.line_start:
self.line_start = (x, y)
else:
x_origin, y_origin = self.line_start
self.line_start = None
line = (x_origin, y_origin, x, y)
arrow = self.form.get_arrow()
color = self.form.get_color()
width = self.form.get_width()
self.canvas.create_line(*line, arrow=arrow,
fill=color, width=width)

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

Весь код целиком можно найти в отдельном файле lesson_18/drawing.py.

Как рисовать линии в Tkinter

Поскольку нужно обрабатывать клики мышью на полотне, свяжем метод draw() с этим типом события. Также определим поле line_start, чтобы отслеживать начальное положение каждой линии:


def __init__(self):
# ...
self.line_start = None
self.form = LineForm(self)
self.canvas = tk.Canvas(self, bg="white")
self.canvas.bind("", self.draw)

Метод draw() содержит основную логику приложения. Первый клик служит для определения начала для каждой линии и ничего не рисует. Координаты он получает из объекта event, который передается обработчику:


def draw(self, event):
x, y = event.x, event.y
if not self.line_start:
self.line_start = (x, y)
else:
# ...

Если у line_start уже есть значение, то мы получаем его и передаем координаты текущего события, чтобы нарисовать линию:


def draw(self, event):
x, y = event.x, event.y
if not self.line_start:
# ...
else:
x_origin, y_origin = self.line_start
self.line_start = None
line = (x_origin, y_origin, x, y)
self.canvas.create_line(*line)
text = "Линия проведена из ({}, {}) к ({}, {})".format(*line)

Метод canvas.create_line() принимает четыре аргумента, где первые два — это горизонтальная и вертикальная координаты начала линии, а вторые два — ее конечной точки.

Вывод текста в canvas

В некоторых случаях появляется необходимость вывести на полотне текст. Для этого нет нужды использовать дополнительный виджет, такой как Label. Класс Canvas включает метод create_text для отображения строки, которой можно управлять точно так же, как и любым другим элементом полотна.

При этом есть возможность использовать те же параметры форматирования, что позволит задавать стиль текста: цвет, размер и семейство шрифтов.

В этом примере объединим виджет Entry с содержимым текстового элемента полотна. И если у первого будет стандартный стиль, то текст на полотне можно будет стилизовать:

Вывод текста на полотне

Текстовый элемент по умолчанию будет отображаться с помощью canvas.create_text() и дополнительными параметрами, которые позволят добавить семейство шрифтов Consolas и синий цвет.

Динамическое поведение текстового элемента реализовано с помощью StringVar. Отслеживая эту переменную Tkinter, можно менять содержимое элемента:


import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Текстовые элементы Canvas")
self.geometry("300x100")

self.var = tk.StringVar()
self.entry = tk.Entry(self, textvariable=self.var)
self.canvas = tk.Canvas(self, bg="white")

self.entry.pack(pady=5)
self.canvas.pack()
self.update()

w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
options = {"font": "courier", "fill": "blue",
"activefill": "red"}
self.text_id = self.canvas.create_text((w / 2, h / 2), **options)
self.var.trace("w", self.write_text)

def write_text(self, *args):
self.canvas.itemconfig(self.text_id, text=self.var.get()) if __name__ == "__main__":
app = App()
app.mainloop()

Можно ознакомиться с этой программой, введя любой текст в поле ввода, что автоматически обновит его на полотне.

Как работает вывод текста на полотно

В первую очередь создается экземпляр Entry с переменной StringVar и виджетом Canvas:


self.var = tk.StringVar()
self.entry = tk.Entry(self, textvariable=self.var)
self.canvas = tk.Canvas(self, bg="white")

После этого виджеты размещаются с помощью вызовов методов geometry manager Pack. Важно отметить, что update() нужно вызывать в корневом окне, благодаря чему Tkinter будет вынужден обрабатывать все изменения, в данном случае — рендеринг виджетов до того, как метод __init__ продолжит выполнение:


self.entry.pack(pady=5)
self.canvas.pack()
self.update()

Это делается, потому что на следующем шаге будут выполняться вычисления размеров полотна, и до тех пор пока geometry manager не разместит виджет, у него не будет реальных значений высоты и ширины.

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

Эти координаты будут определять положение элемента, и вместе с параметрами стиля их нужно передать в метод create_text(). Аргумент-ключевое слово text — это стандартный параметр, но его можно пропустить, потому что он будет задаваться динамически при изменении значения StringVar:


w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
options = { "font": "courier", "fill": "blue",
"activefill": "red" }
self.text_id = self.canvas.create_text((w/2, h/2), **options)
self.var.trace("w", self.write_text)

Идентификатор, который возвращает create_text(), будет сохранен в поле text_id. Он будет использоваться в методе write_text() для ссылки на элемент. А этот метод будет вызван за счет механизма отслеживания операции записи в экземпляре var.

Для обновления параметра text в обработчике write_text() вызывается метод canvas.itemconfig() с идентификатором элемента в качестве первого аргумента и настройки — как второго.

В этой программе используем поле field_id, сохраненное при создании экземпляра App, а также содержимое StringVar с помощью метода get():

Метод write_text() определен таким образом, что он может получать переменное число аргументов, хотя они не нужны, потому что метод trace() переменных Tkinter передает их в функции обратного вызова.

В методе canvas.create_text() есть много других параметров для изменения внешнего вида элементов полотна.

Размещение текста в левом верхнем углу

Параметр anchor позволяет контролировать положение элемента относительно координат, переданных в качестве первого аргумента в canvas.create_text(). По умолчанию это значение равно tk.CENTER, что значит, что текст будет отцентрирован в этих координатах.

Если же его нужно разместить в верхнем левом углу, то достаточно передать (0, 0) и задать значение tk.NW для anchor, что выровняет его в северо-западном положении прямоугольной области, в которой находится текст:


# ...
options = { "font": "courier", "fill": "blue",
"activefill": "red", "anchor": tk.NW }
self.text_id = self.canvas.create_text((0, 0), **options)

Этот код обеспечит такой результат:

Canvas, рисование графики ч.1 / tkinter 18

Перенос строк

По умолчанию содержимое текстового элемента будет выводиться в одну строку. Параметр width же позволяет задать максимальную ширину строки. В результате если она окажется больше, то содержимое перенесется на новую строку:


# ...
options = { "font": "courier", "fill": "blue",
"activefill": "red", "width": 70 }
self.text_id = self.canvas.create_text((w/2, h/2), **options)

Теперь если написать Hello World, часть текста выйдет за пределы заданной ширины и перенесется на новую строку:

Canvas, рисование графики ч.1 / tkinter 18
]]>
Создание и обработка задач / tkinter 17 https://pythonru.com/uroki/sozdanie-i-obrabotka-zadach-tkinter-17 Sat, 28 Nov 2020 13:52:01 +0000 https://pythonru.com/?p=4054 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

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

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

Возьмем пример из материала о «Запланированных действиях», но с паузой в 1, а не 5 секунд.

При изменении состояния кнопки на DISABLED функция обратного вызова продолжает выполнение, поэтому состояние кнопки не меняется до тех пор, пока система находится в состоянии ожидания. Это значит, что она будет ждать завершения time.sleep().

Однако можно сделать так, чтобы Tkinter принудительно обновил все элементы графического интерфейса в режиме ожидания в конкретный момент:


import time
import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.button = tk.Button(self, command=self.start_action,
text="Ждать секунды")
self.button.pack(padx=30, pady=20)

def start_action(self):
self.button.config(state=tk.DISABLED)
self.update_idletasks()
time.sleep(1)
self.button.config(state=tk.NORMAL)
if __name__ == "__main__":
app = App()
app.mainloop()

Как работает обработка задач

Главная фишка здесь — вызов self.update_idletasks(). Благодаря этому изменение состояния кнопки обрабатывается Tkinter до вызова time.sleep(). И в ту секунду, пока функция обратного вызова приостановлена, кнопка выглядит так, как нужно, потому что Tkinter задает это состояние еще до вызова функции обратного вызова.

Для иллюстрации примера был использован метод time.sleep(), но в реальных ситуациях стоит ожидать куда более сложные вычисления.

Создание отдельных процессов

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

В таком случае нужно использовать модуль subprocess для вызова определенной программы из процесса Python.

Следующий пример выполняет запрос на обозначенный DNS или IP адрес:

Создание отдельных процессов

Обычно определяется метод AsyncAction, но в этот раз вызовем subprocess.run() со значением в виджете Entry.

Эта функция запускает отдельный подпроцесс, который, в отличие от потоков, использует другую область памяти. Это значит, что для получения результата команды ping потребуется перенаправить его в стандартный вывод и прочесть в Python-программе:


import threading
import subprocess
import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.entry = tk.Entry(self)
self.button = tk.Button(self, text="Пинг!",
command=self.do_ping)
self.output = tk.Text(self, width=80, height=15)

self.entry.grid(row=0, column=0, padx=5, pady=5)
self.button.grid(row=0, column=1, padx=5, pady=5)
self.output.grid(row=1, column=0, columnspan=2,
padx=5, pady=5)

def do_ping(self):
self.button.config(state=tk.DISABLED)
thread = AsyncAction(self.entry.get())
thread.start()
self.poll_thread(thread)

def poll_thread(self, thread):
if thread.is_alive():
self.after(100, lambda: self.poll_thread(thread))
else:
self.button.config(state=tk.NORMAL)
self.output.delete(1.0, tk.END)
self.output.insert(tk.END, thread.result) class AsyncAction(threading.Thread):
def __init__(self, ip):
super().__init__()
self.ip = ip

def run(self):
self.result = subprocess.run(["ping", self.ip], shell=True,
stdout=subprocess.PIPE).stdout.decode("CP866") if __name__ == "__main__":
app = App()
app.mainloop()

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

Функция run() выполняет подпроцесс, заданный в массиве аргументов. По умолчанию результат включает только код процесса, поэтому нужно также передать параметр stdout с константой PIPE, чтобы обозначить, что стандартный вывод следует передать.

Эта функция вызывается с аргументом-ключевым словом shell и значением True, чтобы для процесса ping не открывалось новое окно терминала:


def run(self):
self.result = subprocess.run(["ping", self.ip], shell=True,
stdout=subprocess.PIPE).stdout.decode("CP866")

Наконец, когда основной поток подтверждает, что операция завершилась, он выводит результат в виджете Text:


def poll_thread(self, thread):
if thread.is_alive():
self.after(100, lambda: self.poll_thread(thread))
else:
self.button.config(state=tk.NORMAL)
self.output.delete(1.0, tk.END)
self.output.insert(tk.END, thread.result)
]]>
Асинхронное приложение ч.2 / tkinter 16 https://pythonru.com/uroki/asinhronnoe-prilozhenie-ch-2-tkinter-16 Sat, 21 Nov 2020 17:27:31 +0000 https://pythonru.com/?p=4021 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Выполнение HTTP-запросов

Общение приложения с удаленным сервером с помощью HTTP — это распространенный случай в асинхронном программирования. Клиент делает запрос, который передается по сети по протоколу TCP/IP. После этого сервер обрабатывает информацию и отправляет клиенту ответ.

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

Есть много сторонних веб-сервисов, которые можно использовать на этапе разработки в целях прототипирования. Однако лучше этого не делать, ведь их API может поменяться или же они вообще станут недоступны.

В этом примере реализуем HTTP-сервер, который генерирует случайный ответ в формате JSON и выведем его в приложении с графическим интерфейсом.


import time
import json
import random
from http.server import HTTPServer, BaseHTTPRequestHandler class RandomRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
# Имитация задержки
time.sleep(3)

# Добавляем заголовки ответа
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()

# Добавляем тело ответа
body = json.dumps({'random': random.random()})
self.wfile.write(bytes(body, "utf8")) def main():
"""Запускает HTTP-сервер на порту 8090"""
server_address = ('', 8090)
httpd = HTTPServer(server_address, RandomRequestHandler)
httpd.serve_forever() if __name__ == "__main__":
main()

Для запуска сервера нужно выполнить скрипт server.py и оставить процесс запущенным для получения запросов на локальном порте 8090.

Клиентское приложение состоит из метки для показа информации пользователям и кнопки для выполнения нового HTTP-запроса на локальный сервер:


import json
import threading
import urllib.request
import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Выполнение HTTP-запросов")
self.label = tk.Label(self, text="Нажмите 'Старт', чтобы получить случайное значение.")
self.button = tk.Button(self, text="Старт",
command=self.start_action)

self.label.pack(padx=60, pady=10)
self.button.pack(pady=10)

def start_action(self):
self.button.config(state=tk.DISABLED)
thread = AsyncAction()
thread.start()
self.check_thread(thread)

def check_thread(self, thread):
if thread.is_alive():
self.after(100, lambda: self.check_thread(thread))
else:
text = "Случайное значение: {}".format(thread.result)
self.label.config(text=text)
self.button.config(state=tk.NORMAL) class AsyncAction(threading.Thread):
def run(self):
self.result = None
url = "http://localhost:8090"
with urllib.request.urlopen(url) as f:
obj = json.loads(f.read().decode("utf-8"))
self.result = obj["random"]
if __name__ == "__main__":
app = App()
app.mainloop()

После завершения запроса метка покажем случайное значение, которое было сгенерировано на сервере:

Выполнение HTTP-запросов

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

Как работают HTTP-запросы

В этом примере класс Thread был расширен для реализации логики, которая должна работать на отдельном потоке с применением более объектно-ориентированного подхода. Это делается за счет переопределения метода run(), который будет отвечать за выполнение HTTP-запроса на локальный сервер:


class AsyncAction(threading.Thread):
def run(self):
# ...

Существует множество клиентских HTTP-библиотек, но в этом примере используем модуль urllib.request из стандартной библиотеки. Он включает функцию urlopen(), которая принимает URL в виде строки и возвращает HTTP-ответ, который может работать как контекстный менеджер. Это позволит безопасно прочитать информацию и закрыть его с помощью with.

Сервер возвращает приблизительной такой JSON-документ (увидеть его можно, открыв http://localhost:8080 в бразуере):

{"random": 0.0915826359180778}

Чтобы декодировать строку в объект, нужно передать содержимое ответа функции loads() из модуля json. Благодаря этому можно получить доступ к случайному значению с помощью словаря и сохранить его в атрибуте result, экземпляр которого создается со значением None. Благодаря этому основной поток не будет считывать этот атрибут в случае ошибки:


def run(self):
self.result = None
url = "http://localhost:8090"
with urllib.request.urlopen(url) as f:
obj = json.loads(f.read().decode("utf-8"))
self.result = obj["random"]

После этого приложение с графическим интерфейсом периодически опрашивает статус потока, как было видно в прошлом примере:


def check_thread(self, thread):
if thread.is_alive():
self.after(100, lambda: self.check_thread(thread))
else:
text = "Random value: {}".format(thread.result)
self.label.config(text=text)
self.button.config(state=tk.NORMAL)

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

Соединение потоков прогрессбаром

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

Приложение будет состоять из горизонтальной шкалы прогресса, которая будет постепенно увеличивать количество прогресса после нажатия на кнопку Старт:

Соединение потоков прогрессбаром

Для симуляции выполнения фоновой задачи инкремент шкалы будет генерироваться в другом потоке, который должен приостанавливать выполнение на 1 секунду для каждого шага.

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


import time
import queue
import threading
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.messagebox as mb class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Пример Progressbar")
self.queue = queue.Queue()
self.progressbar = ttk.Progressbar(self, length=300,
orient=tk.HORIZONTAL)
self.button = tk.Button(self, text="Старт",
command=self.start_action)

self.progressbar.pack(padx=10, pady=10)
self.button.pack(padx=10, pady=10)

def start_action(self):
self.button.config(state=tk.DISABLED)
thread = AsyncAction(self.queue, 20)
thread.start()
self.poll_thread(thread)

def poll_thread(self, thread):
self.check_queue()
if thread.is_alive():
self.after(100, lambda: self.poll_thread(thread))
else:
self.button.config(state=tk.NORMAL)
mb.showinfo("Готово!", "Асинхронное действие завершено")

def check_queue(self):
while self.queue.qsize():
try:
step = self.queue.get(0)
self.progressbar.step(step * 100)
except queue.Empty:
pass class AsyncAction(threading.Thread):
def __init__(self, queue, steps):
super().__init__()
self.queue = queue
self.steps = steps

def run(self):
for _ in range(self.steps):
time.sleep(1)
self.queue.put(1 / self.steps) if __name__ == "__main__":
app = App()
app.mainloop()

Как работает прогрессбар с потоками

Progressbar — это тематический виджет из модуля tkinter.ttk.

Также нужно импортировать модуль queue, который определяет синхронизированные коллекции, такие как Queue. Синхронность — важная тема в средах с несколькими потоками, потому что если к ресурсам с общим доступом попытаться получить доступ одновременно, результат будет непредсказуемым. Такие маловероятные, но все равно возможные сценарии называются состоянием гонки.

Теперь класс App включает такие новые инструкции:


# ...
import queue
import tkinter.ttk as ttk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Пример Progressbar")
self.queue = queue.Queue()
self.progressbar = ttk.Progressbar(self, length=300,
orient=tk.HORIZONTAL)

Как и в предыдущих примерах метод start_action() запускает поток, передавая queue и количество шагов, которые будут симулировать долгоиграющую задачу:


def start_action(self):
self.button.config(state=tk.DISABLED)
thread = AsyncAction(self.queue, 20)
thread.start()
self.poll_thread(thread)

Подкласс AsyncAction определяет конструктор для получения этих параметров, которые позже будут использованы в методе run():


class AsyncAction(threading.Thread):
def __init__(self, queue, steps):
super().__init__()
self.queue = queue
self.steps = steps

def run(self):
for _ in range(self.steps):
time.sleep(1)
self.queue.put(1 / self.steps)

Цикл приостанавливает выполнение потока на 1 секунду и добавляет инкремент в очередь в зависимости от значения атрибута steps.

Элемент, добавленный в очередь, считывается из экземпляра приложения с помощью чтения очереди из check_queue():


def check_queue(self):
while self.queue.qsize():
try:
step = self.queue.get(0)
self.progressbar.step(step * 100)
except queue.Empty:
pass

Следующий метод периодически вызывается из poll_thread(), который опрашивает статус потока и планирует сам себя с помощью after(), пока поток не завершит выполнение:


def poll_thread(self, thread):
self.check_queue()
if thread.is_alive():
self.after(100, lambda: self.poll_thread(thread))
else:
self.button.config(state=tk.NORMAL)
mb.showinfo("Готово!", "Асинхронное действие завершено")

Отмена запланированных действий

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

Возьмем пример из первого приложения и добавим кнопку Stop, которая позволяет остановить запланированное действие.

Она будет активной только при наличии запланированного действия. Это значит, что после нажатия кнопки слева пользователь может подождать 5 секунд или нажать на Stop, чтобы снова сделать ее доступной:

Отмена запланированных действий

Метод after_cancel() отменяет выполнение запланированного действия, используя идентификатор, который вернулся после вызова after(). В этом примере это значение хранится в атрибуте scheduled_id:


import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.button = tk.Button(self, command=self.start_action,
text="Подождите 5 секунд")
self.cancel = tk.Button(self, command=self.cancel_action,
text="Стоп", state=tk.DISABLED)
self.button.pack(padx=30, pady=20, side=tk.LEFT)
self.cancel.pack(padx=30, pady=20, side=tk.LEFT)

def start_action(self):
self.button.config(state=tk.DISABLED)
self.cancel.config(state=tk.NORMAL)
self.scheduled_id = self.after(5000, self.init_buttons)

def init_buttons(self):
self.button.config(state=tk.NORMAL)
self.cancel.config(state=tk.DISABLED)

def cancel_action(self):
print("Отмена событий", self.scheduled_id)
self.after_cancel(self.scheduled_id)
self.init_buttons() if __name__ == "__main__":
app = App()
app.mainloop()

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

Чтобы отменить запланированное действие для функции обратного вызова сперва нужен идентификатор, который возвращает after(). Сохраним его в атрибуте scheduled_id, поскольку он понадобится в отдельном методе:


def start_action(self):
self.button.config(state=tk.DISABLED)
self.cancel.config(state=tk.NORMAL)
self.scheduled_id = self.after(5000, self.init_buttons)

Затем это поле передается в after_callback() обратного вызова кнопки Стоп:


def cancel_action(self):
print("Отмена событий", self.scheduled_id)
self.after_cancel(self.scheduled_id)
self.init_buttons()

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

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

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

К сожалению, не существует официального API для остановки экземпляра Thread. При определении кастомного подкласса может потребоваться добавить флаг, который периодически проверяется в методе run():


class MyAsyncAction(threading.Thread):
def __init__(self):
super().__init__()
self.do_stop = False def run(self):
# Начать выполнение...
if not self.do_stop:
# Продолжить выполнение...

Затем этот флаг может быть изменен внешне с помощью thread.do_stop = True при вызове after_cancel() для остановки и потока.

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

А с Python 3.4 можно использовать модуль asyncio, который включает классы и функции для управления асинхронными операциями, включая отмены. Хотя он и не касается этого материала, на него обязательно стоит обратить внимание.

]]>
Асинхронное приложение ч.1 / tkinter 15 https://pythonru.com/uroki/asinhronnoe-prilozhenie-ch-1-tkinter-15 Sat, 14 Nov 2020 13:16:55 +0000 https://pythonru.com/?p=3999 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Планирование действий

Базовый метод предотвращения блокировки основного потока в Tkinter — это планирование действий, которые будут выполнены после истечения заданного времени.

В этом материале разберемся с тем, как реализовать этот подход в Tkinter с помощью метода after(), который может быть вызван во всех классах виджетов.

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

Это приложение состоит из одной кнопки, которая становится неактивной после нажатия. Через 5 секунд ее снова можно нажать. Простейшая реализация будет выглядеть следующим образом:


import time
import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.button = tk.Button(self, command=self.start_action,
text="Ждать 5 секунд")
self.button.pack(padx=50, pady=20)

def start_action(self):
self.button.config(state=tk.DISABLED)
time.sleep(5)
self.button.config(state=tk.NORMAL) if __name__ == "__main__":
app = App()
app.mainloop()

Если запустить эту программу, то можно заметить, что не кнопка становится неактивной, а весь графический интерфейс зависает на 5 секунд. Это понятно по внешнему виду кнопки, которая в течение 5 секунд выглядит нажатой, а не выключенной. Более того, строка заголовка не будет реагировать на клики мыши все это время:

Асинхронное приложение ч.1 / tkinter 15

Если активировать дополнительные виджеты, например, Entry и Scroll, то это поведение задело бы и их.

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

Как работает планирование действий

Метод after() позволяет регистрировать функцию обратного вызова, которая вызывается после задержки, заданной в миллисекундах в основном цикле Tkinter. По сути, они представляют собой зарегистрированные сигналы-события, которые обрабатываются в те моменты, когда система находится в состоянии ожидания.

Таким образом заменим вызов time.sleep(5) на self.after(5000,callback). Используем экземпляр self, потому что метод after() также доступен в корневом экземпляре Tk, и нет разницы в том, чтобы вызывать его из дочернего виджета:


import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.button = tk.Button(self, command=self.start_action,
text="Ждать 5 секунд")
self.button.pack(padx=50, pady=20)

def start_action(self):
self.button.config(state=tk.DISABLED)
self.after(5000, lambda: self.button.config(state=tk.NORMAL)) if __name__ == "__main__":
app = App()
app.mainloop()

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

Планирование действий

По последнему примеру можно предположить, что метод after() исполняется после заданной в миллисекундах длительности.

Однако на самом деле метод просит Tkinter зарегистрировать событие, что гарантирует, что оно не выполнится ранее намеченного времени. И если основной поток занят, то верхнего предела того, когда оно все-таки выполнится, нет.

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


print("Первый")
self.after(1000, lambda: print("Третий"))
print("Второй")

Этот код выведет «Первый», «Второй» и, наконец, «Третий» спустя секунду. Все это время графический интерфейс будет оставаться доступным, а пользователи смогут продолжать взаимодействовать с ним.

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

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

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

Метод after() возвращает идентификатор запланированного события, который можно передать в метод after_cancel() для отмены выполнения функции обратного вызова.

Дальше рассмотрим, как реализовать остановку запланированного события с помощью этого метода.

Работа в потоках

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

Стандартная библиотека Python включает модуль threading для создания и контроля несколько потоков с помощью высокоуровневого интерфейса, который позволяет работать с простыми классами и методами.

Стоит отметить, что CPython — «эталонная реализация» Python — ограничена GIL (Global Interpreter Lock), механизмом, который не дает нескольким потокам запускать байт-код Python одновременно. Из-за этого невозможно пользоваться преимуществами многопроцессорных систем. Об этом важно помнить при попытке улучшить производительность приложения.

В следующем примере объединены приостановка потока с помощью time.sleep(), а также действие, запланированное с помощью after():


import time
import threading
import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.button = tk.Button(self, command=self.start_action,
text="Ждать 5 секунд")
self.button.pack(padx=50, pady=20)

def start_action(self):
self.button.config(state=tk.DISABLED)
thread = threading.Thread(target=self.run_action)
print(threading.main_thread().name)
print(thread.name)
thread.start()
self.check_thread(thread)

def check_thread(self, thread):
if thread.is_alive():
self.after(100, lambda: self.check_thread(thread))
else:
self.button.config(state=tk.NORMAL)

def run_action(self):
print("Запуск длительного действия...")
time.sleep(5)
print("Длительное действие завершено!") if __name__ == "__main__":
app = App()
app.mainloop()

Как работают треды

Для создания нового объекта Thread можно использовать конструктор и аргумент-ключевое слово target. Он будет вызван на отдельном потоке при использовании его же метода start().

В прошлом примере использовалась ссылка на метод run_action, примененная экземпляру текущего приложения:


thread = threading.Thread(target=self.run_action)
thread.start()

После этого периодически опрашивается статус потока после after(), который планирует тот же метод до завершения потока:


def check_thread(self, thread):
if thread.is_alive():
self.after(100, lambda: self.check_thread(thread))
else:
self.button.config(state=tk.NORMAL)

В прошлом примере задержка была 100 миллисекунд, потому что чаще проверять статус нет необходимости. Хотя это всегда зависит от типа действия на потоке.

Это процесс может быть представлен в виде такой диаграммы:

Асинхронное приложение ч.1 / tkinter 15

Прямоугольник Thread-1 представляет время, во время которого поток занят выполнением time.sleep(5). В то же время MainThread только проверяет статус, и нет ни одной операции, которая приводила бы к зависанию всего интерфейса.

В этом материале мы познакомились с классом Thread, но важно остановиться на некоторых деталях создания их экземпляров и использования в программах на Python.

Методы Thread — start, run и join

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

Если же вызвать join(), то основной был бы заблокирован до остановки нового. Это привело бы к тому же «зависанию», которого мы пытались избежать, даже при использовании нескольких потоков.

Наконец, метод run() — это то, где поток выполняет операцию. В будущем его нужно перезаписывать.

Важно запомнить, что нужно всегда вызывать start() из основного потока, чтобы не блокировать его.

Параметры для метода

При использовании конструктора класса Thread можно задать аргументы для передаваемого метода с помощью параметра args:


def start_action(self):
self.button.config(state=tk.DISABLED)
thread = threading.Thread(target=self.run_action, args=(5,))
thread.start()
self.check_thread(thread)

def run_action(self, timeout):
# ...

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

]]>
Рефакторинг с помощью паттерна MVC / tkinter 14 https://pythonru.com/uroki/refaktoring-s-pomoshhju-patterna-mvc-tkinter Sat, 07 Nov 2020 17:13:59 +0000 https://pythonru.com/?p=3947 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Теперь, когда вся функциональность приложения готова, можно обратить внимание на проблемы в его дизайне. Например, класс App имеет несколько обязанностей: от создания экземпляров виджетов Tkinter до выполнения инструкций SQL.

Хотя кажется довольно простым и очевидным писать методы, которые бы выполняли операции от начала и до конца, этот подход приводит к тому, что код становится все сложнее поддерживать. Определить этот недостаток можно, предположив кое-какие архитектурные изменения, как например, замену реляционной базы данных на REST-бэкенд, связываться с которым приложение будет по HTTP.

Начнем с определения паттерна MVC и того, как он соотносится с разными частями приложения, которое было создано в прошлых материалах.

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

  • model (модель) представляет доменные данные и содержит бизнес-правила для взаимодействия с ними. В этом примере это класс Contact и конкретный код SQLite.
  • view (представление) — графическое представление данных модели. В нашем приложении это виджеты Tkinter, которые и представляют собой графический интерфейс.
  • controller (контроллер) связывает представление и модель, получая пользовательский ввод и обновляя данные. Это — функции обратного вызова и обработчики событий, а также некоторые атрибуты.

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

В первую очередь нужно извлечь весь код, который отвечает за взаимодействие с базой данных, и поместить его в отдельный класс. Это позволит скрыть детали реализации слоя данных, оставив всего лишь 4 метода: get_contacts, add_contact, update_contact и delete_contact:


class ContactsRepository(object):
def __init__(self, conn):
self.conn = conn

def to_values(self, c):
return c.last_name, c.first_name, c.email, c.phone

def get_contacts(self):
sql = """SELECT rowid, last_name, first_name, email, phone
FROM contacts"""
for row in self.conn.execute(sql):
contact = Contact(*row[1:])
contact.rowid = row[0]
yield contact

def add_contact(self, contact):
sql = "INSERT INTO contacts VALUES (?, ?, ?, ?)"
with self.conn:
cursor = self.conn.cursor()
cursor.execute(sql, self.to_values(contact))
contact.rowid = cursor.lastrowid
return contact

def update_contact(self, contact):
sql = """UPDATE contacts
SET last_name = ?, first_name = ?, email = ?, phone = ?
WHERE rowid = ?"""
with self.conn:
self.conn.execute(sql, self.to_values(contact) + (contact.rowid,))
return contact

def delete_contact(self, contact):
sql = "DELETE FROM contacts WHERE rowid = ?"
with self.conn:
self.conn.execute(sql, (contact.rowid,))

Это, вместе с классом Contact, станет моделью приложения.
Представление будет включать лишь код, необходимый для отображения графического интерфейса, и методы контроллера для обновления. Переименуем класс в ContactsView, что лучше выразит его назначение:


class ContactsView(tk.Tk):
def __init__(self):
super().__init__()
self.title("Список контактов")
self.list = ContactList(self, height=15)
self.form = UpdateContactForm(self)
self.btn_new = tk.Button(self, text="Добавить контакт")

self.list.pack(side=tk.LEFT, padx=10, pady=10)
self.form.pack(padx=10, pady=10)
self.btn_new.pack(side=tk.BOTTOM, pady=5)

def set_ctrl(self, ctrl):
self.btn_new.config(command=ctrl.create_contact)
self.list.bind_doble_click(ctrl.select_contact)
self.form.bind_save(ctrl.update_contact)
self.form.bind_delete(ctrl.delete_contact)

def add_contact(self, contact):
self.list.insert(contact)

def update_contact(self, contact, index):
self.list.update(contact, index)

def remove_contact(self, index):
self.form.clear()
self.list.delete(index)

def get_details(self):
return self.form.get_details()

def load_details(self, contact):
self.form.load_details(contact)

Также стоит обратить внимание, что пользовательский ввод обрабатывается контроллером. Для этого добавлен метод set_ctrl, который связывается с функциями обратного вызова Tkinter.
Класс ContactsController теперь будет включать весь оставшийся код — взаимодействие интерфейса и слоя с данными с атрибутами selection и contacts:


class ContactsController(object):
def __init__(self, repo, view):
self.repo = repo
self.view = view
self.selection = None
self.contacts = list(repo.get_contacts())

def create_contact(self):
new_contact = NewContact(self.view).show()
if new_contact:
contact = self.repo.add_contact(new_contact)
self.contacts.append(contact)
self.view.add_contact(contact)

def select_contact(self, index):
self.selection = index
contact = self.contacts[index]
self.view.load_details(contact)

def update_contact(self):
if not self.selection:
return
rowid = self.contacts[self.selection].rowid
update_contact = self.view.get_details()
update_contact.rowid = rowid

contact = self.repo.update_contact(update_contact)
self.contacts[self.selection] = contact
self.view.update_contact(contact, self.selection)

def delete_contact(self):
if not self.selection:
return
contact = self.contacts[self.selection]
self.repo.delete_contact(contact)
self.view.remove_contact(self.selection)

def start(self):
for c in self.contacts:
self.view.add_contact(c)
self.view.mainloop()

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

# Предположим, что __main__.py находится в lesson_14
$ python lesson_14
# Или, если мы сжимаем содержимое каталога
$ python lesson_14.zip

Как работает реализация MVC

Оригинальная реализация MVC была представлена в языке программирования Smalltalk. Ее можно представить следующей схемой:

Оригинальная реализация MVC

На ней можно увидеть, что представление передает пользовательские события контроллеру, который, в свою очередь, обновляет модель. Чтобы показать эти изменения, вводится понятие паттерна наблюдателя. Это значит, что представления подписываются на то, чтобы получать уведомления при обновлении. Таким образом они запрашивают состояние модели и меняют отображаемые данные.

Существует вариация этого дизайна, где нет коммуникации между представлением и моделью. Вместо этого изменения применяются контроллером после обновления модели:

Рефакторинг с помощью паттерна MVC / tkinter 14

Такой подход называется пассивной моделью и является самым распространенным в современных MVC-приложениях, особенно веб-фреймворках. Он использовался и в этом материале, потому что он упрощает ContactsRepository и не требует серьезных изменений в классе ContactsController.

Можно было обратить внимание, что операции обновления и удаления работают благодаря полю rowid, например, в случае с методом update_contact из класса ContactsController:


def update_contact(self):
if not self.selection:
return
rowid = self.contacts[self.selection].rowid
update_contact = self.view.get_details()
update_contact.rowid = rowid

Поскольку это — особенность реализации для базы данных SQLite, ее нужно скрыть от остальных компонентов.

Решение — добавить другое поле классу Contact с именем id или contact_id (важно не забыть, что id — это еще и встроенная функция Python, поэтому некоторые редакторы могут неправильно ее подсветить).

Затем можно предположить, что это поле — полноценная часть данных и представляет собой уникальный идентификатор. А уже детали генерации можно оставить модели.

]]>
Чтение данных из csv и сохранение в SQLite / tkinter 13 https://pythonru.com/uroki/chtenie-dannyh-iz-csv-i-sohranenie-v-sqlite-tkinter-13 Sat, 31 Oct 2020 14:48:25 +0000 https://pythonru.com/?p=3908 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Необходимые файлы есть в папке «lesson_13» из репозитория по ссылке выше.

Чтение записей из CSV-файла

В качестве первой попытки загрузки данных для чтения в приложение используем файл CSV (comma-separated value, то есть — значения, разделенные запятыми). Этот формат сводит в таблицу данные в обычных текстовых файлах. Каждый файл соответствует полю записи, разделенному запятыми, например, вот так:

Gauford,Albertine,agauford0@acme.com,(614) 7171720
Greger,Bryce,bgreger1@acme.com,(616) 3543513
Wetherald,Rickey,rwetherald2@acme.com,(379) 3652495

Такое решение легко реализовать для простых сценариев, особенно если текстовые поля не содержат разрывов строк. Используем модуль csv из стандартной библиотеки, а после загрузки данных заполним ими виджеты из прошлых материалов.

Соберем все виджеты, созданные ранее. После загрузки записей из CSV-файла приложение будет выглядеть как на следующем скриншоте:

Чтение записей из CSV-файла

Помимо импорта класса Contact также импортируем виджеты ContactForm и ContactList:


import csv
import tkinter as tk

from lesson_12.structuring_data import Contact
from lesson_12.widgets import ContactForm, ContactList

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Контакты из CSV")
self.list = ContactList(self, height=12)
self.form = ContactForm(self)
self.contacts = self.load_contacts()

for contact in self.contacts:
self.list.insert(contact)
self.list.pack(side=tk.LEFT, padx=10, pady=10)
self.form.pack(side=tk.LEFT, padx=10, pady=10)
self.list.bind_doble_click(self.show_contact)

def load_contacts(self):
with open("contacts.csv", encoding="utf-8", newline="") as f:
return [Contact(*r) for r in csv.reader(f)]

def show_contact(self, index):
contact = self.contacts[index]
self.form.load_details(contact)

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

Как работает загрузка данных из CSV

Функция load_contacts отвечает за чтение файла CSV и трансформацию всех записей в список экземпляров Contact.

Каждая строка, считанная csv.reader, возвращается в виде кортежа строк, созданного с помощью разделения соответствующей строки по запятым. Поскольку кортеж использует тот же порядок, что и параметры в методе __init__ класса Contact, можно запросто распаковать его с помощью оператора *. Весь этот код можно собрать в одну строку с помощью «list comprehension»:


def load_contacts(self):
with open("contacts.csv", encoding="utf-8", newline="") as f:
return [Contact(*r) for r in csv.reader(f)]

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

Сохранение данных в базе данных SQLite

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

Можно сохранять все записи в тот же текстовый файл после каждого изменения, но это не очень эффективно, особенно когда речь идет об изменении нескольких отдельных строк.

Поскольку вся информация будет храниться локально, для этих целей можно использовать базу данных SQLite. Модуль sqlite3 — это часть стандартной библиотеки, поэтому для ее использования не нужны дополнительные зависимости.

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

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

Сначала создадим соединение с файлом contacts.db, где данные будут храниться. После этого создадим таблицу contacts с текстовыми полями last_name, first_name, email и phone.

Поскольку csv.reader возвращает итерируемый объект, состоящий из кортежей, чьи поля следуют порядку, определенному в CREATE TALE, их можно прямо передать в метод executemany. Это выполнит инструкцию INSERT для каждого кортежа, заменяя вопросительные знаки на реальные значения каждой записи:


import csv
import sqlite3

def main():
with open("contacts.csv", encoding="utf-8", newline="") as f, \
sqlite3.connect("contacts.db") as conn:
conn.execute("""CREATE TABLE contacts (
last_name text,
first_name text,
email text,
phone text
)""")
conn.executemany("INSERT INTO contacts VALUES (?,?,?,?)",
csv.reader(f))

if __name__ == "__main__":
main()

Инструкция with автоматически подтверждает транзакцию и закрывает файл и SQLite-соединение в конце выполнения.

Как работать с базой данных

Для добавления новых контактов в базу данных определим подкласс Toplevel, который будет переиспользовать ContactForm для создания экземпляров новых контактов:


class NewContact(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.contact = None
self.form = ContactForm(self)
self.btn_add = tk.Button(self, text="Подтвердить", command=self.confirm)
self.form.pack(padx=10, pady=10)
self.btn_add.pack(pady=10)

def confirm(self):
self.contact = self.form.get_details()
if self.contact:
self.destroy()

def show(self):
self.grab_set()
self.wait_window()
return self.contact

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

Как работать с базой данных

Также расширим класс ContactForm двумя дополнительными кнопками: одна будет использоваться для обновления информации, а вторая — для удаления выбранного контакта:


class UpdateContactForm(ContactForm):
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self.btn_save = tk.Button(self, text="Сохранить")
self.btn_delete = tk.Button(self, text="Удалить")

self.btn_save.pack(side=tk.RIGHT, ipadx=5, padx=5, pady=5)
self.btn_delete.pack(side=tk.RIGHT, ipadx=5, padx=5, pady=5)

def bind_save(self, callback):
self.btn_save.config(command=callback)

def bind_delete(self, callback):
self.btn_delete.config(command=callback)

Методы bind_save и bind_delete позволят связать функцию обратного вызова с соответствующей кнопкой command.

Для интеграции всех этих изменений добавим соответствующий код в класс App:


class App(tk.Tk):
def __init__(self, conn):
super().__init__()
self.title("Контакты из SQLite")
self.conn = conn
self.selection = None
self.list = ContactList(self, height=15)
self.form = UpdateContactForm(self)
self.btn_new = tk.Button(self, text="Добавить контакт",
command=self.add_contact)
self.contacts = self.load_contacts()

for contact in self.contacts:
self.list.insert(contact)
self.list.pack(side=tk.LEFT, padx=10, pady=10)
self.form.pack(padx=10, pady=10)
self.btn_new.pack(side=tk.BOTTOM, pady=5)

self.list.bind_doble_click(self.show_contact)
self.form.bind_save(self.update_contact)
self.form.bind_delete(self.delete_contact)

Также нужно поменять метод load_contacts для создания контактов из результата запроса:


def load_contacts(self):
contacts = []
sql = "SELECT rowid, last_name, first_name, email, phone FROM contacts"
for row in self.conn.execute(sql):
contact = Contact(*row[1:])
contact.rowid = row[0]
contacts.append(contact)
return contacts

def show_contact(self, index):
self.selection = index
contact = self.contacts[index]
self.form.load_details(contact)

Для добавления контакта в список нужно создавать экземпляр диалога NewContact и вызывать его метод show для получения всех данных. Если значения валидны, то сохраним их в кортеже в том же порядке, что и в инструкции INSERT:


def to_values(self, c):
return (c.last_name, c.first_name, c.email, c.phone)

def add_contact(self):
new_contact = NewContact(self)
contact = new_contact.show()
if not contact:
return
values = self.to_values(contact)
with self.conn as c:
cursor = c.cursor()
cursor.execute("INSERT INTO contacts VALUES (?,?,?,?)", values)
contact.rowid = cursor.lastrowid
self.contacts.append(contact)
self.list.insert(contact)

После выбора контактов их детали можно обновить, получив текущие значения форм. Если они валидны, выполним UPDATE, чтобы задать колонки записей с указанным rowid.

Поскольку поля находятся в том же порядке, что и в INSERT, можем заново использовать метод to_values для создания кортежа из экземпляра контакта. Единственным отличием будет то, что нужно добавить параметр замены для rowid:


def update_contact(self):
if self.selection is None:
return
rowid = self.contacts[self.selection].rowid
contact = self.form.get_details()
if contact:
values = self.to_values(contact)
with self.conn as c:
sql = """UPDATE contacts SET
last_name = ?,
first_name = ?,
email = ?,
phone = ?
WHERE rowid = ?"""
c.execute(sql, values + (rowid,))
contact.rowid = rowid
self.contacts[self.selection] = contact
self.list.update(contact, self.selection)

Для удаления выбранного контакта получаем его rowid и подставляем в DELETE. Когда транзакция подтверждена, контакт удаляется из графического представления: он пропадает из формы и удаляется из списка. Значением атрибута selection становится None, что позволяет не выполнять операции над элементами, которые уже не являются валидными:


def delete_contact(self):
if self.selection is None:
return
rowid = self.contacts[self.selection].rowid
with self.conn as c:
c.execute("DELETE FROM contacts WHERE rowid = ?", (rowid,))
self.form.clear()
self.list.delete(self.selection)
self.selection = None

Наконец, оборачиваем код для создания экземпляра приложения в функцию main:


def main():
with sqlite3.connect("contacts.db") as conn:
app = App(conn)
app.mainloop()

if __name__ == "__main__":
main()

Со всеми этими изменениями приложение будет выглядеть следующим образом:

Как работать с базой данных

Как работают SQL запросы

Такой тип приложения часто называют CRUD (Create, Read, Update Delete — Создание, Чтение, Обновление, Удаление). Эти операции соответствуют SQL-инструкциям, таким как INSERT, SELECT, UPDATE и DELETE.

Теперь посмотрим, как реализовать каждую из операций с помощью класса sqlite3.Connection.

INSERT добавляет новые записи в таблицу, указывая названия колонок и соответствующие значения. Вместо этого можно использовать и порядок колонок.

При создании таблицы в SQLite по умолчанию создается колонка rowid, которой автоматически присваивается уникальное значение для идентификации каждой строки. Поскольку это значение часто требуется для последующих операций, получить его можно с помощью атрибута lastrowid, которое есть у класса Cursor:


sql = "INSERT INTO my_table (col1, col2, col3) VALUES (?, ?, ?)"
with connection:
cursor = connection.cursor()
cursor.execute(sql, (value1, value2, value3))
rowid = cursor.lastrowid

SELECT получает значения одной или нескольких колонок из таблицы. Также можно добавить условие WHERE для фильтрации записей. Это пригодится для реализации поиска и разбиения на страницы, но можно обойтись без этого в простом приложении:


sql = "SELECT rowid, col1, col2, col3 FROM my_table"
for row in connection.execute(sql):
# do something with row

UPDATE обновляет значение одной или нескольких колонок из таблицы. Обычно добавляется WHERE для обновления только тех строк, которые соответствуют определенным критериям — в данном случае можно использовать rowid:


sql = "UPDATE my_table SET col1 = ?, col2 = ?, col3 = ?
WHERE rowid = ?"
with connection:
connection.execute(sql, (value1, value2, value3, rowid))

Наконец, DELETE удаляет одну или несколько записей из таблицы. В данном случае еще важнее использовать WHERE, потому что без условия можно удалить сразу все записи:


sql = "DELETE FROM my_table WHERE rowid = ?"
with connection:
connection.execute(sql, (rowid,))
]]>
ООП в приложении Tkinter / tkinter 12 https://pythonru.com/uroki/oop-v-prilozhenii-tkinter-tkinter Sat, 24 Oct 2020 15:09:09 +0000 https://pythonru.com/?p=3837 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Структурирование данных с помощью класса

Для иллюстрации того, как можно использовать классы Python для моделирования данных, возьмем в качестве примера приложение списка контактов. Хотя пользовательский интерфейс и будет отличаться, все равно нужно будет определить доменную модель, в этом случае — каждый из контактов.

Каждый контакт будет содержать следующую информацию:

  • Имя и фамилию. Эти значения не могут быть пустыми;
  • Адрес электронной почты, например, alex123@example.com;
  • Номер телефона в формате (123) 4567890.

С этой абстракцией можно приступать к написанию кода для класса Contact.

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


def required(value, message):
if not value:
raise ValueError(message)
return value

def matches(value, regex, message):
if value and not regex.match(value):
raise ValueError(message)
return value

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


import re

class Contact(object):
email_regex = re.compile(r"[^@]+@[^@]+\.[^@]+")
phone_regex = re.compile(r"\([0-9]{3}\)\s[0-9]{7}")

def __init__(self, last_name, first_name, email, phone):
self.last_name = last_name
self.first_name = first_name
self.email = email
self.phone = phone

Однако этого определения недостаточно чтобы инициировать валидацию каждого поля. Для этого нужно использовать декоратор @property. С его помощью можно будет получить доступ к внутренним атрибутам:


@property
def last_name(self):
return self._last_name

@last_name.setter
def last_name(self, value):
self._last_name = required(value, "Фамилия обязательна")

Та же техника применяется и к first_name, поскольку и это поле является обязательным. Атрибуты email и phone также используют этот подход и функцию matches с соответствующим регулярным выражением:


@property
def email(self):
return self._email

@email.setter
def email(self, value):
self._email = matches(value, self.email_regex, "Invalid email format")

Готовый код в — structuring_data.py, позже его нужно будет использовать.

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

В этом примере они получают доступ к внутренним атрибутам с нижним подчеркиванием в начале названия:


contact.first_name = "John" # Сохраняется "John" в contact._first_name
print(contact.first_name) # Читается "John" из contact._first_name
contact.last_name = "" # ValueError вызвано функцией проверки данных

Дескриптор property обычно используется с синтаксисом @decorated — важно не забывать использовать одно и то же имя для декорируемых функций:


@property
def last_name(self):
# ...

@last_name.setter
def last_name(self, value):
# ...

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

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


from collections import namedtuple

Contact = namedtuple("Contact", ["last_name", "first_name",
"email", "phone"])

Однако все еще нужно добавить обходной путь реализации валидации полей. Для этого используется пакет attrs, доступный в Python Package Index.

Установить его можно с помощью командной строки и pip:

pip install attrs

После этого все свойства можно заменить на attr.ib. Это также позволяет задавать функцию обратного вызова validator, которая принимает экземпляр класса, атрибут, который будет изменен, и значение, которое нужно задать.

С минимумом изменений можно переписать класс Contact, уменьшив количество строк кода вдвое:


import re
import attr

def required(message):
def func(self, attr, val):
if not val: raise ValueError(message)
return func

def match(pattern, message):
regex = re.compile(pattern)
def func(self, attr, val):
if val and not regex.match(val):
raise ValueError(message)
return func

@attr.s
class Contact(object):
last_name = attr.ib(validator=required("Фамилия обязательна"))
first_name = attr.ib(validator=required("Имя обезательно"))
email = attr.ib(validator=match(r"[^@]+@[^@]+\.[^@]+",
"Ошибка в поле email"))
phone = attr.ib(validator=match(r"\([0-9]{3}\)\s[0-9]{7}",
"Ошибка в поле phone"))

С внешними зависимостями в коде нужно обращать внимание не только на преимущества в плане продуктивности, но также на документацию, лицензирование и поддержку.

Больше информации о пакете attrs можно найти на сайте https://www.atrs.org/en/stable/.

Создание виджетов для отображения информации

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

Помимо пакета Tkinter импортируем класс Contact из прошлого примера:


import tkinter as tk
import tkinter.messagebox as mb

from with_attr import Contact

Нужно убедиться, что файл with_attr.py находится в той же папке. В противном случае инструкция import-from вернет ошибку ImportError.

Создадим список со скроллингом, в котором будут перечислены все контакты. Чтобы каждый элемент был представлен в виде строки, отобразим имя и фамилию каждого контакта:


class ContactList(tk.Frame):
def __init__(self, master, **kwargs):
super().__init__(master)
self.lb = tk.Listbox(self, **kwargs)
scroll = tk.Scrollbar(self, command=self.lb.yview)

self.lb.config(yscrollcommand=scroll.set)
scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.lb.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)

def insert(self, contact, index=tk.END):
text = "{}, {}".format(contact.last_name, contact.first_name)
self.lb.insert(index, text)

def delete(self, index):
self.lb.delete(index, index)

def update(self, contact, index):
self.delete(index)
self.insert(contact, index)

def bind_doble_click(self, callback):
handler = lambda _: callback(self.lb.curselection()[0])
self.lb.bind("", handler)

Для просмотра и редактирования деталей контакта также создадим специальную форму. Возьмем в качестве базового класса LabelFrame с Label и Entry для каждого поля:


class ContactForm(tk.LabelFrame):
fields = ("Фамилия", "Имя", "Email", "Телефон")

def __init__(self, master, **kwargs):
super().__init__(master, text="Contact", padx=10, pady=10, **kwargs)
self.frame = tk.Frame(self)
self.entries = list(map(self.create_field, enumerate(self.fields)))
self.frame.pack()

def create_field(self, field):
position, text = field
label = tk.Label(self.frame, text=text)
entry = tk.Entry(self.frame, width=25)
label.grid(row=position, column=0, pady=5)
entry.grid(row=position, column=1, pady=5)
return entry

def load_details(self, contact):
values = (contact.last_name, contact.first_name,
contact.email, contact.phone)
for entry, value in zip(self.entries, values):
entry.delete(0, tk.END)
entry.insert(0, value)

def get_details(self):
values = [e.get() for e in self.entries]
try:
return Contact(*values)
except ValueError as e:
mb.showerror("Ошибка валидации", str(e), parent=self)

def clear(self):
for entry in self.entries:
entry.delete(0, tk.END)

Как работает создание виджета

Важная особенность класса ContactList то, что он предоставляет возможность добавить функцию обратного вызова для двойного клика. Он также передает индекс, по которому был осуществлен клик в качестве аргумента функции. Это нужно, чтобы скрыть детали реализации внутреннего класса Listbox:


def bind_doble_click(self, callback):
handler = lambda _: callback(self.lb.curselection()[0])
self.lb.bind("<Double-Button-1&rt;", handler)

У ContactForm также есть абстракция для создания экземпляра контакта на основе значений, введенных в Entry:


def get_details(self):
values = [e.get() for e in self.entries]
try:
return Contact(*values)
except ValueError as e:
mb.showerror("Ошибка валидации", str(e), parent=self)

Поскольку в классе Contact есть валидация полей, создание экземпляра контакта вернет ошибку ValueError, если текст будет содержать невалидное значение. Для уведомления пользователя об этом выведем диалоговое окно с сообщением об ошибке.

]]>
Всплывающие окна / tkinter 11 https://pythonru.com/uroki/vsplyvajushhie-okna-tkinter-11 Sat, 17 Oct 2020 15:27:29 +0000 https://pythonru.com/?p=3739 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Открытие дополнительного окна

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

Но есть в Tkinter и другой класс, который используется для создания дополнительных окон верхнего уровня. Он называется Toplevel. Этот класс можно использовать для отображения любого типа окна: от диалоговых до мастер форм.

Начнем с создания простого окна, которое открывается по нажатию кнопки в основном. Дополнительное окно будет включать кнопку, которая закрывает его и возвращает фокус в основное:

Открытие дополнительного окна

Класс виджета Toplevel создает новое окно верхнего уровня, которое выступает в качестве родительского контейнера по аналогии с экземпляром Tk. В отличие от класса Tk можно создавать неограниченное количество окон верхнего уровня:


import tkinter as tk

class About(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.label = tk.Label(self, text="Это всплывающее окно")
self.button = tk.Button(self, text="Закрыть", command=self.destroy)

self.label.pack(padx=20, pady=20)
self.button.pack(pady=5, ipadx=2, ipady=2)

class App(tk.Tk):
def __init__(self):
super().__init__()
self.btn = tk.Button(self, text="Открыть новое окно",
command=self.open_window)
self.btn.pack(padx=50, pady=20)

def open_window(self):
about = About(self)
about.grab_set()

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

Как работают всплывающие окна

Определяем подкласс Toplevel, который будет представлять собой кастомное окно. Его отношение с родительским будет определено в методе __init__. Виджеты добавляются в это окно привычным образом:


class Window(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)

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


def open_window(self):
window = Window(self)
window.grab_set()

Обработка закрытия окна

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

Будем заново использовать класс App из прошлого примера, но изменим класс Window, чтобы он показывал диалоговое окно для подтверждения перед закрытием:

Обработка закрытия окна

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


import tkinter as tk
import tkinter.messagebox as mb

class Window(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.protocol("WM_DELETE_WINDOW", self.confirm_delete)
self.label = tk.Label(self, text="Это всплывающее окно")
self.button = tk.Button(self, text="Закрыть", command=self.destroy)
self.label.pack(padx=20, pady=20)
self.button.pack(pady=5, ipadx=2, ipady=2)

def confirm_delete(self):
message = "Вы уверены, что хотите закрыть это окно?"
if mb.askyesno(message=message, parent=self):
self.destroy()

class App(tk.Tk):
def __init__(self):
super().__init__()
self.btn = tk.Button(self, text="Открыть новое окно",
command=self.open_about)
self.btn.pack(padx=50, pady=20)

def open_about(self):
window = Window(self)
window.grab_set()

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

Этот метод-обработчик показывает диалоговое окно для подтверждения удаления окна. В более сложных программах логика обычно включает дополнительную валидацию.

Как работает проверка закрытия окна

Метод bind() используется для регистрации обработчиков событий виджетов, а метод protocol делает то же самое для протоколов менеджеров окна.

Обработчик WM_DELETE_WINDOW вызывается, когда окно верхнего уровня должно уже закрываться, и по умолчанию Tk уничтожает окно, для которого оно было получено. Поскольку это поведение перезаписывается с помощью обработчика confirm_delete, нужно явно уничтожить окно при подтверждении.

Еще один полезный протокол — WM_TAKE_FOCUS. Он вызывается, когда окно получает фокус.

Стоит запомнить, что для сохранения фокуса в дополнительном окне при открытом диалоговом нужно передать ссылку экземпляру верхнего уровня в параметре parent диалоговой функции:


if mb.askyesno(message=message, parent=self):
self.destroy()

В противном случае диалоговое окно будет считать родительским корневое, и оно будет возникать над вторым. Это может запутать пользователей, поэтому хорошей практикой считается правильное указание родителя для каждого экземпляра верхнего уровня или диалогового окна.

Передача переменных между окнами

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

Основное окно будет включать три кнопки-переключателя для выбора типа пользователя, а второе — форму для заполнения его данных:

Передача переменных между окнами

Для сохранения пользовательских данных создаем namedtuple с полями, которые представляют собой экземпляр каждого пользователя. Эта функция из модуля collections получает имя типа и последовательность имен полей, а возвращает подкласс кортежа для создания простого объекта с заданными полями:


import tkinter as tk
from collections import namedtuple

User = namedtuple("User", ["username", "password", "user_type"])

class UserForm(tk.Toplevel):
def __init__(self, parent, user_type):
super().__init__(parent)
self.username = tk.StringVar()
self.password = tk.StringVar()
self.user_type = user_type

label = tk.Label(self, text="Создать пользователя " + user_type.lower())
entry_name = tk.Entry(self, textvariable=self.username)
entry_pass = tk.Entry(self, textvariable=self.password, show="*")
btn = tk.Button(self, text="Submit", command=self.destroy)

label.grid(row=0, columnspan=2)
tk.Label(self, text="Логин:").grid(row=1, column=0)
tk.Label(self, text="Пароль:").grid(row=2, column=0)
entry_name.grid(row=1, column=1)
entry_pass.grid(row=2, column=1)
btn.grid(row=3, columnspan=2)

def open(self):
self.grab_set()
self.wait_window()
username = self.username.get()
password = self.password.get()
return User(username, password, self.user_type)

class App(tk.Tk):
def __init__(self):
super().__init__()
user_types = ("Админ", "Менеджер", "Клиент")
self.user_type = tk.StringVar()
self.user_type.set(user_types[0])

label = tk.Label(self, text="Пожалуйста, выберите роль пользователя")
radios = [tk.Radiobutton(self, text=t, value=t,
variable=self.user_type) for t in user_types]
btn = tk.Button(self, text="Создать", command=self.open_window)

label.pack(padx=10, pady=10)
for radio in radios:
radio.pack(padx=10, anchor=tk.W)
btn.pack(pady=10)

def open_window(self):
window = UserForm(self, self.user_type.get())
user = window.open()
print(user)

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

Когда поток выполнения возвращается в основное окно, пользовательские данные выводятся в консоль.

Как работает передача данных между окнами

Большая часть кода в этом примере рассматривалась и ранее, а основное отличие — в методе open() класса UserForm, куда перемещен вызов grab_set(). Однако именно метод wait_windows() отвечает за остановку исполнения и гарантирует, что данные не вернутся, пока форма не будет изменена:


def open(self):
self.grab_set()
self.wait_window()
username = self.username.get()
password = self.password.get()
return User(username, password, self.user_type)

Важно отметить, что wait_windows() запускает локальный цикл событий, который завершается после уничтожения окна. Хотя и существует возможность передать виджет, который должен быть удален, этот момент можно пропустить. В таком случае ссылка будет выполнена неявно на экземпляр, который вызвал метод.

Когда экземпляр UserForm уничтожается, выполнение метода open() продолжается, и он возвращает объект User, который теперь может быть использован в классе App:


def open_window(self):
window = UserForm(self, self.user_type.get())
user = window.open()
print(user)
]]>
Создание меню / tkinter 10 https://pythonru.com/uroki/sozdanie-menju-tkinter-10 Sun, 11 Oct 2020 13:56:54 +0000 https://pythonru.com/?p=3695 Скачайте код уроков с 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 доступен весь список параметров.

]]>
Диалоговые (всплывающие) окна / tkinter 9 https://pythonru.com/uroki/dialogovye-vsplyvajushhie-okna-tkinter-9 Sun, 04 Oct 2020 11:05:14 +0000 https://pythonru.com/?p=3654 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Окна уведомлений

Распространенный сценарий применения диалоговых окон — уведомление пользователей о событиях, которые происходят в приложении: сделана новая запись или произошла ошибка при попытке открыть файл. Рассмотрим базовые функции Tkinter, предназначенные для отображения диалоговых окон.

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

Окна предупреждений

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

Окна предупреждений

Эти диалоговые окна открываются с помощью функций showinfo, showwarning и showerror из модуля tkinter.messagebox:


import tkinter as tk
import tkinter.messagebox as mb class App(tk.Tk):
def __init__(self):
super().__init__()
btn_info = tk.Button(self, text="Информационное окно",
command=self.show_info)
btn_warn = tk.Button(self, text="Окно с предупреждением",
command=self.show_warning)
btn_error = tk.Button(self, text="Окно с ошибкой",
command=self.show_error)

opts = {'padx': 40, 'pady': 5, 'expand': True, 'fill': tk.BOTH}
btn_info.pack(**opts)
btn_warn.pack(**opts)
btn_error.pack(**opts)

def show_info(self):
msg = "Ваши настройки сохранены"
mb.showinfo("Информация", msg)

def show_warning(self):
msg = "Временные файлы удалены не правильно"
mb.showwarning("Предупреждение", msg)

def show_error(self):
msg = "Приложение обнаружило неизвестную ошибку"
mb.showerror("Ошибка", msg) if __name__ == "__main__":
app = App()
app.mainloop()

Как работают окна с уведомлениями

В первую очередь нужно импортировать модуль tkinter.messagebox, задав для него алиас mb. В Python2 этот модуль назывался tkMessageBox, поэтому такой синтаксис позволит изолировать проблемы совместимости.

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

  • showinfo — операция была завершена успешно,
  • showwarning — операция была завершена, но что-то пошло не так, как планировалось,
  • showerror — операция не была завершена из-за ошибки.

Все три функции получают две строки в качестве входящих аргументов: заголовок и сообщение.

Сообщение может быть выведено на нескольких строках с помощью символа новой строки \n.

Окна выбора ответа

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

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

В этой программе рассмотрим остальные диалоговые функции из модуля tkinter.messagebox. Каждая кнопка включает метки с описанием типа окна, которое откроется при нажатии:

Окна выбора ответа

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

Как и в прошлом примере сначала нужно импортировать tkinter.messagebox с помощью синтаксиса import … as и вызывать каждую из функций вместе с title и message:


import tkinter as tk
import tkinter.messagebox as mb class App(tk.Tk):
def __init__(self):
super().__init__()
self.create_button(mb.askyesno, "Спросить Да/Нет",
"Вернет True или False")
self.create_button(mb.askquestion, "Задать вопрос ",
"Вернет 'yes' или 'no'")
self.create_button(mb.askokcancel, "Спросить Ок/Отмена",
"Вернет True или False")
self.create_button(mb.askretrycancel, "Спросить Повтор/Отмена",
"Вернет True или False")
self.create_button(mb.askyesnocancel, "Спросить Да/Нет/Отмена",
"Вернет True, False или None")

def create_button(self, dialog, title, message):
command = lambda: print(dialog(title, message))
btn = tk.Button(self, text=title, command=command)
btn.pack(padx=40, pady=5, expand=True, fill=tk.BOTH) if __name__ == "__main__":
app = App()
app.mainloop()

Как работают вопросительные окна

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

Выбор файлов и папок

Диалоговые окна для выбора файлов позволяют выбирать один или несколько файлов из файловой системы. В Tkinter эти функции объявлены в модуле tkinter.filedialog, который также включает окна для выбора папок. Он также позволяет настраивать поведение нового окна: например, фильтрация по расширению или выбор папки по умолчанию.

В этом приложении будет две кнопки. Первая, «Выбрать файл», откроет диалоговое окно для выбора файла. По умолчанию в окне будут только файлы с расширением .txt:

Выбор файлов и папок

Вторая — «Выбор папки». Она будет открывать похожее диалоговое окно для выбора папки.

Обе кнопки будут выводить полный путь к выбранным файлу или папке и не делать ничего, если пользователь выберет вариант Отмена.

Первая кнопка будет вызывать функцию askopenfilename, а вторая — askdirectory:


import tkinter as tk
import tkinter.filedialog as fd class App(tk.Tk):
def __init__(self):
super().__init__()
btn_file = tk.Button(self, text="Выбрать файл",
command=self.choose_file)
btn_dir = tk.Button(self, text="Выбрать папку",
command=self.choose_directory)
btn_file.pack(padx=60, pady=10)
btn_dir.pack(padx=60, pady=10)

def choose_file(self):
filetypes = (("Текстовый файл", "*.txt"),
("Изображение", "*.jpg *.gif *.png"),
("Любой", "*"))
filename = fd.askopenfilename(title="Открыть файл", initialdir="/",
filetypes=filetypes)
if filename:
print(filename)

def choose_directory(self):
directory = fd.askdirectory(title="Открыть папку", initialdir="/")
if directory:
print(directory) if __name__ == "__main__":
app = App()
app.mainloop()

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

Как работают окна выбора файлов и папок

Создадим первое диалоговое окно с помощью функции askopenfilename, которая возвращает строку с полным путем к файлу. Она принимает следующие опциональные аргументы:

  • title — название для диалогового окна.
  • initialdir — начальная папка.
  • filetypes — последовательность из двух строк. Первая — метка с типом файла в читаемом формате, а вторая — шаблон для поиска совпадения с названием файла.
  • multiple — булево значение для определения того, может ли пользователь выбирать несколько файлов.
  • defaultextension — расширение, добавляемое к файлу, если оно не было указано явно.

В этом примере задаем корневую папку в качестве начальной, а также название. В кортеже типов файлов есть следующие три варианта: текстовые файлы, сохраненные с расширением .txt, изображения с .jpg, .gif и .png, а также подстановочный знак («*») для всех файлов.

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


filetypes = (("Текстовый файл", "*.txt"),
("Изображение", "*.jpg *.gif *.png"),
("Любой", "*"))
filename = fd.askopenfilename(title="Открыть файл", initialdir="/",
filetypes=filetypes)

Функция askdirectory также принимает параметры title и initialdir, а также булев параметр mustexist для определения того, должны ли пользователи выбирать существующую папку:


directory = fd.askdirectory(title="Открыть папку", initialdir="/")

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

Например, askopenfile возвращает объект файла, который соответствует выбранному вместо того чтобы вызывать open с путем возвращающим askopenfilename. Но при этом все еще придется проверять, не было ли окно отклонено перед вызовом файловых методов:


import tkinter.filedialog as fd

filetypes = (("Текстовый файл", "*.txt"),)
my_file = fd.askopenfile(title="Открыть файл", filetypes=filetypes)
if my_file:
print(my_file.readlines())
my_file.close()

Сохранение данных в файл

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

Будем использовать диалоговое окно «Сохранить файл» для записи содержимого виджета Text в текстовый файл:

Сохранение данных в файл

Для открытия такого диалогового окна используется функция asksavefile из модуля tkinter.filedialog. Она создает объект файла с режимом записи ('w') или None, если окно было закрыто:


import tkinter as tk
import tkinter.filedialog as fd class App(tk.Tk):
def __init__(self):
super().__init__()
self.text = tk.Text(self, height=10, width=50)
self.btn_save = tk.Button(self, text="Сохранить", command=self.save_file)

self.text.pack()
self.btn_save.pack(pady=10, ipadx=5)

def save_file(self):
contents = self.text.get(1.0, tk.END)
new_file = fd.asksaveasfile(title="Сохранить файл", defaultextension=".txt",
filetypes=(("Текстовый файл", "*.txt"),))
if new_file:
new_file.write(contents)
new_file.close() if __name__ == "__main__":
app = App()
app.mainloop()

Как работает сохранение фалов

Функция asksavefile принимает те же опциональные параметры, что и askopenfile, но также позволяет добавить расширение файла по умолчанию с помощью параметра defaultextension.

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

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


contents = self.text.get(1.0, tk.END)
new_file.write(contents)
new_file.close()

Благодаря последней программе стало понятно, что askopenfile возвращает объект файла, а не его название. Также есть функция asksaveasfilename, которая возвращает путь к выбранному файлу. Ее можно использовать для изменения пути или выполнения валидации перед открытием файла для записи.

]]>
Работа с текстом и курсором / tkinter 8 https://pythonru.com/uroki/rabota-s-tekstom-i-kursorom-tkinter-8 Sat, 26 Sep 2020 12:17:08 +0000 https://pythonru.com/?p=3607 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Изменение иконки курсора

Tkinter позволяет менять внешний вид иконки курсора при наведении на виджет. Это поведение иногда включается по умолчанию, как, например, в случае с виджетом Entry, который показывает курсор текстового выделения.

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

Иконку можно поменять с помощью параметра cursor. В этом примере используется значение watch для демонстрации нативной иконки загрузки, а также question_arrow — для обычной стрелки со знаком вопроса:


import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Демо иконки курсора")
self.resizable(0, 0)
self.label = tk.Label(self, text="Нажмите для старта")
self.btn_launch = tk.Button(self, text="Старт !",
command=self.perform_action)
self.btn_help = tk.Button(self, text="Помощь",
cursor="question_arrow")

btn_opts = {"side": tk.LEFT, "expand": True, "fill": tk.X,
"ipadx": 30, "padx": 20, "pady": 5}
self.label.pack(pady=10)
self.btn_launch.pack(**btn_opts)
self.btn_help.pack(**btn_opts)

def perform_action(self):
self.btn_launch.config(state=tk.DISABLED)
self.btn_help.config(state=tk.DISABLED)
self.label.config(text="Запуск...")
self.after(3000, self.end_action)
self.config(cursor="watch")

def end_action(self):
self.btn_launch.config(state=tk.NORMAL)
self.btn_help.config(state=tk.NORMAL)
self.label.config(text="Готово!")
self.config(cursor="arrow")

def set_watch_cursor(self, widget):
widget._old_cursor = widget.cget("cursor")
widget.config(cursor="watch")
for w in widget.winfo_children():
self.set_watch_cursor(w)

def restore_cursor(self, widget):
widget.config(cursor=widget.old_cursor)
for w in widget.winfo_children():
self.restore_cursor(w) if __name__ == "__main__":
app = App()
app.mainloop()

Полный список валидных значений для cursor( включая те, которые характерны для определенной ОС) можно посмотреть в официальной документации Tcl/TK на сайте https://www.tcl.tk/man/tcl/TkCmd/cursors.htm.

Как работает изменение курсора

Если виджет не определяет параметр cursor, он берет значение из родительского контейнера. Таким образом можно запросто задать нужную иконку для всех виджетов, определив значение на уровне root. Это делается с помощью вызова set_watch_cursor() внутри метода perform_action():


def perform_action(self):
self.config(cursor="watch")
# ...

Исключением здесь является кнопка Помощь, которая явно задает значение question_arrow для курсора. Это же можно сделать при создании экземпляра виджета:


self.btn_help = tk.Button(self, text="Помощь",
cursor="question_arrow")

Стоит также обратить внимание на то, что если нажать на кнопку Старт и разместить курсор над кнопкой Помощь до вызова запланированного метода, то значение будет help, а не watch. Это происходит из-за того что, если параметр cursor виджета задан, то у него более высокая приоритетность по сравнению со значением в родительском контейнере.

Чтобы избежать этого, можно сохранить текущее значение cursor и поменять его на watch, вернув позже. Функцию, которая будет выполнять эту операцию, можно вызывать рекурсивно в дочернем виджете, перебирая список winfo_children():


def perform_action(self):
self.set_watch_cursor(self)
# ...

def end_action(self):
self.restore_cursor(self)
# ...

def set_watch_cursor(self, widget):
widget._old_cursor = widget.cget("cursor")
widget.config(cursor="watch")
for w in widget.winfo_children():
self.set_watch_cursor(w)

def restore_cursor(self, widget):
widget.config(cursor=widget.old_cursor)
for w in widget.winfo_children():
self.restore_cursor(w)

В этом коде свойство _old_cursor было добавлено каждому виджету. При использовании такого же подхода важно помнить, что нельзя вызывать restore_cursor() до set_watch_cursor().

Виджет Text

Виджет Text предлагает расширенную функциональность по сравнению с другими классами виджетов. Он отображает несколько строк редактируемого текста, который можно индексировать по строкам и колонкам. Также на них можно ссылаться с помощью тегов, которые определяют измененные внешний вид и поведение.

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

Виджет Text

Помимо виджета Text это приложение содержит три кнопки, которые вызывают методы для очистки всего содержимого, вставки строки «Hello, world!» в месте, где сейчас находится курсор, и вывода выделения, сделанного с помощью мыши или клавиатуры:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Демо виджета Text")
self.resizable(0, 0)
self.text = tk.Text(self, width=50, height=10)
self.btn_clear = tk.Button(self, text="Очистить",
command=self.clear_text)
self.btn_insert = tk.Button(self, text="Вставить",
command=self.insert_text)
self.btn_print = tk.Button(self, text="Печать",
command=self.print_selection)
self.text.pack()
self.btn_clear.pack(side=tk.LEFT, expand=True, pady=10)
self.btn_insert.pack(side=tk.LEFT, expand=True, pady=10)
self.btn_print.pack(side=tk.LEFT, expand=True, pady=10)

def clear_text(self):
self.text.delete("1.0", tk.END)

def insert_text(self):
self.text.insert(tk.INSERT, "Hello, world")

def print_selection(self):
selection = self.text.tag_ranges(tk.SEL)
if selection:
content = self.text.get(*selection)
print(content) if __name__ == "__main__":
app = App()
app.mainloop()

Как работает текстовый виджет

Изначально виджет Text пустой и имеет ширину на 50 символов, а высоту на 10 строк. Помимо добавления возможности для пользователей вводить любой текст, разберем также методы каждой кнопки, чтобы понимать, как взаимодействовать с виджетом.

Метод delete(start, end) удаляет содержимое с индексами от start до end. Если второй параметр не указан, то удаляются только символы с индексом start.

В этом примере будем удалять весь текст от индекса 1.0 (нулевая колонка первой строчки) до tk.END, которая ссылается на последний символ:


def clear_text(self):
self.text.delete("1.0", tk.END)

Метод insert(index, text) вставляет выбранный текст в положении index. Вызываем его с помощью индекса INDEX, который соответствует позиции курсора:


def insert_text(self):
self.text.insert(tk.INSERT, "Hello, world")

Метод tag_ranges(tag) возвращает кортеж с первым и последним индексами всех диапазонов конкретного tag. Здесь был использован тег tk.SEL, который указывает на текущую позицию. Если ничего не было выбрано, то вызов вернет пустой кортеж. Этот метод объединен с get(start, end), который возвращает текст в заданном диапазоне:

Поскольку тег SEL соответствует лишь одному диапазону, его можно с легкостью извлечь с помощью метода get.

Добавление HTML тегов в виджет Text

В этом разделе разберем, как настраивать поведение последовательности символов с проставленными тегами в виджете Text.

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

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

Например, если пользователь введет следующий текст, то python.org можно отметить тегом как гиперссылку:

Добавление HTML тегов

Определим тег «link», который представляет собой кликабельную гиперссылку. Этот тег будет добавляться к текущему выбранному тексту с помощью кнопки, а клик мышью запустит событие для открытия ссылки в браузере:


import tkinter as tk
import webbrowser class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Демо HTML тегов")
self.text = tk.Text(self, width=50, height=10)
self.btn_link = tk.Button(self, text="Добавить ссылку",
command=self.add_hyperlink)

self.text.tag_config("link", foreground="blue", underline=1)
self.text.tag_bind("link", "", self.open_link)
self.text.tag_bind("link", "",
lambda _: self.text.config(cursor="hand2"))
self.text.tag_bind("link", "",
lambda e: self.text.config(cursor=""))

self.text.pack()
self.btn_link.pack(expand=True)

def add_hyperlink(self):
selection = self.text.tag_ranges(tk.SEL)
if selection:
self.text.tag_add("link", *selection)

def open_link(self, event):
position = "@{},{} + 1c".format(event.x, event.y)
index = self.text.index(position)
prevrange = self.text.tag_prevrange("link", index)
url = self.text.get(*prevrange)
webbrowser.open(url) if __name__ == "__main__":
app = App()
app.mainloop()

Как работает добавление тега

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


def __init__(self):
super().__init__()
self.title("Демо HTML тегов")
self.text = tk.Text(self, width=50, height=10)
self.btn_link = tk.Button(self, text="Добавить ссылку",
command=self.add_hyperlink)

self.text.tag_config("link", foreground="blue", underline=1)
self.text.tag_bind("link", "", self.open_link)
self.text.tag_bind("link", "",
lambda _: self.text.config(cursor="hand2"))
self.text.tag_bind("link", "",
lambda e: self.text.config(cursor=""))

Внутри метода open_link поменяем положение клика на соответствующую строку и колонку с помощью метода index класса Text:


position = "@{},{} + 1c".format(event.x, event.y)
index = self.text.index(position)
prevrange = self.text.tag_prevrange("link", index)

Стоит обратить внимание на то, что положение, соответствующее индексу, по которому был совершен клик, — «@x,y», но оно сдвинут на один символ. Это сделано из-за того, что tag_prevrange возвращает предшествующий диапазон конкретного индекса. В таком случае он бы не возвращал текущий диапазон при клике по первому символу.

Наконец, получаем текст из диапазона и открываем его с помощью браузера по умолчанию, используя для этого функцию open из модуля webbrowser:


url = self.text.get(*prevrange)
webbrowser.open(url)

Поскольку функция webbrowser.open не проверяет, является ли URL валидным, то приложение можно улучшить, добавив базовую валидацию гиперссылки. Например, можно использовать функцию urlparse, чтобы убедиться, что у ссылки есть сетевое положение:

Хотя этот подход не является идеальным, он подойдет на первых этапах, чтобы отбрасывать невалидные ссылки.

В целом, можно использовать теги для создания сложных программ, основанных на тексте: например, IDE с подсветкой синтаксиса. На самом деле, IDLE, которая идет в составе Python, основана на Tkinter.

]]>
Работа с цветами и шрифтами / tkinter 7 https://pythonru.com/uroki/rabota-s-cvetami-i-shriftami-tkinter-7 Sat, 19 Sep 2020 11:04:34 +0000 https://pythonru.com/?p=3589 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Работа с цветами

В примерах из прошлых материалов цвета задавались с помощью их названий: например, white, blue или yellow. Эти значения передаются в виде строк параметрам foreground и background, которые изменяют цвет текста и фона виджета соответственно.

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

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

Работа с цветами tkinter

Цвета определены в формате RGB и выбираются с помощью нативного модального окна. На следующем скриншоте представлено диалоговое окно из Windows 10:

Работа с цветами tkinter

Традиционно будем работать с настройками виджета с помощью кнопок — по одной для каждого параметра. Основное отличие по сравнению с предыдущими примерами в том, что значения могут быть прямо выбраны с помощью диалогового окна askcolor из модуля tkinter.colorchooser:


from functools import partial import tkinter as tk
from tkinter.colorchooser import askcolor class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Демо с цветами")
text = "Шустрая бурая лисица прыгает через ленивого пса"
self.label = tk.Label(self, text=text)
self.fg_btn = tk.Button(self, text="Установить цвет текста",
command=partial(self.set_color, "fg"))
self.bg_btn = tk.Button(self, text="Установить цвет фона",
command=partial(self.set_color, "bg"))

self.label.pack(padx=20, pady=20)
self.fg_btn.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.bg_btn.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

def set_color(self, option):
color = askcolor()[1]
print("Выбрать цвет:", color)
self.label.config(**{option: color})

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

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

Как работает настройка цвета

Обе кнопки используют функцию partial в качестве обратного вызова. Это инструмент из модуля functools, который создает новый вызываемый объект. Он ведет себя как оригинальная функция, но с некоторыми зафиксированными аргументами. Возьмем в качестве примера такую инструкцию:


tk.Button(self, command=partial(self.set_color, "fg"), ...)

Предыдущая инструкция выполняет то же действие, что и следующая:


tk.Button(self, command=lambda: self.set_color("fg"), ...)

Так делается для того, чтобы переиспользовать метод set_color() из модуля functools. Это особенно полезно в более сложных сценариях, например, когда нужно создать несколько функций, и очевидно, что некоторые аргументы заданы заранее.

Нужно лишь помнить тот нюанс, что foreground и background кратко записаны как fg и bg. Эти строки распаковываются с помощью ** при настройке виджета в инструкции:


def set_color(self, option):
color = askcolor()[1]
print("Выбрать цвет:", color)
self.label.config(**{option: color}) # same as (fg=color)
or (bg=color)

askcolor возвращает кортеж с двумя элементами, которые представляют собой выбранный цвет. Первый — кортеж цветов из RGB-значений, а второй — шестнадцатеричное представление в виде строки. Поскольку первый вариант не может быть прямо передан в параметры виджета, используется второй.

Если нужно преобразовать название цвета в RGB-формат, можно использовать метод winfo_rgb() из предыдущего виджета. Поскольку он возвращает кортеж целых чисел от 0 до 65535, которые представляют 16-битные RGB-значения, их можно конвертировать в более привычное представление #RRGGBB, сдвинув вправо 8 битов:


rgb = widget.winfo_rgb("lightblue")
red, green, blue = [x>>8 for x in rgb]
print("#{:02x}{:02x}{:02x}".format(red, green, blue))

В предыдущем коде использовался {:02x} для форматирования каждого целого числа в два шестнадцатеричных.

Задание шрифтов виджета

В Tkinter можно менять шрифт виджета на кнопках, метках и записях. По умолчанию он соответствует системному, но его можно поменять с помощью параметра font.

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

Задание шрифтов виджета

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


import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Демо шрифтов")
text = "Шустрая бурая лисица прыгает через ленивого пса"
self.label = tk.Label(self, text=text)

self.family = tk.StringVar()
self.family.trace("w", self.set_font)
families = ("Times", "Courier", "Helvetica")
self.option = tk.OptionMenu(self, self.family, *families)

self.size = tk.StringVar()
self.size.trace("w", self.set_font)
self.spinbox = tk.Spinbox(self, from_=8, to=18,
textvariable=self.size)

self.family.set(families[0])
self.size.set("10")
self.label.pack(padx=20, pady=20)
self.option.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.spinbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

def set_font(self, *args):
family = self.family.get()
size = self.size.get()
self.label.config(font=(family, size)) if __name__ == "__main__":
app = App()
app.mainloop()

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

Как работает настройка шрифтов?

Кортеж FAMILIES включает три типа шрифтов, которые Tk гарантированно поддерживает на всех платформах: Times (Times New Roman), Courier и Helvetica. Между ними можно переключаться с помощью виджета OptionMenu, который привязан к переменной self.family.

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


def set_font(self, *args):
family = self.family.get()
size = self.size.get()
self.label.config(font=(family, size))

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


widget1.config(font=("Times", "20", "bold"))
widget2.config(font=("Helvetica", "16", "italic underline"))

Полный список всех доступных шрифтов, которые доступны для платформы, можно получить с помощью метода families() из модуля tkinter.font. Поскольку сперва нужно создать экземпляр окна root, можно использовать следующий скрипт:


import tkinter as tk
from tkinter import font
root = tk.Tk()
print(font.families())

Tkinter не вернет ошибку при попытке использовать шрифт не из списка, но попробует подобрать похожий.

Модуль tkinter.font включает класс Font, который можно переиспользовать в нескольких виджетах. Основное преимущество изменения экземпляра font в том, что в таком случае он затрагивает все виджеты, в которых был использован.

Работа с классом Font напоминает работу с дескрипторами шрифтов. Например, этот скрипт создает полужирный шрифт Courier размером 18 пикселей:


from tkinter import font
courier_18 = font.Font(family="Courier", size=18, weight=font.BOLD)

Чтобы получить или изменить значение, можно использовать методы cget и configure:


family = courier_18.cget("family")
courier_18.configure(underline=1)

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

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

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

несколько виджетов с разными стилями

Добавим кое-какие параметры с помощью метода option_add(), который доступен из всех классов виджетов:


import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Демо опции")
self.option_add("*font", "helvetica 10")
self.option_add("*header.font", "helvetica 18 bold")
self.option_add("*subtitle.font", "helvetica 14 italic")
self.option_add("*Button.foreground", "blue")
self.option_add("*Button.background", "white")
self.option_add("*Button.activeBackground", "gray")
self.option_add("*Button.activeForeground", "black")

self.create_label(name="header", text="Это 'header'")
self.create_label(name="subtitle", text="Это 'subtitle'")
self.create_label(text="Это параграф")
self.create_label(text="Это следующий параграф")
self.create_button(text="Больше...")

def create_label(self, **options):
tk.Label(self, **options).pack(padx=20, pady=5, anchor=tk.W)

def create_button(self, **options):
tk.Button(self, **options).pack(padx=5, pady=5, anchor=tk.E)
if __name__ == "__main__":
app = App()
app.mainloop()

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

Как работают стили для виджетов

Начнем с объяснения каждого вызова option_add. Первый из них добавляет параметр, который задает атрибут font для всех виджетов — звездочка представляет любое название приложения:

Следующий вызов ограничивает вызов для элементов с именем header. Чем он конкретнее, тем выше приоритет. Это же имя позже используется при создании экземпляра с меткой name="header":


self.option_add("*header.font", "helvetica 18 bold")

То же применимо и к self.option_add("*subtitle.font", "helvetica 14 italic"), где каждый параметр соответствует своему экземпляру виджета.

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


self.option_add("*Button.foreground", "blue")
self.option_add("*Button.background", "white")
self.option_add("*Button.activeBackground", "gray")
self.option_add("*Button.activeForeground", "black")

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

Эти параметры не применяются к существующим виджетам, а лишь к тем, которые были созданы после изменения базы данных. Таким образом всегда рекомендуется вызывать option_add() в начале приложения.

Есть несколько примеров, где один приоритетнее предыдущего:

  • *Frame*background: работает для фона всех виджетов во фрейме.
  • *Frame.background: фон всех фреймов.
  • *Frame.myButton.background: фон виджета myButton.
  • *myFrame.myButton.background: фон виджета myButton внутри контейнера myFrame.

Чтобы не добавлять параметры в коде, их можно определить в отдельном текстовом файле в таком формате:


*font: helvetica 10
*header.font: helvetica 18 bold
*subtitle.font: helvetica 14 italic
*Button.foreground: blue
*Button.background: white
*Button.activeBackground: gray
*Button.activeForeground: black

Этот файл затем загружается в приложение с помощью метода option_readfile() и заменяет все вызовы к option_add(). В этом примере предположим, что файл называется my_options_file и что находится он в одной папке с кодом:


def __init__(self):
super().__init__()
self.title("Options demo")
self.option_readfile("my_options_file")
# ...

Если такого файла не существует или его формат неверный, Tkinter вернет ошибку TclError.

]]>
Создание скроллбаров / tkinter 6 https://pythonru.com/uroki/sozdanie-skrollbarov-tkinter-6 Sat, 12 Sep 2020 09:41:46 +0000 https://pythonru.com/?p=3576 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

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

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

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

Для демонстрации совместной работы классов Canvas и Scrollbar с целью создания изменяемого фрейма со скроллбарами, создадим приложение, которое будет динамически менять размер за счет загрузки изображения.

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

Создание изменяемого фрейма со скроллбарами

Это активирует горизонтальный и вертикальный скроллбары, которые будут автоматически подстраиваться при изменении основного окна:

горизонтальный и вертикальный скроллбары

У виджета Canvas есть стандартный интерфейс скроллинга, а также метод create_window(). Важно обратить внимание на то, что этот скрипт предполагает размещение файла python.gif в той же директории:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.scroll_x = tk.Scrollbar(self, orient=tk.HORIZONTAL)
self.scroll_y = tk.Scrollbar(self, orient=tk.VERTICAL)
self.canvas = tk.Canvas(self, width=300, height=100,
xscrollcommand=self.scroll_x.set,
yscrollcommand=self.scroll_y.set)
self.scroll_x.config(command=self.canvas.xview)
self.scroll_y.config(command=self.canvas.yview)

self.frame = tk.Frame(self.canvas)
self.btn = tk.Button(self.frame, text="Загрузить изображение",
command=self.load_image)
self.btn.pack()

self.canvas.create_window((0, 0), window=self.frame,
anchor=tk.N + tk.W)

self.canvas.grid(row=0, column=0, sticky="nswe")
self.scroll_x.grid(row=1, column=0, sticky="we")
self.scroll_y.grid(row=0, column=1, sticky="ns")

self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.bind("", self.resize)
self.update_idletasks()
self.minsize(self.winfo_width(), self.winfo_height())

def resize(self, event):
region = self.canvas.bbox(tk.ALL)
self.canvas.configure(scrollregion=region)

def load_image(self):
self.btn.destroy()
self.image = tk.PhotoImage(file="python.gif")
tk.Label(self.frame, image=self.image).pack()

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

Как работают скроллбары в Tkinter

Первые строчки приложения создают скроллбары и присоединяют их к объекту Canvas с помощью параметров xscrollcommand и yscrollcommand, которые ссылаются на метод set() объектов scroll_x и scroll_y соответственно. Этот метод отвечает за перемещение слайдера.

Также нужно настроить параметр command каждого из скроллбаров после определения Canvas:


self.scroll_x = tk.Scrollbar(self, orient=tk.HORIZONTAL)
self.scroll_y = tk.Scrollbar(self, orient=tk.VERTICAL)
self.canvas = tk.Canvas(self, width=300, height=100,
xscrollcommand=self.scroll_x.set,
yscrollcommand=self.scroll_y.set)
self.scroll_x.config(command=self.canvas.xview)
self.scroll_y.config(command=self.canvas.yview)

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

Следующий шаг — добавить фрейм с помощью метода create_window(). Первый аргумент — положение, где нужно разместить виджет, который в свою очередь передается в аргументе window. Поскольку оси x и y виджета размещаются в верхнем левом углу, разместим виджет в положении (0, 0) и выровняем его в этом углу с помощью anchor=tk.NW (северо-запад):


self.frame = tk.Frame(self.canvas)
# ...
self.canvas.create_window((0, 0), window=self.frame, anchor=tk.NW)

Затем зададим переменный размер для первых строки и колонки с помощью методов rowconfigure() и columnconfigure(). Параметр weight обозначает относительную ширину, для распределения дополнительного пространства. Однако в этом примере нет колонок или рядков для изменения размера.

Связывание с событием <Configure> поможет правильно перенастроить Canvas, когда размер основного окна меняется. Обработка такого типа события работает по тому же принципу, что и события мыши и клавиатуры:


self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.bind("<Configure>", self.resize)

В итоге задаем минимальный размер основного окна с текущими шириной и высотой, которые можно получить с помощью методов winfo_width() или winfo_height().

Для получения реального размера контейнера нужно сделать так, чтобы geometry manager прорисовывал все дочерние виджеты в первую очередь с помощью вызова update_idletasks(). Этот виджет доступен во всех классах виджета и он отвечает за то, чтобы Tkinter обработал все события в процессе ожидания: например, перерисовку или новые вычисления размеров:


self.update_idletasks()
self.minsize(self.winfo_width(), self.winfo_height())

Метод resize обрабатывает событие изменения размера окна и обновляет параметр scrollregion, определяющий область Canvas, которую можно скроллить. Чтобы провести вычисления заново, можно использовать метода bbox() с константой ALL. Он возвращает окружающий размер всего виджета Canvas:


def resize(self, event):
region = self.canvas.bbox(tk.ALL)
self.canvas.configure(scrollregion=region)

Tkinter автоматически вызывает несколько событий <Configure> при старте приложения, поэтому нет необходимости вызывать self.resize() в конце метода __init__.

Лишь несколько классов виджетов поддерживают стандартные параметры скроллинга: Listbox, Text и Canvas поддерживают xscrollcommand и yscrollcommand, а Entry только xscrollcommand. На примере было разобрано, как использовать этот паттерн с Canvas, поскольку это может быть общее решение, но та же структура применима для любых виджетов.

Также нужно отметить, что в данном случае не вызывался geometry manager для прорисовки кадра, поскольку create_window() делает это автоматически. Для лучшей организации класса приложения, можно переместить всю функциональность фрейма и внутренние виджеты в отдельный подкласс Frame.

]]>
Создание макетов окна / tkinter 5 https://pythonru.com/uroki/sozdanie-maketov-okna-tkinter-5 Sat, 05 Sep 2020 10:22:32 +0000 https://pythonru.com/?p=3537 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

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

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

В этом материале погрузимся в разные механизмы, которые Tkinter предлагает для формирования макета, группировки виджетов и управления другими атрибутами, например, размером и отступами.

Группировка виджетов с фреймами

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

Еще одна распространенная особенность фреймов — инкапсуляция части функциональности приложения таким образом, что с помощью абстракции можно спрятать детали реализации дочерних виджетов.

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

Создадим приложение, которое будет включать два списка, где первый — это список элементов, а второй изначально пустой. Оба можно пролистывать. Также есть возможность перемещать элементы между ними с помощью двух кнопок по центру:

Группировка виджетов с фреймами

Определим подкласс 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(). Первый возвращает и удаляет текущий выделенный элемент, или не делает ничего, если элемент не был выбран. Второй — вставляет элемент в конец списка.

Эти методы используются в родительском классе для перемещения элемента из одного списка в другой:


def move(self, frame_from, frame_to):
value = frame_from.pop_selection()
if value:
frame_to.insert_item(value)

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


# ...
self.frame_a.pack(side=tk.LEFT, padx=10, pady=10)
self.frame_b.pack(side=tk.RIGHT, padx=10, pady=1

Благодаря фреймам вызовы управлять геометрией макетов проще.

Еще одно преимущество такого подхода — возможность использовать geometry manager в контейнерах каждого виджета. Это могут быть grid() для виджетов во фрейме или pack() для укладывания фрейма в основном окне.

Однако смешивать эти менеджеры в одном контейнере в Tkinter запрещено. Из-за этого приложение просто не будет работать.

Geometry manager Pack

В прошлых материалах можно было обратить внимание на то, что после создания виджета он не отображается на экране автоматически. Для каждого нужно было вызывать метод pack(). Это подразумевает использование соответствующего geometry manager.

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

Предположим, что нужно получить следующий макет для приложения:

Geometry manager Pack

Он состоит из трех строк, где в последней есть три виджета, расположенных рядом друг с другом. В таком случае Pack сможет добавить виджеты как требуется без использования дополнительных фреймов.

Для этого будут использоваться пять виджетов 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. Они делают яснее размеры каждой области:

Geometry manager Pack-2

Как работает Pack

Чтобы лучше понимать принципы работы Pack, разберем пошагово, как виджеты добавляются в родительский контейнер. Стоит обратить особое внимание на значение параметра side, который определяет относительное положение виджета по отношению к следующему в этом же контейнере.

Сначала добавляются две метки в верхней части экрана. Пусть значение параметра side по умолчанию является tk.TOP,все равно зададим его явно, чтобы отличать от тех случаев, где используется tk.LEFT:

Geometry Pack

Дальше добавляем еще три метки со значением tk.LEFT у параметра side, в результате чего они размещаются рядом друг рядом с другом:

Geometry Pack 2

Определение стороны label_e особой роли не играет, поскольку это последний виджет, который добавляется в контейнер.

Важно запомнить, что это основная причина, почему порядок так важен при работе с Pack. Чтобы не столкнутся с непредвиденными результатами в сложных макетах распространенной практикой считается их расположение в пределах фрейма так, что бы они не пересекались.

В таких случаях рекомендуется использовать geometry manager Grid, поскольку он позволяет прямо задавать положение каждого виджета с помощью вызова geometry manager и избегать использования дополнительных фреймов.

В side можно передать не только tk.TOP и tk.LEFT, но также tk.BOTTOM и tk.RIGHT. Они разместят виджеты в другом порядке, но это может быть не интуитивно, ведь мы естественным путем следим сверху вниз и слева направо.

Например, если заменить значение tk.LEFT на tk.RIGHT в последних трех виджетах, их порядок будет следующим: label_e, label_d и label_c.

Geometry manager Grid

Grid — самый гибкий из всех доступных geometry manager. Он полностью переосмысливает концепцию сетки (grid), которая традиционно используется при дизайне пользовательских интерфейсов. Сетка — это двумерная таблица, разделенная на строки и колонки, где каждая ячейка представляет собой пространство, которое доступно для виджета.

Продемонстрируем работу Grid с помощью следующего макета:

Geometry manager Grid

Его можно представить в виде таблицы 3×3, где виджеты во второй и третьей колонках растягиваются на две строки, а виджет в третьей строке занимает все три колонки.

Как и в предыдущем варианте используем 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

Расположение label_a и label_b говорит само за себя: они занимают первую и вторую строки первой колонки соответственно (важно не забывать, что индексация начинается с нуля):

Geometry manager Grid 2

Чтобы растянуть label_c и label_d на несколько ячеек, зададим значение 2 для параметра rowspan. Таким образом они будут занимать две ячейки, начиная с положения, отмеченного опциями row и column. Наконец, значение columnspan для label_e будет 3.

Важно запомнить, что в отличие от Pack есть возможность менять порядок вызовов к grid() для каждого виджета без изменения финального макета.

Параметр sticky определяет границы, к которым виджет должен крепиться. Он выражается в координатах сторон света: север, юг, запад и восток. В Tkinter эти значения выражены константами tk.N, tk.S, tk.W и tk.E, а также их комбинациями: tk.NW, tk.NE, tk.SW и tk.SE.

Например, sticky=tk.N выравнивает виджет у верхней границы ячейки (north – север), а sticky=tk.SE — в правом нижнем углу (south-ease – юго-восток).

Поскольку эти константы представляют соответствующие символы в нижнем регистре, выражение tk.N + tk.S + tk.W + tk.E можно записать в виде строки nwse. Это значит, что виджет должен расширяться одновременно горизонтально и вертикально — по аналогии с работой fill=tk.BOTH из Pack.

Если параметру sticky значение не передается, виджет располагается по центру ячейки.

Geometry manager Place

Менеджер Place позволяет задать положение и размер виджета в абсолютном или относительном значении.

Из трех менеджеров этот является наименее используемым. С другой стороны, он может работать со сложными сценариями, где есть необходимость свободно разместить виджет или перекрыть другой.

Для демонстрации работы Place повторим следующий макет, смешав абсолютные и относительные положения и размеры:

Geometry manager Place

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


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()

Если запустить эту программу, то можно будет увидеть наложение label_c и label_d в центре экрана. Этого не удастся добиться с помощью других менеджеров.

Как работает Place

Первая метка располагается со значением 0.25 у параметров relwidth и relheight. Это значит, что виджет будет занимать 25% ширины и высоты родительского. По умолчанию виджеты расположены в положениях x=0 и y=0, а также выравнены к северо-западу, то есть, верхнему левому углу экрана.

Вторая метка имеет абсолютное положение — x=100. Она выравнена по верхней границе с помощью параметра anchor, который имеет значение tk.N. Тут также определен абсолютный размер с помощью width и height.

Третья метка расположена по центру окна с помощью относительного позиционирования и параметра anchor для tk.CENTER. Важно запомнить, что значение 0.5 для relx и relwidth обозначает половину родительской ширины, а 0.5 для rely и relheight — половину родительской высоты.

Четвертая метка расположена в верхней части label_c. Это делается с помощью переданного аргумента in_ (суффикс используется из-за того, что in — зарезервированное ключевое слово в Python). При использовании in_ можно обратить внимание на то, что выравнивание не является геометрически точным. В этом примере нужно добавить смещение на 2 пикселя в каждом направлении, чтобы идеально перекрыть правый нижний угол label_c.

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

Еще одно важное преимущество Place — возможность совмещать его с Pack и Grid.

Например, представьте, что есть необходимость динамически отобразить текст на виджете при нажатии правой кнопкой мыши на нем. Его можно представить в виде виджета Label, который располагается в относительном положении при нажатии:

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

Группировка полей ввода с помощью виджета LabelFrame

Класс LabelFrame может быть использован для группировки нескольких виджетов ввода. Он представляет собой логическую сущность с соответствующей меткой. Обычно он используется в формах и сильно напоминает виджет Frame.

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

Группировка полей ввода

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


import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
group_1 = tk.LabelFrame(self, padx=15, pady=10,
text="Персональная информация")
group_1.pack(padx=10, pady=5)

tk.Label(group_1, text="Имя").grid(row=0)
tk.Label(group_1, text="Фамилия").grid(row=1)
tk.Entry(group_1).grid(row=0, column=1, sticky=tk.W)
tk.Entry(group_1).grid(row=1, column=1, sticky=tk.W)

group_2 = tk.LabelFrame(self, padx=15, pady=10,
text="Адрес")
group_2.pack(padx=10, pady=5)

tk.Label(group_2, text="Улица").grid(row=0)
tk.Label(group_2, text="Город").grid(row=1)
tk.Label(group_2, text="Индекс").grid(row=2)
tk.Entry(group_2).grid(row=0, column=1, sticky=tk.W)
tk.Entry(group_2).grid(row=1, column=1, sticky=tk.W)
tk.Entry(group_2, width=8).grid(row=2, column=1,
sticky=tk.W)

self.btn_submit = tk.Button(self, text="Отправить")
self.btn_submit.pack(padx=10, pady=10, side=tk.RIGHT) if __name__ == "__main__":
app = App()
app.mainloop()

Как работает группировка полей ввода

Виджет LabelFrame принимает параметр labelWidget для задания виджета, который будет использоваться как метка. Если его нет, отображается строка, переданная в параметре text. Например, вместо создания экземпляра с tk.LabelFrame(master, text="Инфо") можно заменить это на следующие инструкции:


label = tk.Label(master, text="Инфо", ...)
frame = tk.LabelFrame(master, labelwidget=label)
# ...
frame.pack()

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

Динамическое расположение виджетов

Grid легко использовать как для простых, так и для более продвинутых макетов. Он также является мощным инструментом для объединения со списком виджетов.

Рассмотрим, как можно уменьшить количество строк и вызовем geometry manager с помощью всего нескольких строк благодаря «list comprehension», а также встроенным функциям zip и enumerate.

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

Динамическое расположение виджетов

Вместо того чтобы создавать и присваивать каждый виджет отдельному атрибуту, будем работать со списками виджетов. Поскольку при итерации по списку идет отслеживание индекса, можно легко вызвать метод grid() с помощью соответствующего параметра column.

Выполним агрегацию списка меток и виджетов с помощью функции zip. Кнопка будет создана и расположена отдельно, поскольку у нее нет общих с другими виджетами параметров:


import tkinter as tk class App(tk.Tk):
def __init__(self):
super().__init__()
fields = ["Имя", "Фамилия", "Телефон", "Email"]
labels = [tk.Label(self, text=f) for f in fields]
entries = [tk.Entry(self) for _ in fields]
self.widgets = list(zip(labels, entries))
self.submit = tk.Button(self, text="Распечатать",
command=self.print_info)

for i, (label, entry) in enumerate(self.widgets):
label.grid(row=i, column=0, padx=10, sticky=tk.W)
entry.grid(row=i, column=1, padx=10, pady=5)
self.submit.grid(row=len(fields), column=1, sticky=tk.E,
padx=10, pady=10)

def print_info(self):
for label, entry in self.widgets:
print("{} = {}".format(label.cget("text"), entry.get())) if __name__ == "__main__":
app = App()
app.mainloop()

Можно ввести разный текст в каждое из полей и нажать кнопку «Распечатать», чтобы убедиться, что каждый кортеж содержит соответствующие метку и текст.

Как работает динамическое расположение

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

Начиная с Python 3, функция zip возвращает итератор вместо списка, поэтому результат — агрегация с функцией списка. В результате атрибут widgets содержит список кортежей, по которому можно пройти несколько раз:


fields = ["Имя", "Фамилия", "Телефон", "Email"]
labels = [tk.Label(self, text=f) for f in fields]
entries = [tk.Entry(self) for _ in fields]
self.widgets = list(zip(labels, entries))

Теперь нужно вызвать geometry manager для каждого кортежа виджетов. С помощью функции enumerate можно отслеживать индекс каждой итерации и передавать его в виде числа row:


for i, (label, entry) in enumerate(self.widgets):
label.grid(row=i, column=0, padx=10, sticky=tk.W)
entry.grid(row=i, column=1, padx=10, pady=5)

Стоит обратить внимание, что был использован синтаксис for i, (label, entry) in …, потому что нужно распаковать кортеж, сгенерированный с помощью enumerate и затем распаковать каждый кортеж атрибута widgets.

Внутри функции обратного вызова print_info() пройдем по виджетам для вывода текста каждой метки с соответствующими значениями поля. Для получения text из меток используем метод cget(), который позволяет получить значение параметра виджета по его имени.

]]>
Обработка событий и настройка окна / tkinter 4 https://pythonru.com/uroki/obrabotka-sobytij-i-nastrojka-okna-tkinter Sat, 29 Aug 2020 13:42:00 +0000 https://pythonru.com/?p=3501 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Обработка событий с мыши и клавиатуры

Возможность реагировать на события — одна из базовых, но важных тем в приложениях с графическим интерфейсом. Именно она определяет, как пользователи смогут взаимодействовать с программой.

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

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

Выполнить привязку события к виджету можно с помощью метода bind. Следующий пример привязывает некоторые события мыши к экземпляру Frame:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
frame = tk.Frame(self, bg="green",
height=100, width=100)
frame.bind("<Button-1>", self.print_event)
frame.bind("<Double-Button-1>", self.print_event)
frame.bind("<ButtonRelease-1>", self.print_event)
frame.bind("<B1-Motion>", self.print_event)
frame.bind("<Enter>", self.print_event)
frame.bind("<Leave>", self.print_event)
frame.pack(padx=50, pady=50)

def print_event(self, event):
position = "(x={}, y={})".format(event.x, event.y)
print(event.type, "event", position)

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

Все события обрабатываются методом класса print_event(), который выводит тип события и положение мыши в консоли. Можете поэкспериментировать, нажимая на зеленую рамку мышью и двигая ею, пока она будет выводить сообщения события.

Обработка событий с мыши

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


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
entry = tk.Entry(self)
entry.bind("<FocusIn>", self.print_type)
entry.bind("<Key>", self.print_key)
entry.pack(padx=20, pady=20)

def print_type(self, event):
print(event.type)

def print_key(self, event):
args = event.keysym, event.keycode, event.char
print("Знак: {}, Код: {}, Символ: {}".format(*args))

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

В первую очередь программа выведет сообщение события FocusIn. Это произойдет в тот момент, когда фокус окажется в виджете Entry. Можно убедиться также в том, что события срабатывают и в случае с непечатаемыми символами, такими как клавиши стрелок или Backspace.

Как работает отслеживание событий

Метод bind определен в классе widget и принимает три аргумента: событие sequence, функцию callback и опциональную строку add:

Строка sequence использует синтаксис <modifier-type-detail>.

Модификаторы являются опциональными и позволяют задать дополнительные комбинации для общего типа события:

  • Shift – когда пользователь нажимает клавишу Shift.
  • Alt – когда пользователь нажимает клавишу Alt.
  • Control – когда пользователь нажимает клавишу Control.
  • Lock – когда пользователь нажимает клавишу Lock.
  • Shift – когда пользователь нажимает клавишу Shift.
  • Shift – когда пользователь нажимает клавишу Shift lock.
  • Double – когда событие происходит дважды подряд.
  • Triple – когда событие происходит трижды подряд.

Типы события определяют общий тип события:

  • ButtonPress или Button – события, которые генерируются при нажатии кнопки мыши.
  • ButtonRelease – событие, когда кнопка мыши отпускается.
  • Enter – событие при перемещении мыши на виджет.
  • Leave – событие, когда мышь покидает область виджета.
  • FocusIn – событие, когда фокус ввода попадает в виджет.
  • FocusOut – событие, когда виджет теряет фокус ввода.
  • KeyPress или Key – событие для нажатия кнопки.
  • KeyRelease – событие для отпущенной кнопки.
  • Motion – событие при перемещении мыши.

detail – также опциональный параметр, который отвечает за определение конкретной клавиши или кнопки:

  • Для событий мыши 1 — это левая кнопка, 2 — средняя, а 3 — правая
  • Для событий клавиатуры используются сами клавиши. Если это специальные клавиши, то используется специальный символ: enter, Tab, Esc, up, down, right, left, Backspace и функциональные клавиши (от F1 до F12).

Функция callback принимает параметр события. Для событий мыши это следующие атрибуты:

  • x и y текущее положение мыши в пикселях
  • x_root и y_root то же, что и x или y, но относительно верхнего левого угла экрана
  • num – номер кнопки мыши

Для клавиш клавиатуры это следующие атрибуты:

  • char – нажатая клавиша в виде строки
  • keysym – символ нажатой клавиши
  • keycode – код нажатой клавиши

В обоих случаях у события есть атрибут widget, ссылающийся на экземпляр, который сгенерировал событие и type, определяющий тип события.

Рекомендуется определять метод для функции callback, поскольку всегда будет иметься ссылка на экземпляр класса, и таким образом можно будет легко получить доступ к атрибутам widget.

Наконец, параметр add может быть пустым ("") для замены функции callback, если до этого была привязка или + для добавления функции обратного вызова и сохранения старых.

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

Полный список событий доступен в документации Tcl/Tk.

Настройка иконки, названия и размера основного окна

Экземпляр Tk отличается от обычных виджетов тем, как он настраивается. Рассмотрим основные методы, которые позволяют настраивать внешний вид.

Этот кусок кода создает основное окно с заданными названием и иконкой. Его ширина — 400 пикселей, а высота — 200. Плюс, есть разделение в 10px по каждой оси к левому верхнему углу экрана.


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Моя программа")
self.iconbitmap("python.ico")
self.geometry("400x200+10+10")

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

Программа предполагает, что есть валидный ICO-файл python.ico в той же директории, где находится и сам скрипт. Вот результат:

настройка окна Tkinter

Как работает настройка окна

Названия методов title() и iconbitmap() класса Tk говорят сами за себя. Первый настраивает название окна, а второй — принимает путь к иконке для него.

Метод geometry() настраивает размер окна с помощью строки со следующим шаблоном:

{width}x{height}+{of set_x}+{of set_y}

Если в приложении есть дополнительные окна, то эти же методы доступны в классе Toplevel.

Чтобы сделать приложение полноэкранным, нужно заменить вызов метода geometry() на self.state("zoomed").

]]>
Поля выбора значений / tkinter 3 https://pythonru.com/uroki/polja-vybora-znachenij-tkinter-3 Sat, 22 Aug 2020 13:49:13 +0000 https://pythonru.com/?p=3415 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Выбор числовых значений

В предыдущем материале речь шла о работе с вводом текста, но иногда есть необходимость ограничить поле для работы исключительно с числовыми значениями. Это нужно для классов Spinbox и Scale, которые позволяют пользователям выбирать числовое значение из диапазона или списка валидных вариантов. Однако есть отличия в том, как они отображаются и настраиваются.

У этой программы есть Spinbox и Scale для выбора целого числа от 0 до 5:


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.spinbox = tk.Spinbox(self, from_=0, to=5)
self.scale = tk.Scale(self, from_=0, to=5,
orient=tk.HORIZONTAL)
self.btn = tk.Button(self, text="Вывести значения",
command=self.print_values)
self.spinbox.pack()
self.scale.pack()
self.btn.pack()

def print_values(self):
print("Spinbox: {}".format(self.spinbox.get()))
print("Scale: {}".format(self.scale.get()))

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

Для отладки также была добавлена кнопка, которая выводит значение при нажатии:

Выбор числовых значений

Как работает выбор значений

Оба класса принимают параметры from_ и to, которые обозначают диапазон подходящих значений — нижнее подчеркивание в конце является обязательным, потому что параметр from изначально определен в Tck/Tk, хотя является зарезервированным ключевым словом в Python.

Удобная особенность класса Scale — параметр resolution, который настраивает точность округления. Например, значение «0.2» позволит выбирать такие: 0.0, 0.2, 0.4 и так далее. По умолчанию установлено значение 1, поэтому виджет округляет все введенные числа до ближайшего целого.

Также значение каждого виджета можно получить с помощью метода get(). Важное отличие в том, что Spinbox возвращает число в виде строки, а Scale — целое число или число с плавающей точкой, если округление принимает десятичные значения.

Класс Spinbox имеет настройки, которые похожи на те, что есть у Entry: параметры textvariable и validate. Разница лишь в том, что правила будут ограничены числовыми значениями.

Создание полей с радиокнопками (переключателями)

С помощью виджета Radiobutton можно разрешить пользователю выбирать среди нескольких вариантов. Это работает для относительно небольшого количества взаимоисключающих вариантов.

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

В следующем примере создаются три кнопки для параметров Red, Green и Blue. При каждом нажатии выводится название соответствующего цвета в нижнем регистре:


import tkinter as tk

COLORS = [("Red", "red"), ("Green", "green"), ("Blue", "blue")]

class ChoiceApp(tk.Tk):
def __init__(self):
super().__init__()
self.var = tk.StringVar()
self.var.set("red")
self.buttons = [self.create_radio(c) for c in COLORS]
for button in self.buttons:
button.pack(anchor=tk.W, padx=10, pady=5)

def create_radio(self, option):
text, value = option
return tk.Radiobutton(self, text=text, value=value,
command=self.print_option,
variable=self.var)

def print_option(self):
print(self.var.get())

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

Если запустить скрипт, он покажет приложение, где вариант Red уже выбран.

Создание полей с радиокнопками

Как работают радиокнопки

Чтобы не повторять код для инициализации Radiobutton, нужно определить служебный метод, вызываемый из сгенерированного списка. Так, значения каждого кортежа в списке COLORS распаковываются, а локальные переменные передаются в качестве опций в Radiobutton. Очень важно при возможности избегать повторений.

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

Значением по умолчанию в программе является «red». Но что произойдет, если эту строку пропустить, а значение StringVar не будет соответствовать значению ни одной из кнопок? В таком случае оно будет совпадать со значением по умолчанию опции tristatevalue, то есть, пустой строкой. Из-за этого виджет отображается в неопределенном режиме «tri-state». Это можно изменить с помощью метода config(), но еще лучше — задавать правильное значение по умолчанию, чтобы переменная инициализировалась в валидном состоянии.

Реализация чекбоксов

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

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


import tkinter as tk

class SwitchApp(tk.Tk):
def __init__(self):
super().__init__()
self.var = tk.IntVar()
self.cb = tk.Checkbutton(self, text="Активно?",
variable=self.var,
command=self.print_value)
self.cb.pack()

def print_value(self):
print(self.var.get()) if __name__ == "__main__":
app = SwitchApp()
app.mainloop()

В этом примере значение виджета просто выводится каждый раз при нажатии.

Реализация чекбоксов

Как работают чекбоксы

По аналогии с Button Checkbutton принимает параметры Command и text.

С помощью опций onvalue и offvalue можно определить значения для отмеченного и пустого чекбоксов. Используется целочисленная переменная, потому что значения по умолчанию — это 1 и 0. Но это могут быть любые другие целые числа.

С Checkbuttons можно использовать даже другие типы переменных:


var = tk.StringVar()
var.set("OFF")
checkbutton_active = tk.Checkbutton(master, text="Активно?", variable=self.var,
onvalue="ON", offvalue="OFF",
command=update_value)

Единственное ограничение в том, что onvalue и offvalue должны совпадать с типом переменной Tkinter. В таком случае, поскольку «ON» и «OFF» — это строки, то и переменная должна быть StringVar. В противном случае интерпретатор Tcl вернет ошибку при попытке задать соответствующее значение другому типу.

Отображение списка элементов

Виджет Listbox содержит текстовые элементы, которые пользователь может выбрать с помощью мыши или клавиатуры. Есть возможность настроить, будут ли для выбора доступны один или несколько элементов.

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


import tkinter as tk

DAYS = ["Понедельник", "Вторник", "Среда", "Четверг",
"Пятница", "Суббота", "Воскресенье"]
MODES = [tk.SINGLE, tk.BROWSE, tk.MULTIPLE, tk.EXTENDED]
class ListApp(tk.Tk):
def __init__(self):
super().__init__()
self.list = tk.Listbox(self)
self.list.insert(0, *DAYS)
self.print_btn = tk.Button(self, text="Вывести выбор",
command=self.print_selection)
self.btns = [self.create_btn(m) for m in MODES]

self.list.pack()
self.print_btn.pack(fill=tk.BOTH)
for btn in self.btns:
btn.pack(side=tk.LEFT)

def create_btn(self, mode):
cmd = lambda: self.list.config(selectmode=mode)
return tk.Button(self, command=cmd,
text=mode.capitalize())

def print_selection(self):
selection = self.list.curselection()
print([self.list.get(i) for i in selection]) if __name__ == "__main__":
app = ListApp()
app.mainloop()

Попробуйте менять режимы и смотреть на вывод:

Отображение списка элементов

Как работает выбор элементов из списка

Можно создать пустой объект Listbox и добавить все элементы с помощью метода insert(). Индекс 0 обозначает, что элементы должны добавляться в начале списка. В следующей строке список DAYS распаковывается, но отдельные элементы можно добавить в конец с помощью константы END:

self.list.insert(tk.END, "Новый пункт")

Текущая выборка извлекается с помощью метода curselection(). Он возвращает индексы выбранных элементов. А для последующей трансформации их в соответствующие текстовые элементы для каждого элемента в списке вызывается метод get(). В итоге список выводится в STDOUT для отладки.

В этом примере параметр selectmode можно изменить для получения разного поведения:

  • SINGLE — один вариант;
  • BROWSE — один вариант, который можно перемещать с помощью клавиш со стрелками;
  • MULTIPLE — несколько вариантов;
  • EXTENDED — несколько вариантов с диапазонами, которые выбираются кнопками Shift и Ctrl.

Если элементов много, то может возникнуть необходимость добавить вертикальный скроллбар. Для этого нужно задействовать опцию yscrollcommand. В этом примере оба виджета оборачиваются в одно окно. Нужно только не забыть указать параметр fill, чтобы скроллбар занимал все место по оси y.


def __init__(self):
self.frame = tk.Frame(self)
self.scroll = tk.Scrollbar(self.frame, orient=tk.VERTICAL)
self.list = tk.Listbox(self.frame, yscrollcommand=self.scroll.set)
self.scroll.config(command=self.list.yview)
# ...
self.frame.pack()
self.list.pack(side=tk.LEFT)
self.scroll.pack(side=tk.LEFT, fill=tk.Y)

Также существует параметр xscrollcommand для горизонтальной оси.

]]>
Создание, изменение и проверка текста / tkinter 2 https://pythonru.com/uroki/sozdanie-izmenenie-i-proverka-teksta-tkinter-2 Sat, 15 Aug 2020 14:49:06 +0000 https://pythonru.com/?p=3331 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Создание текстовых элементов

Виджет Entry представляет собой текстовый элемент на одной строке. Вместе с классами Label и Button он является одним из самых используемых в Tkinter.

Как создать текстовый элемент

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


import tkinter as tk

class LoginApp(tk.Tk):
def __init__(self):
super().__init__()
self.username = tk.Entry(self)
self.password = tk.Entry(self, show="*")
self.login_btn = tk.Button(self, text="Войти",
command=self.print_login)
self.clear_btn = tk.Button(self, text="Очистить",
command=self.clear_form)
self.username.pack()
self.password.pack()
self.login_btn.pack(fill=tk.BOTH)
self.clear_btn.pack(fill=tk.BOTH)

def print_login(self):
print("Логин: {}".format(self.username.get()))
print("Пароль: {}".format(self.password.get()))

def clear_form(self):
self.username.delete(0, tk.END)
self.password.delete(0, tk.END)
self.username.focus_set()

if __name__ == "__main__":
app = LoginApp()
app.mainloop()
Как создать текстовый элемент

Как работают экземпляры

Экземпляры виджетов Entry создаются в родительском окне или фрейме, будучи переданными в качестве первого аргумента. С помощью опциональных ключевых слов можно задать дополнительные свойства. У username в этом примере таких нет, а у password — аргумент show со строкой «*», который будет выводить каждый символ как звездочку.

С помощью метода get() текущий текст можно будет получить в виде строки. Это используется в методе print_login(), который выводит содержимое Entry в стандартном выводе (stdout).

Метод delete() принимает два аргумента, которые представляют собой диапазон символов для удаления. Важно только помнить, что индексы начинаются с 0 и не включают последний символ. Если передать только один аргумент, то удалится символ на этой позиции.

В методе clear_form() удаляется содержимое от индекса 0 до константы END, в результате чего весь контент очищается. После этого фокус возвращается в поле username.

Содержимое виджета Entry можно модифицировать с помощью метода insert(), который принимает два аргумента:

  • index — позиция, куда нужно вставить текст (индекс первого — 0)
  • string — строка, которая будет вставлена

Стандартный шаблон сброса содержимого на значение по умолчанию — комбинация методов delete() и insert():

Еще один паттерн — добавление текста туда, где находится курсор. Для этого используется константа INSERT:

Как и Button класс Entry также принимает параметры relief и state для изменения стиля контура и состояния. Также стоит отметить, что вызовы delete() и insert() игнорируются, когда состояние равно «disabled» или «readonly».

Отслеживание изменений текста

Переменные Tk позволяют отправлять уведомления приложениям, когда входящие значения меняются. Есть 4 класса переменных в Tkinter: BooleanVar, DoubleVar, IntVar и StringVar. Каждый из них оборачивает значение соответствующего типа Python, который должен соответствовать типу виджета, прикрепленного к переменной.

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

Как отслеживать изменения текста

В следующем примере экземпляр StringVar ассоциирован с Entry, у которого есть параметр textvariable. Такие переменные отслеживают операции записи с помощью метода обратного вызова show_message():


import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.var = tk.StringVar()
self.var.trace("w", self.show_message)
self.entry = tk.Entry(self, textvariable=self.var)
self.btn = tk.Button(self, text="Очистить",
command=lambda: self.var.set(""))
self.label = tk.Label(self)
self.entry.pack()
self.btn.pack()
self.label.pack()

def show_message(self, *args):
value = self.var.get()
text = "Привет, {}!".format(value) if value else ""
self.label.config(text=text)

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

Когда что-то вводится в этот виджет, текст метки обновляется на тот, что был составлен с помощью значения переменной Tk. Например, если ввести слово «Мир», то метка выведет Привет, Мир!. Если текст не вводить совсем, то ничего и не будет выводиться. Для демонстрации возможностей интерактивной настройки содержимого переменной была добавлена кнопка, которая очищает поле по нажатию.

Как отслеживать изменения текста

Как работает изменение текста

Первые строки конструктора приложения создают экземпляр StringVar и прикрепляют функцию обратного вызова для режима записи. Валидные значения этого режима:

  • w — вызывается, когда переменная пишется
  • r — вызывается, когда переменная читается
  • u (от unset) — вызывается, когда переменная удаляется

При вызове функция обратного вызова получает три аргумента: внутреннее имя переменной, пустую строку (она используется в других типах переменных Tk) и режим, который запустил операцию. При объявлении его с *args эти аргументы становятся опциональными, потому что при обратном вызове значения уже не используются.

Метод get() оберток Tk возвращает текущее значение переменной, а метод set() — обновляет его. Они также уведомляют все методы прослушки (trace). Поэтому изменение содержимого поля с помощью графического интерфейса и нажатие кнопки Очистить запускают вызов метода show_message().

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

Валидация текста в полях

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

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

Как валидировать текст

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


import re
import tkinter as tk

class App(tk.Tk):
def __init__(self):
super().__init__()
self.pattern = re.compile("^\w{0,10}$")
self.label = tk.Label(self, text="Введите логин")
vcmd = (self.register(self.validate_username), "%i", "%P")
self.entry = tk.Entry(self, validate="key",
validatecommand=vcmd,
invalidcommand=self.print_error)
self.label.pack()
self.entry.pack(anchor=tk.W, padx=10, pady=10)

def validate_username(self, index, username):
print("Проверка символа" + index)
return self.pattern.match(username) is not None

def print_error(self):
print("Запрещенный символ в логине")

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

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

Как работает валидация текста

Когда параметром validate является key, то валидация запускается при любом изменении содержимого. Значение по умолчанию — none, что значит, что валидации не будет.

Также значениями могут быть focusin или focusout, когда валидация выполняется при получении или потере фокуса. Значение focus выполняет проверку в обоих случаях. Во всех ситуациях валидация проходит, если установить значение all.

Функция validatecommand вызывается каждый раз при запуске валидации. Она должна возвращать true, если введенное содержимое прошло проверку. В противном случае — false.

Поскольку нужно больше информации, чтобы определить, является ли контент валидным, над функцией Python создается обертка Tcl с помощью метода register класса Widget. Затем добавляется замещение для каждого параметра, который будет передаться в функцию. В итоге эти значения группируются в кортеж. Описанное соответствует следующей строке из примера:

vcmd = (self.register(self.validate_username), "%i", "%P")

Можно использовать следующие замещения:

  • %d — тип действия. 1 — добавление, 0 — удаление, -1 — остальное;
  • %i — индекс вставляемой или удаляемой строки;
  • %p — сущность содержимого, если изменение разрешено;
  • %s — строковое содержимое до изменения;
  • %s — строка, которая вставляется или удаляется;
  • %v — тип текущей валидации;
  • %V— тип валидации, которая запускает действие;
  • %w — название виджета Entry.

Параметр invalidcommand принимает функцию, которая вызывается, когда validatecommand возвращает false. Те же замещения могут быть применены и к нему, но в данном примере классу был прямо передан метод print_error().

Документация Tcl/Tk предполагает, что не нужно смешивать параметры validatecommand и textvariable, поскольку невалидое значение переменной Tk вообще отключит проверку. То же самое произойдет, если функция validatecommand не вернет булевое значение.

Подробное введение в регулярные выражения доступно в официальной документации Python по ссылке https://docs.python.org/3.7/howto/regex.html.

]]>
Структура приложения и работа с кнопкам / tkinter 1 https://pythonru.com/uroki/struktura-prilozhenija-i-rabota-s-knopkam-tkinter-1 Sat, 08 Aug 2020 13:27:16 +0000 https://pythonru.com/?p=3244 Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Структурирование приложения Tkinter

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

Простой пример

Возьмем в качестве примера следующую программу:

from tkinter import *
root = Tk()
btn = Button(root, text="Нажми!")
btn.config(command=lambda: print("Привет, Tkinter!"))
btn.pack(padx=120, pady=30)
root.title("Мое приложение Tkinter")
root.mainloop()

Она создает окно с кнопкой, которая выводит Привет, Tkinter! каждый раз при нажатии. Кнопка расположена с внутренним отступом 120px по горизонтальной оси и 30px – по вертикальной. Последняя строка запускает основной цикл, который обрабатывает все пользовательские события и обновляет интерфейс до закрытия основного окна.

Простое окно с кнопкой

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

Wildcard-импорты (from … import *) считаются плохой практикой, поскольку они загрязняют глобальное пространство имен. Здесь они используются для иллюстрации анти-паттерна, который часто встречается в примерах онлайн.

Эти проблемы настройки решаются с помощью базовых техник объектно-ориентированного программирования, что считается хорошей практикой для любых типов программ на Python.

Правильный пример

Чтобы улучшить модуль простой программы, стоит определить несколько классов, которые станут обертками вокруг глобальных переменных:

import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.btn = tk.Button(self, text="Нажми!",
command=self.say_hello)
self.btn.pack(padx=120, pady=30)

def say_hello(self):
print("Привет, Tkinter!")

if __name__ == "__main__":
app = App()
app.title("Мое приложение Tkinter")
app.mainloop()

Теперь каждая переменная хранится в конкретной области видимости, включая функцию command, которая находится в отдельном методе.

Как работает это приложение?

Во-первых нужно заменить wildcard-импорт на импорт в формате import … as для лучшего контроля над глобальным пространством имен.

Затем класс App определяется как подкласс Tk, который теперь ссылается на пространство имен tk. Для правильной инициализации базового класса, вызывается метод __init__() класса Tk с помощью встроенной функции super(). За это отвечают следующие строки:

class App(tk.Tk):
def __init__(self):
super().__init__()
# ...

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

Это может казаться излишним для такой простой программы, но подобный рефакторинг помогает работать с каждой отдельной частью. Создание кнопки отделено от обратного вызова, которые исполняется при нажатии. А генерация приложения перемещена в if __name__ == "main", что является стандартной практикой для исполняемых скриптов в Python.

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

Дополнение о структуре приложения

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

Однако может быть более удобно разделять классы Frame или Toplevel особенно для больших программ, где, например, есть несколько окон. Это все потому что у приложения Tkinter должен быть один экземпляр Tk, а система создает их автоматически при создании экземпляра виджета до создания экземпляра самого Tk.

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

Работа с кнопками

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

Как создать кнопку

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

import tkinter as tk

RELIEFS = [tk.SUNKEN, tk.RAISED, tk.GROOVE, tk.RIDGE, tk.FLAT]


class ButtonsApp(tk.Tk):
def __init__(self):
super().__init__()
self.img = tk.PhotoImage(file="python.gif")
self.btn = tk.Button(self, text="Кнопка с изображением",
image=self.img, compound=tk.LEFT,
command=self.disable_btn)
self.btns = [self.create_btn(r) for r in RELIEFS]
self.btn.pack()
for btn in self.btns:
btn.pack(padx=10, pady=10, side=tk.LEFT)

def create_btn(self, relief):
return tk.Button(self, text=relief, relief=relief)

def disable_btn(self):
self.btn.config(state=tk.DISABLED)


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

Цель программы — показать разные варианты настройки, которые могут быть использованы при создании виджета кнопки.

После выполнения кода выше, возвращается следующее:

Разные варианты настройки кнопок

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

В этом примере также добавляется PhotoImage с помощью параметра image, который имеет приоритет над строкой text. Этот параметр используется для объединения изображения и текста на одной кнопке, определяя местоположение, где будет находиться картинка. Он принимает следующие константы: CENTER, BOTTOM, LEFT, RIGHT и TOP.

Второй ряд кнопок создается с помощью сгенерированного списка и списка значений RELIEF. Метка каждой кнопки соответствует константе, так что можно заметить разницу во внешнем виде.

Для сохранения ссылки на экземпляр PhotoImage использовался атрибут, хотя его и нет вне метода __init__. Причина в том, что изображения удаляются при сборке мусора. Это и происходит, если объявить их в качестве локальных переменных.

Для избежания этого нужно помнить о сохранении ссылки на каждый объект PhotoImage до тех пор, пока окно, где он показывается, не закрыто.

Следующий урок: Работа с текстом (в разработке)

]]>
Обучение Python GUI (уроки по Tkinter) https://pythonru.com/uroki/obuchenie-python-gui-uroki-po-tkinter Wed, 16 Jan 2019 13:14:08 +0000 https://pythonru.com/?p=954

В этом уроке мы узнаем, как разрабатывать графические пользовательские интерфейсы, с помощью разбора некоторых примеров графического интерфейса Python с использованием библиотеки Tkinter.

Библиотека Tkinter установлена в Python в качестве стандартного модуля, поэтому нам не нужно устанавливать что-либо для его использования. Tkinter — очень мощная библиотека. Если вы уже установили Python, можете использовать IDLE, который является интегрированной IDE, поставляемой в Python, эта IDE написана с использованием Tkinter. Звучит круто!

Мы будем использовать Python 3.7 поэтому, если вы все еще используете Python 2.x, настоятельно рекомендуем перейти на Python 3.x, если вы не в курсе нюансов изменения языка, с целью, чтобы вы могли настроить код для запуска без ошибок.

Давайте предположим, что у вас уже есть базовые знания по Python, которые помогут понять что мы будем делать.
Мы начнем с создания окна, в котором мы узнаем, как добавлять виджеты, такие, как кнопки, комбинированные поля и т. д. После этого поэкспериментируем со своими свойствами, поэтому предлагаю начать.

Создание своего первого графического интерфейса

Для начала, следует импортировать Tkinter и создать окно, в котором мы зададим его название:

from tkinter import *


window = Tk()
window.title("Добро пожаловать в приложение PythonRu")
window.mainloop()

Результат будет выглядеть следующим образом:
Обучение Python GUI (уроки по Tkinter)Прекрасно! Наше приложение работает.
Последняя строка вызывает функцию mainloop. Эта функция вызывает бесконечный цикл окна, поэтому окно будет ждать любого взаимодействия с пользователем, пока не будет закрыто.

В случае, если вы забудете вызвать функцию mainloop , для пользователя ничего не отобразится.

Создание виджета Label

Чтобы добавить текст в наш предыдущий пример, мы создадим lbl , с помощью класса Label, например:

lbl = Label(window, text="Привет")

Затем мы установим позицию в окне с помощью функции grid и укажем ее следующим образом:

lbl.grid(column=0, row=0)

Полный код, будет выглядеть следующим образом:

from tkinter import *  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
lbl = Label(window, text="Привет")  
lbl.grid(column=0, row=0)  
window.mainloop()

И вот как будет выглядеть результат:
Обучение Python GUI (уроки по Tkinter)Если функция grid не будет вызвана, текст не будет отображаться.

Настройка размера и шрифта текста

Вы можете задать шрифт текста и размер. Также можно изменить стиль шрифта. Для этого передайте параметр font таким образом:

lbl = Label(window, text="Привет", font=("Arial Bold", 50))

Обучение Python GUI (уроки по Tkinter)Обратите внимание, что параметр font может быть передан любому виджету, для того, чтобы поменять его шрифт, он применяется не только к Label.

Отлично, но стандартное окно слишком мало. Как насчет настройки размера окна?

Настройка размеров окна приложения

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

window.geometry('400x250')

В приведенной выше строке устанавливается окно шириной до 400 пикселей и высотой до 250 пикселей.

Попробуем добавить больше виджетов GUI, например, кнопки и посмотреть, как обрабатывается нажатие кнопок.

Добавление виджета Button

Начнем с добавления кнопки в окно. Кнопка создается и добавляется в окно так же, как и метка:

btn = Button(window, text="Не нажимать!")
btn.grid(column=1, row=0)

Наш код будет выглядеть вот так:

from tkinter import *  
  

window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
lbl = Label(window, text="Привет", font=("Arial Bold", 50))  
lbl.grid(column=0, row=0)  
btn = Button(window, text="Не нажимать!")  
btn.grid(column=1, row=0)  
window.mainloop()

Результат будет следующим:
Обучение Python GUI (уроки по Tkinter)Обратите внимание, что мы помещаем кнопку во второй столбец окна, что равно 1. Если вы забудете и поместите кнопку в том же столбце, который равен 0, он покажет только кнопку.

Изменение цвета текста и фона у Button

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

btn = Button(window, text="Не нажимать!", bg="black", fg="red")

Обучение Python GUI (уроки по Tkinter)Теперь, если вы попытаетесь щелкнуть по кнопке, ничего не произойдет, потому что событие нажатия кнопки еще не написано.

Кнопка Click

Для начала, мы запишем функцию, которую нужно выполнить при нажатии кнопки:

def clicked():
    lbl.configure(text="Я же просил...")

Затем мы подключим ее с помощью кнопки, указав следующую ​​функцию:

btn = Button(window, text="Не нажимать!", command=clicked)

Обратите внимание: мы пишем clicked, а не clicked()с круглыми скобками. Теперь полный код будет выглядеть так:

from tkinter import *  
  
  
def clicked():  
    lbl.configure(text="Я же просил...")  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
lbl = Label(window, text="Привет", font=("Arial Bold", 50))  
lbl.grid(column=0, row=0)  
btn = Button(window, text="Не нажимать!", command=clicked)  
btn.grid(column=1, row=0)  
window.mainloop()

При нажатии на кнопку, результат, как и ожидалось, будет выглядеть следующим образом:
Обучение Python GUI (уроки по Tkinter)Круто!

Получение ввода с использованием класса Entry (текстовое поле Tkinter)

В предыдущих примерах GUI Python мы ознакомились со способами добавления простых виджетов, а теперь попробуем получить пользовательский ввод, используя класс Tkinter Entry (текстовое поле Tkinter).
Вы можете создать текстовое поле с помощью класса Tkinter Entry следующим образом:

txt = Entry(window, width=10)

Затем вы можете добавить его в окно, используя функцию grid.
Наше окно будет выглядеть так:

from tkinter import *  
  
  
def clicked():  
    lbl.configure(text="Я же просил...")  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
lbl = Label(window, text="Привет")  
lbl.grid(column=0, row=0)  
txt = Entry(window,width=10)  
txt.grid(column=1, row=0)  
btn = Button(window, text="Не нажимать!", command=clicked)  
btn.grid(column=2, row=0)  
window.mainloop()

Полученный результат будет выглядеть так:
Обучение Python GUI (уроки по Tkinter)Теперь, если вы нажмете кнопку, она покажет то же самое старое сообщение, но что же будет с отображением введенного текста в виджет Entry?

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

def clicked():
    res = "Привет {}".format(txt.get())
    lbl.configure(text=res)

Если вы нажмете на кнопку — появится текст «Привет » вместе с введенным текстом в виджете записи. Вот полный код:

from tkinter import *  
  
  
def clicked():  
    res = "Привет {}".format(txt.get())  
    lbl.configure(text=res)  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
lbl = Label(window, text="Привет")  
lbl.grid(column=0, row=0)  
txt = Entry(window,width=10)  
txt.grid(column=1, row=0)  
btn = Button(window, text="Клик!", command=clicked)  
btn.grid(column=2, row=0)  
window.mainloop()

Запустите вышеуказанный код и проверьте результат:
Обучение Python GUI (уроки по Tkinter)Прекрасно!

Каждый раз, когда мы запускаем код, нам нужно нажать на виджет ввода, чтобы настроить фокус на ввод текста, но как насчет автоматической настройки фокуса?

Установка фокуса виджета ввода

Здесь все очень просто, ведь все, что нам нужно сделать, — это вызвать функцию focus:

txt.focus()

Когда вы запустите свой код, вы заметите, что виджет ввода в фокусе, который дает возможность сразу написать текст.

Отключить виджет ввода

Чтобы отключить виджет ввода, отключите свойство состояния:

txt = Entry(window,width=10, state='disabled')

Обучение Python GUI (уроки по Tkinter)Теперь вы не сможете ввести какой-либо текст.

Добавление виджета Combobox

Чтобы добавить виджет поля с выпадающем списком, используйте класс Combobox из ttk следующим образом:

from tkinter.ttk import Combobox


combo = Combobox(window)

Затем добавьте свои значения в поле со списком.

from tkinter import *  
from tkinter.ttk import Combobox  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
combo = Combobox(window)  
combo['values'] = (1, 2, 3, 4, 5, "Текст")  
combo.current(1)  # установите вариант по умолчанию  
combo.grid(column=0, row=0)  
window.mainloop()

Обучение Python GUI (уроки по Tkinter)Как видите с примера, мы добавляем элементы combobox, используя значения tuple.
Чтобы установить выбранный элемент, вы можете передать индекс нужного элемента текущей функции.
Чтобы получить элемент select, вы можете использовать функцию get вот таким образом:

combo.get()

Добавление виджета Checkbutton (чекбокса)

С целью создания виджета checkbutton, используйте класс Checkbutton:

from tkinter.ttk import Checkbutton


chk = Checkbutton(window, text='Выбрать')

Кроме того, вы можете задать значение по умолчанию, передав его в параметр var в Checkbutton:

from tkinter import *  
from tkinter.ttk import Checkbutton  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
chk_state = BooleanVar()  
chk_state.set(True)  # задайте проверку состояния чекбокса  
chk = Checkbutton(window, text='Выбрать', var=chk_state)  
chk.grid(column=0, row=0)  
window.mainloop()

Посмотрите на результат:
Обучение Python GUI (уроки по Tkinter)

Установка состояния Checkbutton

Здесь мы создаем переменную типа BooleanVar, которая не является стандартной переменной Python, это переменная Tkinter, затем передаем ее классу Checkbutton, чтобы установить состояние чекбокса как True в приведенном выше примере.

Вы можете установить для BooleanVar значение false, что бы чекбокс не был отмечен.
Так же, используйте IntVar вместо BooleanVar и установите значения 0 и 1.

chk_state = IntVar()
chk_state.set(0) # False
chk_state.set(1) # True

Эти примеры дают тот же результат, что и BooleanVar.

Добавление виджетов Radio Button

Чтобы добавить radio кнопки, используйте класс RadioButton:

rad1 = Radiobutton(window,text='Первый', value=1)

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

from tkinter import *  
from tkinter.ttk import Radiobutton  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
rad1 = Radiobutton(window, text='Первый', value=1)  
rad2 = Radiobutton(window, text='Второй', value=2)  
rad3 = Radiobutton(window, text='Третий', value=3)  
rad1.grid(column=0, row=0)  
rad2.grid(column=1, row=0)  
rad3.grid(column=2, row=0)  
window.mainloop()

Результатом вышеприведенного кода будет следующий:
Обучение Python GUI (уроки по Tkinter)Кроме того, вы можете задать command любой из этих кнопок для определенной функции. Если пользователь нажимает на такую кнопку, она запустит код функции.
Вот пример:

rad1 = Radiobutton(window,text='Первая', value=1, command=clicked)

def clicked():
    # Делайте, что нужно

Достаточно легко!

Получение значения Radio Button (Избранная Radio Button)

Чтобы получить текущую выбранную radio кнопку или ее значение, вы можете передать параметр переменной и получить его значение.

from tkinter import *  
from tkinter.ttk import Radiobutton  
  
  
def clicked():  
    lbl.configure(text=selected.get())  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
selected = IntVar()  
rad1 = Radiobutton(window,text='Первый', value=1, variable=selected)  
rad2 = Radiobutton(window,text='Второй', value=2, variable=selected)  
rad3 = Radiobutton(window,text='Третий', value=3, variable=selected)  
btn = Button(window, text="Клик", command=clicked)  
lbl = Label(window)  
rad1.grid(column=0, row=0)  
rad2.grid(column=1, row=0)  
rad3.grid(column=2, row=0)  
btn.grid(column=3, row=0)  
lbl.grid(column=0, row=1)  
window.mainloop()

Обучение Python GUI (уроки по Tkinter)Каждый раз, когда вы выбираете radio button, значение переменной будет изменено на значение кнопки.

Добавление виджета ScrolledText (текстовая область Tkinter)

Чтобы добавить виджет ScrolledText, используйте класс ScrolledText:

from tkinter import scrolledtext


txt = scrolledtext.ScrolledText(window,width=40,height=10)

Здесь нужно указать ширину и высоту ScrolledText, иначе он заполнит все окно.

from tkinter import *  
from tkinter import scrolledtext  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
txt = scrolledtext.ScrolledText(window, width=40, height=10)  
txt.grid(column=0, row=0)  
window.mainloop()

Результат:
Обучение Python GUI (уроки по Tkinter)

Настройка содержимого Scrolledtext

Используйте метод insert, чтобы настроить содержимое Scrolledtext:

txt.insert(INSERT, 'Текстовое поле')

Удаление/Очистка содержимого Scrolledtext

Чтобы очистить содержимое данного виджета, используйте метод delete:

txt.delete(1.0, END)  # мы передали координаты очистки

Отлично!

Создание всплывающего окна с сообщением

Чтобы показать всплывающее окно с помощью Tkinter, используйте messagebox следующим образом:

from tkinter import messagebox


messagebox.showinfo('Заголовок', 'Текст')

Довольно легко! Давайте покажем окно сообщений при нажатии на кнопку пользователем.

from tkinter import *  
from tkinter import messagebox  
  
  
def clicked():  
    messagebox.showinfo('Заголовок', 'Текст')  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
btn = Button(window, text='Клик', command=clicked)  
btn.grid(column=0, row=0)  
window.mainloop()

Обучение Python GUI (уроки по Tkinter)Когда вы нажмете на кнопку, появится информационное окно.

Показ сообщений о предупреждениях и ошибках

Вы можете показать предупреждающее сообщение или сообщение об ошибке таким же образом. Единственное, что нужно изменить—это функция сообщения.

messagebox.showwarning('Заголовок', 'Текст')  # показывает предупреждающее сообщение
messagebox.showerror('Заголовок', 'Текст')  # показывает сообщение об ошибке

Показ диалоговых окон с выбором варианта

Чтобы показать пользователю сообщение “да/нет”, вы можете использовать одну из следующих функций messagebox:

from tkinter import messagebox


res = messagebox.askquestion('Заголовок', 'Текст')
res = messagebox.askyesno('Заголовок', 'Текст')
res = messagebox.askyesnocancel('Заголовок', 'Текст')
res = messagebox.askokcancel('Заголовок', 'Текст')
res = messagebox.askretrycancel('Заголовок', 'Текст')

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

Если вы кликнете OK, yes или retry, значение станет True, а если выберете no или cancel, значение будет False.
Единственной функцией, которая возвращает одно из трех значений, является функция askyesnocancel; она возвращает True/False/None.

Добавление SpinBox (Виджет спинбокс)

Для создания виджета спинбокса, используйте класс Spinbox:

spin = Spinbox(window, from_=0, to=100)

Таким образом, мы создаем виджет Spinbox, и передаем параметры from и to, чтобы указать диапазон номеров.
Кроме того, вы можете указать ширину виджета с помощью параметра width:

spin = Spinbox(window, from_=0, to=100, width=5)

Проверим пример полностью:

from tkinter import *  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
spin = Spinbox(window, from_=0, to=100, width=5)  
spin.grid(column=0, row=0)  
window.mainloop()

Обучение Python GUI (уроки по Tkinter)Вы можете указать числа для Spinbox, вместо использования всего диапазона следующим образом:

spin = Spinbox(window, values=(3, 8, 11), width=5)

Виджет покажет только эти 3 числа: 3, 8 и 11.

Задать значение по умолчанию для Spinbox

В случае, если вам нужно задать значение по умолчанию для Spinbox, вы можете передать значение параметру textvariable следующим образом:

var = IntVar()
var.set(36)
spin = Spinbox(window, from_=0, to=100, width=5, textvariable=var)

Теперь, если вы запустите программу, она покажет 36 как значение по умолчанию для Spinbox.

Добавление виджета Progressbar

Чтобы создать данный виджет, используйте класс progressbar :

from tkinter.ttk import Progressbar


bar = Progressbar(window, length=200)

Установите значение progressbar таким образом:

bar['value'] = 70

Вы можете установить это значение на основе любого процесса или при выполнении задачи.

Изменение цвета Progressbar

Изменение цвета Progressbar немного сложно. Сначала нужно создать стиль и задать цвет фона, а затем настроить созданный стиль на Progressbar. Посмотрите следующий пример:

from tkinter import *  
from tkinter.ttk import Progressbar  
from tkinter import ttk  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
style = ttk.Style()  
style.theme_use('default')  
style.configure("black.Horizontal.TProgressbar", background='black')  
bar = Progressbar(window, length=200, style='black.Horizontal.TProgressbar')  
bar['value'] = 70  
bar.grid(column=0, row=0)  
window.mainloop()

И в результате вы получите следующее:
Обучение Python GUI (уроки по Tkinter)

Добавление поля загрузки файла

Для добавления поля с файлом, используйте класс filedialog:

from tkinter import filedialog


file = filedialog.askopenfilename()

После того, как вы выберете файл, нажмите “Открыть”; переменная файла будет содержать этот путь к файлу. Кроме того, вы можете запросить несколько файлов:

files = filedialog.askopenfilenames()

Указание типа файлов (расширение фильтра файлов)

Возможность указания типа файлов доступна при использовании параметра filetypes, однако при этом важно указать расширение в tuples.

file = filedialog.askopenfilename(filetypes = (("Text files","*.txt"),("all files","*.*")))

Вы можете запросить каталог, используя метод askdirectory :

dir = filedialog.askdirectory()

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

from os import path
file = filedialog.askopenfilename(initialdir= path.dirname(__file__))

Легко!

Добавление панели меню

Для добавления панели меню, используйте класс menu:

from tkinter import Menu


menu = Menu(window)
menu.add_command(label='Файл')
window.config(menu=menu)

Сначала мы создаем меню, затем добавляем наш первый пункт подменю. Вы можете добавлять пункты меню в любое меню с помощью функции add_cascade() таким образом:

menu.add_cascade(label='Автор', menu=new_item)

Наш код будет выглядеть так:

from tkinter import *  
from tkinter import Menu  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
menu = Menu(window)  
new_item = Menu(menu)  
new_item.add_command(label='Новый')  
menu.add_cascade(label='Файл', menu=new_item)  
window.config(menu=menu)  
window.mainloop()

Обучение Python GUI (уроки по Tkinter)Таким образом, вы можете добавить столько пунктов меню, сколько захотите.

from tkinter import *  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
menu = Menu(window)  
new_item = Menu(menu)  
new_item.add_command(label='Новый')  
new_item.add_separator()  
new_item.add_command(label='Изменить')  
menu.add_cascade(label='Файл', menu=new_item)  
window.config(menu=menu)  
window.mainloop()

Обучение Python GUI (уроки по Tkinter)Теперь мы добавляем еще один пункт меню “Изменить” с разделителем меню. Вы можете заметить пунктирную линию в начале, если вы нажмете на эту строку, она отобразит пункты меню в небольшом отдельном окне.

Можно отключить эту функцию, с помощью tearoff подобным образом:

new_item = Menu(menu, tearoff=0)

Просто отредактируйте new_item, как в приведенном выше примере и он больше не будет отображать пунктирную линию.
Вы так же можете ввести любой код, который работает, при нажатии пользователем на любой элемент меню, задавая свойство команды.

new_item.add_command(label='Новый', command=clicked)

Добавление виджета Notebook (Управление вкладкой)

Для удобного управления вкладками реализуйте следующее:

  • Для начала, создается элемент управления вкладкой, с помощью класса Notebook .
  • Создайте вкладку, используя класс Frame.
  • Добавьте эту вкладку в элемент управления вкладками.
  • Запакуйте элемент управления вкладкой, чтобы он стал видимым в окне.
from tkinter import *  
from tkinter import ttk  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
tab_control = ttk.Notebook(window)  
tab1 = ttk.Frame(tab_control)  
tab_control.add(tab1, text='Первая')  
tab_control.pack(expand=1, fill='both')  
window.mainloop()

Обучение Python GUI (уроки по Tkinter)Таким образом, вы можете добавлять столько вкладок, сколько нужно.

Добавление виджетов на вкладку

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

from tkinter import *  
from tkinter import ttk  
  
  
window = Tk()  
window.title("Добро пожаловать в приложение PythonRu")  
window.geometry('400x250')  
tab_control = ttk.Notebook(window)  
tab1 = ttk.Frame(tab_control)  
tab2 = ttk.Frame(tab_control)  
tab_control.add(tab1, text='Первая')  
tab_control.add(tab2, text='Вторая')  
lbl1 = Label(tab1, text='Вкладка 1')  
lbl1.grid(column=0, row=0)  
lbl2 = Label(tab2, text='Вкладка 2')  
lbl2.grid(column=0, row=0)  
tab_control.pack(expand=1, fill='both')  
window.mainloop()

Обучение Python GUI (уроки по Tkinter)

Добавление интервала для виджетов (Заполнение)

Вы можете добавить отступы для элементов управления, чтобы они выглядели хорошо организованными с использованием свойств padx иpady.

Передайте padx и pady любому виджету и задайте значение.

lbl1 = Label(tab1, text= 'label1', padx=5, pady=5)

Это очень просто!

В этом уроке мы увидели много примеров GUI Python с использованием библиотеки Tkinter. Так же рассмотрели основные аспекты разработки графического интерфейса Python. Не стоит на этом останавливаться. Нет учебника или книги, которая может охватывать все детали. Надеюсь, эти примеры были полезными для вас.

]]>