同时移动多个 tkinter.canvas 图形

Posted

技术标签:

【中文标题】同时移动多个 tkinter.canvas 图形【英文标题】:Move multiple tkinter.canvas figures simultaneously 【发布时间】:2022-01-23 21:06:09 【问题描述】:

我想让多个圆圈同时在给定的轨迹上移动。一切正常,只有一个人移动。每当我添加另一个数字时,它就会开始加速,直到它开始明显冻结。无论我使用什么线程或 canvas.move() 和 canvas.after() 方法,都会发生这种情况。

其实这也很奇怪,因为我的完整版代码在添加更多数字后开始变慢。我发送的那个被简化以显示移动的问题。也许会发生这种情况,因为我用自己的方法绘制了带有许多小方块的轨迹线,但这不是重点。

如何才能以相同的速度同时移动人物而没有太多延迟?我想尝试使用进程而不是线程,但并没有真正理解它们是如何工作的,我怀疑这会显着改变任何事情。

也许您还可以就如何在画布中使用单个像素而不绘制宽度为一个像素的矩形提供建议?

编辑: 忘了提。出于某种原因,即使有一个人物在移动,如果我移动我的鼠标人物的移动速度会减慢,直到我停止触摸鼠标。我想原因是 tkinter 开始注册事件,但我需要它来绘制轨迹,所以删除并不是一个真正的选择。有什么办法可以解决这个问题?

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

【问题讨论】:

“也许您还可以就如何在画布中使用单个像素而不绘制一个像素宽度的矩形提供建议?” - 画布是基于矢量的小部件,无法处理单个像素。 不管怎样,结果证明应用程序非常慢,因为我试图用绘制小矩形的像素来复制工作。用 canvas.create_line() 改变我的绘图方法就可以了,但是我被禁止使用内置函数。我发现使用 PhotoImage 类我可以处理单个像素,所以我稍后会尝试这样做。 【参考方案1】:

使用线程会增加不必要的复杂性。使用after 的代码很容易做动画。

但是,您有一个严重的缺陷。每次绘制一条线时,您都在调用move_figures(),并且每次调用它时,您都在开始另一个 动画循环。由于动画循环会移动所有内容,因此第一个对象每秒移动 1000 次。添加两个元素时,每个元素每秒移动 1000 次两次。三个元素,每秒移动 1000 次 3 次 以此类推。

所以,从删除线程代码开始。然后,只调用一次move_figures(),在其中,您不应该使用after(1, ...) 调用它,因为它试图每秒对其进行 1000 次动画处理。相反,您可以使用 after(10, ...) 或其他大于 1 的数字将负载减少 90%。

您可以通过从App.__init__ 而不是在App.callback 中调用该函数,只调用一次。

【讨论】:

哇,谢谢!它现在可以正常工作,但是当我单击窗口顶部(允许拖动窗口的东西,idk 它是如何调用的)时,一切都会冻结一两秒钟。你知道为什么会这样吗? 另外,每当我移动鼠标时,动画似乎会加快一点。老实说,我对这种行为感到非常困惑。

以上是关于同时移动多个 tkinter.canvas 图形的主要内容,如果未能解决你的问题,请参考以下文章

GUI的最终选择 Tkinter:Canvas组件

tkinter Canvas 实现拖曳与缩放功能

用于Raspberry Pi的Tkinter / Canvas-based kiosk-like程序

linux下同时移动多个文件夹命令mv

linux下同时移动多个文件夹命令mv

了解 Tkinter Canvas 的性能限制