from __future__ import annotations import sys import math import tkinter from typing import Union, TYPE_CHECKING if TYPE_CHECKING: from .widgets.ctk_canvas import CTkCanvas class DrawEngine: """ This is the core of the CustomTkinter library where all the drawing on the tkinter.Canvas happens. A year of experimenting and trying out different drawing methods have led to the current state of this class, and I don't think there's much I can do to make the rendering look better than this with the limited capabilities the tkinter.Canvas offers. Functions: - draw_rounded_rect_with_border() - 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() """ preferred_drawing_method: str = None # 'polygon_shapes', 'font_shapes', 'circle_shapes' def __init__(self, canvas: CTkCanvas): self._canvas = canvas self._round_width_to_even_numbers: bool = True self._round_height_to_even_numbers: bool = True def set_round_to_even_numbers(self, round_width_to_even_numbers: bool = True, round_height_to_even_numbers: bool = True): self._round_width_to_even_numbers: bool = round_width_to_even_numbers self._round_height_to_even_numbers: bool = round_height_to_even_numbers 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_background_corners(self, width: Union[float, int], height: Union[float, int], ): if self._round_width_to_even_numbers: width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only if self._round_height_to_even_numbers: height = math.floor(height / 2) * 2 requires_recoloring = False if not self._canvas.find_withtag("background_corner_top_left"): self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_top_left"), width=0) requires_recoloring = True if not self._canvas.find_withtag("background_corner_top_right"): self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_top_right"), width=0) requires_recoloring = True if not self._canvas.find_withtag("background_corner_bottom_right"): self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_bottom_right"), width=0) requires_recoloring = True if not self._canvas.find_withtag("background_corner_bottom_left"): self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_bottom_left"), width=0) requires_recoloring = True mid_width, mid_height = round(width / 2), round(height / 2) self._canvas.coords("background_corner_top_left", (0, 0, mid_width, mid_height)) self._canvas.coords("background_corner_top_right", (mid_width, 0, width, mid_height)) self._canvas.coords("background_corner_bottom_right", (mid_width, mid_height, width, height)) self._canvas.coords("background_corner_bottom_left", (0, mid_height, mid_width, height)) if requires_recoloring: # new parts were added -> manage z-order self._canvas.tag_lower("background_parts") return requires_recoloring def draw_rounded_rect_with_border(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], border_width: Union[float, int], overwrite_preferred_drawing_method: str = None) -> 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. returns bool if recoloring is necessary """ if self._round_width_to_even_numbers: width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only if self._round_height_to_even_numbers: height = math.floor(height / 2) * 2 corner_radius = round(corner_radius) if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too large corner_radius = min(width / 2, height / 2) border_width = round(border_width) 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 overwrite_preferred_drawing_method is not None: preferred_drawing_method = overwrite_preferred_drawing_method else: preferred_drawing_method = self.preferred_drawing_method if preferred_drawing_method == "polygon_shapes": return self.__draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius) elif preferred_drawing_method == "font_shapes": return self.__draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ()) elif preferred_drawing_method == "circle_shapes": return self.__draw_rounded_rect_with_border_circle_shapes(width, height, corner_radius, border_width, inner_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") self._canvas.tag_lower("background_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") self._canvas.tag_lower("background_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 # border button parts if border_width > 0: if corner_radius > 0: if not self._canvas.find_withtag("border_oval_1"): self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_1", "border_corner_part", "border_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_2", "border_corner_part", "border_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_3", "border_corner_part", "border_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("border_oval_4", "border_corner_part", "border_parts"), width=0) self._canvas.tag_lower("border_parts") requires_recoloring = True self._canvas.coords("border_oval_1", 0, 0, corner_radius * 2 - 1, corner_radius * 2 - 1) self._canvas.coords("border_oval_2", width - corner_radius * 2, 0, width - 1, corner_radius * 2 - 1) self._canvas.coords("border_oval_3", 0, height - corner_radius * 2, corner_radius * 2 - 1, height - 1) self._canvas.coords("border_oval_4", width - corner_radius * 2, height - corner_radius * 2, width - 1, height - 1) else: self._canvas.delete("border_corner_part") 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) self._canvas.tag_lower("border_parts") requires_recoloring = True 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") # inner button parts 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_corner_part", "inner_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_2", "inner_corner_part", "inner_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_3", "inner_corner_part", "inner_parts"), width=0) self._canvas.create_oval(0, 0, 0, 0, tags=("inner_oval_4", "inner_corner_part", "inner_parts"), width=0) self._canvas.tag_raise("inner_parts") requires_recoloring = True self._canvas.coords("inner_oval_1", (border_width, border_width, border_width + inner_corner_radius * 2 - 1, border_width + inner_corner_radius * 2 - 1)) self._canvas.coords("inner_oval_2", (width - border_width - inner_corner_radius * 2, border_width, width - border_width - 1, border_width + inner_corner_radius * 2 - 1)) self._canvas.coords("inner_oval_3", (border_width, height - border_width - inner_corner_radius * 2, border_width + inner_corner_radius * 2 - 1, height - border_width - 1)) self._canvas.coords("inner_oval_4", (width - border_width - inner_corner_radius * 2, height - border_width - inner_corner_radius * 2, width - border_width - 1, height - border_width - 1)) else: self._canvas.delete("inner_corner_part") # delete inner corner parts if not needed 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) self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_2", "inner_rectangle_part", "inner_parts"), width=0) self._canvas.tag_raise("inner_parts") requires_recoloring = True 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)) return requires_recoloring def draw_rounded_rect_with_border_vertical_split(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], border_width: Union[float, int], left_section_width: Union[float, int]) -> bool: """ Draws a rounded rectangle with a corner_radius and border_width on the canvas which is split at left_section_width. The border elements have the tags 'border_parts_left', 'border_parts_lright', the main foreground elements have an 'inner_parts_left' and inner_parts_right' tag, to color the elements accordingly. returns bool if recoloring is necessary """ left_section_width = round(left_section_width) if self._round_width_to_even_numbers: width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only if self._round_height_to_even_numbers: height = math.floor(height / 2) * 2 corner_radius = round(corner_radius) 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_width = round(border_width) 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 left_section_width > width - corner_radius * 2: left_section_width = width - corner_radius * 2 elif left_section_width < corner_radius * 2: left_section_width = corner_radius * 2 if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": return self.__draw_rounded_rect_with_border_vertical_split_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, left_section_width) elif self.preferred_drawing_method == "font_shapes": return self.__draw_rounded_rect_with_border_vertical_split_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, left_section_width, ()) def __draw_rounded_rect_with_border_vertical_split_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, left_section_width: 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_left_1", "border_parts_left", "border_parts", "left_parts")) self._canvas.create_polygon((0, 0, 0, 0), tags=("border_line_right_1", "border_parts_right", "border_parts", "right_parts")) self._canvas.create_rectangle((0, 0, 0, 0), tags=("border_rect_left_1", "border_parts_left", "border_parts", "left_parts"), width=0) self._canvas.create_rectangle((0, 0, 0, 0), tags=("border_rect_right_1", "border_parts_right", "border_parts", "right_parts"), width=0) requires_recoloring = True self._canvas.coords("border_line_left_1", (corner_radius, corner_radius, left_section_width - corner_radius, corner_radius, left_section_width - corner_radius, height - corner_radius, corner_radius, height - corner_radius)) self._canvas.coords("border_line_right_1", (left_section_width + corner_radius, corner_radius, width - corner_radius, corner_radius, width - corner_radius, height - corner_radius, left_section_width + corner_radius, height - corner_radius)) self._canvas.coords("border_rect_left_1", (left_section_width - corner_radius, 0, left_section_width, height)) self._canvas.coords("border_rect_right_1", (left_section_width, 0, left_section_width + corner_radius, height)) self._canvas.itemconfig("border_line_left_1", joinstyle=tkinter.ROUND, width=corner_radius * 2) self._canvas.itemconfig("border_line_right_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_left_1", "inner_parts_left", "inner_parts", "left_parts"), joinstyle=tkinter.ROUND) self._canvas.create_polygon((0, 0, 0, 0), tags=("inner_line_right_1", "inner_parts_right", "inner_parts", "right_parts"), joinstyle=tkinter.ROUND) self._canvas.create_rectangle((0, 0, 0, 0), tags=("inner_rect_left_1", "inner_parts_left", "inner_parts", "left_parts"), width=0) self._canvas.create_rectangle((0, 0, 0, 0), tags=("inner_rect_right_1", "inner_parts_right", "inner_parts", "right_parts"), width=0) requires_recoloring = True self._canvas.coords("inner_line_left_1", corner_radius, corner_radius, left_section_width - inner_corner_radius, corner_radius, left_section_width - inner_corner_radius, height - corner_radius, corner_radius, height - corner_radius) self._canvas.coords("inner_line_right_1", left_section_width + inner_corner_radius, corner_radius, width - corner_radius, corner_radius, width - corner_radius, height - corner_radius, left_section_width + inner_corner_radius, height - corner_radius) self._canvas.coords("inner_rect_left_1", (left_section_width - inner_corner_radius, border_width, left_section_width, height - border_width)) self._canvas.coords("inner_rect_right_1", (left_section_width, border_width, left_section_width + inner_corner_radius, height - border_width)) self._canvas.itemconfig("inner_line_left_1", width=inner_corner_radius * 2) self._canvas.itemconfig("inner_line_right_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") self._canvas.tag_lower("background_parts") return requires_recoloring def __draw_rounded_rect_with_border_vertical_split_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, left_section_width: 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_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) 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) 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) 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_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) 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_left_1", "border_rectangle_part", "border_parts_left", "border_parts", "left_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_left_2", "border_rectangle_part", "border_parts_left", "border_parts", "left_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_right_1", "border_rectangle_part", "border_parts_right", "border_parts", "right_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_right_2", "border_rectangle_part", "border_parts_right", "border_parts", "right_parts"), width=0) requires_recoloring = True # change position of border rectangle parts self._canvas.coords("border_rectangle_left_1", (0, corner_radius, left_section_width, height - corner_radius)) self._canvas.coords("border_rectangle_left_2", (corner_radius, 0, left_section_width, height)) self._canvas.coords("border_rectangle_right_1", (left_section_width, corner_radius, width, height - corner_radius)) self._canvas.coords("border_rectangle_right_2", (left_section_width, 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_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) 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) 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_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) 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_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) 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_left_1", "inner_rectangle_part", "inner_parts_left", "inner_parts", "left_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_right_1", "inner_rectangle_part", "inner_parts_right", "inner_parts", "right_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_left_2", "inner_rectangle_part", "inner_parts_left", "inner_parts", "left_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_right_2", "inner_rectangle_part", "inner_parts_right", "inner_parts", "right_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_left_2") self._canvas.delete("inner_rectangle_right_2") # change position of inner rectangle parts self._canvas.coords("inner_rectangle_left_1", (border_width + inner_corner_radius, border_width, left_section_width, height - border_width)) self._canvas.coords("inner_rectangle_left_2", (border_width, border_width + inner_corner_radius, left_section_width, height - inner_corner_radius - border_width)) self._canvas.coords("inner_rectangle_right_1", (left_section_width, border_width, width - border_width - inner_corner_radius, height - border_width)) self._canvas.coords("inner_rectangle_right_2", (left_section_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") self._canvas.tag_lower("background_parts") return requires_recoloring def draw_rounded_progress_bar_with_border(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], border_width: Union[float, int], progress_value_1: float, progress_value_2: float, orientation: str) -> bool: """ Draws a rounded bar on the canvas, and onntop sits a progress bar from value 1 to value 2 (range 0-1, left to right, bottom to top). The border elements get the 'border_parts' tag", the main elements get the 'inner_parts' tag and the progress elements get the 'progress_parts' tag. The 'orientation' argument defines from which direction the progress starts (n, w, s, e). returns bool if recoloring is necessary """ if self._round_width_to_even_numbers: width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only if self._round_height_to_even_numbers: 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_width = round(border_width) 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" or self.preferred_drawing_method == "circle_shapes": return self.__draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, progress_value_1, progress_value_2, orientation) elif self.preferred_drawing_method == "font_shapes": return self.__draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, progress_value_1, progress_value_2, orientation) 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_slider_with_border_and_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, orientation: str) -> bool: if self._round_width_to_even_numbers: width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only if self._round_height_to_even_numbers: 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" or self.preferred_drawing_method == "circle_shapes": return self.__draw_rounded_slider_with_border_and_button_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, button_length, button_corner_radius, slider_value, orientation) elif self.preferred_drawing_method == "font_shapes": return self.__draw_rounded_slider_with_border_and_button_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, button_length, button_corner_radius, slider_value, orientation) def __draw_rounded_slider_with_border_and_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, 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, 0, slider_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"), 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) 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) return requires_recoloring def __draw_rounded_slider_with_border_and_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, 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, 0, slider_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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_1_b", "slider_corner_part", "slider_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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_2_b", "slider_corner_part", "slider_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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_4_b", "slider_corner_part", "slider_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"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("slider_oval_3_b", "slider_corner_part", "slider_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True elif self._canvas.find_withtag("border_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"), width=0) requires_recoloring = True elif self._canvas.find_withtag("slider_rectangle_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"), 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) if requires_recoloring: # new parts were added -> manage z-order self._canvas.tag_raise("slider_parts") 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: if self._round_width_to_even_numbers: width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only if self._round_height_to_even_numbers: 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 height > 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 height > 2 * corner_radius: self._canvas.delete("scrollbar_rectangle_1") if not self._canvas.find_withtag("scrollbar_rectangle_2") and width > 2 * corner_radius: 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 width > 2 * corner_radius: self._canvas.delete("scrollbar_rectangle_2") if orientation == "vertical": 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) 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_rectangle_1", corner_radius - inner_corner_radius + (width - 2 * corner_radius) * start_value, corner_radius, corner_radius + inner_corner_radius + (width - 2 * corner_radius) * end_value, height - corner_radius) self._canvas.coords("scrollbar_rectangle_2", corner_radius + (width - 2 * corner_radius) * start_value, corner_radius - inner_corner_radius, corner_radius + (width - 2 * corner_radius) * end_value, height - (corner_radius - inner_corner_radius)) 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. returns bool if recoloring is necessary """ size = round(size) requires_recoloring = False if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": x, y, radius = width / 2, height / 2, size / 2.8 if not self._canvas.find_withtag("checkmark"): self._canvas.create_line(0, 0, 0, 0, tags=("checkmark", "create_line"), width=round(height / 8), joinstyle=tkinter.MITER, capstyle=tkinter.ROUND) self._canvas.tag_raise("checkmark") requires_recoloring = True self._canvas.coords("checkmark", x + radius, y - radius, x - radius / 4, y + radius * 0.8, x - radius, y + radius / 6) elif self.preferred_drawing_method == "font_shapes": if not self._canvas.find_withtag("checkmark"): self._canvas.create_text(0, 0, text="Z", font=("CustomTkinter_shapes_font", -size), tags=("checkmark", "create_text"), anchor=tkinter.CENTER) self._canvas.tag_raise("checkmark") requires_recoloring = True self._canvas.coords("checkmark", round(width / 2), round(height / 2)) return requires_recoloring def draw_dropdown_arrow(self, x_position: Union[int, float], y_position: Union[int, float], size: Union[int, float]) -> bool: """ Draws a dropdown bottom facing arrow at (x_position, y_position) in a given size returns bool if recoloring is necessary """ x_position, y_position, size = round(x_position), round(y_position), round(size) requires_recoloring = False if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": if not self._canvas.find_withtag("dropdown_arrow"): self._canvas.create_line(0, 0, 0, 0, tags="dropdown_arrow", width=round(size / 3), joinstyle=tkinter.ROUND, capstyle=tkinter.ROUND) self._canvas.tag_raise("dropdown_arrow") requires_recoloring = True self._canvas.coords("dropdown_arrow", x_position - (size / 2), y_position - (size / 5), x_position, y_position + (size / 5), x_position + (size / 2), y_position - (size / 5)) elif self.preferred_drawing_method == "font_shapes": if not self._canvas.find_withtag("dropdown_arrow"): self._canvas.create_text(0, 0, text="Y", font=("CustomTkinter_shapes_font", -size), tags="dropdown_arrow", anchor=tkinter.CENTER) self._canvas.tag_raise("dropdown_arrow") requires_recoloring = True self._canvas.itemconfigure("dropdown_arrow", font=("CustomTkinter_shapes_font", -size)) self._canvas.coords("dropdown_arrow", x_position, y_position) return requires_recoloring