""" CTkRangeSlider Range slider for customtkinter Author: Akash Bora Version: 0.3 """ import math import tkinter import sys from typing import Union, Tuple, Callable, Optional, List from customtkinter.windows.widgets.core_rendering import DrawEngine from customtkinter.windows.widgets.theme import ThemeManager from customtkinter.windows.widgets.core_rendering import CTkCanvas from customtkinter.windows.widgets.core_widget_classes import CTkBaseClass class CustomDrawEngine: """ This is a custom version of the core of the CustomTkinter library where all the drawing on the tkinter.Canvas happens. It is tailored towards the range slider. """ # Use circle_shapes on macOS to avoid rectangular slider heads, font_shapes elsewhere preferred_drawing_method: str = "circle_shapes" if sys.platform == "darwin" else "font_shapes" def __init__(self, canvas: CTkCanvas): self._canvas = canvas def __calc_optimal_corner_radius(self, user_corner_radius: Union[float, int]) -> Union[float, int]: # optimize for drawing with polygon shapes if self.preferred_drawing_method == "polygon_shapes": if sys.platform == "darwin": return user_corner_radius else: return round(user_corner_radius) # optimize for drawing with antialiased font shapes elif self.preferred_drawing_method == "font_shapes": return round(user_corner_radius) # optimize for drawing with circles and rects elif self.preferred_drawing_method == "circle_shapes": user_corner_radius = 0.5 * round(user_corner_radius / 0.5) # round to 0.5 steps # make sure the value is always with .5 at the end for smoother corners if user_corner_radius == 0: return 0 elif user_corner_radius % 1 == 0: return user_corner_radius + 0.5 else: return user_corner_radius def __draw_rounded_rect_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool: requires_recoloring = False # create border button parts (only if border exists) if border_width > 0: if not self._canvas.find_withtag("border_parts"): self._canvas.create_polygon((0, 0, 0, 0), tags=("border_line_1", "border_parts")) requires_recoloring = True self._canvas.coords("border_line_1", (corner_radius, corner_radius, width - corner_radius, corner_radius, width - corner_radius, height - corner_radius, corner_radius, height - corner_radius)) self._canvas.itemconfig("border_line_1", joinstyle=tkinter.ROUND, width=corner_radius * 2) else: self._canvas.delete("border_parts") # create inner button parts if not self._canvas.find_withtag("inner_parts"): self._canvas.create_polygon((0, 0, 0, 0), tags=("inner_line_1", "inner_parts"), joinstyle=tkinter.ROUND) requires_recoloring = True if corner_radius <= border_width: bottom_right_shift = -1 # weird canvas rendering inaccuracy that has to be corrected in some cases else: bottom_right_shift = 0 self._canvas.coords("inner_line_1", border_width + inner_corner_radius, border_width + inner_corner_radius, width - (border_width + inner_corner_radius) + bottom_right_shift, border_width + inner_corner_radius, width - (border_width + inner_corner_radius) + bottom_right_shift, height - (border_width + inner_corner_radius) + bottom_right_shift, border_width + inner_corner_radius, height - (border_width + inner_corner_radius) + bottom_right_shift) self._canvas.itemconfig("inner_line_1", width=inner_corner_radius * 2) if requires_recoloring: # new parts were added -> manage z-order self._canvas.tag_lower("inner_parts") self._canvas.tag_lower("border_parts") return requires_recoloring def __draw_rounded_rect_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, exclude_parts: tuple) -> bool: requires_recoloring = False # create border button parts if border_width > 0: if corner_radius > 0: # 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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_b", "border_corner_part", "border_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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_b", "border_corner_part", "border_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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_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): self._canvas.delete("border_oval_3_a", "border_oval_3_b") 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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_b", "border_corner_part", "border_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") # change position of border corner parts self._canvas.coords("border_oval_1_a", corner_radius, corner_radius, corner_radius) self._canvas.coords("border_oval_1_b", corner_radius, corner_radius, corner_radius) self._canvas.coords("border_oval_2_a", width - corner_radius, corner_radius, corner_radius) self._canvas.coords("border_oval_2_b", width - corner_radius, corner_radius, corner_radius) self._canvas.coords("border_oval_3_a", width - corner_radius, height - corner_radius, corner_radius) self._canvas.coords("border_oval_3_b", width - corner_radius, height - corner_radius, corner_radius) self._canvas.coords("border_oval_4_a", corner_radius, height - corner_radius, corner_radius) self._canvas.coords("border_oval_4_b", corner_radius, height - corner_radius, corner_radius) else: self._canvas.delete("border_corner_part") # delete border corner parts if not needed # create canvas border rectangle parts if not already created if not self._canvas.find_withtag("border_rectangle_1"): self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_rectangle_part", "border_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_2", "border_rectangle_part", "border_parts"), width=0) requires_recoloring = True # change position of border rectangle parts self._canvas.coords("border_rectangle_1", (0, corner_radius, width, height - corner_radius)) self._canvas.coords("border_rectangle_2", (corner_radius, 0, width - corner_radius, height)) else: self._canvas.delete("border_parts") # create inner button parts if inner_corner_radius > 0: # 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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_b", "inner_corner_part", "inner_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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_b", "inner_corner_part", "inner_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") 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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_b", "inner_corner_part", "inner_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): self._canvas.delete("inner_oval_3_a", "inner_oval_3_b") 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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_b", "inner_corner_part", "inner_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") # change position of border corner parts self._canvas.coords("inner_oval_1_a", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) self._canvas.coords("inner_oval_1_b", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) self._canvas.coords("inner_oval_2_a", width - border_width - inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) self._canvas.coords("inner_oval_2_b", width - border_width - inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) self._canvas.coords("inner_oval_3_a", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) self._canvas.coords("inner_oval_3_b", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) self._canvas.coords("inner_oval_4_a", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) self._canvas.coords("inner_oval_4_b", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) else: self._canvas.delete("inner_corner_part") # delete inner corner parts if not needed # create canvas inner rectangle parts if not already created if not self._canvas.find_withtag("inner_rectangle_1"): self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_1", "inner_rectangle_part", "inner_parts"), width=0) requires_recoloring = True if not self._canvas.find_withtag("inner_rectangle_2") and inner_corner_radius * 2 < height - (border_width * 2): self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_2", "inner_rectangle_part", "inner_parts"), width=0) requires_recoloring = True elif self._canvas.find_withtag("inner_rectangle_2") and not inner_corner_radius * 2 < height - (border_width * 2): self._canvas.delete("inner_rectangle_2") # change position of inner rectangle parts self._canvas.coords("inner_rectangle_1", (border_width + inner_corner_radius, border_width, width - border_width - inner_corner_radius, height - border_width)) self._canvas.coords("inner_rectangle_2", (border_width, border_width + inner_corner_radius, width - border_width, height - inner_corner_radius - border_width)) if requires_recoloring: # new parts were added -> manage z-order self._canvas.tag_lower("inner_parts") self._canvas.tag_lower("border_parts") return requires_recoloring def __draw_rounded_rect_with_border_circle_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool: requires_recoloring = False # create border parts (only if border exists) if border_width > 0: if corner_radius > 0: # create canvas border corner parts if not already created if not self._canvas.find_withtag("border_oval_1"): self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_1", "border_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_2", "border_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_3", "border_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_4", "border_parts"), width=0) requires_recoloring = True # set positions of border corner parts self._canvas.coords("border_oval_1", 0, 0, 2 * corner_radius, 2 * corner_radius) self._canvas.coords("border_oval_2", width - 2 * corner_radius, 0, width, 2 * corner_radius) self._canvas.coords("border_oval_3", width - 2 * corner_radius, height - 2 * corner_radius, width, height) self._canvas.coords("border_oval_4", 0, height - 2 * corner_radius, 2 * corner_radius, height) # create canvas border rectangle parts if not self._canvas.find_withtag("border_rectangle_1"): self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_2", "border_parts"), width=0) requires_recoloring = True # set positions of border rectangle parts self._canvas.coords("border_rectangle_1", corner_radius, 0, width - corner_radius, height) self._canvas.coords("border_rectangle_2", 0, corner_radius, width, height - corner_radius) else: self._canvas.delete("border_parts") # create inner parts if not self._canvas.find_withtag("inner_rectangle_1"): self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_1", "inner_parts"), width=0) requires_recoloring = True if inner_corner_radius > 0: if not self._canvas.find_withtag("inner_oval_1"): self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_1", "inner_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_2", "inner_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_3", "inner_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_4", "inner_parts"), width=0) requires_recoloring = True # set positions of inner corner parts self._canvas.coords("inner_oval_1", border_width, border_width, border_width + 2 * inner_corner_radius, border_width + 2 * inner_corner_radius) self._canvas.coords("inner_oval_2", width - border_width - 2 * inner_corner_radius, border_width, width - border_width, border_width + 2 * inner_corner_radius) self._canvas.coords("inner_oval_3", width - border_width - 2 * inner_corner_radius, height - border_width - 2 * inner_corner_radius, width - border_width, height - border_width) self._canvas.coords("inner_oval_4", border_width, height - border_width - 2 * inner_corner_radius, border_width + 2 * inner_corner_radius, height - border_width) else: self._canvas.delete("inner_oval_1", "inner_oval_2", "inner_oval_3", "inner_oval_4") # set position of inner rectangle part self._canvas.coords("inner_rectangle_1", border_width + inner_corner_radius, border_width, width - border_width - inner_corner_radius, height - border_width) if requires_recoloring: self._canvas.tag_lower("inner_parts") self._canvas.tag_lower("border_parts") return requires_recoloring def __draw_rounded_progress_bar_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, progress_value_1: float, progress_value_2: float, orientation: str) -> bool: requires_recoloring = self.__draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius) if corner_radius <= border_width: bottom_right_shift = 0 # weird canvas rendering inaccuracy that has to be corrected in some cases else: bottom_right_shift = 0 # create progress parts if not self._canvas.find_withtag("progress_parts"): self._canvas.create_polygon((0, 0, 0, 0), tags=("progress_line_1", "progress_parts"), joinstyle=tkinter.ROUND) self._canvas.tag_raise("progress_parts", "inner_parts") requires_recoloring = True if orientation == "w": self._canvas.coords("progress_line_1", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, border_width + inner_corner_radius, border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, border_width + inner_corner_radius, border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, height - (border_width + inner_corner_radius) + bottom_right_shift, border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, height - (border_width + inner_corner_radius) + bottom_right_shift) elif orientation == "s": self._canvas.coords("progress_line_1", border_width + inner_corner_radius, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), width - (border_width + inner_corner_radius), border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), width - (border_width + inner_corner_radius), border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), border_width + inner_corner_radius, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1)) self._canvas.itemconfig("progress_line_1", width=inner_corner_radius * 2) return requires_recoloring def __draw_rounded_progress_bar_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, progress_value_1: float, progress_value_2: float, orientation: str) -> bool: requires_recoloring, requires_recoloring_2 = False, False if inner_corner_radius > 0: # create canvas border corner parts if not already created if not self._canvas.find_withtag("progress_oval_1_a"): self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_1_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_1_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180) self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_2_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_2_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True if not self._canvas.find_withtag("progress_oval_3_a") and round(inner_corner_radius) * 2 < height - 2 * border_width: self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_3_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_3_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180) self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_4_a", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("progress_oval_4_b", "progress_corner_part", "progress_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True elif self._canvas.find_withtag("progress_oval_3_a") and not round( inner_corner_radius) * 2 < height - 2 * border_width: self._canvas.delete("progress_oval_3_a", "progress_oval_3_b", "progress_oval_4_a", "progress_oval_4_b") if not self._canvas.find_withtag("progress_rectangle_1"): self._canvas.create_rectangle(0, 0, 0, 0, tags=("progress_rectangle_1", "progress_rectangle_part", "progress_parts"), width=0) requires_recoloring = True if not self._canvas.find_withtag("progress_rectangle_2") and inner_corner_radius * 2 < height - (border_width * 2): self._canvas.create_rectangle(0, 0, 0, 0, tags=("progress_rectangle_2", "progress_rectangle_part", "progress_parts"), width=0) requires_recoloring = True elif self._canvas.find_withtag("progress_rectangle_2") and not inner_corner_radius * 2 < height - (border_width * 2): self._canvas.delete("progress_rectangle_2") # horizontal orientation from the bottom if orientation == "w": requires_recoloring_2 = self.__draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ()) # set positions of progress corner parts self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, border_width + inner_corner_radius, inner_corner_radius) self._canvas.coords("progress_oval_1_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, border_width + inner_corner_radius, inner_corner_radius) self._canvas.coords("progress_oval_2_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, border_width + inner_corner_radius, inner_corner_radius) self._canvas.coords("progress_oval_2_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, border_width + inner_corner_radius, inner_corner_radius) self._canvas.coords("progress_oval_3_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, height - border_width - inner_corner_radius, inner_corner_radius) self._canvas.coords("progress_oval_3_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, height - border_width - inner_corner_radius, inner_corner_radius) self._canvas.coords("progress_oval_4_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, height - border_width - inner_corner_radius, inner_corner_radius) self._canvas.coords("progress_oval_4_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, height - border_width - inner_corner_radius, inner_corner_radius) # set positions of progress rect parts self._canvas.coords("progress_rectangle_1", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, border_width, border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, height - border_width) self._canvas.coords("progress_rectangle_2", border_width + 2 * inner_corner_radius + (width - 2 * inner_corner_radius - 2 * border_width) * progress_value_1, border_width + inner_corner_radius, border_width + 2 * inner_corner_radius + (width - 2 * inner_corner_radius - 2 * border_width) * progress_value_2, height - inner_corner_radius - border_width) # vertical orientation from the bottom if orientation == "s": requires_recoloring_2 = self.__draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ()) # set positions of progress corner parts self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) self._canvas.coords("progress_oval_1_b", border_width + inner_corner_radius, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) self._canvas.coords("progress_oval_2_a", width - border_width - inner_corner_radius, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) self._canvas.coords("progress_oval_2_b", width - border_width - inner_corner_radius, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) self._canvas.coords("progress_oval_3_a", width - border_width - inner_corner_radius, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) self._canvas.coords("progress_oval_3_b", width - border_width - inner_corner_radius, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) self._canvas.coords("progress_oval_4_a", border_width + inner_corner_radius, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) self._canvas.coords("progress_oval_4_b", border_width + inner_corner_radius, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) # set positions of progress rect parts self._canvas.coords("progress_rectangle_1", border_width + inner_corner_radius, border_width + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), width - border_width - inner_corner_radius, border_width + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1)) self._canvas.coords("progress_rectangle_2", border_width, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), width - border_width, border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1)) return requires_recoloring or requires_recoloring_2 def __draw_rounded_progress_bar_with_border_circle_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, progress_value_1: float, progress_value_2: float, orientation: str) -> bool: requires_recoloring = self.__draw_rounded_rect_with_border_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius) # create progress parts if not self._canvas.find_withtag("progress_rectangle_1"): self._canvas.create_rectangle(0, 0, 0, 0, tags=("progress_rectangle_1", "progress_parts"), width=0) requires_recoloring = True if orientation == "w": x1 = border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1 x2 = border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2 self._canvas.coords("progress_rectangle_1", x1, border_width, x2, height - border_width) elif orientation == "s": y1 = border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2) y2 = border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1) self._canvas.coords("progress_rectangle_1", border_width, y1, width - border_width, y2) if inner_corner_radius > 0: if not self._canvas.find_withtag("progress_oval_1"): self._canvas.create_oval(0, 0, 0, 0, tags=("progress_oval_1", "progress_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("progress_oval_2", "progress_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("progress_oval_3", "progress_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("progress_oval_4", "progress_parts"), width=0) requires_recoloring = True if orientation == "w": self._canvas.coords("progress_oval_1", x1 - inner_corner_radius, border_width, x1 + inner_corner_radius, border_width + 2 * inner_corner_radius) self._canvas.coords("progress_oval_2", x2 - inner_corner_radius, border_width, x2 + inner_corner_radius, border_width + 2 * inner_corner_radius) self._canvas.coords("progress_oval_3", x2 - inner_corner_radius, height - border_width - 2 * inner_corner_radius, x2 + inner_corner_radius, height - border_width) self._canvas.coords("progress_oval_4", x1 - inner_corner_radius, height - border_width - 2 * inner_corner_radius, x1 + inner_corner_radius, height - border_width) elif orientation == "s": self._canvas.coords("progress_oval_1", border_width, y1 - inner_corner_radius, border_width + 2 * inner_corner_radius, y1 + inner_corner_radius) self._canvas.coords("progress_oval_2", width - border_width - 2 * inner_corner_radius, y1 - inner_corner_radius, width - border_width, y1 + inner_corner_radius) self._canvas.coords("progress_oval_3", width - border_width - 2 * inner_corner_radius, y2 - inner_corner_radius, width - border_width, y2 + inner_corner_radius) self._canvas.coords("progress_oval_4", border_width, y2 - inner_corner_radius, border_width + 2 * inner_corner_radius, y2 + inner_corner_radius) else: self._canvas.delete("progress_oval_1", "progress_oval_2", "progress_oval_3", "progress_oval_4") return requires_recoloring def draw_rounded_slider_with_border_and_2_button(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], border_width: Union[float, int], button_length: Union[float, int], button_corner_radius: Union[float, int], slider_value: float, slider_2_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) if button_corner_radius > width / 2 or button_corner_radius > height / 2: # restrict button_corner_radius if it's too larger button_corner_radius = min(width / 2, height / 2) button_length = round(button_length) border_width = round(border_width) button_corner_radius = round(button_corner_radius) corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding) if corner_radius >= border_width: inner_corner_radius = corner_radius - border_width else: inner_corner_radius = 0 if self.preferred_drawing_method == "polygon_shapes": return self.__draw_rounded_slider_with_border_and_2_button_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, button_length, button_corner_radius, slider_value, slider_2_value, orientation) elif self.preferred_drawing_method == "font_shapes": return self.__draw_rounded_slider_with_border_and_2_button_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, button_length, button_corner_radius, slider_value, slider_2_value, orientation) elif self.preferred_drawing_method == "circle_shapes": return self.__draw_rounded_slider_with_border_and_2_button_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius, button_length, button_corner_radius, slider_value, slider_2_value, orientation) def __draw_rounded_slider_with_border_and_2_button_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, button_length: int, button_corner_radius: int, slider_value: float, slider_2_value: float, orientation: str) -> bool: # draw normal progressbar requires_recoloring = self.__draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, slider_value, slider_2_value, orientation) # create slider button part if not self._canvas.find_withtag("slider_parts"): self._canvas.create_polygon((0, 0, 0, 0), tags=("slider_line_1", "slider_parts", "slider_0_parts"), joinstyle=tkinter.ROUND) self._canvas.create_polygon((0, 0, 0, 0), tags=("slider_2_line_1", "slider_parts", "slider_1_parts"), joinstyle=tkinter.ROUND) self._canvas.tag_raise("slider_parts") # manage z-order requires_recoloring = True if corner_radius <= border_width: bottom_right_shift = -1 # weird canvas rendering inaccuracy that has to be corrected in some cases else: bottom_right_shift = 0 if orientation == "w": slider_x_position = corner_radius + (button_length / 2) + (width - 2 * corner_radius - button_length) * slider_value self._canvas.coords("slider_line_1", slider_x_position - (button_length / 2), button_corner_radius, slider_x_position + (button_length / 2), button_corner_radius, slider_x_position + (button_length / 2), height - button_corner_radius, slider_x_position - (button_length / 2), height - button_corner_radius) self._canvas.itemconfig("slider_line_1", width=button_corner_radius * 2) slider_x_position = corner_radius + (button_length / 2) + (width - 2 * corner_radius - button_length) * slider_2_value self._canvas.coords("slider_2_line_1", slider_x_position - (button_length / 2), button_corner_radius, slider_x_position + (button_length / 2), button_corner_radius, slider_x_position + (button_length / 2), height - button_corner_radius, slider_x_position - (button_length / 2), height - button_corner_radius) self._canvas.itemconfig("slider_2_line_1", width=button_corner_radius * 2) elif orientation == "s": slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_value) self._canvas.coords("slider_line_1", button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius, slider_y_position + (button_length / 2), width - button_corner_radius, slider_y_position + (button_length / 2), width - button_corner_radius, slider_y_position - (button_length / 2)) self._canvas.itemconfig("slider_line_1", width=button_corner_radius * 2) slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_2_value) self._canvas.coords("slider_2_line_1", button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius, slider_y_position + (button_length / 2), width - button_corner_radius, slider_y_position + (button_length / 2), width - button_corner_radius, slider_y_position - (button_length / 2)) self._canvas.itemconfig("slider_2_line_1", width=button_corner_radius * 2) return requires_recoloring def __draw_rounded_slider_with_border_and_2_button_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, button_length: int, button_corner_radius: int, slider_value: float, slider_2_value: float, orientation: str) -> bool: # draw normal progressbar requires_recoloring = self.__draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, slider_value, slider_2_value, orientation) # create 4 circles (if not needed, then less) if not self._canvas.find_withtag("slider_oval_1_a"): self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_1_a", "slider_corner_part", "slider_parts", "slider_0_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_1_b", "slider_corner_part", "slider_parts", "slider_0_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True if not self._canvas.find_withtag("slider_oval_2_a") and button_length > 0: self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_a", "slider_corner_part", "slider_parts", "slider_0_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_b", "slider_corner_part", "slider_parts", "slider_0_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True elif self._canvas.find_withtag("slider_oval_2_a") and not button_length > 0: self._canvas.delete("slider_oval_2_a", "slider_oval_2_b") if not self._canvas.find_withtag("slider_oval_4_a") and height > 2 * button_corner_radius: self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_4_a", "slider_corner_part", "slider_parts", "slider_0_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_4_b", "slider_corner_part", "slider_parts", "slider_0_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True elif self._canvas.find_withtag("slider_oval_4_a") and not height > 2 * button_corner_radius: self._canvas.delete("slider_oval_4_a", "slider_oval_4_b") if not self._canvas.find_withtag("slider_oval_3_a") and button_length > 0 and height > 2 * button_corner_radius: self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_3_a", "slider_corner_part", "slider_parts", "slider_0_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_3_b", "slider_corner_part", "slider_parts", "slider_0_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True elif self._canvas.find_withtag("slider_oval_3_a") and not (button_length > 0 and height > 2 * button_corner_radius): self._canvas.delete("slider_oval_3_a", "slider_oval_3_b") # create the 2 rectangles (if needed) if not self._canvas.find_withtag("slider_rectangle_1") and button_length > 0: self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_1", "slider_rectangle_part", "slider_parts", "slider_0_parts"), width=0) requires_recoloring = True elif self._canvas.find_withtag("slider елдер_1") and not button_length > 0: self._canvas.delete("slider_rectangle_1") if not self._canvas.find_withtag("slider_rectangle_2") and height > 2 * button_corner_radius: self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_2", "slider_rectangle_part", "slider_parts", "slider_0_parts"), width=0) requires_recoloring = True elif self._canvas.find_withtag("slider_rectangle_2") and not height > 2 * button_corner_radius: self._canvas.delete("slider_rectangle_2") # set positions of circles and rectangles if orientation == "w": slider_x_position = corner_radius + (button_length / 2) + (width - 2 * corner_radius - button_length) * slider_value self._canvas.coords("slider_oval_1_a", slider_x_position - (button_length / 2), button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_1_b", slider_x_position - (button_length / 2), button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_2_a", slider_x_position + (button_length / 2), button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_2_b", slider_x_position + (button_length / 2), button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_3_a", slider_x_position + (button_length / 2), height - button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_3_b", slider_x_position + (button_length / 2), height - button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_4_a", slider_x_position - (button_length / 2), height - button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_4_b", slider_x_position - (button_length / 2), height - button_corner_radius, button_corner_radius) self._canvas.coords("slider_rectangle_1", slider_x_position - (button_length / 2), 0, slider_x_position + (button_length / 2), height) self._canvas.coords("slider_rectangle_2", slider_x_position - (button_length / 2) - button_corner_radius, button_corner_radius, slider_x_position + (button_length / 2) + button_corner_radius, height - button_corner_radius) elif orientation == "s": slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_value) self._canvas.coords("slider_oval_1_a", button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_1_b", button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_2_a", button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_2_b", button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_3_a", width - button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_3_b", width - button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_4_a", width - button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_4_b", width - button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) self._canvas.coords("slider_rectangle_1", 0, slider_y_position - (button_length / 2), width, slider_y_position + (button_length / 2)) self._canvas.coords("slider_rectangle_2", button_corner_radius, slider_y_position - (button_length / 2) - button_corner_radius, width - button_corner_radius, slider_y_position + (button_length / 2) + button_corner_radius) # second button # create 4 circles (if not needed, then less) if not self._canvas.find_withtag("slider_oval_2_1_a"): self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_1_a", "slider_corner_part", "slider_parts", "slider_1_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_1_b", "slider_corner_part", "slider_parts", "slider_1_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True if not self._canvas.find_withtag("slider_oval_2_2_a") and button_length > 0: self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_2_a", "slider_corner_part", "slider_parts", "slider_1_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_2_b", "slider_corner_part", "slider_parts", "slider_1_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True elif self._canvas.find_withtag("slider_oval_2_2_a") and not button_length > 0: self._canvas.delete("slider_oval_2_2_a", "slider_oval_2_2_b") if not self._canvas.find_withtag("slider_oval_2_4_a") and height > 2 * button_corner_radius: self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_4_a", "slider_corner_part", "slider_parts", "slider_1_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_4_b", "slider_corner_part", "slider_parts", "slider_1_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True elif self._canvas.find_withtag("slider_oval_2_4_a") and not height > 2 * button_corner_radius: self._canvas.delete("slider_oval_2_4_a", "slider_oval_2_4_b") if not self._canvas.find_withtag("slider_oval_2_3_a") and button_length > 0 and height > 2 * button_corner_radius: self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_3_a", "slider_corner_part", "slider_parts", "slider_1_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_3_b", "slider_corner_part", "slider_parts", "slider_1_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True elif self._canvas.find_withtag("slider_oval_2_3_a") and not (button_length > 0 and height > 2 * button_corner_radius): self._canvas.delete("slider_oval_2_3_a", "slider_oval_2_3_b") # create the 2 rectangles (if needed) if not self._canvas.find_withtag("slider_rectangle_2_1") and button_length > 0: self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_2_1", "slider_rectangle_part", "slider_parts", "slider_1_parts"), width=0) requires_recoloring = True elif self._canvas.find_withtag("slider_rectangle_2_1") and not button_length > 0: self._canvas.delete("slider_rectangle_2_1") if not self._canvas.find_withtag("slider_rectangle_2_2") and height > 2 * button_corner_radius: self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_2_2", "slider_rectangle_part", "slider_parts", "slider_1_parts"), width=0) requires_recoloring = True elif self._canvas.find_withtag("slider_rectangle_2_2") and not height > 2 * button_corner_radius: self._canvas.delete("slider_rectangle_2_2") # set positions of circles and rectangles if orientation == "w": slider_x_position = corner_radius + (button_length / 2) + (width - 2 * corner_radius - button_length) * slider_2_value self._canvas.coords("slider_oval_2_1_a", slider_x_position - (button_length / 2), button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_2_1_b", slider_x_position - (button_length / 2), button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_2_2_a", slider_x_position + (button_length / 2), button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_2_2_b", slider_x_position + (button_length / 2), button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_2_3_a", slider_x_position + (button_length / 2), height - button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_2_3_b", slider_x_position + (button_length / 2), height - button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_2_4_a", slider_x_position - (button_length / 2), height - button_corner_radius, button_corner_radius) self._canvas.coords("slider_oval_2_4_b", slider_x_position - (button_length / 2), height - button_corner_radius, button_corner_radius) self._canvas.coords("slider_rectangle_2_1", slider_x_position - (button_length / 2), 0, slider_x_position + (button_length / 2), height) self._canvas.coords("slider_rectangle_2_2", slider_x_position - (button_length / 2) - button_corner_radius, button_corner_radius, slider_x_position + (button_length / 2) + button_corner_radius, height - button_corner_radius) elif orientation == "s": slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_2_value) self._canvas.coords("slider_oval_2_1_a", button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_2_1_b", button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_2_2_a", button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_2_2_b", button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_2_3_a", width - button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_2_3_b", width - button_corner_radius, slider_y_position + (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_2_4_a", width - button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) self._canvas.coords("slider_oval_2_4_b", width - button_corner_radius, slider_y_position - (button_length / 2), button_corner_radius) self._canvas.coords("slider_rectangle_2_1", 0, slider_y_position - (button_length / 2), width, slider_y_position + (button_length / 2)) self._canvas.coords("slider_rectangle_2_2", button_corner_radius, slider_y_position - (button_length / 2) - button_corner_radius, width - button_corner_radius, slider_y_position + (button_length / 2) + button_corner_radius) if requires_recoloring: # new parts were added -> manage z-order self._canvas.tag_raise("slider_parts") return requires_recoloring def __draw_rounded_slider_with_border_and_2_button_circle_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, button_length: int, button_corner_radius: int, slider_value: float, slider_2_value: float, orientation: str) -> bool: requires_recoloring = self.__draw_rounded_progress_bar_with_border_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius, slider_value, slider_2_value, orientation) # create slider button parts if not self._canvas.find_withtag("slider_0_parts"): self._canvas.create_oval(0, 0, 0, 0, tags=("slider_oval_1", "slider_parts", "slider_0_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("slider_oval_2", "slider_parts", "slider_0_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("slider_oval_3", "slider_parts", "slider_0_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("slider_oval_4", "slider_parts", "slider_0_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_1", "slider_parts", "slider_0_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_2", "slider_parts", "slider_0_parts"), width=0) requires_recoloring = True if not self._canvas.find_withtag("slider_1_parts"): self._canvas.create_oval(0, 0, 0, 0, tags=("slider_oval_2_1", "slider_parts", "slider_1_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("slider_oval_2_2", "slider_parts", "slider_1_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("slider_oval_2_3", "slider_parts", "slider_1_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("slider_oval_2_4", "slider_parts", "slider_1_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_2_1", "slider_parts", "slider_1_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("slider_rectangle_2_2", "slider_parts", "slider_1_parts"), width=0) requires_recoloring = True if orientation == "w": slider_x_position = corner_radius + (button_length / 2) + (width - 2 * corner_radius - button_length) * slider_value self._canvas.coords("slider_oval_1", slider_x_position - button_length / 2 - button_corner_radius, 0, slider_x_position - button_length / 2 + button_corner_radius, 2 * button_corner_radius) self._canvas.coords("slider_oval_2", slider_x_position + button_length / 2 - button_corner_radius, 0, slider_x_position + button_length / 2 + button_corner_radius, 2 * button_corner_radius) self._canvas.coords("slider_oval_3", slider_x_position + button_length / 2 - button_corner_radius, height - 2 * button_corner_radius, slider_x_position + button_length / 2 + button_corner_radius, height) self._canvas.coords("slider_oval_4", slider_x_position - button_length / 2 - button_corner_radius, height - 2 * button_corner_radius, slider_x_position - button_length / 2 + button_corner_radius, height) self._canvas.coords("slider_rectangle_1", slider_x_position - button_length / 2, 0, slider_x_position + button_length / 2, height) self._canvas.coords("slider_rectangle_2", slider_x_position - button_length / 2 - button_corner_radius, button_corner_radius, slider_x_position + button_length / 2 + button_corner_radius, height - button_corner_radius) slider_x_position = corner_radius + (button_length / 2) + (width - 2 * corner_radius - button_length) * slider_2_value self._canvas.coords("slider_oval_2_1", slider_x_position - button_length / 2 - button_corner_radius, 0, slider_x_position - button_length / 2 + button_corner_radius, 2 * button_corner_radius) self._canvas.coords("slider_oval_2_2", slider_x_position + button_length / 2 - button_corner_radius, 0, slider_x_position + button_length / 2 + button_corner_radius, 2 * button_corner_radius) self._canvas.coords("slider_oval_2_3", slider_x_position + button_length / 2 - button_corner_radius, height - 2 * button_corner_radius, slider_x_position + button_length / 2 + button_corner_radius, height) self._canvas.coords("slider_oval_2_4", slider_x_position - button_length / 2 - button_corner_radius, height - 2 * button_corner_radius, slider_x_position - button_length / 2 + button_corner_radius, height) self._canvas.coords("slider_rectangle_2_1", slider_x_position - button_length / 2, 0, slider_x_position + button_length / 2, height) self._canvas.coords("slider_rectangle_2_2", slider_x_position - button_length / 2 - button_corner_radius, button_corner_radius, slider_x_position + button_length / 2 + button_corner_radius, height - button_corner_radius) elif orientation == "s": slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_value) self._canvas.coords("slider_oval_1", 0, slider_y_position - button_length / 2 - button_corner_radius, 2 * button_corner_radius, slider_y_position - button_length / 2 + button_corner_radius) self._canvas.coords("slider_oval_2", 0, slider_y_position + button_length / 2 - button_corner_radius, 2 * button_corner_radius, slider_y_position + button_length / 2 + button_corner_radius) self._canvas.coords("slider_oval_3", width - 2 * button_corner_radius, slider_y_position + button_length / 2 - button_corner_radius, width, slider_y_position + button_length / 2 + button_corner_radius) self._canvas.coords("slider_oval_4", width - 2 * button_corner_radius, slider_y_position - button_length / 2 - button_corner_radius, width, slider_y_position - button_length / 2 + button_corner_radius) self._canvas.coords("slider_rectangle_1", 0, slider_y_position - button_length / 2, width, slider_y_position + button_length / 2) self._canvas.coords("slider_rectangle_2", button_corner_radius, slider_y_position - button_length / 2 - button_corner_radius, width - button_corner_radius, slider_y_position + button_length / 2 + button_corner_radius) slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_2_value) self._canvas.coords("slider_oval_2_1", 0, slider_y_position - button_length / 2 - button_corner_radius, 2 * button_corner_radius, slider_y_position - button_length / 2 + button_corner_radius) self._canvas.coords("slider_oval_2_2", 0, slider_y_position + button_length / 2 - button_corner_radius, 2 * button_corner_radius, slider_y_position + button_length / 2 + button_corner_radius) self._canvas.coords("slider_oval_2_3", width - 2 * button_corner_radius, slider_y_position + button_length / 2 - button_corner_radius, width, slider_y_position + button_length / 2 + button_corner_radius) self._canvas.coords("slider_oval_2_4", width - 2 * button_corner_radius, slider_y_position - button_length / 2 - button_corner_radius, width, slider_y_position - button_length / 2 + button_corner_radius) self._canvas.coords("slider_rectangle_2_1", 0, slider_y_position - button_length / 2, width, slider_y_position + button_length / 2) self._canvas.coords("slider_rectangle_2_2", button_corner_radius, slider_y_position - button_length / 2 - button_corner_radius, width - button_corner_radius, slider_y_position + button_length / 2 + button_corner_radius) if requires_recoloring: self._canvas.tag_raise("slider_parts") return requires_recoloring class CTkRangeSlider(CTkBaseClass): """ Range slider with rounded corners, border, number of steps, variable support, vertical orientation. For detailed information check out the documentation. """ def __init__(self, master: any, width: Optional[int] = None, height: Optional[int] = None, corner_radius: Optional[int] = None, button_corner_radius: Optional[int] = None, border_width: Optional[int] = None, button_length: Optional[int] = None, bg_color: Union[str, Tuple[str, str]] = "transparent", fg_color: Optional[Union[str, Tuple[str, str]]] = None, border_color: Union[str, Tuple[str, str]] = "transparent", progress_color: Optional[Union[str, Tuple[str, str]]] = None, button_color: Optional[Union[str, Tuple[str, str]]] = None, button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, from_: int = 0, to: int = 1, state: str = "normal", number_of_steps: Union[int, None] = None, hover: bool = True, command: Union[Callable[[float], None], Tuple[Callable[[float], None], Callable[[float], None]], None] = None, variables: Union[Tuple[tkinter.Variable, tkinter.Variable], None] = None, orientation: str = "horizontal", **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__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # color self._border_color = self._check_color_type(border_color, transparency=True) self._fg_color = ThemeManager.theme["CTkSlider"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) self._progress_color = ThemeManager.theme["CTkSlider"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True) if button_color is None: self._button_color_0 = ThemeManager.theme["CTkSlider"]["button_color"] if button_color is None else self._check_color_type(button_color) self._button_color_1 = ThemeManager.theme["CTkSlider"]["button_color"] if button_color is None else self._check_color_type(button_color) else: if (type(button_color[0]) and type(button_color[1])) is tuple: self._button_color_0 = ThemeManager.theme["CTkSlider"]["button_color"] if button_color[0] is None else self._check_color_type(button_color[0]) self._button_color_1 = ThemeManager.theme["CTkSlider"]["button_color"] if button_color[1] is None else self._check_color_type(button_color[1]) else: self._button_color_0 = ThemeManager.theme["CTkSlider"]["button_color"] if button_color is None else self._check_color_type(button_color) self._button_color_1 = ThemeManager.theme["CTkSlider"]["button_color"] if button_color is None else self._check_color_type(button_color) self._button_hover_color = ThemeManager.theme["CTkSlider"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) # shape self._corner_radius = ThemeManager.theme["CTkSlider"]["corner_radius"] if corner_radius is None else corner_radius self._button_corner_radius = ThemeManager.theme["CTkSlider"]["button_corner_radius"] if button_corner_radius is None else button_corner_radius self._border_width = ThemeManager.theme["CTkSlider"]["border_width"] if border_width is None else border_width self._button_length = ThemeManager.theme["CTkSlider"]["button_length"] if button_length is None else button_length self._values = 0, 1 # initial values of slider in percent self._orientation = orientation self._hover_states = False, False self._hover = hover self._from_ = from_ self._to = to self._number_of_steps = number_of_steps self._output_values = self._from_ + (self._values[0] * (self._to - self._from_)), self._from_ + (self._values[1] * (self._to - self._from_)) if self._corner_radius < self._button_corner_radius: self._corner_radius = self._button_corner_radius # callback and control variables self._command = command self._variables: tuple[tkinter.Variable] = variables self._variable_callback_blocked = False self._variable_callback_name = [None, None] self._state = state self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) self._canvas = CTkCanvas(master=self, highlightthickness=0, width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) self._canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe") self._draw_engine = CustomDrawEngine(self._canvas) self._create_bindings() self._set_cursor() self._draw() # initial draw if self._variables is not None: self._variable_callback_name[0] = self._variables[0].trace_add("write", self._variable_callback) self._variable_callback_name[1] = self._variables[1].trace_add("write", self._variable_callback) self._variable_callback_blocked = True self.set([self._variables[0].get(), self._variables[1].get()], from_variable_callback=True) self._variable_callback_blocked = False def _create_bindings(self, sequence: Optional[str] = None): """ set necessary bindings for functionality of widget, will overwrite other bindings """ if sequence is None or sequence == "": self._canvas.bind("", self._on_enter) if sequence is None or sequence == "": self._canvas.bind("", self._on_enter) if sequence is None or sequence == "": self._canvas.bind("", self._on_leave) if sequence is None or sequence == "": self._canvas.bind("", self._clicked) if sequence is None or sequence == "": self._canvas.bind("", self._clicked) 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() 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() def _destroy(self): # remove variable_callback from variable callbacks if variable exists if self._variables is not None: self._variables[0].trace_remove("write", self._variable_callback_name) super().destroy() def _set_cursor(self): if self._state == "normal" and self._cursor_manipulation_enabled: if sys.platform == "darwin": self.configure(cursor="pointinghand") elif sys.platform.startswith("win"): self.configure(cursor="hand2") elif self._state == "disabled" and self._cursor_manipulation_enabled: if sys.platform == "darwin": self.configure(cursor="arrow") elif sys.platform.startswith("win"): self.configure(cursor="arrow") def _draw(self, no_color_updates=False): super()._draw(no_color_updates) if self._orientation.lower() == "horizontal": orientation = "w" elif self._orientation.lower() == "vertical": orientation = "s" else: orientation = "w" requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_2_button(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_width), self._apply_widget_scaling(self._button_length), self._apply_widget_scaling(self._button_corner_radius), self._values[0],self._values[1], orientation) if no_color_updates is False or requires_recoloring: self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) if self._border_color == "transparent": self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._bg_color), outline=self._apply_appearance_mode(self._bg_color)) else: self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._border_color), outline=self._apply_appearance_mode(self._border_color)) self._canvas.itemconfig("inner_parts", fill=self._apply_appearance_mode(self._fg_color), outline=self._apply_appearance_mode(self._fg_color)) if self._progress_color == "transparent": self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._fg_color), outline=self._apply_appearance_mode(self._fg_color)) else: self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._progress_color), outline=self._apply_appearance_mode(self._progress_color)) if (self._hover_states[0] and self._hover) is True: self._canvas.itemconfig("slider_0_parts", fill=self._apply_appearance_mode(self._button_hover_color), outline=self._apply_appearance_mode(self._button_hover_color)) else: self._canvas.itemconfig("slider_0_parts", fill=self._apply_appearance_mode(self._button_color_0), outline=self._apply_appearance_mode(self._button_color_0)) if (self._hover_states[1] and self._hover) is True: self._canvas.itemconfig("slider_1_parts", fill=self._apply_appearance_mode(self._button_hover_color), outline=self._apply_appearance_mode(self._button_hover_color)) else: self._canvas.itemconfig("slider_1_parts", fill=self._apply_appearance_mode(self._button_color_1), outline=self._apply_appearance_mode(self._button_color_1)) def _clicked(self, event=None): if self._state == "normal": if self._orientation.lower() == "horizontal": clickPos = self._reverse_widget_scaling(event.x / self._current_width) if clickPos < self._values[0] or abs(clickPos-self._values[0]) < abs(clickPos-self._values[1]): if self._active_slider: self._values = clickPos, self._values[1] else: if not self._active_slider: self._values = self._values[0], clickPos else: clickPos = 1 - self._reverse_widget_scaling(event.y / self._current_height) if clickPos < self._values[0] or abs(clickPos-self._values[0]) < abs(clickPos-self._values[1]): if self._active_slider: self._values = clickPos, self._values[1] else: if not self._active_slider: self._values = self._values[0], clickPos self._values=[max(min(x, 1.), 0.) for x in self._values] self._output_values = (self._round_to_step_size(self._from_ + (self._values[0] * (self._to - self._from_))), self._round_to_step_size(self._from_ + (self._values[1] * (self._to - self._from_)))) self._values = (self._output_values[0] - self._from_) / (self._to - self._from_), (self._output_values[1] - self._from_) / (self._to - self._from_) self._draw(no_color_updates=False) if self._variables is not None: self._variable_callback_blocked = True self._variables[0].set(round(self._output_values[0]) if isinstance(self._variables[0], tkinter.IntVar) else self._output_values[0]) self._variables[1].set(round(self._output_values[1]) if isinstance(self._variables[1], tkinter.IntVar) else self._output_values[1]) self._variable_callback_blocked = False if self._command is not None: if type(self._command) is tuple: if self._active_slider: self._command[0](self._output_values[0]) else: self._command[1](self._output_values[1]) else: self._command(self._output_values) def _on_enter(self, event=0): if self._state == "normal": if self._orientation.lower() == "horizontal": enterPos = self._reverse_widget_scaling(event.x / self._current_width) if enterPos < self._values[0] or abs(enterPos-self._values[0]) <= abs(enterPos-self._values[1]): highlightTag='slider_0_parts' normalTag='slider_1_parts' color = self._button_color_1 self._hover_states = True, False self._active_slider = True else: highlightTag='slider_1_parts' normalTag='slider_0_parts' color = self._button_color_0 self._hover_states = False, True self._active_slider = False else: enterPos = 1 - self._reverse_widget_scaling(event.y / self._current_height) if enterPos < self._values[0] or abs(enterPos-self._values[0]) <= abs(enterPos-self._values[1]): highlightTag='slider_0_parts' normalTag='slider_1_parts' color = self._button_color_1 self._hover_states = True, False self._active_slider = True else: highlightTag='slider_1_parts' normalTag='slider_0_parts' color = self._button_color_0 self._hover_states = False, True self._active_slider = False if self._hover: self._canvas.itemconfig(highlightTag, fill=self._apply_appearance_mode(self._button_hover_color), outline=self._apply_appearance_mode(self._button_hover_color)) self._canvas.itemconfig(normalTag, fill=self._apply_appearance_mode(color), outline=self._apply_appearance_mode(color)) def _on_leave(self, event=0): self._hover_states = False, False self._canvas.itemconfig("slider_0_parts", fill=self._apply_appearance_mode(self._button_color_0), outline=self._apply_appearance_mode(self._button_color_0)) self._canvas.itemconfig("slider_1_parts", fill=self._apply_appearance_mode(self._button_color_1), outline=self._apply_appearance_mode(self._button_color_1)) def _round_to_step_size(self, values): if self._number_of_steps is not None: step_size = (self._to - self._from_) / self._number_of_steps if type(values) is list: values = [self._to - (round((self._to - x) / step_size) * step_size) for x in values] else: values = self._to - (round((self._to - values) / step_size) * step_size) return values else: return values def get(self) -> float: return self._output_values def set(self, output_values: List[float], from_variable_callback=False): if self._from_ < self._to: output_values = [max(min(x, self._to), self._from_) for x in output_values] else: output_values = [max(min(x, self._from_), self._to) for x in output_values] self._output_values = self._round_to_step_size(output_values) self._values = ((self._output_values[0] - self._from_) / (self._to - self._from_), (self._output_values[1] - self._from_) / (self._to - self._from_)) self._draw(no_color_updates=False) if self._variables is not None and not from_variable_callback: self._variable_callback_blocked = True self._variables[0].set(round(self._output_values[0]) if isinstance(self._variables[0], tkinter.IntVar) else self._output_values[0]) self._variables[1].set(round(self._output_values[1]) if isinstance(self._variables[1], tkinter.IntVar) else self._output_values[1]) self._variable_callback_blocked = False def _variable_callback(self, var_name, index, mode): if not self._variable_callback_blocked: self.set([self._variables[0].get(),self._variables[1].get()], from_variable_callback=True) def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): """ called on the tkinter.Canvas """ if not (add == "+" or add is True): raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") self._canvas.bind(sequence, command, add=True) def unbind(self, sequence: str = None, funcid: str = None): """ called on the tkinter.Label and tkinter.Canvas """ if funcid is not None: raise ValueError("'funcid' argument can only be None, because there is a bug in" + " tkinter and its not clear whether the internal callbacks will be unbinded or not") self._canvas.unbind(sequence, None) self._create_bindings(sequence=sequence) # restore internal callbacks for sequence def configure(self, require_redraw=False, **kwargs): if "state" in kwargs: self._state = kwargs.pop("state") self._set_cursor() require_redraw = True if "fg_color" in kwargs: self._fg_color = self._check_color_type(kwargs.pop("fg_color")) require_redraw = True if "progress_color" in kwargs: self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True) require_redraw = True if "button_color" in kwargs: self._button_color_0 = self._check_color_type(kwargs["button_color"]) self._button_color_1 = self._check_color_type(kwargs.pop("button_color")) require_redraw = True if "button_hover_color" in kwargs: self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color")) require_redraw = True if "border_color" in kwargs: self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True) require_redraw = True if "border_width" in kwargs: self._border_width = kwargs.pop("border_width") require_redraw = True if "from_" in kwargs: self._from_ = kwargs.pop("from_") if "to" in kwargs: self._to = kwargs.pop("to") if "number_of_steps" in kwargs: self._number_of_steps = kwargs.pop("number_of_steps") if "hover" in kwargs: self._hover = kwargs.pop("hover") if "command" in kwargs: self._command = kwargs.pop("command") if "variables" in kwargs: if self._variables is not None: self._variables[0].trace_remove("write", self._variable_callback_name[0]) self._variables[1].trace_remove("write", self._variable_callback_name[1]) self._variables = kwargs["variables"] if self._variables is not None and self._variables != "": self._variable_callback_name[0] = self._variables[0].trace_add("write", self._variable_callback) self._variable_callback_name[1] = self._variables[1].trace_add("write", self._variable_callback) self.set([self._variables[0].get(), self._variables[1].get()], from_variable_callback=True) else: self._variables = None del kwargs["variables"] if "corner_radius" in kwargs: self._corner_radius = kwargs.pop("corner_radius") require_redraw = True if "button_corner_radius" in kwargs: self._button_corner_radius = kwargs.pop("button_corner_radius") require_redraw = True if "button_length" in kwargs: self._button_length = kwargs.pop("button_length") require_redraw = True super().configure(require_redraw=require_redraw, **kwargs) def cget(self, attribute_name: str) -> any: if attribute_name == "corner_radius": return self._corner_radius elif attribute_name == "button_corner_radius": return self._button_corner_radius elif attribute_name == "border_width": return self._border_width elif attribute_name == "button_length": return self._button_length elif attribute_name == "fg_color": return self._fg_color elif attribute_name == "border_color": return self._border_color elif attribute_name == "progress_color": return self._progress_color elif attribute_name == "button_color": return self._button_color_0 elif attribute_name == "button_hover_color": return self._button_hover_color elif attribute_name == "from_": return self._from_ elif attribute_name == "to": return self._to elif attribute_name == "state": return self._state elif attribute_name == "number_of_steps": return self._number_of_steps elif attribute_name == "hover": return self._hover elif attribute_name == "command": return self._command elif attribute_name == "variables": return self._variables elif attribute_name == "orientation": return self._orientation else: return super().cget(attribute_name) def focus(self): return self._canvas.focus() def focus_set(self): return self._canvas.focus_set() def focus_force(self): return self._canvas.focus_force()