Скачайте код уроков с 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()
больше раз за один и тот же промежуток времени.
С помощью минимальной фиксированной задержки есть возможность одинаково обрабатывать элементы на разных машинах.