from tkinter import NSEW, Canvas, Event, ttk from classes.custom.themed_frame import ThemedFrame FIT_WIDTH = "fit_width" FIT_HEIGHT = "fit_height" class ScrollableFrame(ThemedFrame): """ There is no way to scroll so we are going to create a canvas and place the frame there. Scrolling the canvas will give the illusion of scrolling the frame Partly taken from: https://blog.tecladocode.com/tkinter-scrollable-frames/ https://stackoverflow.com/a/17457843/11106801 master_frame--------------------------------------------------------- | dummy_canvas----------------------------------------- y_scroll-- | | | self--------------------------------------------- | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | ------------------------------------------------ | | | | | ---------------------------------------------------- --------- | | x_scroll--------------------------------------------- | | | | | | ---------------------------------------------------- | -------------------------------------------------------------------- """ def __init__(self, master=None, scroll_speed:int=2, hscroll:bool=False, vscroll:bool=True, scrollbar_kwargs={}, **kwargs): assert isinstance(scroll_speed, int), "`scroll_speed` must be an int" self.scroll_speed = scroll_speed self.master_frame = ThemedFrame(master) self.master_frame.grid_rowconfigure(0, weight=1) self.master_frame.grid_columnconfigure(0, weight=1) self.dummy_canvas = Canvas(self.master_frame, highlightthickness=0, **kwargs) super().__init__(self.dummy_canvas) # Create the 2 scrollbars if vscroll: self.v_scrollbar = ttk.Scrollbar(self.master_frame, orient="vertical", command=self.dummy_canvas.yview, **scrollbar_kwargs) self.v_scrollbar.grid(row=0, column=1, sticky=NSEW) self.dummy_canvas.configure(yscrollcommand=self.v_scrollbar.set) if hscroll: self.h_scrollbar = ttk.Scrollbar(self.master_frame, orient="horizontal", command=self.dummy_canvas.xview, **scrollbar_kwargs) self.h_scrollbar.grid(row=1, column=0, sticky=NSEW) self.dummy_canvas.configure(xscrollcommand=self.h_scrollbar.set) # Bind to the mousewheel scrolling self.dummy_canvas.bind_all("", self.scrolling_windows, add=True) self.dummy_canvas.bind_all("", self.scrolling_linux, add=True) self.dummy_canvas.bind_all("", self.scrolling_linux, add=True) self.bind("", self.scrollbar_scrolling, add=True) # Place `self` inside `dummy_canvas` self.dummy_canvas.create_window((0, 0), window=self, anchor="nw") # Place `dummy_canvas` inside `master_frame` self.dummy_canvas.grid(row=0, column=0, sticky=NSEW) self.pack = self.master_frame.pack self.grid = self.master_frame.grid self.place = self.master_frame.place self.pack_forget = self.master_frame.pack_forget self.grid_forget = self.master_frame.grid_forget self.place_forget = self.master_frame.place_forget def scrolling_windows(self, event:Event) -> None: assert event.delta != 0, "On Windows, `event.delta` should never be 0" y_steps = int(-event.delta/abs(event.delta)*self.scroll_speed) self.dummy_canvas.yview_scroll(y_steps, "units") def scrolling_linux(self, event:Event) -> None: y_steps = self.scroll_speed if event.num == 4: y_steps *= -1 self.dummy_canvas.yview_scroll(y_steps, "units") def scrollbar_scrolling(self, event:Event) -> None: region = list(self.dummy_canvas.bbox("all")) region[2] = max(self.dummy_canvas.winfo_width(), region[2]) region[3] = max(self.dummy_canvas.winfo_height(), region[3]) self.dummy_canvas.configure(scrollregion=region) def resize(self, fit:str=None, height:int=None, width:int=None) -> None: """ Resizes the frame to fit the widgets inside. You must either specify (the `fit`) or (the `height` or/and the `width`) parameter. Parameters: fit:str `fit` can be either `FIT_WIDTH` or `FIT_HEIGHT`. `FIT_WIDTH` makes sure that the frame's width can fit all of the widgets. `FIT_HEIGHT` is simmilar height:int specifies the height of the frame in pixels width:int specifies the width of the frame in pixels To do: ALWAYS_FIT_WIDTH ALWAYS_FIT_HEIGHT """ if height is not None: self.dummy_canvas.config(height=height) if width is not None: self.dummy_canvas.config(width=width) if fit == FIT_WIDTH: super().update() self.dummy_canvas.config(width=super().winfo_width()) elif fit == FIT_HEIGHT: super().update() self.dummy_canvas.config(height=super().winfo_height()) else: raise ValueError("Unknow value for the `fit` parameter.") fit = resize # import tkinter as tk # from tkinter import ttk # from classes.custom.themed_frame import ThemedFrame # class ScrollableFrame(ThemedFrame): # def __init__(self, container, *args, **kwargs): # super().__init__(container, *args, **kwargs) # self.canvas = tk.Canvas(self) # scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview) # self.canvas.bind_all("", self._on_mousewheel) # self.scrollable_frame = ThemedFrame(self.canvas) # self.scrollable_frame.bind( # "", # lambda e: self.canvas.configure( # scrollregion=self.canvas.bbox("all") # ) # ) # self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") # self.canvas.configure(yscrollcommand=scrollbar.set) # self.canvas.pack(side="left", fill="both", expand=True) # scrollbar.pack(side="right", fill="y") # def _on_mousewheel(self, event): # shift = (event.state & 0x1) != 0 # scroll = -1 if event.delta > 0 else 1 # if shift: # self.canvas.xview_scroll(scroll, "units") # else: # self.canvas.yview_scroll(scroll, "units")