Продолжаем рассматривать особенности работы с полотном для рисования в Python. Сегодня мы рассмотрим, как добавлять геометрические фигуры, как работает рисование фигур, как искать элементы по их содержанию, а также ряд других важных вопросов.
Добавление геометрических фигур
Сегодня рассмотрим три базовых элемента полотна: прямоугольники, овалы, дуги. Все они отображаются в рамках собственного контейнера. Следовательно, для нахождения их положения необходимо иметь две точки: верхний левый угол и правый нижний.
Создадим приложение, которое позволит пользователям рисовать эти типы полотна. С помощью соответствующих кнопок человек может выбрать, что именно рисовать. Положение элементов определяется, как и в случае с линиями, двумя кликами. Первый указывает на верхний левый угол, а второй – на правый нижний. Также ряд параметров задаются непосредственно при отрисовке фигуры.
Вот, как будет выглядеть эта простенькая рисовалка. Программа сохраняет тип элемента, который был выбран с помощью кнопки, а потом рисует этот элемент после кликов левой кнопкой мыши.
Технически это реализуется следующим образом. С помощью левого клика мышью по полотну запускается обработчик события, который обрабатывает месторасположение первого угла элемента, который рисуется. После того, как совершается второй клик, интерпретатор считывает расположение второго угла. После этого отрисовка элемента осуществляется автоматически, без действий пользователя.
Реализуется это с помощью следующего кода.
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. Это дает возможность заморозить кнопку и текущую форму цикла в качестве аргументов функции обратного вызова.
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_rectangle(x0, y0, x1, y,1, ** options) – позволяет задать прямоугольник, углы которого задаются координатами с индексом 0 и 1 соответственно.
- canvas.create_oval(x0, y0, x1, y1, **options) – дает возможность нарисовать эллипс или круг в зависимости от того, какие значения координат указываются. Напоминаем, что круг – это технически эллипс с краями, равноудаленными от центра.
- canvas.create_arc(x0, y0, x1, y1, **options) – отрисовывает четверть эллипса, который размещается в прямоугольник с координатами, заданными в качестве аргументов. Логика такая же самая, как и в предыдущих функциях.
Выполнение поиска по местоположению
С помощью класса 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, с помощью которого сохраняется ссылка на текущий объект. Кроме этого, метод mouse_motion() осуществляет обработку события <Motion>.
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()
После этого нами создаются четыре элемента с определенным положением таким образом, что достаточно просто показать самый ближний к указателю.
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() мы также задаем цвет текущего элемента. 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")
Если поле current равняется None, то действия попросту не выполняются.
Как перемещать объекты на полотне
Все элементы, которые размещаются на полотне, могут перемещаться. Для этого нет даже необходимости задавать конкретные координаты. Необходимо просто задать ряд параметров.
Например, заблокировать перемещение за пределы определенной области.
Это – пример кода программы, позволяющей перемещать элементы полотна с помощью стрелок на клавиатуре. При этом стоит лимит на область, в пределах которой его можно перемещать.
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()