Home > Software design >  Move multiple tkinter.canvas figures simultaneously
Move multiple tkinter.canvas figures simultaneously

Time:12-23

I want to make many circles to move on a given trajectory simultaneously. Everything works fine only with a single figure moving. Whenever I add another figure it starts speeding up until it starts freezing apparently. This happens no matter what I use threads or canvas.move() and canvas.after() methods.

Actually this is also quite weird, because with my full version of code it started to slow down after adding more figures. The one that I sent is simplified in order to show the issue with moving. Maybe that happens, because I used my own method to draw trajectory lines with many little squares, but that is not the point.

What can be done to move figures at the same time with the same speed without that much of a lag? I wanted to try to use procceses instead of threads, but did not really understand how they work and I doubt that would change anything significantly.

Maybe you could also give an advice on how to work with individual pixels in canvas without drawing rectangle with width of one pixel?

EDIT: Forgot to mention. For some reason even if there is a single figure moving, if I move my mouse figure's moving will slow down until I stop touching the mouse. I suppose the reason is that tkinter starts registering events, but I need it to draw the trajectory, so removing is not really an option. What can be done to get rid of this issue?

import math
import tkinter as tk
from enum import Enum
import threading

class Trajectory:
    # This trajectory should do the same trick both for circles, squares and maybe images
    # x0, y0 - start point, x1, y1 - end point
    def __init__(self, x0, y0, x1, y1, id, diameter):
        print('aaaa', id)
        self.x0 = x0
        self.y0 = y0
        self.x1 = x1
        self.y1 = y1
        self.id = id
        self.sign = 1
        self.diameter = diameter
        self.dir = self.get_normalized_dir()

    def has_arrived(self, x, y):
        return math.sqrt((self.x1 - (x   self.diameter // 2)) * (self.x1 - (x   self.diameter // 2))  
                         (self.y1 - (y   self.diameter // 2)) * (self.y1 - (y   self.diameter // 2))) < 1

    def get_normalized_dir(self):
        L = math.sqrt((self.x1 - self.x0) * (self.x1 - self.x0)   (self.y1 - self.y0) * (self.y1 - self.y0))
        return (self.x1 - self.x0) / L, (self.y1 - self.y0) / L

    def swap_points(self):
        self.x0, self.y0, self.x1, self.y1 = self.x1, self.y1, self.x0, self.y0

    def __str__(self):
        return f'x0 {self.x0} y0 {self.y0} x1 {self.x1} y1 {self.y1} id {self.id} sign {self.sign}'


class App(tk.Tk):
    def __init__(self):
        # action_33 was intented as an Easter egg to smth (at least I think so). However,
        # I forgot what it meant :(
        super().__init__()
        self.bind('<Motion>', self.on_mouse)
        self.geometry('400x400')
        self.resizable(False, False)
        self.canvas = tk.Canvas(self, bg='white', width=400, height=400)
        self.canvas.pack(fill="both", expand=True)
        self.start_point = []
        self.end_point = []
        self.is_drawing = False
        self.OUTLINE = 'black'
        self.canvas.bind("<Button-1>", self.callback)
        self.title('Object trajetory')
        self.bg_line = None
        self.figure_color = 'green'
        self.figures = []  # will store only trajectory class
        self.diameter = 40

    def move_figures(self):
        # if not self.is_drawing:
        for figure in self.figures:
            self.canvas.move(figure.id, figure.dir[0] * 0.1 * figure.sign, figure.dir[1] * 0.1 * figure.sign)
            if figure.has_arrived(self.canvas.coords(figure.id)[0], self.canvas.coords(figure.id)[1]):
                figure.sign = -figure.sign
                figure.swap_points()
        self.canvas.after(1, self.move_figures)

    def move_for_thread(self, figure):
        while True:
            self.canvas.move(figure.id, figure.dir[0] * 0.1 * figure.sign, figure.dir[1] * 0.1 * figure.sign)
            if figure.has_arrived(self.canvas.coords(figure.id)[0], self.canvas.coords(figure.id)[1]):
                figure.sign = -figure.sign
                figure.swap_points()

    def delete_shadow_line(self):
        if self.bg_line is not None:
            self.canvas.delete(self.bg_line)

    def on_mouse(self, event):
        if self.is_drawing:
            self.delete_shadow_line()
            self.bg_line = self.canvas.create_line(self.start_point[0], self.start_point[1], event.x, event.y)

    def callback(self, event):
        if not self.is_drawing:
            self.start_point = [event.x, event.y]
            self.is_drawing = True
        else:
            self.is_drawing = False
            self.bg_line = None
            self.end_point = [event.x, event.y]
            fig = self.canvas.create_oval(self.start_point[0] - self.diameter // 2,
                                          self.start_point[1] - self.diameter // 2,
                                          self.start_point[0]   self.diameter // 2,
                                          self.start_point[1]   self.diameter // 2,
                                          fill=self.figure_color, outline='')
            # self.figures.append(
            # Trajectory(self.start_point[0], self.start_point[1], self.end_point[0], self.end_point[1], fig,
            # self.diameter))

            # self.move_figures()
            traj = Trajectory(self.start_point[0], self.start_point[1], self.end_point[0], self.end_point[1], fig, self.diameter)
            t = threading.Thread(target=self.move_for_thread, args=(traj,))
            t.start()


if __name__ == '__main__':
    print('a')
    app = App()
    app.mainloop()

CodePudding user response:

Using threads is adding more complexity than necessary. The code that uses after is easily capable of doing the animation.

However, you have a critical flaw. You're calling move_figures() every time you draw a line, and each time you call it you are starting another animation loop. Since the animation loop moves everything, the first object gets moved 1000 times per second. When you add two elements, each element is moving 1000 times per second twice. Three elements, it's moving 1000 times per second three times and so on.

So, start by removing the threading code. Then, call move_figures() exactly once, and within it, you should not be calling it with after(1, ...) since that's attempting to animate it 1000 times per second. Instead, you can reduce the load by 90% by using after(10, ...) or some other number bigger than 1.

You can call the function exactly once by calling it from App.__init__ rather than in App.callback.

  • Related