Tkinter画布移动会留下像素痕迹

4
我正在使用Tkinter画布制作一个游戏,其中点会在屏幕上移动。我使用tkinter.Canvas.create_oval(...)将每个点放置在一个位置,并使用tkinter.Canvas.move(pointID,delta_x,delta_y)移动点。
我的问题是,当这些点移动时,它们似乎会留下轨迹。我制作了一个简化的示例来演示我的问题。
from tkinter import Canvas,mainloop,Tk
import numpy as np
import random
import traceback
import threading
import time
from queue import Queue

class Point:
    def __init__(self,the_canvas,uID):
        self.uID = uID
        self.location = np.ones((2)) * 200
        self.color = "#"+"".join([random.choice('0123456789ABCDEF') for j in range(6)])
        self.the_canvas = the_canvas
        self.the_canvas.create_oval(200,200,200,200,
                     fill=self.color,outline=self.color,width=6,
                     tags=('runner'+str(self.uID),'runner'))
    def move(self):
        delta = (np.random.random((2))-.5)*20
        self.the_canvas.move('runner'+str(self.uID),delta[0],delta[1])

def queue_func():
    while True:
        time.sleep(.25)
        try:
            next_action = the_queue.get(False)
            next_action()
        except Exception as e: 
            if str(e) != "": 
                print(traceback.format_exc())

the_queue = Queue()
the_thread = threading.Thread(target=queue_func)
the_thread.daemon = True
the_thread.start()

window = Tk()
window.geometry('400x400')
the_canvas = Canvas(window,width=400,height=400,background='black')
the_canvas.grid(row=0,column=0)

points = {}
for i in range(100):
    points[i] = Point(the_canvas,i)

def random_movement():
    while True:
        for point in points.values():
            point.move()

the_queue.put(random_movement)

mainloop()

结果大致如下:

example1

我需要能够干净地移动点,而不会留下任何痕迹。

  • 我尝试更改move()函数,使每个点根据其标签被删除并在新位置重新绘制,但这导致相同的问题。
  • 我尝试在Canvas.oval配置中使用fill=''outline='',但这没有帮助。
  • 这些像素试验的行为似乎是不稳定的,就像它们会随着时间的推移消失,只留下有限数量的足迹。
  • 我尝试从移动循环中删除time.sleep(.2),这似乎使问题更加明显。

example2

  • 我发现清除这些流氓彩色像素的唯一方法是运行canvas.delete("all"),因此目前我的唯一解决方案是不断删除和重新绘制所有内容。这对我来说似乎不是一个好的解决方案。

有什么好的方法可以避免这些“像素痕迹”? 对我来说,它似乎只是一个漏洞,但也许我在某个地方犯了错误。


就我个人而言,我在我的 Mac 上无法复制这个问题。我没有看到你的截图中出现的任何伪影。这可能与您使用线程有关——tkinter 有时会在处理线程时出现困难。除非您正在尝试移动数万个点,否则我认为您不需要所有线程的开销。 - Bryan Oakley
@BryanOakley FWIW,我能在我的Windows 7机器上重现它,但我也认为线程是主要问题,即使在删除线程后,我也注意到其中一个运行中有非常少量的幽灵效应。 - r.ook
相同的结果。当您删除线程并改用after()循环时,伪影大部分会消失。但是仍然存在一些伪影。 - Mike - SMT
似乎问题可能与窗口中椭圆形的边框有关。我通过移除边框进行了测试,所有的伪影都消失了。 - Mike - SMT
2个回答

5

在查找了一些资料后,我发现了这篇文章:Python3 tkinter.Canvas.move() method makes artifacts on screen

问题出在椭圆的边框上。所以我做的是去掉边框,并稍微增大椭圆的大小来弥补,看起来这个方法行得通。

如果你修改这一行:

self.the_canvas.create_oval(200, 200, 200, 200, 
                            fill=self.color, outline=self.color, width=6,
                            tags=('runner' + str(self.uID), 'runner'))

转换为:

self.the_canvas.create_oval(200,200,206,206,
                            fill=self.color,outline='', width=0,
                            tags=('runner'+str(self.uID),'runner'))

无论是否使用线程,问题都应该消失。

如果您想查看没有线程的代码是什么样子,这里有一个例子:

import tkinter as tk
import numpy as np
import random


class Point:
    def __init__(self, the_canvas, uID):
        self.uID = uID
        self.location = np.ones((2)) * 200
        self.color = "#"+"".join([random.choice('0123456789ABCDEF') for j in range(6)])
        self.the_canvas = the_canvas
        self.the_canvas.create_oval(200, 200, 206, 206,
                                    fill=self.color, outline='', width=0,
                                    tags=('runner'+str(self.uID), 'runner'))

    def move(self):
        delta = (np.random.random((2))-.5)*20

        self.the_canvas.move('runner'+str(self.uID), delta[0], delta[1])

window = tk.Tk()
window.geometry('400x400')
the_canvas = tk.Canvas(window, width=400, height=400, background='black')
the_canvas.grid(row=0, column=0)

points = {}
for i in range(100):
    points[i] = Point(the_canvas, i)

def random_movement():
    for point in points.values():
        point.move()
    window.after(50, random_movement)

random_movement()
window.mainloop()

结果:

在此输入图片描述


这是一个展示IT技术相关内容的图片。

@Idlehands,谢谢提醒,我在粘贴代码时确实漏掉了window.mainloop() - Mike - SMT
啊,不好意思,我有点困惑,因为我把它写成了 window.after(25, random_movement); window.mainloop(),而且一直在尝试弄清楚为什么 random_movement() 没有实时绘制任何东西。我错过了递归的 after() 调用。感觉自己像个傻瓜。 - r.ook
@Idlehands 哈哈,这种事情经常发生。我也曾经错过一些显而易见的东西,直到别人指出来我才发现。 - Mike - SMT

3

奇怪的是问题似乎来自于create_oval()中的width=6,去掉它似乎可以解决问题。原始设置create_oval()的方式会产生一个零面积(即不存在)和边框宽度为6的椭圆形...这显然是一个麻烦的组合。

所以这里是新代码,带有一些额外的修改:

from tkinter import Canvas,mainloop,Tk
import numpy as np
import random
import traceback
import time

class Point:
    def __init__(self,uID):
        self.uID = uID
        self.location = np.ones((2)) * 200
        self.color = "#" + "".join([random.choice('0123456789ABCDEF') for j in range(6)])

    def move(self):
        delta = (np.random.random((2)) - .5) * 20
        self.location += delta

    def draw(self,canv):
        x0, y0 = self.location
        canv.create_oval(
             x0, y0, x0 + 2, y0 + 2,
             fill=self.color, outline=self.color,
             tags=('runner'+str(self.uID),'runner')
        )

def repeater(window):
    the_canvas.delete('all')
    for i in points:
        i.move()
        i.draw(the_canvas)
    window.after(25, repeater, window)

window = Tk()
window.geometry('400x400')
the_canvas = Canvas(window,width=400,height=400,background='black')
the_canvas.grid(row=0,column=0)

points = []
for i in range(100):
    points.append(Point(i))

repeater(window)
window.mainloop()

另外,删除画布上的所有元素是清除它的方法。这并不浪费,因为您无论如何都会更新它上面的所有元素。


是的,问题与边框有关。不过最好将颜色设置为无,宽度设置为零。这样可以防止在窗口中出现任何伪影。我之所以建议将 width=0 设置为0,而不是完全删除它,是因为如果未定义宽度,则默认宽度为1。 - Mike - SMT
只要宽度不大于对象(即椭圆形)本身,那么这应该是可以的吧? - sam46

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接