Wie funktioniert die tkinter canvas scale funktion?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
lfg
User
Beiträge: 2
Registriert: Mittwoch 11. Oktober 2023, 12:27

Ich möchte eine Map in tkinter programmieren in der man Elemte hinzufügen/verändern kann. Dabei bin ich bei der grundlegenden Navigation sprich Pan/Zoom auf das Problem gestoßen das ich die relative Position nach aufruf der Scale funktion nicht beibehalten kann.
Ich habe dies auch schon mit einem relativen zentralelement versucht zu dem ich dann immer relativ den versatz berechnen kann. Dies ist jedoch sehr ineffizient. Am liebsten wär mir nur die Berechnung in der
canvas_zoom(self, x, y, scale_multiplier)
methode anzupassen. Alternativ hatte ich schon vergeblich versucht den Sourcecode der methode zu findet, falls jemand weiß wie man soetwas anstellt würde das wahrscheinlich auch helfen.

Code: Alles auswählen

import tkinter as tk

class App(tk.Tk):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.status_bar : StatusBar = StatusBar(self, bg = "lightgrey")
        self.status_bar.pack(fill = "x", side = "bottom")

        self.map_canvas : MapCanvas = MapCanvas(self, self.status_bar, bg = "white")
        self.map_canvas.pack(fill = "both", expand = True)

    def mainloop(self):

        super().mainloop()

class StatusBar(tk.Frame):

    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)

        self.position_label : tk.Label = tk.Label(self, text = "Position: --, --")
        self.position_label.pack(side = "left")

        self.cursor_position_label : tk.Label = tk.Label(self, text = "Cursor: --, --")
        self.cursor_position_label.pack(side = "left")

        self.scale_label : tk.Label = tk.Label(self, text = f"Scale: --")
        self.scale_label.pack(side = "right")

    def set_position(self, position : tuple[float, float]):

        x = position[0]
        y = position[1]

        self.position_label.config(text = f"Position: {x:.4f}, {y:.4f}")

    def set_cursor_position(self, cursor_position : tuple[float, float]):

        x = cursor_position[0]
        y = cursor_position[1]

        self.cursor_position_label.config(text = f"Cursor: {x:.4f}, {y:.4f}")

    def set_scale(self, scale : float):

        self.scale_label.config(text = f"Scale: {scale:.6f}")

class MapCanvas(tk.Canvas):

    def __init__(self, master : App, statusBar : StatusBar, **kwargs):
        super().__init__(master, **kwargs)

        self.current_offset : tuple[float, float] = (0.0, 0.0)
        self.current_scale : float = 20/1 # pixel / meter
        self.last_x : float = 0
        self.last_y : float = 0
        self.width : int = self.winfo_width()
        self.height : int = self.winfo_height()

        self.bind("<Configure>", self.on_configure)

        self.bind("<Motion>", self.on_motion)
        self.bind("<Button-1>", self.on_b1_click)
        self.bind("<Button-3>", self.on_b3_click)
        self.bind("<B1-Motion>", self.on_b1_motion)
        self.bind("<MouseWheel>", self.on_mouse_wheel)

        self.status_bar : StatusBar = statusBar
        self.update_status_position()
        self.update_status_cursor_position()
        self.update_status_scale()

    def on_configure(self, event) -> None:

        dx = (event.width - self.width)/2
        dy = (event.height - self.height)/2

        self.width = event.width
        self.height = event.height

        self.move("all", dx, dy)

    def on_motion(self, event) -> None:

        self.last_x = event.x
        self.last_y = event.y

        self.update_status_cursor_position()

    def on_b1_click(self, event) -> None:

        pass

    def on_b3_click(self, event) -> None:

        print(f"do something with right click at {self.get_cursor_position()}")

    def on_b1_motion(self, event) -> None:

        dx = event.x - self.last_x
        dy = event.y - self.last_y

        self.last_x = event.x
        self.last_y = event.y

        self.canvas_pan(dx, dy)

    def on_mouse_wheel(self, event) -> None:

        if event.delta > 0:
            scale_multiplier = 1.1
        else:
            scale_multiplier = 0.9

        self.canvas_zoom(event.x, event.y, scale_multiplier)

    def canvas_pan(self, dx, dy) -> None:

        self.move("all", dx, dy)

        old_offset_x, old_offset_y = self.current_offset
        cs = self.current_scale
        self.current_offset = (old_offset_x - dx / cs, old_offset_y + dy / cs)

        self.update_status_position()

    def canvas_zoom(self, x, y, scale_multiplier) -> None:

        self.scale("all", x, y, scale_multiplier, scale_multiplier)

        self.current_scale *= scale_multiplier

        # TODO: fix this
        ds = scale_multiplier
        cx, cy = self.current_offset
        self.current_offset = (x + (cx - x) * ds, y + (cy - y) * ds)
        self.current_offset = (cx + (ds - 1) * (cx - x), cy + (ds - 1) * (cy - y))

        self.update_status_scale()
        if x != self.width/2 or y != self.height/2:
            self.update_status_position()
        if self.last_x != x or self.last_y != y:
            self.update_status_cursor_position()

    def create_rectangle(self, x1, y1, x2, y2, **kwargs) -> int:

        cs = self.current_scale
        x1 = x1 * cs + self.current_offset[0] + self.width/2
        y1 = -y1 * cs + self.current_offset[1] + self.height/2
        x2 = x2 * cs + self.current_offset[0] + self.width/2
        y2 = -y2 * cs + self.current_offset[1] + self.height/2

        return super().create_rectangle(x1, y1, x2, y2, **kwargs)

    def create_oval(self, x1, y1, x2, y2, **kwargs) -> int:

        cs = self.current_scale
        x1 = x1 * cs + self.current_offset[0] + self.width/2
        y1 = -y1 * cs + self.current_offset[1] + self.height/2
        x2 = x2 * cs + self.current_offset[0] + self.width/2
        y2 = -y2 * cs + self.current_offset[1] + self.height/2

        return super().create_oval(x1, y1, x2, y2, **kwargs)

    def create_line(self, *args, **kwargs) -> int:

        coords = list(args)

        cs = self.current_scale
        for i in range(0, len(coords), 2):
            coords[i] = coords[i] * cs + self.current_offset[0] + self.width/2
            coords[i + 1] = -coords[i + 1] * cs + self.current_offset[1] + self.height/2

        return super().create_line(*coords, **kwargs)

    def get_cursor_position(self) -> tuple[float, float]:

        cs = self.current_scale
        return (self.current_offset[0] + (self.last_x - self.width/2) / cs, self.current_offset[1] - (self.last_y - self.height/2) / cs)
        return (self.current_offset[0] + (self.last_x) / cs, self.current_offset[1] - (self.last_y) / cs)

    def update_status_position(self) -> None:

        self.status_bar.set_position(self.current_offset)

    def update_status_cursor_position(self) -> None:

        cursor_position = self.get_cursor_position()

        self.status_bar.set_cursor_position(cursor_position)

    def update_status_scale(self) -> None:

        self.status_bar.set_scale(self.current_scale)

    def set_current_offset(self, offset : tuple[float, float]) -> None:

        # TODO: Implement this method
        pass

    def set_current_scale(self, scale : float) -> None:

        x = self.winfo_width()/2
        y = self.winfo_height()/2
        delta = self.current_scale / scale

        self.canvas_zoom(x, y, delta)

if __name__ == "__main__":

    app = App()
    app.map_canvas.create_oval(-5, -5, 0, 0, fill = "red", outline = "red")
    app.map_canvas.create_oval(0, 0, 5, 5, fill = "black", outline = "black")
    grid_number = 10
    grid_space = 1
    for i in range(int(-grid_number/2), int(grid_number/2)+1):
        app.map_canvas.create_line(-grid_number/2, i * grid_space, grid_number/2, i * grid_space, fill = "grey")
        app.map_canvas.create_line(i * grid_space, -grid_number/2, i * grid_space, grid_number/2, fill = "grey")
    app.mainloop()
Antworten