Скачайте код уроков с 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 соответствует расстоянию по вертикальной оси и увеличивается по мере движения снизу вверх;
Можно обратить внимание на то, что эти координаты точно соответствуют атрибутам 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)
Этот код обеспечит такой результат:
Перенос строк
По умолчанию содержимое текстового элемента будет выводиться в одну строку. Параметр width
же позволяет задать максимальную ширину строки. В результате если она окажется больше, то содержимое перенесется на новую строку:
# ...
options = { "font": "courier", "fill": "blue",
"activefill": "red", "width": 70 }
self.text_id = self.canvas.create_text((w/2, h/2), **options)
Теперь если написать Hello World
, часть текста выйдет за пределы заданной ширины и перенесется на новую строку: