Раньше мы много говорили о стандартном виджете Tkinter. Тем не менее, почти ничего не было сказано о виджете Canvas. Все дело в том, что он предоставляет огромное количество дополнительных возможностей, и поэтому заслуживает того, чтобы о нем говорили отдельно.
Что такое Canvas? Это слово переводится с английского, как «полотно», что очень хорошо отражает специфику использования этого виджета. Это прямоугольная область, в которой можно выводить геометрические фигуры и другие виджеты Tkinter. С его помощью можно сконструировать интерфейс мечты.
Все объекты, которые находятся внутри Canvas, называются вложенными. Каждый из них имеет идентификатор, с помощью которого можно получить к нему доступ и выполнять ряд операций.
Давайте более подробно поговорим об этом классе и о методах, которые он использует, на реальных примерах.
Что такое система координат?
Любое полотно в любой компьютерной программе имеет свою систему координат. Стоит только вспомнить Paint. В этой программе каждая точка имеет координаты, которые можно посмотреть внизу окна. То же самое и с Canvas. Это полотно имеет две оси – 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>.
С помощью площади полотна также можно отображать элементы с отрицательными значениями координат. В таком случае элемент будет частично виден у левой или верхней границы. То есть, сами отрицательные координаты не видны невооруженным взглядом, но элемент можно частично разместить за пределами экрана, отобразив только его часть.
Точно так же можно сделать так, чтобы была видна часть элемента у правого и нижнего края. В этом случае его необходимо разместить немного за пределами полотна.
Как осуществляется рисование линий и стрелок?
Линии и стрелки – это одно из базовых действий, которые можно выполнить на полотне. Это основа любого рисунка. Рисование осуществляется от точки А к точке Б.
С их помощью можно создавать как непосредственно линии, так и более сложные фигуры (например, многоугольники). Правда, для этого можно использовать другой метод. Тем не менее, давайте начнем с простого: создадим программу, которая с помощью кликов по полотну дает возможность рисовать линии.
Отображение каждой из них будет осуществляться после того, как пользователь укажет точку начала линии и точку конца.
Кроме этого, можно детализировать цвет, толщину, и так далее.
Простыми словами, создадим программу, которая является неким мини-аналогом Paint. Выглядеть она будет так.
С помощью класса App мы создаем пустое полотно вместе с обработчиком событий кликов.
Получить сведения о линии можно из класса LineForm. Такой подход с выделением специального класса для того, чтобы использовать конкретный компонент позволяет не вникать в детали реализации класса и просто спокойно работать с виджетом Canvas.
Проще говоря, мы не зацикливаемся на реализации класса LimeForm в этом коде.
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()
А уже сам код можно найти в отдельном файле. Такой подход помогает значительно уменьшить количество строк кода, что увеличит его читаемость. Настоятельно рекомендуется делать это.
Рисование линий в 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)
В функцию create_line(), которая является методом объекта класса canvas мы передаем четыре аргумента, где первые два – координаты начала линии (по горизонтали и вертикали), а вторые обозначают конечную точку.
Вывод текста в canvas
В ряде ситуаций надо отобразить текст на полотне. Выше мы использовали отдельный виджет для этой цели, но это делать вовсе необязательно. Ведь можно воспользоваться методом create_text, который относится к объектам класса Canvas. А управлять этой строкой можно точно так же, как и любым другим элементом.
При этом сохраняется возможность применять те же параметры форматирования. То есть, задавать цвет, размер, семейство шрифтов текста.
В данном примере выполним объединение виджета Entry с тем, что находится в текстовом элементе полотна. И если у первого будет такой же стиль, то на полотне уже текст можно будет персонализировать.
Текстовый элемент по умолчанию реализуется с использованием canvas.create_text() и дополнительными настройками, с помощью которых можно добавить синий цвет.
А если необходимо задать параметры динамического поведения текстового элемента, необходимо воспользоваться StringVar. Это позволяет изменять его содержимое.
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()
Размещение текста в левом верхнем углу
С помощью параметра 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)