diff --git a/Readme.md b/Readme.md index dca878d..c592de1 100644 --- a/Readme.md +++ b/Readme.md @@ -105,6 +105,12 @@ how to position the text and image at once with the ``compound`` option: | _`image_example.py` on Windows 11_ ### +### Scrollable Frames +Scrollable frames are possible in vertical or horizontal orientation and can be combined +with any other widgets. +![](documentation_images/scrollable_frame_example_Windows.png) +| _`scrollable_frame_example.py` on Windows 11_ + ### Integration of TkinterMapView widget In the following example I used a TkinterMapView which integrates well with a CustomTkinter program. It's a tile based map widget which displays diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json index 73a1f4d..838e26b 100644 --- a/customtkinter/assets/themes/blue.json +++ b/customtkinter/assets/themes/blue.json @@ -127,6 +127,9 @@ "scrollbar_button_color": ["gray55", "gray41"], "scrollbar_button_hover_color": ["gray40", "gray53"] }, + "CTkScrollableFrame": { + "label_fg_color": ["gray78", "gray23"] + }, "DropdownMenu": { "fg_color": ["gray90", "gray20"], "hover_color": ["gray75", "gray28"], diff --git a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py index 5f119bf..610f46a 100644 --- a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py +++ b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py @@ -193,12 +193,15 @@ class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClas if master_widget is None: master_widget = self.master - if isinstance(master_widget, (windows.widgets.core_widget_classes.CTkBaseClass, windows.CTk, windows.CTkToplevel)): + if isinstance(master_widget, (windows.widgets.core_widget_classes.CTkBaseClass, windows.CTk, windows.CTkToplevel, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame)): if master_widget.cget("fg_color") is not None and master_widget.cget("fg_color") != "transparent": return master_widget.cget("fg_color") + elif isinstance(master_widget, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame): + return self._detect_color_of_master(master_widget.master.master.master) + # if fg_color of master is None, try to retrieve fg_color from master of master - elif hasattr(master_widget.master, "master"): + elif hasattr(master_widget, "master"): return self._detect_color_of_master(master_widget.master) elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget diff --git a/customtkinter/windows/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py index 0648d92..d79a944 100644 --- a/customtkinter/windows/widgets/ctk_button.py +++ b/customtkinter/windows/widgets/ctk_button.py @@ -428,6 +428,7 @@ class CTkButton(CTkBaseClass): if "command" in kwargs: self._command = kwargs.pop("command") + self._set_cursor() if "compound" in kwargs: self._compound = kwargs.pop("compound") diff --git a/customtkinter/windows/widgets/ctk_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py index 1017e2c..99e4269 100644 --- a/customtkinter/windows/widgets/ctk_scrollable_frame.py +++ b/customtkinter/windows/widgets/ctk_scrollable_frame.py @@ -9,10 +9,14 @@ import sys from .ctk_frame import CTkFrame from .ctk_scrollbar import CTkScrollbar from .appearance_mode import CTkAppearanceModeBaseClass +from .scaling import CTkScalingBaseClass from .core_widget_classes import CTkBaseClass +from .ctk_label import CTkLabel +from .font import CTkFont +from .theme import ThemeManager -class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass): +class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass): def __init__(self, master: any, width: int = 200, @@ -23,121 +27,246 @@ class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass): bg_color: Union[str, Tuple[str, str]] = "transparent", fg_color: Optional[Union[str, Tuple[str, str]]] = None, border_color: Optional[Union[str, Tuple[str, str]]] = None, + scrollbar_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + scrollbar_button_color: Optional[Union[str, Tuple[str, str]]] = None, + scrollbar_button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + label_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + label_text_color: Optional[Union[str, Tuple[str, str]]] = None, + label_text: str = "", + label_font: Optional[Union[tuple, CTkFont]] = None, + label_anchor: str = "center", orientation: Literal["vertical", "horizontal"] = "vertical"): self._orientation = orientation - self.parent_frame = CTkFrame(master=master, width=width, height=height, corner_radius=corner_radius, - border_width=border_width, bg_color=bg_color, fg_color=fg_color, border_color=border_color) - self.parent_frame.grid_propagate(0) - self.parent_canvas = tkinter.Canvas(master=self.parent_frame, highlightthickness=0, width=0, height=0) + # dimensions independent of scaling + self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height + self._desired_height = height + + self._parent_frame = CTkFrame(master=master, width=0, height=0, corner_radius=corner_radius, + border_width=border_width, bg_color=bg_color, fg_color=fg_color, border_color=border_color) + self._parent_canvas = tkinter.Canvas(master=self._parent_frame, highlightthickness=0) self._set_scroll_increments() if self._orientation == "horizontal": - self.scrollbar = CTkScrollbar(master=self.parent_frame, orientation="horizontal", command=self.parent_canvas.xview) - self.parent_canvas.configure(xscrollcommand=self.scrollbar.set) + self._scrollbar = CTkScrollbar(master=self._parent_frame, orientation="horizontal", command=self._parent_canvas.xview, + fg_color=scrollbar_fg_color, button_color=scrollbar_button_color, button_hover_color=scrollbar_button_hover_color) + self._parent_canvas.configure(xscrollcommand=self._scrollbar.set) elif self._orientation == "vertical": - self.scrollbar = CTkScrollbar(master=self.parent_frame, orientation="vertical", command=self.parent_canvas.yview) - self.parent_canvas.configure(yscrollcommand=self.scrollbar.set) + self._scrollbar = CTkScrollbar(master=self._parent_frame, orientation="vertical", command=self._parent_canvas.yview, + fg_color=scrollbar_fg_color, button_color=scrollbar_button_color, button_hover_color=scrollbar_button_hover_color) + self._parent_canvas.configure(yscrollcommand=self._scrollbar.set) + + self._label_text = label_text + self._label = CTkLabel(self._parent_frame, text=label_text, anchor=label_anchor, font=label_font, + corner_radius=self._parent_frame.cget("corner_radius"), text_color=label_text_color, + fg_color=ThemeManager.theme["CTkScrollableFrame"]["label_fg_color"] if label_fg_color is None else label_fg_color) + + tkinter.Frame.__init__(self, master=self._parent_canvas, highlightthickness=0) + CTkAppearanceModeBaseClass.__init__(self) + CTkScalingBaseClass.__init__(self, scaling_type="widget") + self._create_grid() - tkinter.Frame.__init__(self, master=self.parent_canvas, highlightthickness=0) - CTkAppearanceModeBaseClass.__init__(self) + self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) - self.bind("", lambda e: self.parent_canvas.configure(scrollregion=self.parent_canvas.bbox("all"))) - self.parent_canvas.bind("", self._fit_frame_dimensions_to_canvas) - self.bind_all("", self._mouse_wheel_all) - self.bind_all("", self._keyboard_shift_press_all) - self.bind_all("", self._keyboard_shift_press_all) - self.bind_all("", self._keyboard_shift_release_all) - self.bind_all("", self._keyboard_shift_release_all) - self._create_window_id = self.parent_canvas.create_window(0, 0, window=self, anchor="nw") + self.bind("", lambda e: self._parent_canvas.configure(scrollregion=self._parent_canvas.bbox("all"))) + self._parent_canvas.bind("", self._fit_frame_dimensions_to_canvas) + self.bind_all("", self._mouse_wheel_all, add="+") + self.bind_all("", self._keyboard_shift_press_all, add="+") + self.bind_all("", self._keyboard_shift_press_all, add="+") + self.bind_all("", self._keyboard_shift_release_all, add="+") + self.bind_all("", self._keyboard_shift_release_all, add="+") + self._create_window_id = self._parent_canvas.create_window(0, 0, window=self, anchor="nw") - tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self.parent_frame.cget("fg_color"))) + if self._parent_frame.cget("fg_color") == "transparent": + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + else: + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) self._shift_pressed = False def destroy(self): tkinter.Frame.destroy(self) CTkAppearanceModeBaseClass.destroy(self) + CTkScalingBaseClass.destroy(self) + + def _create_grid(self): + border_spacing = self._apply_widget_scaling(self._parent_frame.cget("corner_radius") + self._parent_frame.cget("border_width")) + + if self._orientation == "horizontal": + self._parent_frame.grid_columnconfigure(0, weight=1) + self._parent_frame.grid_rowconfigure(1, weight=1) + self._parent_canvas.grid(row=1, column=0, sticky="nsew", padx=border_spacing, pady=(border_spacing, 0)) + self._scrollbar.grid(row=2, column=0, sticky="nsew", padx=border_spacing) + + if self._label_text is not None and self._label_text != "": + self._label.grid(row=0, column=0, sticky="ew", padx=border_spacing, pady=border_spacing) + else: + self._label.grid_forget() + + elif self._orientation == "vertical": + self._parent_frame.grid_columnconfigure(0, weight=1) + self._parent_frame.grid_rowconfigure(1, weight=1) + self._parent_canvas.grid(row=1, column=0, sticky="nsew", padx=(border_spacing, 0), pady=border_spacing) + self._scrollbar.grid(row=1, column=1, sticky="nsew", padx=border_spacing) + + if self._label_text is not None and self._label_text != "": + self._label.grid(row=0, column=0, columnspan=2, sticky="ew", padx=border_spacing, pady=border_spacing) + else: + self._label.grid_forget() def _set_appearance_mode(self, mode_string): super()._set_appearance_mode(mode_string) - tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self.parent_frame.cget("fg_color"))) + + if self._parent_frame.cget("fg_color") == "transparent": + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + else: + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + + def _set_scaling(self, new_widget_scaling, new_window_scaling): + super()._set_scaling(new_widget_scaling, new_window_scaling) + + self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def _set_dimensions(self, width=None, height=None): + if width is not None: + self._desired_width = width + if height is not None: + self._desired_height = height + + self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) def configure(self, **kwargs): - if "fg_color" in kwargs: - self.parent_frame.configure(fg_color=kwargs.pop("fg_color")) - tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self.parent_frame.cget("fg_color"))) - - for child in self.winfo_children(): - if isinstance(child, CTkBaseClass): - child.configure(bg_color=self.parent_frame.cget("fg_color")) - if "corner_radius" in kwargs: - self.parent_frame.configure(corner_radius=kwargs.pop("corner_radius")) + new_corner_radius = kwargs.pop("corner_radius") + self._parent_frame.configure(corner_radius=new_corner_radius) + if self._label is not None: + self._label.configure(corner_radius=new_corner_radius) self._create_grid() if "border_width" in kwargs: - self.parent_frame.configure(border_width=kwargs.pop("border_width")) + self._parent_frame.configure(border_width=kwargs.pop("border_width")) self._create_grid() - self.parent_frame.configure(**kwargs) + if "fg_color" in kwargs: + self._parent_frame.configure(fg_color=kwargs.pop("fg_color")) + + if self._parent_frame.cget("fg_color") == "transparent": + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color"))) + else: + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color"))) + + for child in self.winfo_children(): + if isinstance(child, CTkBaseClass): + child.configure(bg_color=self._parent_frame.cget("fg_color")) + + if "scrollbar_fg_color" in kwargs: + self._scrollbar.configure(fg_color=kwargs.pop("scrollbar_fg_color")) + + if "scrollbar_button_color" in kwargs: + self._scrollbar.configure(fg_color=kwargs.pop("scrollbar_button_color")) + + if "scrollbar_button_hover_color" in kwargs: + self._scrollbar.configure(fg_color=kwargs.pop("scrollbar_button_hover_color")) + + if "width" in kwargs: + self._set_dimensions(width=kwargs.pop("width")) + + if "height" in kwargs: + self._set_dimensions(height=kwargs.pop("height")) + + if "label_text" in kwargs: + self._label_text = kwargs.pop("label_text") + self._label.configure(text=self._label_text) + self._create_grid() + + if "label_font" in kwargs: + self._label.configure(font=kwargs.pop("label_font")) + + if "label_text_color" in kwargs: + self._label.configure(text_color=kwargs.pop("label_text_color")) + + if "label_fg_color" in kwargs: + self._label.configure(fg_color=kwargs.pop("label_fg_color")) + + if "label_anchor" in kwargs: + self._label.configure(anchor=kwargs.pop("label_anchor")) + + self._parent_frame.configure(**kwargs) + + def cget(self, attribute_name: str): + if attribute_name == "width": + return self._desired_width + elif attribute_name == "height": + return self._desired_height + + elif attribute_name == "label_text": + return self._label_text + elif attribute_name == "label_font": + return self._label.cget("font") + elif attribute_name == "label_text_color": + return self._label.cget("_text_color") + elif attribute_name == "label_fg_color": + return self._label.cget("fg_color") + elif attribute_name == "label_anchor": + return self._label.cget("anchor") + + elif attribute_name.startswith("scrollbar_fg_color"): + return self._scrollbar.cget("fg_color") + elif attribute_name.startswith("scrollbar_button_color"): + return self._scrollbar.cget("button_color") + elif attribute_name.startswith("scrollbar_button_hover_color"): + return self._scrollbar.cget("button_hover_color") + + else: + return self._parent_frame.cget(attribute_name) def _fit_frame_dimensions_to_canvas(self, event): if self._orientation == "horizontal": - self.parent_canvas.itemconfigure(self._create_window_id, height=self.parent_canvas.winfo_height()) + self._parent_canvas.itemconfigure(self._create_window_id, height=self._parent_canvas.winfo_height()) elif self._orientation == "vertical": - self.parent_canvas.itemconfigure(self._create_window_id, width=self.parent_canvas.winfo_width()) + self._parent_canvas.itemconfigure(self._create_window_id, width=self._parent_canvas.winfo_width()) def _set_scroll_increments(self): if sys.platform.startswith("win"): - self.parent_canvas.configure(xscrollincrement=1, yscrollincrement=1) + self._parent_canvas.configure(xscrollincrement=1, yscrollincrement=1) elif sys.platform == "darwin": - self.parent_canvas.configure(xscrollincrement=4, yscrollincrement=8) - - def _create_grid(self): - border_spacing = self.parent_frame.cget("corner_radius") + self.parent_frame.cget("border_width") - self.parent_frame.grid_columnconfigure(0, weight=1) - self.parent_frame.grid_rowconfigure(0, weight=1) - - if self._orientation == "horizontal": - self.parent_frame.grid_rowconfigure(1, weight=0) - self.parent_canvas.grid(row=0, column=0, sticky="nsew", - padx=border_spacing, pady=(border_spacing, 0)) - self.scrollbar.grid(row=1, column=0, sticky="nsew", - padx=border_spacing) - elif self._orientation == "vertical": - self.parent_frame.grid_columnconfigure(1, weight=0) - self.parent_canvas.grid(row=0, column=0, sticky="nsew", - pady=border_spacing, padx=(border_spacing, 0)) - self.scrollbar.grid(row=0, column=1, sticky="nsew", - pady=border_spacing) + self._parent_canvas.configure(xscrollincrement=4, yscrollincrement=8) def _mouse_wheel_all(self, event): if self.check_if_master_is_canvas(event.widget): if sys.platform.startswith("win"): if self._shift_pressed: - if self.parent_canvas.xview() != (0.0, 1.0): - self.parent_canvas.xview("scroll", -int(event.delta/6), "units") + if self._parent_canvas.xview() != (0.0, 1.0): + self._parent_canvas.xview("scroll", -int(event.delta / 6), "units") else: - if self.parent_canvas.yview() != (0.0, 1.0): - self.parent_canvas.yview("scroll", -int(event.delta/6), "units") + if self._parent_canvas.yview() != (0.0, 1.0): + self._parent_canvas.yview("scroll", -int(event.delta / 6), "units") elif sys.platform == "darwin": if self._shift_pressed: - if self.parent_canvas.xview() != (0.0, 1.0): - self.parent_canvas.xview("scroll", -event.delta, "units") + if self._parent_canvas.xview() != (0.0, 1.0): + self._parent_canvas.xview("scroll", -event.delta, "units") else: - if self.parent_canvas.yview() != (0.0, 1.0): - self.parent_canvas.yview("scroll", -event.delta, "units") + if self._parent_canvas.yview() != (0.0, 1.0): + self._parent_canvas.yview("scroll", -event.delta, "units") else: if self._shift_pressed: - if self.parent_canvas.xview() != (0.0, 1.0): - self.parent_canvas.xview("scroll", -event.delta, "units") + if self._parent_canvas.xview() != (0.0, 1.0): + self._parent_canvas.xview("scroll", -event.delta, "units") else: - if self.parent_canvas.yview() != (0.0, 1.0): - self.parent_canvas.yview("scroll", -event.delta, "units") + if self._parent_canvas.yview() != (0.0, 1.0): + self._parent_canvas.yview("scroll", -event.delta, "units") def _keyboard_shift_press_all(self, event): self._shift_pressed = True @@ -146,7 +275,7 @@ class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass): self._shift_pressed = False def check_if_master_is_canvas(self, widget): - if widget == self.parent_canvas: + if widget == self._parent_canvas: return True elif widget.master is not None: return self.check_if_master_is_canvas(widget.master) @@ -154,34 +283,34 @@ class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass): return False def pack(self, **kwargs): - self.parent_frame.pack(**kwargs) + self._parent_frame.pack(**kwargs) def place(self, **kwargs): - self.parent_frame.place(**kwargs) + self._parent_frame.place(**kwargs) def grid(self, **kwargs): - self.parent_frame.grid(**kwargs) + self._parent_frame.grid(**kwargs) def pack_forget(self): - self.parent_frame.pack_forget() + self._parent_frame.pack_forget() def place_forget(self, **kwargs): - self.parent_frame.place_forget() + self._parent_frame.place_forget() def grid_forget(self, **kwargs): - self.parent_frame.grid_forget() + self._parent_frame.grid_forget() def grid_remove(self, **kwargs): - self.parent_frame.grid_remove() + self._parent_frame.grid_remove() def grid_propagate(self, **kwargs): - self.parent_frame.grid_propagate() + self._parent_frame.grid_propagate() def grid_info(self, **kwargs): - self.parent_frame.grid_info() + return self._parent_frame.grid_info() def lift(self, aboveThis=None): - self.parent_frame.lift(aboveThis) + self._parent_frame.lift(aboveThis) def lower(self, belowThis=None): - self.parent_frame.lower(belowThis) + self._parent_frame.lower(belowThis) diff --git a/documentation_images/scrollable_frame_example_Windows.png b/documentation_images/scrollable_frame_example_Windows.png new file mode 100644 index 0000000..9b74602 Binary files /dev/null and b/documentation_images/scrollable_frame_example_Windows.png differ diff --git a/examples/complex_example.py b/examples/complex_example.py index 978a706..072bc64 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -87,21 +87,9 @@ class App(customtkinter.CTk): self.radio_button_3 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=2) self.radio_button_3.grid(row=3, column=2, pady=10, padx=20, sticky="n") - # create checkbox and switch frame - self.checkbox_slider_frame = customtkinter.CTkFrame(self) - self.checkbox_slider_frame.grid(row=1, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew") - self.checkbox_1 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) - self.checkbox_1.grid(row=1, column=0, pady=(20, 10), padx=20, sticky="n") - self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) - self.checkbox_2.grid(row=2, column=0, pady=10, padx=20, sticky="n") - self.switch_1 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame, command=lambda: print("switch 1 toggle")) - self.switch_1.grid(row=3, column=0, pady=10, padx=20, sticky="n") - self.switch_2 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame) - self.switch_2.grid(row=4, column=0, pady=(10, 20), padx=20, sticky="n") - # create slider and progressbar frame self.slider_progressbar_frame = customtkinter.CTkFrame(self, fg_color="transparent") - self.slider_progressbar_frame.grid(row=1, column=1, columnspan=2, padx=(20, 0), pady=(20, 0), sticky="nsew") + self.slider_progressbar_frame.grid(row=1, column=1, padx=(20, 0), pady=(20, 0), sticky="nsew") self.slider_progressbar_frame.grid_columnconfigure(0, weight=1) self.slider_progressbar_frame.grid_rowconfigure(4, weight=1) self.seg_button_1 = customtkinter.CTkSegmentedButton(self.slider_progressbar_frame) @@ -117,12 +105,32 @@ class App(customtkinter.CTk): self.progressbar_3 = customtkinter.CTkProgressBar(self.slider_progressbar_frame, orientation="vertical") self.progressbar_3.grid(row=0, column=2, rowspan=5, padx=(10, 20), pady=(10, 10), sticky="ns") + # create scrollable frame + self.scrollable_frame = customtkinter.CTkScrollableFrame(self, label_text="CTkScrollableFrame") + self.scrollable_frame.grid(row=1, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew") + self.scrollable_frame.grid_columnconfigure(0, weight=1) + self.scrollable_frame_switches = [] + for i in range(100): + switch = customtkinter.CTkSwitch(master=self.scrollable_frame, text=f"CTkSwitch {i}") + switch.grid(row=i, column=0, padx=10, pady=(0, 20)) + self.scrollable_frame_switches.append(switch) + + # create checkbox and switch frame + self.checkbox_slider_frame = customtkinter.CTkFrame(self) + self.checkbox_slider_frame.grid(row=1, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew") + self.checkbox_1 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) + self.checkbox_1.grid(row=1, column=0, pady=(20, 0), padx=20, sticky="n") + self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) + self.checkbox_2.grid(row=2, column=0, pady=(20, 0), padx=20, sticky="n") + self.checkbox_3 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) + self.checkbox_3.grid(row=3, column=0, pady=20, padx=20, sticky="n") + # set default values self.sidebar_button_3.configure(state="disabled", text="Disabled CTkButton") - self.checkbox_2.configure(state="disabled") - self.switch_2.configure(state="disabled") + self.checkbox_3.configure(state="disabled") self.checkbox_1.select() - self.switch_1.select() + self.scrollable_frame_switches[0].select() + self.scrollable_frame_switches[4].select() self.radio_button_3.configure(state="disabled") self.appearance_mode_optionemenu.set("Dark") self.scaling_optionemenu.set("100%") diff --git a/examples/scrollable_frame_example.py b/examples/scrollable_frame_example.py new file mode 100644 index 0000000..06e3986 --- /dev/null +++ b/examples/scrollable_frame_example.py @@ -0,0 +1,132 @@ +import customtkinter +import os +from PIL import Image + + +class ScrollableCheckBoxFrame(customtkinter.CTkScrollableFrame): + def __init__(self, master, item_list, command=None, **kwargs): + super().__init__(master, **kwargs) + + self.command = command + self.checkbox_list = [] + for i, item in enumerate(item_list): + self.add_item(item) + + def add_item(self, item): + checkbox = customtkinter.CTkCheckBox(self, text=item) + if self.command is not None: + checkbox.configure(command=self.command) + checkbox.grid(row=len(self.checkbox_list), column=0, pady=(0, 10)) + self.checkbox_list.append(checkbox) + + def remove_item(self, item): + for checkbox in self.checkbox_list: + if item == checkbox.cget("text"): + checkbox.destroy() + self.checkbox_list.remove(checkbox) + return + + def get_checked_items(self): + return [checkbox.cget("text") for checkbox in self.checkbox_list if checkbox.get() == 1] + + +class ScrollableRadiobuttonFrame(customtkinter.CTkScrollableFrame): + def __init__(self, master, item_list, command=None, **kwargs): + super().__init__(master, **kwargs) + + self.command = command + self.radiobutton_variable = customtkinter.StringVar() + self.radiobutton_list = [] + for i, item in enumerate(item_list): + self.add_item(item) + + def add_item(self, item): + radiobutton = customtkinter.CTkRadioButton(self, text=item, value=item, variable=self.radiobutton_variable) + if self.command is not None: + radiobutton.configure(command=self.command) + radiobutton.grid(row=len(self.radiobutton_list), column=0, pady=(0, 10)) + self.radiobutton_list.append(radiobutton) + + def remove_item(self, item): + for radiobutton in self.radiobutton_list: + if item == radiobutton.cget("text"): + radiobutton.destroy() + self.radiobutton_list.remove(radiobutton) + return + + def get_checked_item(self): + return self.radiobutton_variable.get() + + +class ScrollableLabelButtonFrame(customtkinter.CTkScrollableFrame): + def __init__(self, master, command=None, **kwargs): + super().__init__(master, **kwargs) + self.grid_columnconfigure(0, weight=1) + + self.command = command + self.radiobutton_variable = customtkinter.StringVar() + self.label_list = [] + self.button_list = [] + + def add_item(self, item, image=None): + label = customtkinter.CTkLabel(self, text=item, image=image, compound="left", padx=5, anchor="w") + button = customtkinter.CTkButton(self, text="Command", width=100, height=24) + if self.command is not None: + button.configure(command=lambda: self.command(item)) + label.grid(row=len(self.label_list), column=0, pady=(0, 10), sticky="w") + button.grid(row=len(self.button_list), column=1, pady=(0, 10), padx=5) + self.label_list.append(label) + self.button_list.append(button) + + def remove_item(self, item): + for label, button in zip(self.label_list, self.button_list): + if item == label.cget("text"): + label.destroy() + button.destroy() + self.label_list.remove(label) + self.button_list.remove(button) + return + + +class App(customtkinter.CTk): + def __init__(self): + super().__init__() + + self.title("CTkScrollableFrame example") + self.grid_rowconfigure(0, weight=1) + self.columnconfigure(2, weight=1) + + # create scrollable checkbox frame + self.scrollable_checkbox_frame = ScrollableCheckBoxFrame(master=self, width=200, command=self.checkbox_frame_event, + item_list=[f"item {i}" for i in range(50)]) + self.scrollable_checkbox_frame.grid(row=0, column=0, padx=15, pady=15, sticky="ns") + self.scrollable_checkbox_frame.add_item("new item") + + # create scrollable radiobutton frame + self.scrollable_radiobutton_frame = ScrollableRadiobuttonFrame(master=self, width=500, command=self.radiobutton_frame_event, + item_list=[f"item {i}" for i in range(100)]) + self.scrollable_radiobutton_frame.grid(row=0, column=1, padx=15, pady=15, sticky="ns") + self.scrollable_radiobutton_frame.configure(width=200) + self.scrollable_radiobutton_frame.remove_item("item 3") + + # create scrollable label and button frame + current_dir = os.path.dirname(os.path.abspath(__file__)) + self.scrollable_label_button_frame = ScrollableLabelButtonFrame(master=self, width=300, command=self.label_button_frame_event, corner_radius=0) + self.scrollable_label_button_frame.grid(row=0, column=2, padx=0, pady=0, sticky="nsew") + for i in range(20): # add items with images + self.scrollable_label_button_frame.add_item(f"image and item {i}", image=customtkinter.CTkImage(Image.open(os.path.join(current_dir, "test_images", "chat_light.png")))) + + def checkbox_frame_event(self): + print(f"checkbox frame modified: {self.scrollable_checkbox_frame.get_checked_items()}") + + def radiobutton_frame_event(self): + print(f"radiobutton frame modified: {self.scrollable_radiobutton_frame.get_checked_item()}") + + def label_button_frame_event(self, item): + print(f"label button frame clicked: {item}") + + +if __name__ == "__main__": + customtkinter.set_appearance_mode("dark") + app = App() + app.mainloop() diff --git a/test/manual_integration_tests/test_scrollable_frame.py b/test/manual_integration_tests/test_scrollable_frame.py new file mode 100644 index 0000000..c7c0127 --- /dev/null +++ b/test/manual_integration_tests/test_scrollable_frame.py @@ -0,0 +1,37 @@ +import customtkinter + + +app = customtkinter.CTk() +app.grid_columnconfigure(2, weight=1) +app.grid_rowconfigure(1, weight=1) + +toplevel = customtkinter.CTkToplevel() +switch = customtkinter.CTkSwitch(toplevel, text="Mode", command=lambda: customtkinter.set_appearance_mode("dark" if switch.get() == 1 else "light")) +switch.grid(row=0, column=0, padx=50, pady=50) + +frame_1 = customtkinter.CTkScrollableFrame(app, orientation="vertical", label_text="should not appear", fg_color="transparent") +frame_1.grid(row=0, column=0, padx=20, pady=20) +frame_1.configure(label_text=None) + +frame_2 = customtkinter.CTkScrollableFrame(app, orientation="vertical", label_text="CTkScrollableFrame") +frame_2.grid(row=1, column=0, padx=20, pady=20) + +frame_3 = customtkinter.CTkScrollableFrame(app, orientation="horizontal") +frame_3.grid(row=0, column=1, padx=20, pady=20) + +frame_4 = customtkinter.CTkScrollableFrame(app, orientation="horizontal", label_fg_color="transparent") +frame_4.grid(row=1, column=1, padx=20, pady=20) +frame_4.configure(label_text="CTkScrollableFrame") + +frame_5 = customtkinter.CTkScrollableFrame(app, orientation="vertical", label_text="CTkScrollableFrame", corner_radius=0) +frame_5.grid(row=0, column=2, rowspan=2, sticky="nsew") + +for i in range(100): + customtkinter.CTkCheckBox(frame_1).grid(row=i, padx=10, pady=10) + customtkinter.CTkCheckBox(frame_2).grid(row=i, padx=10, pady=10) + customtkinter.CTkCheckBox(frame_3).grid(row=0, column=i, padx=10, pady=10) + customtkinter.CTkCheckBox(frame_4).grid(row=0, column=i, padx=10, pady=10) + customtkinter.CTkCheckBox(frame_5).grid(row=i, padx=10, pady=10) + + +app.mainloop()