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

Скачайте код уроков с 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

Подписывайтесь на канал в Дзене

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

Обучение Python и Data Science

Профессия Data Scientist

Профессия Data Scientist

11 520 5 760 ₽/мес.
Профессия Python-разработчик

Профессия Python-разработчик

7 820 3 910 ₽/мес.
Профессия Python Fullstack

Профессия Python Fullstack

7 820 3 910 ₽/мес.
Курс Аналитик данных с нуля

Курс Аналитик данных с нуля

6 500 3 900 ₽/мес.

Появились вопросы? Задайте на Яндекс.Кью

У сайта есть сообщество на Кью >> Python Q <<. Там я, эксперты и участники отвечаем на вопросы по python и программированию.