pythontkintertkinter-canvaspython-turtle

how to fix the undo function for a paint app made using turtle and tkinter


i am making a paint app in python. it has functions for making shapes and drawing using click and drag.

i've been trying to implement an undo button to my app. it work fine to undo the shapes but not so well to undo the click and drag lines.

my function :

def undo ():
    if la[len(la)-1] =='s' : 
        turtle.undo()
        del(la[len(la)-1])
        while(turtle.isdown()==True) :
            turtle.undo()
        turtle.undo()
        turtle.undo()
    if la[len(la)-1]=='d' :
        turtle.undo()
        del(la[len(la)-1])
        while turtle.xcor()!=xp[len(xp)-1] and turtle.ycor()!=yp[len(yp)-1] :
            turtle.undo()
        del(xp[len(xp)-1])
        del(yp[len(yp)-1])

la is an array that memorizes whether the last action was drawing a line or a shape.

if its a shape ('s') it undoes while the pen is down ( since the shapes are made using turtle action the pen doesn't lift between those action ).

if its a line ('d') it should undo until the coords of the turtle are the same as the coords of the beginning of the line ( memorized in xp and yp ).

well sometimes it undoes more than it should and sometimes it doesn't undo at all. the thing is that most times it works perfectly so i don't know what causes the problems. most frequently it has problems when there's a shape before the line, it undoes part of the shape too. and of course if you cross the begging of the line multiple times it stops undoing but that can be fixed by counting how many times you crossed it.

edit :

minimal working program :

from tkinter import *
from functools import partial
from turtle import TurtleScreen, RawTurtle, Shape


menu = Tk()

frame=Frame(menu, bd=1, bg='#d1c7c7')
frame.grid(row=1, column=6, rowspan=26 )
canvas=Canvas(frame,width=1000, height=700)
canvas.grid(column=6,row=1, rowspan=26)

size=5

la=['n']
xp=[]
yp=[]

# turtle actions
def draw(x, y):
    turtle.ondrag(None)
    turtle.down()
    turtle.goto(x, y)
    turtle.up()
    screen.update()
    turtle.ondrag(draw)
def move(x, y):
    global xp
    global yp
    global la
    xp.append(turtle.xcor())
    yp.append(turtle.ycor())
    la.append('d') 
    screen.onscreenclick(None)
    turtle.goto(x, y)
    screen.onclick(move)
    screen.update()
def main():
    turtle.shape("circle")
    polygon = turtle.get_shapepoly()
    fixed_color_turtle = Shape("compound")
    fixed_color_turtle.addcomponent(polygon, "", "")
    screen.register_shape('fixed', fixed_color_turtle)
    turtle.shape("fixed")
    turtle.penup()
    turtle.pensize(5)
    turtle.turtlesize(2000,2000)
    turtle.ondrag(draw)
    screen.onscreenclick(move)
    screen.update()

def setcolor (color) :
    turtle.pencolor(color)
    turtle.fillcolor(color)




bblack = Button(
    menu,
    bg='black',
    width=10,
    command=partial(setcolor,'black')
).grid(column=1, row=2)
bred = Button(
    menu,
    bg='red',
    width=10,
    command=partial(setcolor,'red')
).grid(column=1, row=3)

bdraw = Button(
    menu,
    text='pen',
    width=10,
    command=main
).grid(column=1, row=4)

def square(x, y):
    global la
    la.append('s')
    turtle.turtlesize(1,1)
    screen.onclick(None)
    turtle.ondrag(None)
    turtle.goto(x-size*8.3, y-size*8.3)
    turtle.pendown()
    turtle.begin_fill()
    for i in range(4):
        turtle.forward(size*18)
        turtle.left(360 / 4)
    turtle.end_fill()
    turtle.penup()
    possqr()
def possqr():
    screen.onclick(square)
bsquare = Button(
    menu,
    text='square',
    width=10,
    command=possqr
).grid(column=1, row=5)
def triangle(x, y):
    global la
    la.append('s')
    turtle.turtlesize(1,1)
    screen.onclick(None)
    turtle.ondrag(None)
    turtle.goto(x-size*8.5, y-size*6)
    turtle.pendown()
    turtle.begin_fill()
    for i in range(3):
        turtle.forward(size*18)
        turtle.left(360 / 3)
    turtle.end_fill()
    turtle.penup()
    postriangle()
def postriangle():
    screen.onclick(triangle)
btriangle = Button(
    menu,
    text='triangle',
    width=10,
    command=postriangle
).grid(column=1, row=6)

Label(menu, text='COLORS').grid(column=1, row=1)



def undo ():
    if la[len(la)-1] =='s' : 
        turtle.undo()
        del(la[len(la)-1])
        while(turtle.isdown()==True) :
            turtle.undo()
        turtle.undo()
        turtle.undo()
    if la[len(la)-1]=='d' :
        turtle.undo()
        del(la[len(la)-1])
        while turtle.xcor()!=xp[len(xp)-1] and turtle.ycor()!=yp[len(yp)-1] :
            turtle.undo()
        del(xp[len(xp)-1])
        del(yp[len(yp)-1])
bundo = Button(
    menu,
    text='undo',
    width=10,
    command=undo
).grid(column=2, row=1)

screen = TurtleScreen(canvas)
screen.tracer(False)

pen_color = 'black'

turtle = RawTurtle(screen)
main()
mainloop()

Solution

  • There is one issue in your code you can fix to resolve the flaw in the logic of the undo() function:

        elif la[len(la)-1]=='d' : ### <<< ###
    
    

    With a single if you risk that both of the if blocks are executed on one undo() action which results then in unexpected behavior.

    To improve the behavior with not proper updated graphics you can add:

        screen.update()
        screen.getcanvas().update_idletasks()
    

    at the end of the undo() function.

    This said it needs to be mentioned that the turtle module is designed for simplicity, so the reliability of updating the graphics after undo() operations may in some complex scenarios lead to not properly updated screen.

    Please notice, that the default limit of undo operations in case of freehand drawing or adding a huge amount of squares and triangles can result in inability to remove all of what is seen on the screen.

    In case of RawTurtle you need to set a "private" variable to a higher value if you want higher amount of undo levels than the default one which is easily exhausted in such drawing scenario to for example:

    turtle._undobuffersize=1_000_000
    

    You may also try to replace the tkinter part with turtle based buttons where you click on turtles which you use as buttons. This way you reduce the complexity of the screen update which may result in more reliable updates of the entire graphics after undo actions.