如何获取Tkinter Canvas缩放操作产生的偏移量?
解决Tkinter Canvas缩放时的坐标偏移问题
问题背景
基于Tkinter开发的绘图应用,已实现鼠标拖拽移动Canvas、滚轮在光标位置缩放的功能,但存在矛盾:
- 以Canvas(0,0)为中心缩放:绘图坐标正确,但导航体验差(缩放时视图跳转到角落)
- 以鼠标位置为中心缩放:导航体验好,但
canvas.scale()会产生偏移,导致绘图坐标错位
需要计算缩放产生的偏移量,用来修正绘图坐标。
关键问题代码
拖拽功能代码
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))
绘图函数代码
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))
完整示例代码(原始版本)
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()
解决方案
核心思路:在缩放前后分别记录鼠标对应的画布坐标,计算两者的差值作为缩放偏移量,绘图时用该偏移量修正坐标。
步骤1:初始化缩放偏移变量
在MainWindow.__init__中取消注释并初始化缩放偏移变量:
self.resiz_ofst_x = self.resiz_ofst_y = 0 # init
步骤2:修改缩放函数计算偏移量
更新bind_resizing里的resize函数,在缩放前后计算坐标偏移:
def bind_resizing(self): def resize(event): scale_factor = 1.001 ** event.delta self.zoom_factor *= scale_factor # 缩放前:获取鼠标当前窗口位置对应的画布坐标 pre_scale_x = self.cnv.canvasx(event.x) pre_scale_y = self.cnv.canvasy(event.y) # 执行以鼠标位置为中心的缩放 self.cnv.scale(tk.ALL, event.x, event.y, scale_factor, scale_factor) # 缩放后:获取同一窗口位置对应的新画布坐标 post_scale_x = self.cnv.canvasx(event.x) post_scale_y = self.cnv.canvasy(event.y) # 计算并累加缩放偏移量 self.resiz_ofst_x += post_scale_x - pre_scale_x self.resiz_ofst_y += post_scale_y - pre_scale_y # 更新页面尺寸(保持原有逻辑) self.page_w = round(self.page_w * scale_factor, 3) self.page_h = round(self.page_h * scale_factor, 3) 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))
步骤3:修正绘图坐标
更新get_mouse_coordinates_on_page函数,把缩放偏移量加入坐标计算:
def get_mouse_coordinates_on_page(self, event): self.mouseX = event.x + self.drag_ofst_x + self.resiz_ofst_x self.mouseY = event.y + self.drag_ofst_y + self.resiz_ofst_y self.rev_mouseY = -self.mouseY + self.page_w
修改后的完整代码
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 # 缩放前记录鼠标对应的画布坐标 pre_scale_x = self.cnv.canvasx(event.x) pre_scale_y = self.cnv.canvasy(event.y) # 以鼠标窗口位置为中心缩放 self.cnv.scale(tk.ALL, event.x, event.y, scale_factor, scale_factor) # 缩放后记录同一窗口位置对应的画布坐标 post_scale_x = self.cnv.canvasx(event.x) post_scale_y = self.cnv.canvasy(event.y) # 计算并累加缩放偏移量 self.resiz_ofst_x += post_scale_x - pre_scale_x self.resiz_ofst_y += post_scale_y - pre_scale_y # 更新页面尺寸 self.page_w = round(self.page_w * scale_factor, 3) self.page_h = round(self.page_h * scale_factor, 3) 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.drag_ofst_y = self.cnv.canvasy(0) 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) 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) self.master.bind('<Motion>', motion) def get_mouse_coordinates_on_page(self, event): """ The "page" is the white rectangle Updated mouseX == actual mouse
相关产品推荐
相关产品推荐

