Tkinter. Canvas, рисование графики (часть 3)

В прошлый раз мы научились создавать фигуры на полотне программы и перемещать их. Сегодня мы продолжим рассматривать объект класса Canvas. В частности, разберем, как обнаруживать пересечение нескольких объектов, удалять элементы, которые были добавлены ранее, как связывать события с элементами полотна, принципы drag and drop (и что это такое). Также разберем ряд других особенностей. 

Определение пересечений между объектами

Кроме перемещения объектов пользователь также может определять, пересекается ли один с другим. Как этого добиться? Этот результат достигается за счет того, что все объекты находятся в прямоугольных контейнерах. А чтобы определять пересечения, используется метод find_overlapping(), который находится в классе Canvas.

Эта программа создает несколько прямоугольников. Пользователь может управлять синим прямоугольником, перемещая его так, как душе угодно.

Если обнаруживается пересечение с каким-то маленьким квадратиком, он становится желтым. 

В целом, код очень напоминает тот, который был реализован в предыдущей части. Поэтому рассмотрим лишь те части, которые отвечают за создание новых прямоугольников и обращение к методу canvas.find_overlapping().

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 = w = self.canvas.winfo_width()

        self.height = h = self.canvas.winfo_height()




        pos = (w / 2 - 15, h / 2 - 15, w / 2 + 15, h / 2 + 15)

        self.item = self.canvas.create_rectangle(*pos, fill="blue")

        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="green")




        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):

        all_items = self.canvas.find_all()

        for item in filter(lambda i: i is not self.item, all_items):

            self.canvas.itemconfig(item, fill="green")




        x0, y0, x1, y1 = self.canvas.coords(self.item)

        items = self.canvas.find_overlapping(x0, y0, x1, y1)

        for item in filter(lambda i: i is not self.item, items):

            self.canvas.itemconfig(item, fill="yellow")




        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




        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()

Определение исключений

До того, как элементы начали пересекаться между собой, все квадраты на полотне имеют зеленый цвет. Чтобы получить идентификаторы данных объектов, необходимо воспользоваться методом canvas.find_all()

    def process_movements(self):

        all_items = self.canvas.find_all()

        for item in filter(lambda i: i is not self.item, all_items):

            self.canvas.itemconfig(item, fill="green")

Когда мы сбросили цвета элементов, воспользуемся методом find_overlapping(), который вызывается из объекта класса Canvas. Таким образом, мы получим все пересечения элементов. При этом тот объект, который перемещается, исключается из цикла, в то время как цвет остальных элементов становится желтым, как приводится на рисунке выше.

    def process_movements(self):

        # ...

        x0, y0, x1, y1 = self.canvas.coords(self.item)

        items = self.canvas.find_overlapping(x0, y0, x1, y1)

        for item in filter(lambda i: i is not self.item, items):

            self.canvas.itemconfig(item, fill="yellow")

Эта функция продолжит выполняться, перемещая синий прямоугольник на определенное расстояние. Он же себя снова планирует, используя process_movements() после этого. 

Если требуется понять, когда движущийся объект перекрывает другой полностью (а не лишь частично), то используется другой метод. Параметры те же самые, только отличается название. Для этой цели используется функция canvas.find_enclosed().

Как удалять элементы?

Кроме добавления и редактирования объектов, их также можно удалять с помощью метода delete(). В целом, никаких особенностей, отличающих его от других, нет. Тем не менее, есть определенные шаблоны. 

Необходимо понимать, что рендеринг зависит от количества объектов на полотне. Поэтому удаление неиспользуемых напрямую влияет на производительность.

Давайте попробуем создать программу, которая выбирает несколько кругов на полотне и удаляет их. Особых правил не будет, она их будет удалять полностью случайным образом.

Запускать удаление элементов будет специальная кнопка на экране. В свою очередь, другая будет отвечать за их создание. Tkinter. Canvas, рисование графики (часть 3)

Чтобы размещение объектов на полотне было случайным, необходимо воспользоваться модулем random, а именно, – функцией randint. А выбор цвета будет осуществляться, исходя из определенного набора, а также будет использоваться вызов choice для этой операции. 

После генерации удаление элементов возможно с использованием обработчика on_click или кнопки Clearitems, которая задействует функцию обратного вызова clear_all. Этот метод вызывает canvas.delete().

import random

import tkinter as tk




class App(tk.Tk):

    colors = ("red", "yellow", "green", "blue", "orange")




    def __init__(self):

        super().__init__()

        self.title("Удаление элементов холста")




        self.canvas = tk.Canvas(self, bg="white")

        frame = tk.Frame(self)

        generate_btn = tk.Button(frame, text="Создавать элементы",

                                 command=self.generate_items)

        clear_btn = tk.Button(frame, text="Удалить элементы",

                              command=self.clear_items)




        self.canvas.pack()

        frame.pack(fill=tk.BOTH)

        generate_btn.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

        clear_btn.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)




        self.update()

        self.width = self.canvas.winfo_width()

        self.height = self.canvas.winfo_height()




        self.canvas.bind("<Button-1>", self.on_click)

        self.generate_items()




    def on_click(self, event):

        item = self.canvas.find_withtag(tk.CURRENT)

        self.canvas.delete(item)




    def generate_items(self):

        self.clear_items()

        for _ in range(10):

            x = random.randint(0, self.width)

            y = random.randint(0, self.height)

            color = random.choice(self.colors)

            self.canvas.create_oval(x, y, x + 20, y + 20, fill=color)




    def clear_items(self):

        self.canvas.delete(tk.ALL)




if __name__ == "__main__":

    app = App()

    app.mainloop()

Принцип удаления элементов

Методу canvas.delete() передается аргумент, который может служить идентификатором элемента или тегом. С его помощью осуществляется один или несколько объектов. Их количество зависит от того, сколько раз этот тег будет использоваться. 

Чтобы посмотреть, как работает удаление элемента по идентификатору, необходимо посмотреть на содержимое обработчика on_click.

    def on_click(self, event):

        item = self.canvas.find_withtag(tk.CURRENT)

        self.canvas.delete(item)

Важно! Если кликнуть по пустой точке, то методом canvas.find_withtag(tk.CURRENT) будет возвращено значение None. Тем не менее, его передача методу delete() не будет считаться ошибкой, поскольку этот параметр не совпадает ни с одним тегом или идентификатором

Простыми словами, этот параметр может использоваться. Правда, никакого действия в этом случае не будет. 

В функции обратного вызова clear_items() также находится другой пример удаления элементов. Здесь используется тег ALL, соответствующий всем элементам и удаляющих их из полотна. 

    def clear_items(self):

        self.canvas.delete(tk.ALL)

При этом нет необходимости добавлять тег ALL каждый раз. Он работает по умолчанию.

Как связывать события с элементами полотна?

Вам уже известно, каким образом настраивается связь событий с виджетами. С элементами, содержащимися в полотне, также можно это делать. Это значительно удобнее, чем каждый раз определять необходимые действия через объект Canvas.

Эта программа демонстрирует, как работает drag and drop для элементов полотна. То есть, перетягивание. 

import tkinter as tk




class App(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("Drag and drop")




        self.dnd_item = None

        self.canvas = tk.Canvas(self, bg="white")

        self.canvas.pack()




        self.canvas.create_rectangle(30, 30, 60, 60, fill="green",

                                     tags="draggable")

        self.canvas.create_oval(120, 120, 150, 150, fill="red",

                                tags="draggable")




        self.canvas.tag_bind("draggable", "<ButtonPress-1>",

                             self.button_press)

        self.canvas.tag_bind("draggable", "<Button1-Motion>",

                             self.button_motion)




    def button_press(self, event):

        item = self.canvas.find_withtag(tk.CURRENT)

        self.dnd_item = (item, event.x, event.y)




    def button_motion(self, event):

        x, y = event.x, event.y

        item, x0, y0 = self.dnd_item

        self.canvas.move(item, x - x0, y - y0)

        self.dnd_item = (item, x, y)




if __name__ == "__main__":

    app = App()

    app.mainloop()

Рендеринг полотна в файл PostScript

Можно также сохранить содержимое полотна в файл формата PostScript. Давайте немного модифицируем код, чтобы в нем появилась возможность выводить содержимое в этот файл. 

import tkinter as tk

from lesson_18.drawing import LineForm




class App(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("Базовый холст")




        self.line_start = None

        self.form = LineForm(self)

        self.render_btn = tk.Button(self, text="Render canvas",

                                    command=self.render_canvas)

        self.canvas = tk.Canvas(self, bg="white")

        self.canvas.bind("<Button-1>", self.draw)




        self.form.grid(row=0, column=0, padx=10, pady=10)

        self.render_btn.grid(row=1, column=0)

        self.canvas.grid(row=0, column=1, rowspan=2)




    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)




    def render_canvas(self):

        self.canvas.postscript(file="output.ps", colormode="color")




if __name__ == "__main__":

    app = App()

    app.mainloop()

ОфисГуру
Adblock
detector