pythontkintercanvastkinter-canvaspan

When I scan_dragto to pan my canvas, all canvas items "move" properly but their coordinates don't change


Keep in mind that by no means would I refer to myself as a programmer of any sort so please go easy on me and explain it as if I were a 5-year old.

I would like to be able to pan the viewport(canvas) and have the canvas items follow AND also be able to move each one of them separately. When creating the two example rectangles, I give them their default coordinates, if I click on them and drag them, they move properly.

After I pan the canvas and I try to click on the rectangles, despite them looking as though they moved properly, I cannot click on them BUT can still get a registered click on them if I click at the position they were before the pan.

from tkinter import *

def make():
    #makes a rectangle(for now)
    global lbl,lbl2
    lbl2=canvas.create_rectangle(50,30,70,50,fill="white",tags="tag")
    lbl=canvas.create_rectangle(20,0,40,20,fill="red",tags="tag")


def click_on_nodes(event):
    #start position of a rectangle when we click to move it(found online)
    global selected
    selected = canvas.find_overlapping(event.x-5, event.y-5, event.x+5, event.y+5)
    if selected:
        canvas.tag_raise(selected)
        canvas.selected = selected[-1]  # select the top-most item
        canvas.startxy = (event.x, event.y)
        print(canvas.selected, canvas.startxy)
    else:
        canvas.selected = None
        print("none")

def drag_the_node(event):
    #position of rectangle while dragging it
    if selected:
        print(f"coords are: {canvas.coords(lbl)}")

        # calculate distance moved from last position
        dx, dy = event.x-canvas.startxy[0], event.y-canvas.startxy[1]
        # move the selected item
        canvas.move(canvas.selected, dx, dy)
        # update last position
        canvas.startxy = (event.x, event.y)
    else:
        canvas.selected==None

def pan_click(event):
    canvas.scan_mark(event.x,event.y)

def pan_move(event):
    canvas.scan_dragto(event.x,event.y,gain=1)
    print(f" winfox: {canvas.coords(lbl)}")

root=Tk()
canvas=Canvas(root,bg="#222222",width=500,height=500)
canvas.pack()
make()

#move the rectangles LEFT CLICK
canvas.bind("<Button-1>",click_on_nodes,add="+")
canvas.bind("<B1-Motion>",drag_the_node,add="+")

#pan the viewport RIGHT CLICK
canvas.bind("<ButtonPress-3>",pan_click)
canvas.bind("<B3-Motion>",pan_move)
canvas.bind("<ButtonRelease-3>",mouse_up)

root.mainloop()

Solution

  • Initially the origin (0, 0) of the canvas is at top-left corner of the view port , therefore event.x and event.y (coordinates relative to the top-left corner of the canvas) match with the real coordinates.

    However after the view port is changed by panning, the two coordinate systems are out of sync. You need to convert (event.x, event.y) to canvas coordinate system using canvas.canvasx() and canvas.canvasy():

    def click_on_nodes(event):
        #start position of a rectangle when we click to move it(found online)
        global selected
        # convert (event.x, event.y) to canvas coordinates
        x, y = canvas.canvasx(event.x), canvas.canvasy(event.y)
        selected = canvas.find_overlapping(x-5, y-5, x+5, y+5)
        if selected:
            canvas.tag_raise(selected)
            canvas.selected = selected[-1]  # select the top-most item
            canvas.startxy = (x, y)
            print(canvas.selected, canvas.startxy)
        else:
            canvas.selected = None
            print("none")
    
    def drag_the_node(event):
        #position of rectangle while dragging it
        if selected:
            print(f"coords are: {canvas.coords(lbl)}")
            # convert (event.x, event.y) to canvas coordinates
            x, y = canvas.canvasx(event.x), canvas.canvasy(event.y)
            # calculate distance moved from last position
            dx, dy = x-canvas.startxy[0], y-canvas.startxy[1]
            # move the selected item
            canvas.move(canvas.selected, dx, dy)
            # update last position
            canvas.startxy = (x, y)
        else:
            canvas.selected==None