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()