You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何获取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
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 23:48:17