Python海龟模块奇怪的光标跳动

7

我正在尝试使用turtle库进行鼠标绘制,下面是一段示例代码,但有时在鼠标移动过程中光标会跳跃:

#!/usr/bin/env python
import turtle
import sys

width = 600
height = 300
def gothere(event):
    turtle.penup()
    x = event.x
    y = event.y
    print "gothere (%d,%d)"%(x,y)
    turtle.goto(x,y)
    turtle.pendown()

def movearound(event):
    x = event.x
    y = event.y
    print "movearound (%d,%d)"%(x,y)
    turtle.goto(x,y)

def release(event):
    print "release"
    turtle.penup()

def circle(x,y,r):
    turtle.pendown() 
    turtle.goto(x,y)
    turtle.circle(r)
    turtle.penup()
    return

def reset(event):
    print "reset"
    turtle.clear()

#------------------------------------------------#
sys.setrecursionlimit(90000)
turtle.screensize(canvwidth=width, canvheight=height, bg=None)
turtle.reset()
turtle.speed(0)
turtle.setup(width, height)

canvas = turtle.getcanvas()

canvas.bind("<Button-1>", gothere)
canvas.bind("<B1-Motion>", movearound)
canvas.bind("<ButtonRelease-1>", release)
canvas.bind("<Escape>",reset)

screen = turtle.Screen()
screen.setworldcoordinates(0,height,width,0)
screen.listen()

turtle.mainloop()
#------------------------------------------------#

请参考下面的gif以查看实际的行为:

在此输入图片描述

不确定是否有任何API调用错误!

3
sys.setrecursionlimit(90000) 对我来说有些可疑。如果你添加这个代码是因为之前遇到了一个非常长的堆栈跟踪的异常错误,那么我怀疑这个错误与你现在遇到的问题有关。 - Kevin
看起来这种情况发生在乌龟到达光标位置之前移动鼠标。当你点击后,你可以看到乌龟向光标移动——如果你在它到达光标之前开始移动鼠标,它就不会在你拖动时绘制线条,一旦你松开鼠标按钮,就会出现奇怪的跳跃行为。 - Aran-Fey
2
如果我删除setrecursionlimit行,则此代码偶尔会产生重复进入movearound的回溯。我怀疑这是因为movearound调用gotogoto调用updateupdate检查鼠标更新并可能再次调用movearound。如果Tkinter不总是按接收顺序评估鼠标事件,则可能解释海龟的抖动移动。http://effbot.org/tkinterbook/widget.htm说在回调中使用`update()`可能会导致“恶性竞争条件”,这似乎正是这里发生的情况。 - Kevin
因为我没有一个快速的解决方案可以解决这个问题,所以将上述评论发布为评论而不是答案;最好的方法就是简单地不要在绑定到画布的函数内调用goto()。但如果他不这样做,那么OP该如何实现他想要的行为呢? - Kevin
添加 self.tracer(2,0) 似乎解决了你的问题! - TwistedSim
3个回答

5
我看到您的代码存在几个问题:
  • 您将面向对象接口和函数式接口混合使用turtle模块。我建议您只选择一种,而不是两种接口都使用。请参见我的import更改,以强制仅使用面向对象编程。

  • 您正在使用低级tkinter鼠标和键盘事件,而非turtle自己的事件处理方法。我建议您尝试在turtle层面上进行操作(尽管与您的实现相比存在一些小问题,请参见下文)。

  • 在事件处理程序中未关闭事件处理可能会导致意外的递归。在那些需要大量时间的事件处理程序中关闭事件可以清理图形界面。

这是我根据上述要求重构后的代码。唯一的问题是,与原始版本不同,“移动海龟到此处”和“开始拖曳”需要两次点击,一次是屏幕点击将海龟移动到当前位置,一次是点击海龟开始拖曳。这是由于turtle提供给tkinter事件的接口有所不同。(Python 3在这方面略有改进,但对于此情况并不适用。)

为了解决这一问题,我使用了一个更大的海龟光标,并添加了朝向逻辑:

from turtle import Turtle, Screen, mainloop

WIDTH = 600
HEIGHT = 300

def gothere(x, y):
    screen.onscreenclick(gothere)  # disable events inside handler

    turtle.penup()
    print("gothere (%d,%d)" % (x, y))
    turtle.goto(x, y)
    turtle.pendown()

    screen.onscreenclick(gothere)

def movearound(x, y):
    turtle.ondrag(None)  # disable events inside handler

    turtle.setheading(turtle.towards(x, y))
    print("movearound (%d,%d)" % (x, y))
    turtle.goto(x, y)

    turtle.ondrag(movearound)

def release(x, y):
    print("release (%d,%d)" % (x, y))
    turtle.penup()

def reset():
    print("reset")
    turtle.clear()

screen = Screen()
screen.setup(WIDTH, HEIGHT)
# screen.setworldcoordinates(0, HEIGHT, WIDTH, 0)  # should work fine either way

turtle = Turtle('turtle')
turtle.speed('fastest')

turtle.ondrag(movearound)
turtle.onrelease(release)

screen.onscreenclick(gothere)
screen.onkey(reset, "Escape")

screen.listen()

mainloop()  # normally screen.mainloop() but not in Python 2

但是请参见这个答案,我展示了如何使tkinter的onmove事件可用于turtle。

...“移动到此处”然后“开始拖动”的限制对用户来说非常不舒适?我们该如何改进?

结合我上面的代码和我链接的替代答案,我们得到了这个解决方案,它类似于您开始的方式,但没有故障,并且以更符合海龟风格的方式呈现:
from turtle import Turtle, Screen, mainloop
from functools import partial

WIDTH = 600
HEIGHT = 300

VERBOSE = False

def onscreenmove(self, fun, btn=1, add=None):  # method missing from turtle.py

    if fun is None:
        self.cv.unbind('<Button%s-Motion>' % btn)
    else:
        def eventfun(event):
            fun(self.cv.canvasx(event.x) / self.xscale, -self.cv.canvasy(event.y) / self.yscale)

        self.cv.bind('<Button%s-Motion>' % btn, eventfun, add)

def onscreenrelease(self, fun, btn=1, add=None):  # method missing from turtle.py

    if fun is None:
        self.cv.unbind("<Button%s-ButtonRelease>" % btn)
    else:
        def eventfun(event):
            fun(self.cv.canvasx(event.x) / self.xscale, -self.cv.canvasy(event.y) / self.yscale)

        self.cv.bind("<Button%s-ButtonRelease>" % btn, eventfun, add)

def gothere(x, y):

    if VERBOSE:
        print("gothere (%d,%d)" % (x, y))

    turtle.penup()
    turtle.goto(x, y)
    turtle.pendown()

def movearound(x, y):

    screen.onscreenmove(None)  # disable events inside handler

    if VERBOSE:
        print("movearound (%d,%d)" % (x, y))


    turtle.setheading(turtle.towards(x, y))
    turtle.goto(x, y)

    screen.onscreenmove(movearound)  # reenable events

def release(x, y):

    if VERBOSE:
        print("release (%d,%d)" % (x, y))

    turtle.penup()

def reset():

    if VERBOSE:
        print("reset")

    turtle.clear()

screen = Screen()
screen.setup(WIDTH, HEIGHT)
screen.onscreenrelease = partial(onscreenrelease, screen)  # install missing methods
screen.onscreenmove = partial(onscreenmove, screen)

turtle = Turtle('turtle')
turtle.speed('fastest')

screen.onscreenclick(gothere)
screen.onscreenrelease(release)
screen.onscreenmove(movearound)

screen.onkey(reset, "Escape")
screen.listen()

mainloop()  # normally screen.mainloop() but not in Python 2

非常好,但“移动到此处”然后“开始拖动”的限制对用户而言非常不舒适。我们该如何改进呢? - lucky1928
@lucky1928,我通过重新编写代码来增强我的答案,添加了一些缺失的tkinter事件到turtle中,以获得您所需的交互式样式。 - cdlane

4

我同意cdlane的看法,尽可能避免使用Tkinter级别的事件绑定,但我认为不改变整体设计也可以解决问题。我的解决方案只需要增加一个导入:

from collections import deque

还有一个新版本的movearound

pending = deque()
def movearound(event):
    x = event.x
    y = event.y
    pending.append((x,y))
    if len(pending) == 1:
        while pending:
            x,y = pending[0]
            turtle.goto(x,y)
            pending.popleft()

正如我在评论中所指出的,如果窗口的事件队列被阻塞,goto可以调用movearound,这可能会导致堆栈溢出或竞争条件,使海龟以不寻常的方式移动。这种方法旨在通过只让最顶层的movearound实例调用goto来防止任意深度的递归。当且仅当非递归调用时,if len(pending) == 1:才能成功;所有递归调用都将看到一个比它们更大的队列。while pending:循环按照它们到达的顺序处理所有已构建的事件。

结果:一只忠实地跟随光标路径的海龟,尽管它以自己的乌龟步速:

enter image description here


1
谢谢,太棒了。你能否在下面发布完整的代码? - lucky1928

0

添加turtle.tracer(2,0)似乎使问题消失了。这可能是一个临时解决方案,因为我不知道它是否只是将问题隐藏在另一个问题后面。

...
screen = turtle.Screen()
screen.setworldcoordinates(0,height,width,0)
screen.listen()
turtle.tracer(2, 0)  # This line

turtle.mainloop()
...

在文档中我们可以读到:
“打开/关闭海龟动画并设置更新绘图的延迟时间。”
正如@Keven所指出的,问题似乎是回调函数中隐含的“update”。这将减少2个“update”调用的数量。我认为它也可能会消除递归调用。
注意:我不知道为什么,但删除“screen.setworldcoordinates(0,height,width,0)”会消除故障行为,但递归调用“update”仍然存在。

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