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

Python Tkinter缩放Canvas时维持原始地图坐标显示问题

可缩放地图原始坐标显示异常问题修复

我开发了一款带地图的游戏,支持鼠标滚轮缩放地图,用户可在地图上建造城市、移动单位并攻击其他玩家单位。核心需求是无论缩放级别如何,都能准确显示地图的原始坐标(比如发送“单位X在(200,400)位置被发现/攻击”这类消息),但该功能在多位置缩放场景下失效。

简化复现代码如下:

import tkinter as tk
from tkinter import ttk

class ZoomableCanvas(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Zoomable Canvas")
        self.geometry("700x700")

        # Create the main canvas frame
        self.canvas_frame = ttk.Frame(self)
        self.canvas_frame.pack(fill="both", expand=True)

        # Create the canvas
        self.canvas = tk.Canvas(self.canvas_frame, width=600, height=600, bg='white', scrollregion=(0, 0, 800, 800))
        self.canvas.pack(side="left", fill="both", expand=True)

        # Add bottom frame with label
        self.bottom_frame = ttk.Frame(self)
        self.bottom_frame.pack(side="bottom", fill="x")
        self.coordinates_label = ttk.Label(self.bottom_frame, text="Mouse Coordinates: (0, 0)")
        self.coordinates_label.pack()

        # Generate 9 points, spaced each 200px apart, in a grid pattern
        for x in range(1, 4):
            for y in range(1, 4):
                x_pos = x * 200
                y_pos = y * 200
                self.draw_cross(x_pos, y_pos, tags='initial_coords', color="red")
                self.canvas.create_text(x_pos + 10, y_pos + 10, text=f'{x_pos, y_pos}', fill='red', tags='initial_coords')

        # Bind events for zooming and mouse movement
        self.canvas.bind("<MouseWheel>", self.zoom)
        self.canvas.bind("<Motion>", self.update_coordinates_label)

        # Zoom scale
        self.scale = 1.0
        self.total_zoom = 1.0
        self.previous_zoom = 1.0
        self.zoom_center = (0, 0)


    def draw_cross(self, x, y, tags, color="black", size=3):
        # Draw a cross at the specified coordinates
        self.canvas.create_line(x - size, y, x + size, y, fill=color, tags=tags)
        self.canvas.create_line(x, y - size, x, y + size, fill=color, tags=tags)

    def zoom(self, event):
        self.canvas.delete('coords')  # Clear previous coordinates

        # Zoom in or out based on mouse wheel direction
        self.previous_zoom = self.total_zoom
        if event.delta > 0:
            self.total_zoom += 0.25
            if self.total_zoom > 2.0:
                self.total_zoom = 2.0  # Limit zooming in to a maximum of 2.0
                return
            else:
                self.scale = self.total_zoom / self.previous_zoom
            print(f"Zooming in factor: {self.scale}, Total Zoom: {self.total_zoom}, Prev Zoom: {self.previous_zoom}, Pos: ({event.x}, {event.y})")
        else:
            self.total_zoom -= 0.25
            if self.total_zoom < 1.0:
                self.total_zoom = 1.0  # Limit zooming in to a minimum of 1.0
                return
            else:
                self.scale = self.total_zoom / self.previous_zoom
            print(f"Zooming out factor: {self.scale}, Total Zoom: {self.total_zoom}, Prev Zoom: {self.previous_zoom}, Pos: ({event.x}, {event.y})")

        self.zoom_center = (event.x, event.y)
        self.canvas.scale("all", event.x, event.y, self.scale, self.scale)
        #self.canvas.configure(scrollregion=self.canvas.bbox("all"))
        self.previous_scale = self.scale
        
        for x in range(1, 4):
            for y in range(1, 4):
                x_pos = x * 200
                y_pos = y * 200
                self.draw_cross(x_pos, y_pos, tags='coords', color="black")
                self.canvas.create_text(x_pos - 10, y_pos - 10, text=f'{x_pos, y_pos}', fill='black', tags='coords')


    def update_coordinates_label(self, event):
        scaled_x = self.zoom_center[0] - (self.zoom_center[0] - event.x) / self.total_zoom
        scaled_y = self.zoom_center[1] - (self.zoom_center[1] - event.y) / self.total_zoom

        # Get the mouse position relative to the canvas
        canvas_x = self.canvas.canvasx(event.x)
        canvas_y = self.canvas.canvasy(event.y)

        # Update the coordinates label with the transformed values
        self.coordinates_label.config(
            text=f"{event.x=}, {event.y=} {canvas_x=}, {canvas_y=} \nScaled_coords: ({scaled_x:.2f}, {scaled_y:.2f}) \nScale: {self.scale:.2f} Zoom: {self.total_zoom:.2f}"
            )



if __name__ == "__main__":
    app = ZoomableCanvas()
    app.mainloop()

问题现象

  • 当鼠标固定在同一位置(如(200,200))反复缩放时,鼠标移至红色标记处,坐标显示正常;
  • 若先在A位置缩放,再切换到B位置(如(400,400))缩放,标签显示的坐标无法对应原始标记位置,仅单位置缩放时正常。

问题根源

原代码仅用最后一次缩放的中心zoom_center和总缩放倍数total_zoom计算原始坐标,但多次不同中心的缩放会累积复杂的坐标变换,单一的中心变量无法覆盖所有变换过程,导致逆计算错误。

修复方案

通过维护偏移量和总缩放倍数来跟踪完整的坐标变换,每次缩放时更新偏移量,确保屏幕坐标到原始坐标的转换准确。

修复后的完整代码:

import tkinter as tk
from tkinter import ttk

class ZoomableCanvas(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Zoomable Canvas")
        self.geometry("700x700")

        # Create the main canvas frame
        self.canvas_frame = ttk.Frame(self)
        self.canvas_frame.pack(fill="both", expand=True)

        # Create the canvas
        self.canvas = tk.Canvas(self.canvas_frame, width=600, height=600, bg='white', scrollregion=(0, 0, 800, 800))
        self.canvas.pack(side="left", fill="both", expand=True)

        # Add bottom frame with label
        self.bottom_frame = ttk.Frame(self)
        self.bottom_frame.pack(side="bottom", fill="x")
        self.coordinates_label = ttk.Label(self.bottom_frame, text="Mouse Coordinates: (0, 0)")
        self.coordinates_label.pack()

        # Generate 9 points, spaced each 200px apart, in a grid pattern
        for x in range(1, 4):
            for y in range(1, 4):
                x_pos = x * 200
                y_pos = y * 200
                self.draw_cross(x_pos, y_pos, tags='initial_coords', color="red")
                self.canvas.create_text(x_pos + 10, y_pos + 10, text=f'{x_pos, y_pos}', fill='red', tags='initial_coords')

        # Bind events for zooming and mouse movement
        self.canvas.bind("<MouseWheel>", self.zoom)
        self.canvas.bind("<Motion>", self.update_coordinates_label)

        # Zoom and transform tracking
        self.total_zoom = 1.0
        self.offset_x = 0.0
        self.offset_y = 0.0


    def draw_cross(self, x, y, tags, color="black", size=3):
        # Draw a cross at the specified coordinates
        self.canvas.create_line(x - size, y, x + size, y, fill=color, tags=tags)
        self.canvas.create_line(x, y - size, x, y + size, fill=color, tags=tags)

    def zoom(self, event):
        self.canvas.delete('coords')  # Clear previous coordinates

        # Calculate new zoom level
        new_zoom = self.total_zoom
        if event.delta > 0:
            new_zoom += 0.25
            if new_zoom > 2.0:
                new_zoom = 2.0
        else:
            new_zoom -= 0.25
            if new_zoom < 1.0:
                new_zoom = 1.0

        if new_zoom == self.total_zoom:
            return  # No zoom change, exit early

        # Calculate scale factor and update offsets
        scale_factor = new_zoom / self.total_zoom
        cx, cy = event.x, event.y

        # Update offsets to maintain zoom center position
        self.offset_x = cx * (1 - scale_factor) + self.offset_x * scale_factor
        self.offset_y = cy * (1 - scale_factor) + self.offset_y * scale_factor

        # Apply zoom to canvas elements
        self.canvas.scale("all", cx, cy, scale_factor, scale_factor)
        self.total_zoom = new_zoom

        # Redraw coordinate markers at original positions (converted to current screen coords)
        for x in range(1, 4):
            for y in range(1, 4):
                original_x = x * 200
                original_y = y * 200
                # Convert original coords to current screen position
                screen_x = self.offset_x + original_x * self.total_zoom
                screen_y = self.offset_y + original_y * self.total_zoom
                self.draw_cross(screen_x, screen_y, tags='coords', color="black")
                self.canvas.create_text(screen_x - 10, screen_y - 10, text=f'{original_x, original_y}', fill='black', tags='coords')

        print(f"Zoom factor: {scale_factor:.2f}, Total Zoom: {self.total_zoom:.2f}, Offset: ({self.offset_x:.2f}, {self.offset_y:.2f})")

    def update_coordinates_label(self, event):
        # Convert screen mouse position to original map coordinates
        original_x = (event.x - self.offset_x) / self.total_zoom
        original_y = (event.y - self.offset_y) / self.total_zoom

        # Get canvas-relative coordinates (for reference)
        canvas_x = self.canvas.canvasx(event.x)
        canvas_y = self.canvas.canvasy(event.y)

        # Update label with accurate coordinates
        self.coordinates_label.config(
            text=f"Screen: ({event.x}, {event.y}) | Canvas: ({canvas_x:.2f}, {canvas_y:.2f}) \nOriginal Coords: ({original_x:.2f}, {original_y:.2f}) \nTotal Zoom: {self.total_zoom:.2f}"
        )

if __name__ == "__main__":
    app = ZoomableCanvas()
    app.mainloop()

关键修改点

  1. 新增偏移量跟踪:添加offset_x和offset_y变量,记录原始原点相对于当前屏幕原点的偏移;
  2. 缩放时更新偏移量:每次缩放根据缩放中心和比例计算新的偏移量,确保坐标变换的累积性;
  3. 修正坐标转换逻辑:通过(屏幕坐标 - 偏移量) / 总缩放倍数直接计算原始坐标,无需依赖单一缩放中心;
  4. 修正标记绘制位置:将原始坐标转换为当前屏幕坐标后再绘制黑色标记,确保标记位置与缩放后的地图匹配。

内容的提问来源于stack exchange,提问作者Mike Duke

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 15:19:52