Getting scale offset inside tkinter canvas

60 Views Asked by At

I am doing a drawing app using tkinter, with the very classic navigation setup : mouse click & drag moves my canvas, and mouse scroll zooms in at cursor position. It's ok for the basic implementation but there is an offset generated using canvas.scale() that I need to understand to keep everything aligned.

The moving part is ok, like so:

def bind_dragging(self):
   def do_scanmark(event):
       self.cnv.scan_mark(event.x, event.y)

    def do_dragto(event):
        """ drag to cursor pos and update offset dist"""
        self.cnv.scan_dragto(event.x, event.y, gain=1)
        GUI.cnv_dragOfst_x = self.cnv.canvasx(0) # update global var with canvas offset value
        GUI.cnv_dragOfst_y = self.cnv.canvasy(0)

    self.cnv.bind('<B1-Motion>', lambda event: do_dragto(event))
    self.cnv.bind('<ButtonPress-1>', lambda event: do_scanmark(event))

For the resizing implementation, I know I can simply do the following, in which the scale center is at the canvas (0,0). But it's not really convenient, because the point 0,0 in my situation can be quite far from where I can actually be on the canvas.

def bind_resizing(self):
    def resize(event):
        scale_factor = 1.001 ** event.delta
        self.cnv.scale(tk.ALL, 0, 0, scale_factor, scale_factor)
    self.cnv.bind("<MouseWheel>", lambda event: resize(event))

Then, I can make that center the actual position of my cursor inside the canvas (adding the offset created my the mouse drag) :

def bind_resizing(self):
    def resize(event):
        scale_factor = 1.001 ** event.delta
        x, y = event.x+GUI.cnv_dragOfst_x, event.y+GUI.cnv_dragOfst_y
        self.cnv.scale(tk.ALL, x, y, scale_factor, scale_factor)
        #GUI.cnv_resizOfst_x, GUI.cnv_resizOfst_y = ?, ?
    
    self.cnv.bind("<MouseWheel>", lambda event: resize(event))

This is working fine from the navigation point of view, but that scaling creates an offset on the canvas, which mess up where I am currently drawing.

My drawing function looks like this:

def draw_listener(self):
    def draw(self, event):
        x = event.x + GUI.cnv_dragOfst_x + GUI.cnv_resizOfst_x
        y = event.y + GUI.cnv_dragOfst_y + GUI.cnv_resizOfst_y

        # drawing new point at (x,y)
        # ....

    self.master.bind("<B1-Motion>", lambda event: draw(event))

So that's my question: does someone have any idea how to get the offset value generated from the scaling of the canvas ? (those values would be stored in the variables GUI.cnv_resizOfst_x/y) Maybe it's just a mathematic problem, I don't know...

I hope I made it clear. Please tell me if not Thank you very much for any help!

thasor

EDIT

This is a full example of what's happening. It does two things: draw a line on a page, and get the coordinates out of it. This is a much lighter version so it is easier to understand. Although what I need is to get the coordinates system reversed on the Y axis. There are 2 cases in the bind_resizing(): if I keep the scale center at canvas (0,0), i can get the proper coordinates, if I take it as the mouse position, it gives wrong ones.

from time import sleep
import tkinter as tk

class Drawing:
    """ draw lines on the tkinter canvas """

    def __init__(self, canvas):
        self.canvas = canvas 
        self.last_x = self.last_y = None

    def line(self, x, y):
        """ :x0 y0 x1 y1: in px """
        if self.last_x:
            line = self.canvas.create_line(self.last_x, self.last_y, x, y, width=2, fill="black")
        self.last_x, self.last_y = x, y

class MainWindow():

    def __init__(self, root):
        self.master = root
        self.master.geometry('%dx%d+%d+%d' % (800, 600, 0, 0))

        # init GUI variables    
        self.page_w = 3800 # (px) constant
        self.page_h = 2500 # (px) constant
        self.page_ofst_x = 0#100 # (px) constant
        self.page_ofst_y = 0#250 # (px) constant
        self.drag_ofst_x = self.drag_ofst_y = 0 # init
        # self.resiz_ofst_x = self.resiz_ofst_y = 0 # init
        self.zoom_factor = 1 # init

        # init tools
        self.tools = {
            'hand': {'shortcut':'m', 'cursor': 'fleur'},    # to navigate inside the canvas
            'pen': {'shortcut':'n', 'cursor': 'pencil'}     # to draw inside the canvas
        }
        self.curr_tool = self.last_tool = None
        
        # mouse init
        self.drag_fid = self.drag_fid2 = None # init dragging function
        self.mouseX = self.mouseY = self.rev_mouseY = self.last_mouseY = self.last_mouseX = None # mouse position

        # create canvas
        self.cnv = tk.Canvas( self.master, width=self.page_w, height=self.page_h, bg="#dddddd", bd=0, highlightthickness=0)
        self.cnv.grid(row=0, column=0)
        self.d = Drawing(self.cnv)

        # display a blank page
        self.page = self.cnv.create_rectangle(0, 0, self.page_w, self.page_h, fill="white", outline="")

        # attach events
        self.bind_resizing() # attach mouse wheel event
        self.select_tool('hand') # attach click&drag event

        # create_shortcuts for the 2 tools
        self.master.bind(self.tools['pen']['shortcut'], lambda event: self.select_tool('pen'))
        self.master.bind(self.tools['hand']['shortcut'], lambda event: self.select_tool('hand'))

        self.update_coordinates()
        self.draw_listener()


    def bind_resizing(self):

        def resize(event):
            """ scale all elem on canvas + relevant variables """

            scale_factor = 1.001 ** event.delta # tkinter uses different delta selon le degré de zoom
            self.zoom_factor *= scale_factor

            # CASE 1: absolute zero: gives the proper coordinates but not convenient in navigation
            self.cnv.scale(tk.ALL, 0, 0, scale_factor, scale_factor)

            # CASE 2: mouse position: gives wrong coordinates, convenient in navigation
            self.cnv.scale(tk.ALL, self.mouseX, self.mouseY, scale_factor, scale_factor)

            # drawing boundaries on page 
            self.page_w = round(self.page_w * scale_factor, 3)
            self.page_h = round(self.page_h * scale_factor, 3)

            # following page
            self.page_ofst_x = round(self.page_ofst_x * scale_factor, 3)
            self.page_ofst_y = round(self.page_ofst_y * scale_factor, 3)
            
        self.cnv.bind("<MouseWheel>", lambda event: resize(event))


    def select_tool(self, tool):
        """ self.curr_tool : previous selected tool """

        def bind_dragging():
            def do_scanmark(event):
               self.cnv.scan_mark(event.x, event.y)

            def do_dragto(event):
                """ drag to cursor dest and update offset dist"""
                self.cnv.scan_dragto(event.x, event.y, gain=1)
                self.drag_ofst_x = self.cnv.canvasx(0) # self.cnv.canvasx(0) == self.cnv.canvasy(event.y) - event.y
                self.drag_ofst_y = self.cnv.canvasy(0) # self.cnv.canvasy(0) == self.cnv.canvasx(event.x) - event.x
                #print(f"canvas offset: {self.drag_ofst_x}, {self.drag_ofst_y}")

            if not self.drag_fid: self.drag_fid = self.cnv.bind('<B1-Motion>', lambda event: do_dragto(event))
            if not self.drag_fid2: self.drag_fid2 = self.cnv.bind('<ButtonPress-1>', lambda event: do_scanmark(event))

        def unbind_dragging():
            self.drag_fid = self.cnv.unbind("<B1-Motion>", self.drag_fid) # return None
            self.drag_fid2 = self.cnv.unbind('<ButtonPress-1>', self.drag_fid2)


        if tool in self.tools.keys():
            if tool == 'pen':
                if self.curr_tool == 'hand':
                    unbind_dragging()
            elif tool == 'hand':
                bind_dragging()

            self.curr_tool = tool
            self.cnv.config(cursor=self.tools[tool]["cursor"])
            print(f"'{self.curr_tool}' tool selected")

    def update_coordinates(self):
        """ Update mouse coordinates on page at each frame (using event listener) """
        def motion(event):
            self.get_mouse_coordinates_on_page(event) # also triggered by the draw listener
        self.master.bind('<Motion>', motion)

    def get_mouse_coordinates_on_page(self, event):
        """ 
        The "page" is the white rectangle
        Updated mouseX == actual mouse position on canvas + canvas offset by dragging
        self.drag_ofst_x updated in do_dragto() ==> self.cnv.canvasx(0) 
        """
        self.mouseX = event.x + self.drag_ofst_x 
        self.mouseY = event.y + self.drag_ofst_y 
        self.rev_mouseY = -self.mouseY + self.page_w

    def draw_listener(self):

        def draw(event):
            """ only draw if tool is a pen and if movment is captured """
            if self.curr_tool == 'pen':
                self.get_mouse_coordinates_on_page(event) # updates self.mouseX and self.mouseY
                if self.mouseY != self.last_mouseY or self.mouseX != self.last_mouseX:
                    self.last_mouseX, self.last_mouseY = self.mouseX, self.mouseY
                    self.d.line(self.mouseX, self.mouseY)
                    print(f"{self.mouseX}, {self.rev_mouseY}")
                    sleep(1/60) #s

        # start record on click & drag
        self.master.bind("<B1-Motion>", lambda event: draw(event))

def run():
    root = tk.Tk()
    app = MainWindow(root)
    root.mainloop()
0

There are 0 best solutions below