TomSchimansky / CustomTkinter

A modern and customizable python UI-library based on Tkinter
MIT License
11.3k stars 1.06k forks source link

Zoom into Image / Label #2142

Open medhanshrath-t opened 9 months ago

medhanshrath-t commented 9 months ago

It is currently possible to zoom into a label using the mouse scroll? If not, does anyone have an idea how I could implement it?

LorenzoMattia commented 9 months ago

Hi @medhanshrath-t , do you want to zoom in an image contained in the label? Or just zoom the label text?

medhanshrath-t commented 9 months ago

I want to zoom into the image, similar to how you can open an image in Windows and then zoom into it.

LorenzoMattia commented 9 months ago

Hi @medhanshrath-t, you can think about something like this:

import os
import customtkinter
from PIL import Image

class CustomLabel(customtkinter.CTkLabel):
    def __init__(self, master: customtkinter.CTkBaseClass, path_to_image: str, **kwargs) -> None:
        super().__init__(master, **kwargs)

        self.path_to_image = path_to_image

        # bind left mouse click to view image method
        self.bind("<Button-1>", self.view_image)

        self.bind("<Enter>", self.on_enter)
        self.bind("<Leave>", self.on_leave)

    def view_image(self, _ = None) -> None:
        # open image with your default os image viewer
        os.system(self.path_to_image)

    # make the label appear as clickable
    def on_enter(self, _ = None) -> None:
        self.configure(cursor = "hand2")

    def on_leave(self, _ = None) -> None:
        self.configure(cursor = "")

class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        self.geometry("1000x1000")

        # create a container frame
        self.main_frame = customtkinter.CTkFrame(self)
        self.main_frame.pack(expand = customtkinter.YES, fill = customtkinter.BOTH)

        # here goes the path to the image you want to show
        image_path = "example.jpg"

        # create the image
        self.img = customtkinter.CTkImage(Image.open(image_path), size = (400, 300))

        self.image_lbl = CustomLabel(self.main_frame, path_to_image=image_path, text="", image = self.img)
        self.image_lbl.pack(expand = customtkinter.YES, fill = customtkinter.BOTH)

app = App()
app.mainloop()

The use of the CustomLabel class is not strictly necessary as long as you still bind the event to the corresponding handler method to which you should pass the image path. Even less necessary are the two on_enter and on_leave methods.

Hope this helps!

medhanshrath-t commented 9 months ago

Hey, this wasn't exactly what I was looking for, I want the zooming to happen in the application, but yeah this is actually useful. I am implementing this as an alternative

LorenzoMattia commented 9 months ago

Hi, @medhanshrath-t, I am glad that even if not the desired one, my solution is still appreciable for you.

However, if you want to make the label zoom inside the app, you can implement a zooming algorithm using some image processing libraries (PIL, opencv...) and then bind the zoom level to the mouse wheel when hovering over the label, like: self.image_lbl.bind("<MouseWheel>", zoom_function)

medhanshrath-t commented 8 months ago

I actually found code for a zoomable label for TKinter and modified it so that it works with CTk. It works great on its own, but when I place it in my app it doesn't let me zoom into the sides and once it is zoomed you can not scroll to the edges of the image. I am using grid for packing it.


    """
    Label in which the images can be zoomed into.
    """
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        self.pil_image = None
        self.zoom_cycle = 0
        self.__old_event = None
        self.width = kwargs['width']
        self.height = kwargs['height']
        self.create_bindings()
        self.reset_transform()

    def create_bindings(self):
        self.master.bind("<Button-1>", self.mouse_down_left)                   # MouseDown
        self.master.bind("<B1-Motion>", self.mouse_move_left)                  # MouseDrag
        self.master.bind("<Double-Button-1>", self.mouse_double_click_left)    # MouseDoubleClick
        self.master.bind("<MouseWheel>", self.mouse_wheel)                     # MouseWheel

    def set_image(self, filename=None, pil_image=None):
        self.pil_image = pil_image if pil_image else Image.open(filename)
        self.zoom_fit(self.pil_image.width, self.pil_image.height)
        self.draw_image(self.pil_image)

    # -------------------------------------------------------------------------------
    # Mouse events
    # -------------------------------------------------------------------------------
    def mouse_down_left(self, event):
        self.__old_event = event

    def mouse_move_left(self, event):
        if (self.pil_image == None):
            return

        self.translate(event.x - self.__old_event.x, event.y - self.__old_event.y) if self.__old_event else None
        self.redraw_image()
        self.__old_event = event

    def mouse_double_click_left(self, event):
        if self.pil_image == None:
            return
        self.zoom_fit(self.pil_image.width, self.pil_image.height)
        self.redraw_image() 

    def mouse_wheel(self, event):
        if self.pil_image == None:
            return

        if (event.delta < 0):
            if self.zoom_cycle <= 0:
                return
            # Rotate upwards and shrink
            self.scale_at(0.8, event.x, event.y)
            self.zoom_cycle -= 1
        else:
            if self.zoom_cycle >= 9:
                return
            #  Rotate downwards and enlarge
            self.scale_at(1.25, event.x, event.y)
            self.zoom_cycle += 1

        self.redraw_image()

    # -------------------------------------------------------------------------------
    # Affine Transformation for Image Display
    # -------------------------------------------------------------------------------

    def reset_transform(self):
        self.mat_affine = np.eye(3)

    def translate(self, offset_x, offset_y,zoom = False):
        mat = np.eye(3)
        mat[0, 2] = float(offset_x)
        mat[1, 2] = float(offset_y)

        scale = self.mat_affine[0, 0]
        max_y = scale * 3072
        max_x = scale * 4096
        self.mat_affine = np.dot(mat, self.mat_affine)

        if not zoom:
            if abs(self.mat_affine[0,2]) > abs(max_x-self.width):
                self.mat_affine[0,2] = -(max_x-self.width)
            if abs(self.mat_affine[1,2]) > abs(max_y-self.height):
                self.mat_affine[1,2] = -(max_y-self.height)

        if self.mat_affine[0, 2] > 0.0:
            self.mat_affine[0, 2] = 0.0
        if self.mat_affine[1,2] > 0.0:
            self.mat_affine[1,2]  = 0.0

    def scale(self, scale:float):
        mat = np.eye(3)
        mat[0, 0] = scale
        mat[1, 1] = scale
        self.mat_affine = np.dot(mat, self.mat_affine)

    def scale_at(self, scale:float, cx:float, cy:float):
        self.translate(-cx, -cy, True)
        self.scale(scale)
        self.translate(cx, cy)

    def zoom_fit(self, image_width, image_height):
        self.master.update()

        if (image_width * image_height <= 0) or (self.width * self.height <= 0):
            return

        self.reset_transform()

        scale = 1.0
        offsetx = 0.0
        offsety = 0.0
        if (self.width * image_height) > (image_width * self.height):
            scale = self.height / image_height
            offsetx = (self.width - image_width * scale) / 2
        else:
            scale = self.width / image_width
            offsety = (self.height - image_height * scale) / 2
        self.scale(scale)
        self.translate(offsetx, offsety)
        self.zoom_cycle = 0

    def to_image_point(self, x, y):
        '''Convert coordinates from the canvas to the image'''
        if self.pil_image == None:
            return []
        mat_inv = np.linalg.inv(self.mat_affine)
        image_point = np.dot(mat_inv, (x, y, 1.))
        if  image_point[0] < 0 or image_point[1] < 0 or image_point[0] > self.pil_image.width or image_point[1] > self.pil_image.height:
            return []
        return image_point

    # -------------------------------------------------------------------------------
    # Drawing 
    # -------------------------------------------------------------------------------

    def draw_image(self, pil_image):
        if pil_image == None:
            return

        self.pil_image = pil_image

        mat_inv = np.linalg.inv(self.mat_affine)

        affine_inv = (
            mat_inv[0, 0], mat_inv[0, 1], mat_inv[0, 2],
            mat_inv[1, 0], mat_inv[1, 1], mat_inv[1, 2]
        )

        dst = self.pil_image.transform(
            (self.width, self.height),
            Image.AFFINE,
            affine_inv,
            Image.NEAREST
        )

        ctk_image = CTkImage(light_image=dst, size=(self.width, self.height))
        self.configure(image=ctk_image)

    def redraw_image(self):
        '''Redraw the image'''
        if self.pil_image == None:
            return
        self.draw_image(self.pil_image)```