CustomTkinter
CustomTkinter copied to clipboard
Zoom into Image / Label
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?
Hi @medhanshrath-t , do you want to zoom in an image contained in the label? Or just zoom the label text?
I want to zoom into the image, similar to how you can open an image in Windows and then zoom into it.
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!
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
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)
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)```