有关turtle的相关使用请参考《python图形绘制库turtle中文开发文档及示例大全》
本篇文为turtle库的实现剖析,但不涉及 python 的 TK库。
开始
入口探寻
在turtle中,直走是使用 forward 或者 fd 函数;在本机安装好了 turtle 库后,在以下的目录下找到了 turtle.py 文件:
我们先从常规的方式从入口开始探究turtle库的基本实现;新建一个turtle对象:
tt=Turtle()
在文件中找到 class Turtle:
class Turtle(RawTurtle):"""RawTurtle auto-creating (scrolled) canvas.When a Turtle object is created or a function derived from someTurtle method is called a TurtleScreen object is automatically created."""_pen = None_screen = Nonedef __init__(self,shape=_CFG["shape"],undobuffersize=_CFG["undobuffersize"],visible=_CFG["visible"]):if Turtle._screen is None:Turtle._screen = Screen()RawTurtle.__init__(self, Turtle._screen,shape=shape,undobuffersize=undobuffersize,visible=visible)
从注释中可以的到此类将会自动创建 TurtleScreen
对象以及 canvas
,这一点在 __init__
方法中有代码过程;之后调用了 RawTurtle
的 __init__
创建 turtle的动画部分,实现如下:
screens = []def __init__(self, canvas=None,shape=_CFG["shape"],undobuffersize=_CFG["undobuffersize"],visible=_CFG["visible"]):if isinstance(canvas, _Screen):self.screen = canvaselif isinstance(canvas, TurtleScreen):if canvas not in RawTurtle.screens:RawTurtle.screens.append(canvas)self.screen = canvaselif isinstance(canvas, (ScrolledCanvas, Canvas)):for screen in RawTurtle.screens:if screen.cv == canvas:self.screen = screenbreakelse:self.screen = TurtleScreen(canvas)RawTurtle.screens.append(self.screen)else:raise TurtleGraphicsError("bad canvas argument %s" % canvas)screen = self.screenTNavigator.__init__(self, screen.mode())TPen.__init__(self)screen._turtles.append(self)self.drawingLineItem = screen._createline()self.turtle = _TurtleImage(screen, shape)self._poly = Noneself._creatingPoly = Falseself._fillitem = self._fillpath = Noneself._shown = visibleself._hidden_from_screen = Falseself.currentLineItem = screen._createline()self.currentLine = [self._position]self.items = [self.currentLineItem]self.stampItems = []self._undobuffersize = undobuffersizeself.undobuffer = Tbuffer(undobuffersize)self._update()
创建完一个turtle对象后,调用一下 forward 函数画一根线段。
我们打开 turtle 文件,按照一般形式的函数定义,查询 forward 函数的定义:
从注释中了解到,调用函数可以使用 forward | fd ,参数为传入一个距离;具体使用方法请参考文章头标注的文章,在这里并不做太多解释。
在 forward 函数底部,发现调用了 _go 方法:self._go(distance)
。查看 _go 方法:
def _go(self, distance):"""move turtle forward by specified distance"""ende = self._position + self._orient * distanceself._goto(ende)
在 _go 方法中,传入了 距离,并且 ende 赋值为 self._position + self._orient * distance
,先搞懂 _position 、_orient 、distance 这几个成员是什么东西。
_go 方法位于 TNavigator 类中,在 TNavigator 的 init 方法中,使用了 reset 方法,reset方法中有 _position 、_orient 的初始化:
def reset(self):"""reset turtle to its initial valuesWill be overwritten by parent class"""self._position = Vec2D(0.0, 0.0)self._orient = TNavigator.START_ORIENTATION[self._mode]
我们再查看 Vec2D :
class Vec2D(tuple):"""A 2 dimensional vector class, used as a helper classfor implementing turtle graphics.May be useful for turtle graphics programs also.Derived from tuple, so a vector is a tuple!Provides (for a, b vectors, k number):a+b vector additiona-b vector subtractiona*b inner productk*a and a*k multiplication with scalar|a| absolute value of aa.rotate(angle) rotation"""def __new__(cls, x, y):return tuple.__new__(cls, (x, y))def __add__(self, other):return Vec2D(self[0]+other[0], self[1]+other[1])def __mul__(self, other):if isinstance(other, Vec2D):return self[0]*other[0]+self[1]*other[1]return Vec2D(self[0]*other, self[1]*other)def __rmul__(self, other):if isinstance(other, int) or isinstance(other, float):return Vec2D(self[0]*other, self[1]*other)def __sub__(self, other):return Vec2D(self[0]-other[0], self[1]-other[1])def __neg__(self):return Vec2D(-self[0], -self[1])def __abs__(self):return (self[0]**2 + self[1]**2)**0.5def rotate(self, angle):"""rotate self counterclockwise by angle"""perp = Vec2D(-self[1], self[0])angle = angle * math.pi / 180.0c, s = math.cos(angle), math.sin(angle)return Vec2D(self[0]*c+perp[0]*s, self[1]*c+perp[1]*s)def __getnewargs__(self):return (self[0], self[1])def __repr__(self):return "(%.2f,%.2f)" % self
查看 Vec2D 后得知,其实也就是一个元组;在查看 TNavigator 类:
class TNavigator(object):"""Navigation part of the RawTurtle.Implements methods for turtle movement."""START_ORIENTATION = {"standard": Vec2D(1.0, 0.0),"world" : Vec2D(1.0, 0.0),"logo" : Vec2D(0.0, 1.0) }DEFAULT_MODE = "standard"DEFAULT_ANGLEOFFSET = 0DEFAULT_ANGLEORIENT = 1
随后查看 TNavigator.START_ORIENTATION[self._mode]
,在 TNavigator
类中得知 _mode
为 standard
。此时 TNavigator.START_ORIENTATION[self._mode]
为 Vec2D(1.0, 0.0)
。
接下来查看 _goto 方法:
def _goto(self, end):"""Move the pen to the point end, thereby drawing a lineif pen is down. All other methods for turtle movement dependon this one."""## Version with undo-stuffgo_modes = ( self._drawing,self._pencolor,self._pensize,isinstance(self._fillpath, list))screen = self.screenundo_entry = ("go", self._position, end, go_modes,(self.currentLineItem,self.currentLine[:],screen._pointlist(self.currentLineItem),self.items[:]))if self.undobuffer:self.undobuffer.push(undo_entry)start = self._positionif self._speed and screen._tracing == 1:diff = (end-start)diffsq = (diff[0]*screen.xscale)**2 + (diff[1]*screen.yscale)**2nhops = 1+int((diffsq**0.5)/(3*(1.1**self._speed)*self._speed))delta = diff * (1.0/nhops)for n in range(1, nhops):if n == 1:top = Trueelse:top = Falseself._position = start + delta * nif self._drawing:screen._drawline(self.drawingLineItem,(start, self._position),self._pencolor, self._pensize, top)self._update()if self._drawing:screen._drawline(self.drawingLineItem, ((0, 0), (0, 0)),fill="", width=self._pensize)# Turtle now at end,if self._drawing: # now update currentLineself.currentLine.append(end)if isinstance(self._fillpath, list):self._fillpath.append(end)###### vererbung!!!!!!!!!!!!!!!!!!!!!!self._position = endif self._creatingPoly:self._poly.append(end)if len(self.currentLine) > 42: # 42! answer to the ultimate question# of life, the universe and everythingself._newLine()self._update() #count=True)
在 goto_方法中,最开头的注释说明了该方法的作用“从当前的位置移动到传入的end参数坐标点,在移动的过程中,绘制出线段,并且所有的 turtle 绘制方法都基于这个 goto_方法”。goto_方法中,开始定义了一个元组 go_modes :
go_modes = ( self._drawing,self._pencolor,self._pensize,isinstance(self._fillpath, list))
在go_modes 元组中,传入了 _drawing、_pencolor、_pensize,并且调用了 isinstance 方法判断 _fillpath 是否为 list;并且接下来构造了一个 undo_entry元组。判断 if self.undobuffer:
后,为空或者Null 则 self.undobuffer.push(undo_entry)
。之后为默认状态下的绘制方法:
if self._speed and screen._tracing == 1:diff = (end-start)diffsq = (diff[0]*screen.xscale)**2 + (diff[1]*screen.yscale)**2nhops = 1+int((diffsq**0.5)/(3*(1.1**self._speed)*self._speed))delta = diff * (1.0/nhops)for n in range(1, nhops):if n == 1:top = Trueelse:top = Falseself._position = start + delta * nif self._drawing:screen._drawline(self.drawingLineItem,(start, self._position),self._pencolor, self._pensize, top)self._update()if self._drawing:screen._drawline(self.drawingLineItem, ((0, 0), (0, 0)),fill="", width=self._pensize)
其中 start为当前位置的坐标点,end是目标位置的坐标点,以上最主要的方法中最重要是:_drawline
,使用_drawline
传入了配置参数、坐标序列、笔颜色、绘制线的宽度以及 是否指定 polyitem
;(具体坐标序列的算法我没搞清楚,希望有知道的同学可以告诉我这是咋算的,是什么公式,谢谢!)。
查看 _drawline 的实现:
def _drawline(self, lineitem, coordlist=None,fill=None, width=None, top=False):"""Configure lineitem according to provided arguments:coordlist is sequence of coordinatesfill is drawing colorwidth is width of drawn line.top is a boolean value, which specifies if polyitemwill be put on top of the canvas' displaylist so itwill not be covered by other items."""if coordlist is not None:cl = []for x, y in coordlist:cl.append(x * self.xscale)cl.append(-y * self.yscale)self.cv.coords(lineitem, *cl)if fill is not None:self.cv.itemconfigure(lineitem, fill=fill)if width is not None:self.cv.itemconfigure(lineitem, width=width)if top:self.cv.tag_raise(lineitem)
以上文章暂未全部剖析实现,之后将会更新。