python乌龟怪异的光标跳转
python turtle weird cursor jump
我正在尝试使用鼠标绘制海龟,我得到了下面的演示代码,但在鼠标移动期间有时光标会跳转:
#!/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 调用错误!
添加 turtle.tracer(2,0)
似乎可以使问题消失。这可能是一个临时解决方案,因为我不知道它是否只是将问题隐藏在另一个问题之后。
...
screen = turtle.Screen()
screen.setworldcoordinates(0,height,width,0)
screen.listen()
turtle.tracer(2, 0) # This line
turtle.mainloop()
...
在文档中我们可以读到:
Turns turtle animation on/off and set delay for update drawings.
正如@Keven 指出的那样,问题似乎是回调中的隐式 update
。这减少了 2 次 update
调用。我认为它也可以删除递归调用。
注意:我不知道为什么,但删除 screen.setworldcoordinates(0,height,width,0)
会消除故障行为,但对 update
的递归调用仍然存在。
我发现您的代码有几个问题:
您将 object-oriented 接口与 turtle 混合使用
该模块的功能接口。我推荐一个或
其他,但不是两者。看我的 import
改成 force OOP-only.
您正在使用低级别的 tkinter 鼠标和按键事件,而不是
乌龟自己的事件。我建议你尝试在乌龟级别工作
(尽管与您的实现相比这引入了一个小故障,请参阅
下面。)
您通过不关闭事件引入了意外的递归
在你的事件处理程序中。在这些处理程序中禁用事件
花费大量时间将清理您的图形。
这是我按照上述几行对您的代码进行的修改。一个小问题是,与您原来的不同,"move turtle here" 和 "start dragging" 将需要两次点击,一次屏幕点击将乌龟移动到当前位置,一次点击乌龟开始拖动。这是由于 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
但也请参阅 this answer,其中我展示了如何使 tkinter 的 onmove
事件对 turtle 可用。
... the "move here" then "start drag" limitation is very not
comfortable for user? How can we improve that?
将我上面的代码与我链接到的替代答案结合起来,我们得到了这个解决方案,它与您开始的地方类似,但没有故障,而且更 turtle-like 风格:
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
我同意 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()
正如我在评论中指出的那样,如果备份 window 的事件队列,goto
可以调用 movearound
,这可能会导致堆栈溢出或竞争条件让乌龟以不寻常的方式移动。这种方法旨在通过仅让 movearound
的最顶层实例调用 goto
来防止任意深度递归。 if len(pending) == 1:
应该只对非递归调用成功;所有递归调用都会看到一个比这更大的队列。 while pending:
循环遍历所有构建的事件,按它们到达的顺序处理它们。
结果:一只乌龟忠实地跟随光标的路径,尽管是以它自己的乌龟速度:
我正在尝试使用鼠标绘制海龟,我得到了下面的演示代码,但在鼠标移动期间有时光标会跳转:
#!/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 调用错误!
添加 turtle.tracer(2,0)
似乎可以使问题消失。这可能是一个临时解决方案,因为我不知道它是否只是将问题隐藏在另一个问题之后。
...
screen = turtle.Screen()
screen.setworldcoordinates(0,height,width,0)
screen.listen()
turtle.tracer(2, 0) # This line
turtle.mainloop()
...
在文档中我们可以读到:
Turns turtle animation on/off and set delay for update drawings.
正如@Keven 指出的那样,问题似乎是回调中的隐式 update
。这减少了 2 次 update
调用。我认为它也可以删除递归调用。
注意:我不知道为什么,但删除 screen.setworldcoordinates(0,height,width,0)
会消除故障行为,但对 update
的递归调用仍然存在。
我发现您的代码有几个问题:
您将 object-oriented 接口与 turtle 混合使用 该模块的功能接口。我推荐一个或 其他,但不是两者。看我的
import
改成 force OOP-only.您正在使用低级别的 tkinter 鼠标和按键事件,而不是 乌龟自己的事件。我建议你尝试在乌龟级别工作 (尽管与您的实现相比这引入了一个小故障,请参阅 下面。)
您通过不关闭事件引入了意外的递归 在你的事件处理程序中。在这些处理程序中禁用事件 花费大量时间将清理您的图形。
这是我按照上述几行对您的代码进行的修改。一个小问题是,与您原来的不同,"move turtle here" 和 "start dragging" 将需要两次点击,一次屏幕点击将乌龟移动到当前位置,一次点击乌龟开始拖动。这是由于 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
但也请参阅 this answer,其中我展示了如何使 tkinter 的 onmove
事件对 turtle 可用。
... the "move here" then "start drag" limitation is very not comfortable for user? How can we improve that?
将我上面的代码与我链接到的替代答案结合起来,我们得到了这个解决方案,它与您开始的地方类似,但没有故障,而且更 turtle-like 风格:
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
我同意 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()
正如我在评论中指出的那样,如果备份 window 的事件队列,goto
可以调用 movearound
,这可能会导致堆栈溢出或竞争条件让乌龟以不寻常的方式移动。这种方法旨在通过仅让 movearound
的最顶层实例调用 goto
来防止任意深度递归。 if len(pending) == 1:
应该只对非递归调用成功;所有递归调用都会看到一个比这更大的队列。 while pending:
循环遍历所有构建的事件,按它们到达的顺序处理它们。
结果:一只乌龟忠实地跟随光标的路径,尽管是以它自己的乌龟速度: