From 79ecd2e94611f6173353649551cacd62bd3acb66 Mon Sep 17 00:00:00 2001 From: Tom Schimansky Date: Sun, 19 Jun 2022 21:16:19 +0200 Subject: [PATCH] added CTkScrollbar --- customtkinter/__init__.py | 1 + customtkinter/assets/themes/blue.json | 8 +- customtkinter/draw_engine.py | 167 +++++++++++++-- customtkinter/widgets/ctk_scrollbar.py | 200 ++++++++++++++++++ customtkinter/widgets/ctk_slider.py | 8 +- .../test_scrollbar.py | 37 ++++ 6 files changed, 403 insertions(+), 18 deletions(-) create mode 100644 customtkinter/widgets/ctk_scrollbar.py create mode 100644 test/manual_integration_tests/test_scrollbar.py diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index de749d3..bb59d8d 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -53,6 +53,7 @@ from .widgets.ctk_canvas import CTkCanvas from .widgets.ctk_switch import CTkSwitch from .widgets.ctk_optionmenu import CTkOptionMenu from .widgets.ctk_combobox import CTkComboBox +from .widgets.ctk_scrollbar import CTkScrollbar # import windows from .windows.ctk_tk import CTk diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json index 7c17eb5..24b2aab 100644 --- a/customtkinter/assets/themes/blue.json +++ b/customtkinter/assets/themes/blue.json @@ -33,7 +33,9 @@ "combobox_button_hover": ["#6E7174", "#7A848D"], "dropdown_color": ["gray90", "gray20"], "dropdown_hover": ["gray75", "gray28"], - "dropdown_text": ["gray10", "#DCE4EE"] + "dropdown_text": ["gray10", "#DCE4EE"], + "scrollbar": ["gray75", "gray25"], + "scrollbar_hover": ["gray50", "gray40"] }, "text": { "macOS": { @@ -70,6 +72,8 @@ "switch_border_width": 3, "switch_corner_radius": 1000, "switch_button_corner_radius": 1000, - "switch_button_length": 0 + "switch_button_length": 0, + "scrollbar_corner_radius": 6, + "scrollbar_border_spacing": 4 } } diff --git a/customtkinter/draw_engine.py b/customtkinter/draw_engine.py index 8f2a2f5..9af3347 100644 --- a/customtkinter/draw_engine.py +++ b/customtkinter/draw_engine.py @@ -20,6 +20,7 @@ class DrawEngine: - draw_rounded_rect_with_border_vertical_split() - draw_rounded_progress_bar_with_border() - draw_rounded_slider_with_border_and_button() + - draw_rounded_scrollbar() - draw_checkmark() - draw_dropdown_arrow() @@ -38,7 +39,7 @@ class DrawEngine: else: return round(user_corner_radius) - # optimize forx drawing with antialiased font shapes + # optimize for drawing with antialiased font shapes elif self.preferred_drawing_method == "font_shapes": return round(user_corner_radius) @@ -490,22 +491,27 @@ class DrawEngine: # create canvas border corner parts if not already created, but only if needed, and delete if not needed if not self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" not in exclude_parts: self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_a", "border_corner_part", "border_parts_left", "border_parts", "left_parts"), anchor=tkinter.CENTER) - self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_b", "border_corner_part", "border_parts_left", "border_parts", "left_parts"), anchor=tkinter.CENTER, angle=180) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_b", "border_corner_part", "border_parts_left", "border_parts", "left_parts"), anchor=tkinter.CENTER, + angle=180) requires_recoloring = True elif self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" in exclude_parts: self._canvas.delete("border_oval_1_a", "border_oval_1_b") if not self._canvas.find_withtag("border_oval_2_a") and width > 2 * corner_radius and "border_oval_2" not in exclude_parts: - self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_a", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), anchor=tkinter.CENTER) - self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_b", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), anchor=tkinter.CENTER, angle=180) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_a", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), + anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_b", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), + anchor=tkinter.CENTER, angle=180) requires_recoloring = True elif self._canvas.find_withtag("border_oval_2_a") and (not width > 2 * corner_radius or "border_oval_2" in exclude_parts): self._canvas.delete("border_oval_2_a", "border_oval_2_b") if not self._canvas.find_withtag("border_oval_3_a") and height > 2 * corner_radius \ and width > 2 * corner_radius and "border_oval_3" not in exclude_parts: - self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_a", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), anchor=tkinter.CENTER) - self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), anchor=tkinter.CENTER, angle=180) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_a", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), + anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_parts_right", "border_parts", "right_parts"), + anchor=tkinter.CENTER, angle=180) requires_recoloring = True elif self._canvas.find_withtag("border_oval_3_a") and (not (height > 2 * corner_radius and width > 2 * corner_radius) or "border_oval_3" in exclude_parts): @@ -513,7 +519,8 @@ class DrawEngine: if not self._canvas.find_withtag("border_oval_4_a") and height > 2 * corner_radius and "border_oval_4" not in exclude_parts: self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_a", "border_corner_part", "border_parts_left", "border_parts", "left_parts"), anchor=tkinter.CENTER) - self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_b", "border_corner_part", "border_parts_left", "border_parts", "left_parts"), anchor=tkinter.CENTER, angle=180) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_b", "border_corner_part", "border_parts_left", "border_parts", "left_parts"), anchor=tkinter.CENTER, + angle=180) requires_recoloring = True elif self._canvas.find_withtag("border_oval_4_a") and (not height > 2 * corner_radius or "border_oval_4" in exclude_parts): self._canvas.delete("border_oval_4_a", "border_oval_4_b") @@ -554,14 +561,16 @@ class DrawEngine: # create canvas border corner parts if not already created, but only if they're needed and delete if not needed if not self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" not in exclude_parts: self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_a", "inner_corner_part", "inner_parts_left", "inner_parts", "left_parts"), anchor=tkinter.CENTER) - self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_b", "inner_corner_part", "inner_parts_left", "inner_parts", "left_parts"), anchor=tkinter.CENTER, angle=180) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_b", "inner_corner_part", "inner_parts_left", "inner_parts", "left_parts"), anchor=tkinter.CENTER, + angle=180) requires_recoloring = True elif self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" in exclude_parts: self._canvas.delete("inner_oval_1_a", "inner_oval_1_b") if not self._canvas.find_withtag("inner_oval_2_a") and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_2" not in exclude_parts: - self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_a", "inner_corner_part","inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER) - self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_b", "inner_corner_part", "inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER, angle=180) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_a", "inner_corner_part", "inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_b", "inner_corner_part", "inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER, + angle=180) requires_recoloring = True elif self._canvas.find_withtag("inner_oval_2_a") and (not width - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_2" in exclude_parts): self._canvas.delete("inner_oval_2_a", "inner_oval_2_b") @@ -569,7 +578,8 @@ class DrawEngine: if not self._canvas.find_withtag("inner_oval_3_a") and height - (2 * border_width) > 2 * inner_corner_radius \ and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_3" not in exclude_parts: self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_a", "inner_corner_part", "inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER) - self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_b", "inner_corner_part", "inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER, angle=180) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_b", "inner_corner_part", "inner_parts_right", "inner_parts", "right_parts"), anchor=tkinter.CENTER, + angle=180) requires_recoloring = True elif self._canvas.find_withtag("inner_oval_3_a") and (not (height - (2 * border_width) > 2 * inner_corner_radius and width - (2 * border_width) > 2 * inner_corner_radius) or "inner_oval_3" in exclude_parts): @@ -577,7 +587,8 @@ class DrawEngine: if not self._canvas.find_withtag("inner_oval_4_a") and height - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_4" not in exclude_parts: self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_a", "inner_corner_part", "inner_parts_left", "inner_parts", "left_parts"), anchor=tkinter.CENTER) - self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_b", "inner_corner_part", "inner_parts_left", "inner_parts", "left_parts"), anchor=tkinter.CENTER, angle=180) + self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_b", "inner_corner_part", "inner_parts_left", "inner_parts", "left_parts"), anchor=tkinter.CENTER, + angle=180) requires_recoloring = True elif self._canvas.find_withtag("inner_oval_4_a") and (not height - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_4" in exclude_parts): self._canvas.delete("inner_oval_4_a", "inner_oval_4_b") @@ -959,6 +970,138 @@ class DrawEngine: return requires_recoloring + def draw_rounded_scrollbar(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], + border_spacing: Union[float, int], start_value: float, end_value: float, orientation: str) -> bool: + width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only + height = math.floor(height / 2) * 2 + + if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger + corner_radius = min(width / 2, height / 2) + + border_spacing = round(border_spacing) + corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding) + + if corner_radius >= border_spacing: + inner_corner_radius = corner_radius - border_spacing + else: + inner_corner_radius = 0 + + if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": + return self.__draw_rounded_scrollbar_polygon_shapes(width, height, corner_radius, inner_corner_radius, + start_value, end_value, orientation) + elif self.preferred_drawing_method == "font_shapes": + return self.__draw_rounded_scrollbar_font_shapes(width, height, corner_radius, inner_corner_radius, + start_value, end_value, orientation) + + def __draw_rounded_scrollbar_polygon_shapes(self, width: int, height: int, corner_radius: int, inner_corner_radius: int, + start_value: float, end_value: float, orientation: str) -> bool: + requires_recoloring = False + + if not self._canvas.find_withtag("border_parts"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_parts"), width=0) + requires_recoloring = True + self._canvas.coords("border_rectangle_1", 0, 0, width, height) + + if not self._canvas.find_withtag("scrollbar_parts"): + self._canvas.create_polygon((0, 0, 0, 0), tags=("scrollbar_polygon_1", "scrollbar_parts"), joinstyle=tkinter.ROUND) + self._canvas.tag_raise("scrollbar_parts", "border_parts") + requires_recoloring = True + + if orientation == "vertical": + self._canvas.coords("scrollbar_polygon_1", + corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, + width - corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, + width - corner_radius, corner_radius + (height - 2 * corner_radius) * end_value, + corner_radius, corner_radius + (height - 2 * corner_radius) * end_value) + elif orientation == "horizontal": + self._canvas.coords("scrollbar_polygon_1", + corner_radius + (width - 2 * corner_radius) * start_value, corner_radius, + corner_radius + (width - 2 * corner_radius) * end_value, corner_radius, + corner_radius + (width - 2 * corner_radius) * end_value, height - corner_radius, + corner_radius + (width - 2 * corner_radius) * start_value, height - corner_radius,) + + self._canvas.itemconfig("scrollbar_polygon_1", width=inner_corner_radius * 2) + + return requires_recoloring + + def __draw_rounded_scrollbar_font_shapes(self, width: int, height: int, corner_radius: int, inner_corner_radius: int, + start_value: float, end_value: float, orientation: str) -> bool: + requires_recoloring = False + + if not self._canvas.find_withtag("border_parts"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_parts"), width=0) + requires_recoloring = True + self._canvas.coords("border_rectangle_1", 0, 0, width, height) + + if inner_corner_radius > 0: + if not self._canvas.find_withtag("scrollbar_oval_1_a"): + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_1_a", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_1_b", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + + if not self._canvas.find_withtag("scrollbar_oval_2_a") and width > 2 * corner_radius: + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_2_a", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_2_b", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("scrollbar_oval_2_a") and not width > 2 * corner_radius: + self._canvas.delete("scrollbar_oval_2_a", "scrollbar_oval_2_b") + + if not self._canvas.find_withtag("scrollbar_oval_3_a") and height > 2 * corner_radius and width > 2 * corner_radius: + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_3_a", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_3_b", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("scrollbar_oval_3_a") and not (height > 2 * corner_radius and width > 2 * corner_radius): + self._canvas.delete("scrollbar_oval_3_a", "scrollbar_oval_3_b") + + if not self._canvas.find_withtag("scrollbar_oval_4_a") and height > 2 * corner_radius: + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_4_a", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER) + self._canvas.create_aa_circle(0, 0, 0, tags=("scrollbar_oval_4_b", "scrollbar_corner_part", "scrollbar_parts"), anchor=tkinter.CENTER, angle=180) + requires_recoloring = True + elif self._canvas.find_withtag("scrollbar_oval_4_a") and not height > 2 * corner_radius: + self._canvas.delete("scrollbar_oval_4_a", "scrollbar_oval_4_b") + else: + self._canvas.delete("scrollbar_corner_part") + + if not self._canvas.find_withtag("scrollbar_rectangle_1") and width > 2 * corner_radius: + self._canvas.create_rectangle(0, 0, 0, 0, tags=("scrollbar_rectangle_1", "scrollbar_rectangle_part", "scrollbar_parts"), width=0) + requires_recoloring = True + elif self._canvas.find_withtag("scrollbar_rectangle_1") and not width > 2 * corner_radius: + self._canvas.delete("scrollbar_rectangle_1") + + if not self._canvas.find_withtag("scrollbar_rectangle_2"): + self._canvas.create_rectangle(0, 0, 0, 0, tags=("scrollbar_rectangle_2", "scrollbar_rectangle_part", "scrollbar_parts"), width=0) + requires_recoloring = True + elif self._canvas.find_withtag("scrollbar_rectangle_2") and not height > 2 * corner_radius: + self._canvas.delete("scrollbar_rectangle_2") + + self._canvas.coords("scrollbar_rectangle_1", + corner_radius - inner_corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, + width - (corner_radius - inner_corner_radius), corner_radius + (height - 2 * corner_radius) * end_value) + self._canvas.coords("scrollbar_rectangle_2", + corner_radius, corner_radius - inner_corner_radius + (height - 2 * corner_radius) * start_value, + width - (corner_radius), corner_radius + inner_corner_radius + (height - 2 * corner_radius) * end_value) + + if orientation == "vertical": + self._canvas.coords("scrollbar_oval_1_a", corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_1_b", corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_2_a", width - corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_2_b", width - corner_radius, corner_radius + (height - 2 * corner_radius) * start_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_3_a", width - corner_radius, corner_radius + (height - 2 * corner_radius) * end_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_3_b", width - corner_radius, corner_radius + (height - 2 * corner_radius) * end_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_4_a", corner_radius, corner_radius + (height - 2 * corner_radius) * end_value, inner_corner_radius) + self._canvas.coords("scrollbar_oval_4_b", corner_radius, corner_radius + (height - 2 * corner_radius) * end_value, inner_corner_radius) + if orientation == "horizontal": + self._canvas.coords("scrollbar_oval_1_a", corner_radius + (width - 2 * corner_radius) * start_value, corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_1_b", corner_radius + (width - 2 * corner_radius) * start_value, corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_2_a", corner_radius + (width - 2 * corner_radius) * end_value, corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_2_b", corner_radius + (width - 2 * corner_radius) * end_value, corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_3_a", corner_radius + (width - 2 * corner_radius) * end_value, height - corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_3_b", corner_radius + (width - 2 * corner_radius) * end_value, height - corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_4_a", corner_radius + (width - 2 * corner_radius) * start_value, height - corner_radius, inner_corner_radius) + self._canvas.coords("scrollbar_oval_4_b", corner_radius + (width - 2 * corner_radius) * start_value, height - corner_radius, inner_corner_radius) + + return requires_recoloring + def draw_checkmark(self, width: Union[float, int], height: Union[float, int], size: Union[int, float]) -> bool: """ Draws a rounded rectangle with a corner_radius and border_width on the canvas. The border elements have a 'border_parts' tag, the main foreground elements have an 'inner_parts' tag to color the elements accordingly. diff --git a/customtkinter/widgets/ctk_scrollbar.py b/customtkinter/widgets/ctk_scrollbar.py new file mode 100644 index 0000000..4382d88 --- /dev/null +++ b/customtkinter/widgets/ctk_scrollbar.py @@ -0,0 +1,200 @@ +from .ctk_canvas import CTkCanvas +from ..theme_manager import ThemeManager +from ..draw_engine import DrawEngine +from .widget_base_class import CTkBaseClass + + +class CTkScrollbar(CTkBaseClass): + def __init__(self, *args, + bg_color=None, + fg_color=None, + scrollbar_color="default_theme", + scrollbar_hover_color="default_theme", + border_spacing="default_theme", + corner_radius="default_theme", + width=None, + height=None, + orientation="vertical", + command=None, + hover=True, + **kwargs): + + # set default dimensions according to orientation + if width is None: + if orientation.lower() == "vertical": + width = 16 + else: + width = 200 + if height is None: + if orientation.lower() == "vertical": + height = 200 + else: + height = 16 + + # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass + super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) + + # color + self.fg_color = fg_color + self.scrollbar_color = ThemeManager.theme["color"]["scrollbar"] if scrollbar_color == "default_theme" else scrollbar_color + self.scrollbar_hover_color = ThemeManager.theme["color"]["scrollbar_hover"] if scrollbar_hover_color == "default_theme" else scrollbar_hover_color + + # shape + self.corner_radius = ThemeManager.theme["shape"]["scrollbar_corner_radius"] if corner_radius == "default_theme" else corner_radius + self.border_spacing = ThemeManager.theme["shape"]["scrollbar_border_spacing"] if border_spacing == "default_theme" else border_spacing + + self.hover = hover + self.hover_state = False + self.command = command + self.orientation = orientation + self.start_value: float = 0 # 0 to 1 + self.end_value: float = 1 # 0 to 1 + + self.canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self.apply_widget_scaling(self._current_width), + height=self.apply_widget_scaling(self._current_height)) + self.canvas.place(x=0, y=0, relwidth=1, relheight=1) + self.canvas.configure(bg="green") + self.draw_engine = DrawEngine(self.canvas) + + self.canvas.bind("", self.on_enter) + self.canvas.bind("", self.on_leave) + self.canvas.tag_bind("border_parts", "", self.clicked) + self.canvas.bind("", self.clicked) + self.canvas.bind("", self.mouse_scroll_event) + self.bind('', self.update_dimensions_event) + + self.draw() + + def set_scaling(self, *args, **kwargs): + super().set_scaling(*args, **kwargs) + + self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height)) + self.draw(no_color_updates=True) + + def set_dimensions(self, width=None, height=None): + super().set_dimensions(width, height) + + self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), + height=self.apply_widget_scaling(self._desired_height)) + self.draw(no_color_updates=True) + + def draw(self, no_color_updates=False): + requires_recoloring = self.draw_engine.draw_rounded_scrollbar(self.apply_widget_scaling(self._current_width), + self.apply_widget_scaling(self._current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_spacing), + self.start_value, self.end_value, + self.orientation) + + if no_color_updates is False or requires_recoloring: + if self.hover_state is True: + self.canvas.itemconfig("scrollbar_parts", + fill=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode), + outline=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode)) + else: + self.canvas.itemconfig("scrollbar_parts", + fill=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode), + outline=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode)) + + if self.fg_color is None: + self.canvas.itemconfig("border_parts", + fill=ThemeManager.single_color(self.bg_color, self._appearance_mode), + outline=ThemeManager.single_color(self.bg_color, self._appearance_mode)) + else: + self.canvas.itemconfig("border_parts", + fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), + outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) + + def set(self, start_value: float, end_value: float): + self.start_value = float(start_value) + self.end_value = float(end_value) + self.scrollbar_height = self.end_value - self.start_value + self.draw() + + def get(self): + return self.start_value, self.end_value + + def configure(self, *args, **kwargs): + require_redraw = False # some attribute changes require a call of self.draw() at the end + + if "bg_color" in kwargs: + if kwargs["bg_color"] is None: + self.bg_color = self.detect_color_of_master() + else: + self.bg_color = kwargs["bg_color"] + require_redraw = True + del kwargs["bg_color"] + + if "fg_color" in kwargs: + self.fg_color = kwargs["fg_color"] + require_redraw = True + del kwargs["fg_color"] + + if "scrollbar_color" in kwargs: + self.scrollbar_color = kwargs["scrollbar_color"] + require_redraw = True + del kwargs["scrollbar_color"] + + if "scrollbar_hover_color" in kwargs: + self.scrollbar_hover_color = kwargs["scrollbar_hover_color"] + require_redraw = True + del kwargs["scrollbar_hover_color"] + + if "command" in kwargs: + self.command = kwargs["command"] + del kwargs["command"] + + if "corner_radius" in kwargs: + self.corner_radius = kwargs["corner_radius"] + require_redraw = True + del kwargs["corner_radius"] + + if "border_spacing" in kwargs: + self.border_spacing = kwargs["border_spacing"] + require_redraw = True + del kwargs["border_spacing"] + + if "width" in kwargs: + self.set_dimensions(width=kwargs["width"]) + del kwargs["width"] + + if "height" in kwargs: + self.set_dimensions(height=kwargs["height"]) + del kwargs["height"] + + super().configure(*args, **kwargs) + + if require_redraw: + self.draw() + + def on_enter(self, event=0): + if self.hover is True: + self.hover_state = True + self.canvas.itemconfig("scrollbar_parts", + outline=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode), + fill=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode)) + + def on_leave(self, event=0): + self.hover_state = False + self.canvas.itemconfig("scrollbar_parts", + outline=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode), + fill=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode)) + + def clicked(self, event): + if self.orientation == "vertical": + value = ((event.y - self.border_spacing) / (self._current_height - 2 * self.border_spacing)) / self._widget_scaling + current_scrollbar_length = self.end_value - self.start_value + value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2))) + self.start_value = value - (current_scrollbar_length / 2) + self.end_value = value + (current_scrollbar_length / 2) + self.draw() + + if self.command is not None: + self.command('moveto', self.start_value) + + def mouse_scroll_event(self, event=None): + if self.command is not None: + self.command('scroll', event.delta, 'units') + diff --git a/customtkinter/widgets/ctk_slider.py b/customtkinter/widgets/ctk_slider.py index 4712885..94889d3 100644 --- a/customtkinter/widgets/ctk_slider.py +++ b/customtkinter/widgets/ctk_slider.py @@ -61,7 +61,7 @@ class CTkSlider(CTkBaseClass): self.border_width = ThemeManager.theme["shape"]["slider_border_width"] if border_width == "default_theme" else border_width self.button_length = ThemeManager.theme["shape"]["slider_button_length"] if button_length == "default_theme" else button_length self.value = 0.5 # initial value of slider in percent - self.orient = orient + self.orientation = orient self.hover_state = False self.from_ = from_ self.to = to @@ -139,9 +139,9 @@ class CTkSlider(CTkBaseClass): self.configure(cursor="arrow") def draw(self, no_color_updates=False): - if self.orient.lower() == "horizontal": + if self.orientation.lower() == "horizontal": orientation = "w" - elif self.orient.lower() == "vertical": + elif self.orientation.lower() == "vertical": orientation = "s" else: orientation = "w" @@ -185,7 +185,7 @@ class CTkSlider(CTkBaseClass): def clicked(self, event=None): if self.state == "normal": - if self.orient.lower() == "horizontal": + if self.orientation.lower() == "horizontal": self.value = (event.x / self._current_width) / self._widget_scaling else: self.value = 1 - (event.y / self._current_height) / self._widget_scaling diff --git a/test/manual_integration_tests/test_scrollbar.py b/test/manual_integration_tests/test_scrollbar.py new file mode 100644 index 0000000..8b63c74 --- /dev/null +++ b/test/manual_integration_tests/test_scrollbar.py @@ -0,0 +1,37 @@ +import tkinter +import customtkinter + +# customtkinter.DrawEngine.preferred_drawing_method = "font_shapes" + + +def to_scollbar(*args, **kwargs): + tk_textbox_scrollbar.set(*args, **kwargs) + ctk_textbox_scrollbar.set(*args, **kwargs) + ctk_textbox_scrollbar.update_idletasks() + tk_textbox_scrollbar.update_idletasks() + print("to_scollbar:", args, **kwargs) + + +def from_scrollbar(*args, **kwargs): + tk_textbox.yview(*args, **kwargs) + print("from_scrollbar:", args, **kwargs) + + +app = customtkinter.CTk() +app.grid_rowconfigure(0, weight=1) +app.grid_columnconfigure(0, weight=1) + +tk_textbox = tkinter.Text(app) +tk_textbox.grid(row=0, column=0, sticky="nsew") + +ctk_textbox_scrollbar = customtkinter.CTkScrollbar(app, command=from_scrollbar, fg_color="red") +ctk_textbox_scrollbar.grid(row=0, column=1, padx=0, sticky="ns") + +tk_textbox_scrollbar = tkinter.Scrollbar(app, command=from_scrollbar) +tk_textbox_scrollbar.grid(row=0, column=2, padx=1, sticky="ns") + +tk_textbox.configure(yscrollcommand=to_scollbar) + +tk_textbox.insert("insert", "\n".join([str(i) for i in range(100)])) + +app.mainloop()