pythoncanvastkinterzoomingpanning

Removing Effect Of tkinter canvas.scan_mark on mouse coordinates


I'm developing a GUI with python using tkinter. I've written the code for providing zooming functionalities such as "zoom in", "zoom out", "window zoom", "previous zoom", "restore full zoom" and, finally, "pan". For 'pan' command I used canvas.scan_mark() and canvas.scan_dragto() methods. All the mentioned commands work fine except that when I press 'pan' and then 'window zoom' or try to draw selection rectangle, where I find, unexpectedly, that the window zoom or selection rectangles are shifted from the current mouse position by the shift value of pan command, although I unbound all the mouse events at the end of the 'pan' command. I tried something like xi, yi = canvas.xview()[0], canvas.yview()[0] and then canvas.xview_moveto(xi), canvas.yview_moveto(yi). It gets the canvas view back to its orginal position but has not solved the problem. In addition, it disturbs the 'Restore full zoom' and 'zoom previous' commands. Please help me fixing this problem up.

Many thanks.

For short description of the problem, please see the code below. It's not what I'm using in my program, but it describe the problem. Try to press the zoom button first and pan. Give it another trial by pressing pan button first followed by zoom one and see the difference.

from tkinter import *

root = Tk()
root.resizable(False, False)
frame = Frame(root)
frame.pack(expand=YES, fill=BOTH)

canv = Canvas(frame, bg='white', width=800, height=600)
canv.pack(side=TOP, expand=YES, fill=BOTH)
canv.create_rectangle(100,100,200,200, fill='red', width=3)
canv.create_oval(250,250,450,450, fill='blue', width=3)

canv.create_line(500,500,500,500, fill='white')

def pan():
    canv.bind('<Button-1>', startpan)
    canv.bind('<B1-Motion>', dragpan)
    canv.bind('<ButtonRelease-1>', endpan)
    canv.config(cursor='hand1')

def startpan(event):
    canv.scan_mark(event.x, event.y)

def dragpan(event):
    canv.scan_dragto(event.x, event.y, 1)

def endpan(event):
    unbind_events()

def unbind_events():
    canv.unbind('<Button-1>')
    canv.unbind('<B1-Motion>')
    canv.unbind('<ButtonRelease-1>')
    canv.config(cursor='arrow')

def zoom_window():
    canv.bind('<Button-1>', startzoomwindow)
    canv.bind('<B1-Motion>', dragzoomwindow)
    canv.bind('<ButtonRelease-1>', endzoomwindow)

def startzoomwindow(event):
    global x1, y1
    x1, y1 = event.x, event.y

def dragzoomwindow(event):
    global rect
    x2, y2 = event.x, event.y
    rect = canv.create_rectangle(x1, y1, x2, y2, width=2, outline='red')
    canv.delete(canv.find_below(rect))

def endzoomwindow(event):
    canv.delete(rect)
    x, y =  0.5 * (x1 + event.x), 0.5 * (y1 + event.y)
    rect_width = abs(event.x - x1)
    rect_height = abs(event.y - y1)
    canvwidth = canv.winfo_width() 
    canvheight = canv.winfo_height()
    factor = min(canvwidth / rect_width, canvheight / rect_height)
    canv.scale(ALL, x, y, factor, factor)
    unbind_events()

butnframe = Frame(frame)
butnframe.pack(side=TOP, expand=YES, fill=X)
Button(butnframe, text='Zoom Window', command=zoom_window).pack(side=LEFT)
Button(butnframe, text='Pan', command=pan).pack(side=RIGHT)

Solution

  • When you bind to mouse events, the coordinates that are reported are relative to the window. If you've scrolled the canvas, the mouse clicks will be off by the amount that you have scrolled and/or zoomed.

    To get around this you need to convert the window coordinate to the canvas coordinate. You do this with the canvasx and canvasy methods of the canvas:

    import tkinter as tk
    ...
    canvas = tk.Canvas(...)
    canvas.bind('<3>', do_something)
    ...
    
    def do_something(event):
        x = canvas.canvasx(event.x)
        y = canvas.canvasy(event.y)
        ...