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()
关键修改点
- 新增偏移量跟踪:添加
offset_x和offset_y变量,记录原始原点相对于当前屏幕原点的偏移; - 缩放时更新偏移量:每次缩放根据缩放中心和比例计算新的偏移量,确保坐标变换的累积性;
- 修正坐标转换逻辑:通过
(屏幕坐标 - 偏移量) / 总缩放倍数直接计算原始坐标,无需依赖单一缩放中心; - 修正标记绘制位置:将原始坐标转换为当前屏幕坐标后再绘制黑色标记,确保标记位置与缩放后的地图匹配。
内容的提问来源于stack exchange,提问作者Mike Duke
相关产品推荐
相关产品推荐

