pythontkinterpolyline

Tkinter - Moving the dots on a polyline with three path


Based on a simple Line drawn using canvas.create_line, such as this image here, three dots, based on the start, middle, and end of the line are created, example here.

I want to be able to click and drag the part of the Line where there is a dot, so I made the following MRE:

import tkinter as tk

root = tk.Tk()

canvas = tk.Canvas(root, width=400, height=400)
canvas.pack()

line = canvas.create_line(50, 50, 350, 50, width=3)

start_x, start_y, end_x, end_y = canvas.coords(line)

start_dot = canvas.create_oval(start_x - 5, start_y - 5, start_x + 5, start_y + 5, fill='blue', tags=('start_dot',))
end_dot = canvas.create_oval(end_x - 5, end_y - 5, end_x + 5, end_y + 5, fill='red', tags=('end_dot',))

mid_x = (start_x + end_x) / 2
mid_y = start_y

mid_dot = canvas.create_oval(mid_x - 5, mid_y - 5, mid_x + 5, mid_y + 5, fill='green', tags=('mid_dot',))


def update_line(event):
    global end_x, end_y, start_y, start_x, mid_y, mid_x, curve
    x, y = event.x, event.y
    item = canvas.find_withtag(tk.CURRENT)[0]
    if item == start_dot:
        canvas.coords(start_dot, x - 5, y - 5, x + 5, y + 5)
        canvas.coords(line, x, y, end_x, end_y)
    elif item == end_dot:
        canvas.coords(end_dot, x - 5, y - 5, x + 5, y + 5)
        canvas.coords(line, start_x, start_y, x, y)
    elif item == mid_dot:
        dx = x - (start_x + end_x) // 2
        dy = y - (start_y + end_y) // 2
        canvas.coords(mid_dot, mid_x + dx - 5, mid_y + dy - 5, mid_x + dx + 5, mid_y + dy + 5)
        canvas.coords(line, start_x, start_y, mid_x + dx, mid_y + dy, end_x, end_y)
    else:
        return

    test = canvas.coords(line)
    if len(test) == 4:
        start_x, start_y, end_x, end_y, = canvas.coords(line)
        mid_x = (start_x + end_x) / 2
        mid_y = (start_y + end_y) / 2
        canvas.coords(mid_dot, mid_x - 5, mid_y - 5, mid_x + 5, mid_y + 5)
    else:
        pass


canvas.tag_bind('start_dot', '<B1-Motion>', update_line)
canvas.tag_bind('end_dot', '<B1-Motion>', update_line)
canvas.tag_bind('mid_dot', '<B1-Motion>', update_line)

root.mainloop()

This somewhat works, at least for only moving the start and end correctly. The part that doesn't work yet is updating the Line correctly after moving the middle dot and then moving either the start or end dot of the Line. Here is a gif showcasing the problem, and here the expected behavior (the rectangle can be ignored).

I noticed canvas.coords output 5 values instead of the usual 4 that I'm used to.

How can I make the above works with the middle path/dot too?

P.S.:I use the term "polyline" but I'm not sure if this is what I'm actually doing here (it does look similar when looking on google image). Feel free to mention a better fitting term for this if there is one.


Solution

  • Thanks to jasonharper's comment, I managed to find a way to solve this:

    Your life will be so much easier if you draw the line with three points always - rather than having to special-case having two or three points based on whether the middle point had ever been moved (which is what it would take to fix your current approach). – jasonharper Apr 8 at 3:11

    First a solution that does not directly follow the wanted behavior (works for all three path/dot, but does not move the entire line when using only start and end dot):

    import tkinter as tk
    
    root = tk.Tk()
    
    canvas = tk.Canvas(root, width=400, height=400)
    canvas.pack()
    
    line = canvas.create_line(50, 50, 350, 50, width=3)
    
    start_x, start_y, end_x, end_y = canvas.coords(line)
    
    start_dot = canvas.create_oval(start_x - 5, start_y - 5, start_x + 5, start_y + 5, fill='blue', tags=('start_dot',))
    end_dot = canvas.create_oval(end_x - 5, end_y - 5, end_x + 5, end_y + 5, fill='red', tags=('end_dot',))
    
    mid_x = (start_x + end_x) / 2
    mid_y = start_y
    
    mid_dot = canvas.create_oval(mid_x - 5, mid_y - 5, mid_x + 5, mid_y + 5, fill='green', tags=('mid_dot',))
    
    def update_line(event):
        global start_x, start_y, end_x, end_y, mid_x, mid_y
    
        x, y = event.x, event.y
        item = canvas.find_withtag(tk.CURRENT)[0]
    
        if item == start_dot:
            start_x, start_y = x, y
        elif item == end_dot:
            end_x, end_y = x, y
        elif item == mid_dot:
            mid_x, mid_y = x, y
    
        canvas.coords(line, start_x, start_y, mid_x, mid_y, end_x, end_y)
    
        canvas.coords(start_dot, start_x - 5, start_y - 5, start_x + 5, start_y + 5)
        canvas.coords(end_dot, end_x - 5, end_y - 5, end_x + 5, end_y + 5)
        canvas.coords(mid_dot, mid_x - 5, mid_y - 5, mid_x + 5, mid_y + 5)
    
    canvas.tag_bind('start_dot', '<B1-Motion>', update_line)
    canvas.tag_bind('end_dot', '<B1-Motion>', update_line)
    canvas.tag_bind('mid_dot', '<B1-Motion>', update_line)
    
    root.mainloop()
    

    And now, a solution that both move the entire line at first (only using start and end dots), and then move it partially after the middle dot have been used at least once:

    import tkinter as tk
    
    root = tk.Tk()
    
    canvas = tk.Canvas(root, width=400, height=400)
    canvas.pack()
    
    line = canvas.create_line(50, 50, 350, 50, width=3)
    
    start_x, start_y, end_x, end_y = canvas.coords(line)
    
    start_dot = canvas.create_oval(start_x - 5, start_y - 5, start_x + 5, start_y + 5, fill='blue', tags=('start_dot',))
    end_dot = canvas.create_oval(end_x - 5, end_y - 5, end_x + 5, end_y + 5, fill='red', tags=('end_dot',))
    
    mid_x = (start_x + end_x) / 2
    mid_y = start_y
    
    mid_dot = canvas.create_oval(mid_x - 5, mid_y - 5, mid_x + 5, mid_y + 5, fill='green', tags=('mid_dot',))
    has_middle_dot_moved = False
    
    
    def update_line(event):
        global end_x, end_y, start_y, start_x, mid_y, mid_x, has_middle_dot_moved
        x, y = event.x, event.y
        item = canvas.find_withtag(tk.CURRENT)[0]
        if not has_middle_dot_moved:
            if item == start_dot:
                canvas.coords(start_dot, x - 5, y - 5, x + 5, y + 5)
                canvas.coords(line, x, y, end_x, end_y)
            elif item == end_dot:
                canvas.coords(end_dot, x - 5, y - 5, x + 5, y + 5)
                canvas.coords(line, start_x, start_y, x, y)
            elif item == mid_dot:
                has_middle_dot_moved = True
            start_x, start_y, end_x, end_y, = canvas.coords(line)
            mid_x = (start_x + end_x) / 2
            mid_y = (start_y + end_y) / 2
            canvas.coords(mid_dot, mid_x - 5, mid_y - 5, mid_x + 5, mid_y + 5)
        else:
            if item == start_dot:
                start_x, start_y = x, y
            elif item == end_dot:
                end_x, end_y = x, y
            elif item == mid_dot:
                mid_x, mid_y = x, y
    
            canvas.coords(line, start_x, start_y, mid_x, mid_y, end_x, end_y)
    
            canvas.coords(start_dot, start_x - 5, start_y - 5, start_x + 5, start_y + 5)
            canvas.coords(end_dot, end_x - 5, end_y - 5, end_x + 5, end_y + 5)
            canvas.coords(mid_dot, mid_x - 5, mid_y - 5, mid_x + 5, mid_y + 5)
    
    
    canvas.tag_bind('start_dot', '<B1-Motion>', update_line)
    canvas.tag_bind('end_dot', '<B1-Motion>', update_line)
    canvas.tag_bind('mid_dot', '<B1-Motion>', update_line)
    
    root.mainloop()