[Python] Python Tkinter - Maintaining coordinates when scaling Canvas

Discussão em 'Python' iniciado por Stack, Julho 26, 2025.

  1. Stack

    Stack Membro Participativo

    I have a map where the user can build cities and move around their units and attack other player's units. The mouse wheel can be used to zoom in or out. No matter what the level of zoom is, I want to be able to see the original map coordinates, for example, to send messages like "Unit X was seen/attacked at coordinates (200, 400)". But I can't make it work for all cases.

    Here is a simplified version of the zoomable map. I have placed labels and markers at certain coordinates to illustrate my point.

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


    If I place the mouse on (200,200) and zoom in once, I can move the mouse over all red crosses in the map and the label shows their original/scaled coordinates (the ones the user is interested in because that's where their cities/units are). I have also shown canvas coordinates with black markers just to help me analyze the problem.

    If I zoom in/out from the same (200, 200) coordinates, everything is fine.

    However, if I zoom in from, say, (200, 200) and then I zoom in again from (400, 400), or any other coordinate, now the "Scaled_coords" displayed in the label don't show the red markers' original coordinates. If I zoom in 3-4 times from the same mouse position, the original red marker coordinates are correctly show in the label. Likewise if I zoom out from that same position. But if I zoom somewhere, move the mouse, zoom some more, it doesn't work as I would like it to.

    Thank you in advance.

    Continue reading...

Compartilhe esta Página