added type hints and private hints to all widget classes, fixed #497, removed get and set methods from some widgets

This commit is contained in:
Tom Schimansky 2022-10-02 03:23:10 +02:00
parent 5717fc68e2
commit d6075ad544
20 changed files with 2552 additions and 2488 deletions

View File

@ -10,7 +10,10 @@ from .widget_base_class import CTkBaseClass
class CTkButton(CTkBaseClass):
""" button with border, rounded corners, hover effect, image support """
"""
Button with rounded corners, border, hover effect, image support, click command and textvariable.
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color: Union[str, Tuple[str, str], None] = None,
@ -33,34 +36,34 @@ class CTkButton(CTkBaseClass):
command: Callable = None,
**kwargs):
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self.border_color = ThemeManager.theme["color"]["button_border"] if border_color == "default_theme" else border_color
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self._fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self._hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self._border_color = ThemeManager.theme["color"]["button_border"] if border_color == "default_theme" else border_color
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self._text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
# shape
self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width
self._corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._border_width = ThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width
# text, font, image
self.image = image
self.image_label = None
self.text = text
self.text_label = None
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self._image = image
self._image_label = None
self._text = text
self._text_label = None
self._text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# callback and hover functionality
self.command = command
self.textvariable = textvariable
self.state = state
self.hover = hover
self.compound = compound
self.click_animation_running = False
self._command = command
self._textvariable = textvariable
self._state = state
self._hover = hover
self._compound = compound
self._click_animation_running = False
# configure grid system (2x2)
self.grid_rowconfigure(0, weight=1)
@ -69,309 +72,301 @@ class CTkButton(CTkBaseClass):
self.grid_columnconfigure(1, weight=1)
# canvas
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(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
self.draw_engine = DrawEngine(self.canvas)
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(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
self._draw_engine = DrawEngine(self._canvas)
# canvas event bindings
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.clicked)
self.canvas.bind("<Button-1>", self.clicked)
self.bind('<Configure>', self.update_dimensions_event)
self._canvas.bind("<Enter>", self._on_enter)
self._canvas.bind("<Leave>", self._on_leave)
self._canvas.bind("<Button-1>", self._clicked)
self._canvas.bind("<Button-1>", self._clicked)
self.bind('<Configure>', self._update_dimensions_event)
# configure cursor and initial draw
self.set_cursor()
self.draw()
self._set_cursor()
self._draw()
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
if self.text_label is not None:
self.text_label.destroy()
self.text_label = None
if self.image_label is not None:
self.image_label.destroy()
self.image_label = None
if self._text_label is not None:
self._text_label.destroy()
self._text_label = None
if self._image_label is not None:
self._image_label.destroy()
self._image_label = None
self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
height=self.apply_widget_scaling(self._desired_height))
self.draw()
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: int = None, height: int = None):
super().set_dimensions(width, height)
def _set_dimensions(self, width: int = None, height: int = 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()
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw()
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(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))
def _draw(self, no_color_updates=False):
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(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))
if no_color_updates is False or requires_recoloring:
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
# set color for the button border parts (outline)
self.canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self._border_color, self._appearance_mode),
fill=ThemeManager.single_color(self._border_color, self._appearance_mode))
# set color for inner button parts
if self.fg_color is None:
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode),
fill=ThemeManager.single_color(self.bg_color, self._appearance_mode))
if self._fg_color is None:
self._canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode),
fill=ThemeManager.single_color(self._bg_color, self._appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode))
# create text label if text given
if self.text is not None and self.text != "":
if self._text is not None and self._text != "":
if self.text_label is None:
self.text_label = tkinter.Label(master=self,
font=self.apply_font_scaling(self.text_font),
text=self.text,
textvariable=self.textvariable)
if self._text_label is None:
self._text_label = tkinter.Label(master=self,
font=self._apply_font_scaling(self._text_font),
text=self._text,
textvariable=self._textvariable)
self.text_label.bind("<Enter>", self.on_enter)
self.text_label.bind("<Leave>", self.on_leave)
self.text_label.bind("<Button-1>", self.clicked)
self.text_label.bind("<Button-1>", self.clicked)
self._text_label.bind("<Enter>", self._on_enter)
self._text_label.bind("<Leave>", self._on_leave)
self._text_label.bind("<Button-1>", self._clicked)
self._text_label.bind("<Button-1>", self._clicked)
if no_color_updates is False:
# set text_label fg color (text color)
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
self._text_label.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode))
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)))
if self._state == tkinter.DISABLED:
self._text_label.configure(fg=(ThemeManager.single_color(self._text_color_disabled, self._appearance_mode)))
else:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
self._text_label.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode))
if self.fg_color is None:
self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
if self._fg_color is None:
self._text_label.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
else:
self.text_label.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._text_label.configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode))
else:
# delete text_label if no text given
if self.text_label is not None:
self.text_label.destroy()
self.text_label = None
if self._text_label is not None:
self._text_label.destroy()
self._text_label = None
# create image label if image given
if self.image is not None:
if self._image is not None:
if self.image_label is None:
self.image_label = tkinter.Label(master=self)
if self._image_label is None:
self._image_label = tkinter.Label(master=self)
self.image_label.bind("<Enter>", self.on_enter)
self.image_label.bind("<Leave>", self.on_leave)
self.image_label.bind("<Button-1>", self.clicked)
self.image_label.bind("<Button-1>", self.clicked)
self._image_label.bind("<Enter>", self._on_enter)
self._image_label.bind("<Leave>", self._on_leave)
self._image_label.bind("<Button-1>", self._clicked)
self._image_label.bind("<Button-1>", self._clicked)
if no_color_updates is False:
# set image_label bg color (background color of label)
if self.fg_color is None:
self.image_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
if self._fg_color is None:
self._image_label.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
else:
self.image_label.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._image_label.configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self.image_label.configure(image=self.image) # set image
self._image_label.configure(image=self._image) # set image
else:
# delete text_label if no text given
if self.image_label is not None:
self.image_label.destroy()
self.image_label = None
if self._image_label is not None:
self._image_label.destroy()
self._image_label = None
# create grid layout with just an image given
if self.image_label is not None and self.text_label is None:
self.image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) # bottom pady with +1 for rounding to even
if self._image_label is not None and self._text_label is None:
self._image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1)) # bottom pady with +1 for rounding to even
# create grid layout with just text given
if self.image_label is None and self.text_label is not None:
self.text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
padx=self.apply_widget_scaling(self.corner_radius),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) # bottom pady with +1 for rounding to even
if self._image_label is None and self._text_label is not None:
self._text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
padx=self._apply_widget_scaling(self._corner_radius),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1)) # bottom pady with +1 for rounding to even
# create grid layout of image and text label in 2x2 grid system with given compound
if self.image_label is not None and self.text_label is not None:
if self.compound == tkinter.LEFT or self.compound == "left":
self.image_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
self.text_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
elif self.compound == tkinter.TOP or self.compound == "top":
self.image_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
pady=(self.apply_widget_scaling(self.border_width), 2))
self.text_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
pady=(2, self.apply_widget_scaling(self.border_width)))
elif self.compound == tkinter.RIGHT or self.compound == "right":
self.image_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
self.text_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
elif self.compound == tkinter.BOTTOM or self.compound == "bottom":
self.image_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
pady=(2, self.apply_widget_scaling(self.border_width)))
self.text_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
pady=(self.apply_widget_scaling(self.border_width), 2))
if self._image_label is not None and self._text_label is not None:
if self._compound == tkinter.LEFT or self._compound == "left":
self._image_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)), 2),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1))
self._text_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
padx=(2, max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width))),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1))
elif self._compound == tkinter.TOP or self._compound == "top":
self._image_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)),
pady=(self._apply_widget_scaling(self._border_width), 2))
self._text_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)),
pady=(2, self._apply_widget_scaling(self._border_width)))
elif self._compound == tkinter.RIGHT or self._compound == "right":
self._image_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
padx=(2, max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width))),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1))
self._text_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)), 2),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1))
elif self._compound == tkinter.BOTTOM or self._compound == "bottom":
self._image_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)),
pady=(2, self._apply_widget_scaling(self._border_width)))
self._text_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)),
pady=(self._apply_widget_scaling(self._border_width), 2))
def configure(self, require_redraw=False, **kwargs):
if "text" in kwargs:
self.text = kwargs.pop("text")
if self.text_label is None:
self._text = kwargs.pop("text")
if self._text_label is None:
require_redraw = True # text_label will be created in .draw()
else:
self.text_label.configure(text=self.text)
self._text_label.configure(text=self._text)
if "text_font" in kwargs:
self.text_font = kwargs.pop("text_font")
if self.text_label is not None:
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self._text_font = kwargs.pop("text_font")
if self._text_label is not None:
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
if "state" in kwargs:
self.state = kwargs.pop("state")
self.set_cursor()
self._state = kwargs.pop("state")
self._set_cursor()
require_redraw = True
if "image" in kwargs:
self.image = kwargs.pop("image")
self._image = kwargs.pop("image")
require_redraw = True
if "corner_radius" in kwargs:
self.corner_radius = kwargs.pop("corner_radius")
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "compound" in kwargs:
self.compound = kwargs.pop("compound")
self._compound = kwargs.pop("compound")
require_redraw = True
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
if "border_color" in kwargs:
self.border_color = kwargs.pop("border_color")
self._border_color = kwargs.pop("border_color")
require_redraw = True
if "hover_color" in kwargs:
self.hover_color = kwargs.pop("hover_color")
self._hover_color = kwargs.pop("hover_color")
require_redraw = True
if "text_color" in kwargs:
self.text_color = kwargs.pop("text_color")
self._text_color = kwargs.pop("text_color")
require_redraw = True
if "command" in kwargs:
self.command = kwargs.pop("command")
self._command = kwargs.pop("command")
if "textvariable" in kwargs:
self.textvariable = kwargs.pop("textvariable")
if self.text_label is not None:
self.text_label.configure(textvariable=self.textvariable)
self._textvariable = kwargs.pop("textvariable")
if self._text_label is not None:
self._text_label.configure(textvariable=self._textvariable)
if "width" in kwargs:
self.set_dimensions(width=kwargs.pop("width"))
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self.set_dimensions(height=kwargs.pop("height"))
self._set_dimensions(height=kwargs.pop("height"))
super().configure(require_redraw=require_redraw, **kwargs)
def set_cursor(self):
def _set_cursor(self):
if Settings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and self.command is not None and Settings.cursor_manipulation_enabled:
if self._state == tkinter.DISABLED:
if sys.platform == "darwin" and self._command is not None and Settings.cursor_manipulation_enabled:
self.configure(cursor="arrow")
elif sys.platform.startswith("win") and self.command is not None and Settings.cursor_manipulation_enabled:
elif sys.platform.startswith("win") and self._command is not None and Settings.cursor_manipulation_enabled:
self.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and self.command is not None and Settings.cursor_manipulation_enabled:
elif self._state == tkinter.NORMAL:
if sys.platform == "darwin" and self._command is not None and Settings.cursor_manipulation_enabled:
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and self.command is not None and Settings.cursor_manipulation_enabled:
elif sys.platform.startswith("win") and self._command is not None and Settings.cursor_manipulation_enabled:
self.configure(cursor="hand2")
def set_image(self, image):
""" will be removed in next major """
self.configure(image=image)
def set_text(self, text):
""" will be removed in next major """
self.configure(text=text)
def on_enter(self, event=None):
if self.hover is True and self.state == tkinter.NORMAL:
if self.hover_color is None:
inner_parts_color = self.fg_color
def _on_enter(self, event=None):
if self._hover is True and self._state == tkinter.NORMAL:
if self._hover_color is None:
inner_parts_color = self._fg_color
else:
inner_parts_color = self.hover_color
inner_parts_color = self._hover_color
# set color of inner button parts to hover color
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(inner_parts_color, self._appearance_mode),
fill=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(inner_parts_color, self._appearance_mode),
fill=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
# set text_label bg color to button hover color
if self.text_label is not None:
self.text_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
if self._text_label is not None:
self._text_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
# set image_label bg color to button hover color
if self.image_label is not None:
self.image_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
if self._image_label is not None:
self._image_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
def on_leave(self, event=None):
self.click_animation_running = False
def _on_leave(self, event=None):
self._click_animation_running = False
if self.hover is True:
if self.fg_color is None:
inner_parts_color = self.bg_color
if self._hover is True:
if self._fg_color is None:
inner_parts_color = self._bg_color
else:
inner_parts_color = self.fg_color
inner_parts_color = self._fg_color
# set color of inner button parts
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(inner_parts_color, self._appearance_mode),
fill=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(inner_parts_color, self._appearance_mode),
fill=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
# set text_label bg color (label color)
if self.text_label is not None:
self.text_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
if self._text_label is not None:
self._text_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
# set image_label bg color (image bg color)
if self.image_label is not None:
self.image_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
if self._image_label is not None:
self._image_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
def click_animation(self):
if self.click_animation_running:
self.on_enter()
def _click_animation(self):
if self._click_animation_running:
self._on_enter()
def clicked(self, event=None):
if self.command is not None:
if self.state != tkinter.DISABLED:
def _clicked(self, event=None):
if self._command is not None:
if self._state != tkinter.DISABLED:
# click animation: change color with .on_leave() and back to normal after 100ms with click_animation()
self.on_leave()
self.click_animation_running = True
self.after(100, self.click_animation)
self._on_leave()
self._click_animation_running = True
self.after(100, self._click_animation)
self.command()
self._command()

View File

@ -4,11 +4,32 @@ from typing import Union, Tuple
class CTkCanvas(tkinter.Canvas):
"""
Canvas with additional functionality to draw antialiased circles on Windows/Linux.
Call .init_font_character_mapping() at program start to load the correct character
dictionary according to the operating system. Characters (circle sizes) are optimised
to look best for rendering CustomTkinter shapes on the different operating systems.
- .create_aa_circle() creates antialiased circle and returns int identifier.
- .coords() is modified to support the aa-circle shapes correctly like you would expect.
- .itemconfig() is also modified to support aa-cricle shapes.
The aa-circles are created by choosing a character from the custom created and loaded
font 'CustomTkinter_shapes_font'. It contains circle shapes with different sizes filling
either the whole character space or just pert of it (characters A to R). Circles with a smaller
radius need a smaller circle character to look correct when rendered on the canvas.
For an optimal result, the draw-engine creates two aa-circles on top of each other, while
one is rotated by 90 degrees. This helps to make the circle look more symetric, which is
not can be a problem when using only a single circle character.
"""
radius_to_char_fine: dict = None # dict to map radius to font circle character
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.aa_circle_canvas_ids = set()
self._aa_circle_canvas_ids = set()
@classmethod
def init_font_character_mapping(cls):
@ -43,7 +64,7 @@ class CTkCanvas(tkinter.Canvas):
else:
cls.radius_to_char_fine = radius_to_char_fine_windows_10
def get_char_from_radius(self, radius: int) -> str:
def _get_char_from_radius(self, radius: int) -> str:
if radius >= 20:
return "A"
else:
@ -52,10 +73,10 @@ class CTkCanvas(tkinter.Canvas):
def create_aa_circle(self, x_pos: int, y_pos: int, radius: int, angle: int = 0, fill: str = "white",
tags: Union[str, Tuple[str, ...]] = "", anchor: str = tkinter.CENTER) -> int:
# create a circle with a font element
circle_1 = self.create_text(x_pos, y_pos, text=self.get_char_from_radius(radius), anchor=anchor, fill=fill,
circle_1 = self.create_text(x_pos, y_pos, text=self._get_char_from_radius(radius), anchor=anchor, fill=fill,
font=("CustomTkinter_shapes_font", -radius * 2), tags=tags, angle=angle)
self.addtag_withtag("ctk_aa_circle_font_element", circle_1)
self.aa_circle_canvas_ids.add(circle_1)
self._aa_circle_canvas_ids.add(circle_1)
return circle_1
@ -66,13 +87,13 @@ class CTkCanvas(tkinter.Canvas):
super().coords(coords_id, *args[:2])
if len(args) == 3:
super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self.get_char_from_radius(args[2]))
super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self._get_char_from_radius(args[2]))
elif type(tag_or_id) == int and tag_or_id in self.aa_circle_canvas_ids:
elif type(tag_or_id) == int and tag_or_id in self._aa_circle_canvas_ids:
super().coords(tag_or_id, *args[:2])
if len(args) == 3:
super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self.get_char_from_radius(args[2]))
super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self._get_char_from_radius(args[2]))
else:
super().coords(tag_or_id, *args)
@ -83,14 +104,14 @@ class CTkCanvas(tkinter.Canvas):
del kwargs_except_outline["outline"]
if type(tag_or_id) == int:
if tag_or_id in self.aa_circle_canvas_ids:
if tag_or_id in self._aa_circle_canvas_ids:
super().itemconfigure(tag_or_id, *args, **kwargs_except_outline)
else:
super().itemconfigure(tag_or_id, *args, **kwargs)
else:
configure_ids = self.find_withtag(tag_or_id)
for configure_id in configure_ids:
if configure_id in self.aa_circle_canvas_ids:
if configure_id in self._aa_circle_canvas_ids:
super().itemconfigure(configure_id, *args, **kwargs_except_outline)
else:
super().itemconfigure(configure_id, *args, **kwargs)

View File

@ -1,6 +1,6 @@
import tkinter
import sys
from typing import Union
from typing import Union, Tuple, Callable
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -10,313 +10,316 @@ from .widget_base_class import CTkBaseClass
class CTkCheckBox(CTkBaseClass):
""" tkinter custom checkbox with border, rounded corners and hover effect """
"""
Checkbox with rounded corners, border, variable support and hover effect.
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
hover_color="default_theme",
border_color="default_theme",
border_width="default_theme",
checkmark_color="default_theme",
width=24,
height=24,
corner_radius="default_theme",
text_font="default_theme",
text_color="default_theme",
text="CTkCheckBox",
text_color_disabled="default_theme",
hover=True,
command=None,
state=tkinter.NORMAL,
onvalue=1,
offvalue=0,
variable=None,
textvariable=None,
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str]] = "default_theme",
hover_color: Union[str, Tuple[str, str]] = "default_theme",
border_color: Union[str, Tuple[str, str]] = "default_theme",
border_width: Union[int, str] = "default_theme",
checkmark_color: Union[str, Tuple[str, str]] = "default_theme",
width: int = 24,
height: int = 24,
corner_radius: Union[int, str] = "default_theme",
text_font: any = "default_theme",
text_color: Union[str, Tuple[str, str]] = "default_theme",
text: str = "CTkCheckBox",
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
hover: bool = True,
command: Callable = None,
state: str = tkinter.NORMAL,
onvalue: Union[int, str] = 1,
offvalue: Union[int, str] = 0,
variable: tkinter.Variable = None,
textvariable: tkinter.Variable = None,
**kwargs):
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self.border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
self.checkmark_color = ThemeManager.theme["color"]["checkmark"] if checkmark_color == "default_theme" else checkmark_color
self._fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self._hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self._border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
self._checkmark_color = ThemeManager.theme["color"]["checkmark"] if checkmark_color == "default_theme" else checkmark_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["checkbox_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["checkbox_border_width"] if border_width == "default_theme" else border_width
self._corner_radius = ThemeManager.theme["shape"]["checkbox_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._border_width = ThemeManager.theme["shape"]["checkbox_border_width"] if border_width == "default_theme" else border_width
# text
self.text = text
self.text_label: Union[tkinter.Label, None] = None
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self._text = text
self._text_label: Union[tkinter.Label, None] = None
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self._text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self._text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# callback and hover functionality
self.command = command
self.state = state
self.hover = hover
self.check_state = False
self._command = command
self._state = state
self._hover = hover
self._check_state = False
self.onvalue = onvalue
self.offvalue = offvalue
self.variable: tkinter.Variable = variable
self.variable_callback_blocked = False
self.textvariable: tkinter.Variable = textvariable
self.variable_callback_name = None
self._onvalue = onvalue
self._offvalue = offvalue
self._variable: tkinter.Variable = variable
self._variable_callback_blocked = False
self._textvariable: tkinter.Variable = textvariable
self._variable_callback_name = None
# configure grid system (1x3)
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=1)
self.grid_rowconfigure(0, weight=1)
self.bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self._desired_width),
height=self.apply_widget_scaling(self._desired_height))
self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self._bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
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(row=0, column=0, padx=0, pady=0, columnspan=1, rowspan=1)
self.draw_engine = DrawEngine(self.canvas)
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(row=0, column=0, padx=0, pady=0, columnspan=1, rowspan=1)
self._draw_engine = DrawEngine(self._canvas)
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.toggle)
self._canvas.bind("<Enter>", self._on_enter)
self._canvas.bind("<Leave>", self._on_leave)
self._canvas.bind("<Button-1>", self.toggle)
self.text_label = tkinter.Label(master=self,
bd=0,
text=self.text,
justify=tkinter.LEFT,
font=self.apply_font_scaling(self.text_font),
textvariable=self.textvariable)
self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self.text_label["anchor"] = "w"
self._text_label = tkinter.Label(master=self,
bd=0,
text=self._text,
justify=tkinter.LEFT,
font=self._apply_font_scaling(self._text_font),
textvariable=self._textvariable)
self._text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self._text_label["anchor"] = "w"
self.text_label.bind("<Enter>", self.on_enter)
self.text_label.bind("<Leave>", self.on_leave)
self.text_label.bind("<Button-1>", self.toggle)
self._text_label.bind("<Enter>", self._on_enter)
self._text_label.bind("<Leave>", self._on_leave)
self._text_label.bind("<Button-1>", self.toggle)
# register variable callback and set state according to variable
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.check_state = True if variable.get() == self.onvalue else False
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._onvalue else False
self.draw() # initial draw
self.set_cursor()
self._draw() # initial draw
self._set_cursor()
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
self.canvas.delete("checkmark")
self.bg_canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
self.draw()
self._canvas.delete("checkmark")
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_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):
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(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))
def _draw(self, no_color_updates=False):
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(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))
if self.check_state is True:
self.draw_engine.draw_checkmark(self.apply_widget_scaling(self._current_width),
self.apply_widget_scaling(self._current_height),
self.apply_widget_scaling(self._current_height * 0.58))
if self._check_state is True:
self._draw_engine.draw_checkmark(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._current_height * 0.58))
else:
self.canvas.delete("checkmark")
self._canvas.delete("checkmark")
self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._bg_canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
if self.check_state is True:
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
if self._check_state is True:
self._canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode))
if "create_line" in self.canvas.gettags("checkmark"):
self.canvas.itemconfig("checkmark", fill=ThemeManager.single_color(self.checkmark_color, self._appearance_mode))
if "create_line" in self._canvas.gettags("checkmark"):
self._canvas.itemconfig("checkmark", fill=ThemeManager.single_color(self._checkmark_color, self._appearance_mode))
else:
self.canvas.itemconfig("checkmark", fill=ThemeManager.single_color(self.checkmark_color, self._appearance_mode))
self._canvas.itemconfig("checkmark", fill=ThemeManager.single_color(self._checkmark_color, self._appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode),
fill=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode),
fill=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self._border_color, self._appearance_mode),
fill=ThemeManager.single_color(self._border_color, self._appearance_mode))
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)))
if self._state == tkinter.DISABLED:
self._text_label.configure(fg=(ThemeManager.single_color(self._text_color_disabled, self._appearance_mode)))
else:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
self._text_label.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode))
self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._text_label.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
def configure(self, require_redraw=False, **kwargs):
if "text" in kwargs:
self.text = kwargs.pop("text")
self.text_label.configure(text=self.text)
self._text = kwargs.pop("text")
self._text_label.configure(text=self._text)
if "text_font" in kwargs:
self.text_font = kwargs.pop("text_font")
if self.text_label is not None:
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self._text_font = kwargs.pop("text_font")
if self._text_label is not None:
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
if "state" in kwargs:
self.state = kwargs.pop("state")
self.set_cursor()
self._state = kwargs.pop("state")
self._set_cursor()
require_redraw = True
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
if "hover_color" in kwargs:
self.hover_color = kwargs.pop("hover_color")
self._hover_color = kwargs.pop("hover_color")
require_redraw = True
if "text_color" in kwargs:
self.text_color = kwargs.pop("text_color")
self._text_color = kwargs.pop("text_color")
require_redraw = True
if "border_color" in kwargs:
self.border_color = kwargs.pop("border_color")
self._border_color = kwargs.pop("border_color")
require_redraw = True
if "command" in kwargs:
self.command = kwargs.pop("command")
self._command = kwargs.pop("command")
if "textvariable" in kwargs:
self.textvariable = kwargs.pop("textvariable")
self.text_label.configure(textvariable=self.textvariable)
self._textvariable = kwargs.pop("textvariable")
self._text_label.configure(textvariable=self._textvariable)
if "variable" in kwargs:
if self.variable is not None and self.variable != "":
self.variable.trace_remove("write", self.variable_callback_name) # remove old variable callback
if self._variable is not None and self._variable != "":
self._variable.trace_remove("write", self._variable_callback_name) # remove old variable callback
self.variable = kwargs.pop("variable")
self._variable = kwargs.pop("variable")
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.check_state = True if self.variable.get() == self.onvalue else False
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._onvalue else False
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def set_cursor(self):
def _set_cursor(self):
if Settings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if self._state == tkinter.DISABLED:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
elif self._state == tkinter.NORMAL:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
if self.text_label is not None:
self.text_label.configure(cursor="pointinghand")
self._canvas.configure(cursor="pointinghand")
if self._text_label is not None:
self._text_label.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
if self.text_label is not None:
self.text_label.configure(cursor="hand2")
self._canvas.configure(cursor="hand2")
if self._text_label is not None:
self._text_label.configure(cursor="hand2")
def on_enter(self, event=0):
if self.hover is True and self.state == tkinter.NORMAL:
if self.check_state is True:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self.hover_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self.hover_color, self._appearance_mode))
def _on_enter(self, event=0):
if self._hover is True and self._state == tkinter.NORMAL:
if self._check_state is True:
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self._hover_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self._hover_color, self._appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self.hover_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self._hover_color, self._appearance_mode))
def on_leave(self, event=0):
if self.hover is True:
if self.check_state is True:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
def _on_leave(self, event=0):
if self._hover is True:
if self._check_state is True:
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._border_color, self._appearance_mode),
outline=ThemeManager.single_color(self._border_color, self._appearance_mode))
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
if self.variable.get() == self.onvalue:
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
if self._variable.get() == self._onvalue:
self.select(from_variable_callback=True)
elif self.variable.get() == self.offvalue:
elif self._variable.get() == self._offvalue:
self.deselect(from_variable_callback=True)
def toggle(self, event=0):
if self.state == tkinter.NORMAL:
if self.check_state is True:
self.check_state = False
self.draw()
if self._state == tkinter.NORMAL:
if self._check_state is True:
self._check_state = False
self._draw()
else:
self.check_state = True
self.draw()
self._check_state = True
self._draw()
if self.variable is not None:
self.variable_callback_blocked = True
self.variable.set(self.onvalue if self.check_state is True else self.offvalue)
self.variable_callback_blocked = False
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
self._variable_callback_blocked = False
if self.command is not None:
self.command()
if self._command is not None:
self._command()
def select(self, from_variable_callback=False):
self.check_state = True
self.draw()
self._check_state = True
self._draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.onvalue)
self.variable_callback_blocked = False
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._onvalue)
self._variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
self.check_state = False
self.draw()
self._check_state = False
self._draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.offvalue)
self.variable_callback_blocked = False
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._offvalue)
self._variable_callback_blocked = False
def get(self):
return self.onvalue if self.check_state is True else self.offvalue
def get(self) -> Union[int, str]:
return self._onvalue if self._check_state is True else self._offvalue

View File

@ -1,5 +1,6 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, List
from .dropdown_menu import DropdownMenu
from .ctk_canvas import CTkCanvas
@ -10,288 +11,293 @@ from .widget_base_class import CTkBaseClass
class CTkComboBox(CTkBaseClass):
"""
Combobox with dropdown menu, rounded corners, border, variable support.
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
border_color="default_theme",
button_color="default_theme",
button_hover_color="default_theme",
dropdown_color="default_theme",
dropdown_hover_color="default_theme",
dropdown_text_color="default_theme",
variable=None,
values=None,
command=None,
width=140,
height=28,
corner_radius="default_theme",
border_width="default_theme",
text_font="default_theme",
dropdown_text_font="default_theme",
text_color="default_theme",
text_color_disabled="default_theme",
hover=True,
state=tkinter.NORMAL,
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str]] = "default_theme",
border_color: Union[str, Tuple[str, str]] = "default_theme",
button_color: Union[str, Tuple[str, str]] = "default_theme",
button_hover_color: Union[str, Tuple[str, str]] = "default_theme",
dropdown_color: Union[str, Tuple[str, str]] = "default_theme",
dropdown_hover_color: Union[str, Tuple[str, str]] = "default_theme",
dropdown_text_color: Union[str, Tuple[str, str]] = "default_theme",
variable: tkinter.Variable = None,
values: List[str] = None,
command: Callable = None,
width: int = 140,
height: int = 28,
corner_radius: int = "default_theme",
border_width: int = "default_theme",
text_font: any = "default_theme",
dropdown_text_font: any = "default_theme",
text_color: Union[str, Tuple[str, str]] = "default_theme",
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
hover: bool = True,
state: str = tkinter.NORMAL,
**kwargs):
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color variables
self.fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
self.border_color = ThemeManager.theme["color"]["combobox_border"] if border_color == "default_theme" else border_color
self.button_color = ThemeManager.theme["color"]["combobox_border"] if button_color == "default_theme" else button_color
self.button_hover_color = ThemeManager.theme["color"]["combobox_button_hover"] if button_hover_color == "default_theme" else button_hover_color
self._fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
self._border_color = ThemeManager.theme["color"]["combobox_border"] if border_color == "default_theme" else border_color
self._button_color = ThemeManager.theme["color"]["combobox_border"] if button_color == "default_theme" else button_color
self._button_hover_color = ThemeManager.theme["color"]["combobox_button_hover"] if button_hover_color == "default_theme" else button_hover_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width
self._corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width
# text and font
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self._text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self._text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# callback and hover functionality
self.command = command
self.textvariable = variable
self.state = state
self.hover = hover
self._command = command
self._textvariable = variable
self._state = state
self._hover = hover
if values is None:
self.values = ["CTkComboBox"]
self._values = ["CTkComboBox"]
else:
self.values = values
self._values = values
self.dropdown_menu = DropdownMenu(master=self,
values=self.values,
command=self.dropdown_callback,
fg_color=dropdown_color,
hover_color=dropdown_hover_color,
text_color=dropdown_text_color,
text_font=dropdown_text_font)
self._dropdown_menu = DropdownMenu(master=self,
values=self._values,
command=self._dropdown_callback,
fg_color=dropdown_color,
hover_color=dropdown_hover_color,
text_color=dropdown_text_color,
text_font=dropdown_text_font)
# configure grid system (1x1)
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(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
self.draw_engine = DrawEngine(self.canvas)
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(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
self.draw_engine = DrawEngine(self._canvas)
self.entry = tkinter.Entry(master=self,
state=self.state,
width=1,
bd=0,
highlightthickness=0,
font=self.apply_font_scaling(self.text_font))
self._entry = tkinter.Entry(master=self,
state=self._state,
width=1,
bd=0,
highlightthickness=0,
font=self._apply_font_scaling(self._text_font))
left_section_width = self._current_width - self._current_height
self.entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew",
padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(3)),
max(self.apply_widget_scaling(self._current_width - left_section_width + 3), self.apply_widget_scaling(3))))
self._entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew",
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)),
max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))))
# insert default value
if len(self.values) > 0:
self.entry.insert(0, self.values[0])
if len(self._values) > 0:
self._entry.insert(0, self._values[0])
else:
self.entry.insert(0, "CTkComboBox")
self._entry.insert(0, "CTkComboBox")
self.draw() # initial draw
self._draw() # initial draw
# event bindings
self.canvas.tag_bind("right_parts", "<Enter>", self.on_enter)
self.canvas.tag_bind("dropdown_arrow", "<Enter>", self.on_enter)
self.canvas.tag_bind("right_parts", "<Leave>", self.on_leave)
self.canvas.tag_bind("dropdown_arrow", "<Leave>", self.on_leave)
self.canvas.tag_bind("right_parts", "<Button-1>", self.clicked)
self.canvas.tag_bind("dropdown_arrow", "<Button-1>", self.clicked)
self.bind('<Configure>', self.update_dimensions_event)
self._canvas.tag_bind("right_parts", "<Enter>", self._on_enter)
self._canvas.tag_bind("dropdown_arrow", "<Enter>", self._on_enter)
self._canvas.tag_bind("right_parts", "<Leave>", self._on_leave)
self._canvas.tag_bind("dropdown_arrow", "<Leave>", self._on_leave)
self._canvas.tag_bind("right_parts", "<Button-1>", self._clicked)
self._canvas.tag_bind("dropdown_arrow", "<Button-1>", self._clicked)
self.bind('<Configure>', self._update_dimensions_event)
if self.textvariable is not None:
self.entry.configure(textvariable=self.textvariable)
if self._textvariable is not None:
self._entry.configure(textvariable=self._textvariable)
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
# change entry font size and grid padding
left_section_width = self._current_width - self._current_height
self.entry.configure(font=self.apply_font_scaling(self.text_font))
self.entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew",
padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(3)),
max(self.apply_widget_scaling(self._current_width - left_section_width + 3), self.apply_widget_scaling(3))))
self._entry.configure(font=self._apply_font_scaling(self._text_font))
self._entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew",
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)),
max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))))
self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
height=self.apply_widget_scaling(self._desired_height))
self.draw()
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: int = None, height: int = None):
super().set_dimensions(width, height)
def _set_dimensions(self, width: int = None, height: int = 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()
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw()
def draw(self, no_color_updates=False):
def _draw(self, no_color_updates=False):
left_section_width = self._current_width - self._current_height
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(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(left_section_width))
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(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(left_section_width))
requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self.apply_widget_scaling(self._current_width - (self._current_height / 2)),
self.apply_widget_scaling(self._current_height / 2),
self.apply_widget_scaling(self._current_height / 3))
requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self._apply_widget_scaling(self._current_width - (self._current_height / 2)),
self._apply_widget_scaling(self._current_height / 2),
self._apply_widget_scaling(self._current_height / 3))
if no_color_updates is False or requires_recoloring or requires_recoloring_2:
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self.canvas.itemconfig("inner_parts_left",
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self.canvas.itemconfig("border_parts_left",
outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
self.canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
self.canvas.itemconfig("border_parts_right",
outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts_left",
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts_left",
outline=ThemeManager.single_color(self._border_color, self._appearance_mode),
fill=ThemeManager.single_color(self._border_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self._border_color, self._appearance_mode),
fill=ThemeManager.single_color(self._border_color, self._appearance_mode))
self._canvas.itemconfig("border_parts_right",
outline=ThemeManager.single_color(self._border_color, self._appearance_mode),
fill=ThemeManager.single_color(self._border_color, self._appearance_mode))
self.entry.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
disabledforeground=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode),
disabledbackground=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._entry.configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode),
fg=ThemeManager.single_color(self._text_color, self._appearance_mode),
disabledforeground=ThemeManager.single_color(self._text_color_disabled, self._appearance_mode),
disabledbackground=ThemeManager.single_color(self._fg_color, self._appearance_mode))
if self.state == tkinter.DISABLED:
self.canvas.itemconfig("dropdown_arrow",
fill=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode))
if self._state == tkinter.DISABLED:
self._canvas.itemconfig("dropdown_arrow",
fill=ThemeManager.single_color(self._text_color_disabled, self._appearance_mode))
else:
self.canvas.itemconfig("dropdown_arrow",
fill=ThemeManager.single_color(self.text_color, self._appearance_mode))
self._canvas.itemconfig("dropdown_arrow",
fill=ThemeManager.single_color(self._text_color, self._appearance_mode))
def open_dropdown_menu(self):
self.dropdown_menu.open(self.winfo_rootx(),
self.winfo_rooty() + self.apply_widget_scaling(self._current_height + 0))
def _open_dropdown_menu(self):
self._dropdown_menu.open(self.winfo_rootx(),
self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0))
def configure(self, require_redraw=False, **kwargs):
if "state" in kwargs:
self.state = kwargs.pop("state")
self.entry.configure(state=self.state)
self._state = kwargs.pop("state")
self._entry.configure(state=self._state)
require_redraw = True
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
if "button_color" in kwargs:
self.button_color = kwargs.pop("button_color")
self._button_color = kwargs.pop("button_color")
require_redraw = True
if "button_hover_color" in kwargs:
self.button_hover_color = kwargs.pop("button_hover_color")
self._button_hover_color = kwargs.pop("button_hover_color")
require_redraw = True
if "text_color" in kwargs:
self.text_color = kwargs.pop("text_color")
self._text_color = kwargs.pop("text_color")
require_redraw = True
if "text_font" in kwargs:
self.text_font = kwargs.pop("text_font")
self.entry.configure(font=self.apply_font_scaling(self.text_font))
self._text_font = kwargs.pop("text_font")
self._entry.configure(font=self._apply_font_scaling(self._text_font))
if "command" in kwargs:
self.command = kwargs.pop("command")
self._command = kwargs.pop("command")
if "variable" in kwargs:
self.textvariable = kwargs.pop("variable")
self.entry.configure(textvariable=self.textvariable)
self._textvariable = kwargs.pop("variable")
self._entry.configure(textvariable=self._textvariable)
if "width" in kwargs:
self.set_dimensions(width=kwargs.pop("width"))
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self.set_dimensions(height=kwargs.pop("height"))
self._set_dimensions(height=kwargs.pop("height"))
if "values" in kwargs:
self.values = kwargs.pop("values")
self.dropdown_menu.configure(values=self.values)
self._values = kwargs.pop("values")
self._dropdown_menu.configure(values=self._values)
if "dropdown_color" in kwargs:
self.dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color"))
self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color"))
if "dropdown_hover_color" in kwargs:
self.dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
if "dropdown_text_color" in kwargs:
self.dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
if "dropdown_text_font" in kwargs:
self.dropdown_menu.configure(text_font=kwargs.pop("dropdown_text_font"))
self._dropdown_menu.configure(text_font=kwargs.pop("dropdown_text_font"))
super().configure(require_redraw=require_redraw, **kwargs)
def on_enter(self, event=0):
if self.hover is True and self.state == tkinter.NORMAL and len(self.values) > 0:
if sys.platform == "darwin" and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
def _on_enter(self, event=0):
if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0:
if sys.platform == "darwin" and len(self._values) > 0 and Settings.cursor_manipulation_enabled:
self._canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and len(self._values) > 0 and Settings.cursor_manipulation_enabled:
self._canvas.configure(cursor="hand2")
# set color of inner button parts to hover color
self.canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
self.canvas.itemconfig("border_parts_right",
outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self._button_hover_color, self._appearance_mode),
fill=ThemeManager.single_color(self._button_hover_color, self._appearance_mode))
self._canvas.itemconfig("border_parts_right",
outline=ThemeManager.single_color(self._button_hover_color, self._appearance_mode),
fill=ThemeManager.single_color(self._button_hover_color, self._appearance_mode))
def on_leave(self, event=0):
if self.hover is True:
if sys.platform == "darwin" and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
elif sys.platform.startswith("win") and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
def _on_leave(self, event=0):
if self._hover is True:
if sys.platform == "darwin" and len(self._values) > 0 and Settings.cursor_manipulation_enabled:
self._canvas.configure(cursor="arrow")
elif sys.platform.startswith("win") and len(self._values) > 0 and Settings.cursor_manipulation_enabled:
self._canvas.configure(cursor="arrow")
# set color of inner button parts
self.canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self.button_color, self._appearance_mode),
fill=ThemeManager.single_color(self.button_color, self._appearance_mode))
self.canvas.itemconfig("border_parts_right",
outline=ThemeManager.single_color(self.button_color, self._appearance_mode),
fill=ThemeManager.single_color(self.button_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self._button_color, self._appearance_mode),
fill=ThemeManager.single_color(self._button_color, self._appearance_mode))
self._canvas.itemconfig("border_parts_right",
outline=ThemeManager.single_color(self._button_color, self._appearance_mode),
fill=ThemeManager.single_color(self._button_color, self._appearance_mode))
def dropdown_callback(self, value: str):
if self.state == "readonly":
self.entry.configure(state="normal")
self.entry.delete(0, tkinter.END)
self.entry.insert(0, value)
self.entry.configure(state="readonly")
def _dropdown_callback(self, value: str):
if self._state == "readonly":
self._entry.configure(state="normal")
self._entry.delete(0, tkinter.END)
self._entry.insert(0, value)
self._entry.configure(state="readonly")
else:
self.entry.delete(0, tkinter.END)
self.entry.insert(0, value)
self._entry.delete(0, tkinter.END)
self._entry.insert(0, value)
if self.command is not None:
self.command(value)
if self._command is not None:
self._command(value)
def set(self, value: str):
if self.state == "readonly":
self.entry.configure(state="normal")
self.entry.delete(0, tkinter.END)
self.entry.insert(0, value)
self.entry.configure(state="readonly")
if self._state == "readonly":
self._entry.configure(state="normal")
self._entry.delete(0, tkinter.END)
self._entry.insert(0, value)
self._entry.configure(state="readonly")
else:
self.entry.delete(0, tkinter.END)
self.entry.insert(0, value)
self._entry.delete(0, tkinter.END)
self._entry.insert(0, value)
def get(self) -> str:
return self.entry.get()
return self._entry.get()
def clicked(self, event=0):
if self.state is not tkinter.DISABLED and len(self.values) > 0:
self.open_dropdown_menu()
def _clicked(self, event=0):
if self._state is not tkinter.DISABLED and len(self._values) > 0:
self._open_dropdown_menu()

View File

@ -1,4 +1,5 @@
import tkinter
from typing import Union, Tuple
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -7,23 +8,28 @@ from .widget_base_class import CTkBaseClass
class CTkEntry(CTkBaseClass):
"""
Entry with rounded corners, border, textvariable support, focus and placeholder.
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
text_color="default_theme",
placeholder_text_color="default_theme",
text_font="default_theme",
placeholder_text=None,
corner_radius="default_theme",
border_width="default_theme",
border_color="default_theme",
width=140,
height=28,
state=tkinter.NORMAL,
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
text_color: Union[str, Tuple[str, str]] = "default_theme",
placeholder_text_color: Union[str, Tuple[str, str]] = "default_theme",
text_font: Union[str, Tuple[str, str]] = "default_theme",
placeholder_text: str = None,
corner_radius: int = "default_theme",
border_width: int = "default_theme",
border_color: Union[str, Tuple[str, str]] = "default_theme",
width: int = 140,
height: int = 28,
state: str = tkinter.NORMAL,
textvariable: tkinter.Variable = None,
**kwargs):
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
if "master" in kwargs:
super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs.pop("master"))
else:
@ -34,219 +40,221 @@ class CTkEntry(CTkBaseClass):
self.grid_columnconfigure(0, weight=1)
# color
self.fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.placeholder_text_color = ThemeManager.theme["color"]["entry_placeholder_text"] if placeholder_text_color == "default_theme" else placeholder_text_color
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self.border_color = ThemeManager.theme["color"]["entry_border"] if border_color == "default_theme" else border_color
self._fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self._placeholder_text_color = ThemeManager.theme["color"]["entry_placeholder_text"] if placeholder_text_color == "default_theme" else placeholder_text_color
self._text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self._border_color = ThemeManager.theme["color"]["entry_border"] if border_color == "default_theme" else border_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width
self._corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width
# placeholder text
self.placeholder_text = placeholder_text
self.placeholder_text_active = False
self.pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back
self._is_focused: bool = True
self._placeholder_text = placeholder_text
self._placeholder_text_active = False
self._pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back
# textvariable
self.textvariable = textvariable
self._textvariable = textvariable
self._state = state
self.state = state
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._canvas.grid(column=0, row=0, sticky="nswe")
self._draw_engine = DrawEngine(self._canvas)
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self._current_width),
height=self.apply_widget_scaling(self._current_height))
self.canvas.grid(column=0, row=0, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas)
self._entry = tkinter.Entry(master=self,
bd=0,
width=1,
highlightthickness=0,
font=self._apply_font_scaling(self._text_font),
state=self._state,
textvariable=self._textvariable,
**kwargs)
self._entry.grid(column=0, row=0, sticky="nswe",
padx=self._apply_widget_scaling(self._corner_radius) if self._corner_radius >= 6 else self._apply_widget_scaling(6),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1)))
self.entry = tkinter.Entry(master=self,
bd=0,
width=1,
highlightthickness=0,
font=self.apply_font_scaling(self.text_font),
state=self.state,
textvariable=self.textvariable,
**kwargs)
self.entry.grid(column=0, row=0, sticky="nswe",
padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6),
pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width + 1)))
super().bind('<Configure>', self._update_dimensions_event)
self._entry.bind('<FocusOut>', self._entry_focus_out)
self._entry.bind('<FocusIn>', self._entry_focus_in)
super().bind('<Configure>', self.update_dimensions_event)
self.entry.bind('<FocusOut>', self.entry_focus_out)
self.entry.bind('<FocusIn>', self.entry_focus_in)
self._activate_placeholder()
self._draw()
self.activate_placeholder()
self.draw()
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
def set_scaling(self, *args, **kwargs):
super().set_scaling( *args, **kwargs)
self._entry.configure(font=self._apply_font_scaling(self._text_font))
self._entry.grid(column=0, row=0, sticky="we",
padx=self._apply_widget_scaling(self._corner_radius) if self._corner_radius >= 6 else self._apply_widget_scaling(6))
self.entry.configure(font=self.apply_font_scaling(self.text_font))
self.entry.grid(column=0, row=0, sticky="we",
padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6))
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height))
self._draw()
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)
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()
self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
height=self.apply_widget_scaling(self._desired_height))
self.draw()
def _draw(self, no_color_updates=False):
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
def draw(self, no_color_updates=False):
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(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))
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(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))
if requires_recoloring or no_color_updates is False:
if ThemeManager.single_color(self.fg_color, self._appearance_mode) is not None:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self.entry.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
disabledbackground=ThemeManager.single_color(self.fg_color, self._appearance_mode),
highlightcolor=ThemeManager.single_color(self.fg_color, self._appearance_mode),
fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
disabledforeground=ThemeManager.single_color(self.text_color, self._appearance_mode),
insertbackground=ThemeManager.single_color(self.text_color, self._appearance_mode))
if ThemeManager.single_color(self._fg_color, self._appearance_mode) is not None:
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self._entry.configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode),
disabledbackground=ThemeManager.single_color(self._fg_color, self._appearance_mode),
highlightcolor=ThemeManager.single_color(self._fg_color, self._appearance_mode),
fg=ThemeManager.single_color(self._text_color, self._appearance_mode),
disabledforeground=ThemeManager.single_color(self._text_color, self._appearance_mode),
insertbackground=ThemeManager.single_color(self._text_color, self._appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self.entry.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode),
disabledbackground=ThemeManager.single_color(self.bg_color, self._appearance_mode),
highlightcolor=ThemeManager.single_color(self.bg_color, self._appearance_mode),
fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
disabledforeground=ThemeManager.single_color(self.text_color, self._appearance_mode),
insertbackground=ThemeManager.single_color(self.text_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self._entry.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode),
disabledbackground=ThemeManager.single_color(self._bg_color, self._appearance_mode),
highlightcolor=ThemeManager.single_color(self._bg_color, self._appearance_mode),
fg=ThemeManager.single_color(self._text_color, self._appearance_mode),
disabledforeground=ThemeManager.single_color(self._text_color, self._appearance_mode),
insertbackground=ThemeManager.single_color(self._text_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._border_color, self._appearance_mode),
outline=ThemeManager.single_color(self._border_color, self._appearance_mode))
if self.placeholder_text_active:
self.entry.config(fg=ThemeManager.single_color(self.placeholder_text_color, self._appearance_mode))
if self._placeholder_text_active:
self._entry.config(fg=ThemeManager.single_color(self._placeholder_text_color, self._appearance_mode))
def bind(self, *args, **kwargs):
self.entry.bind(*args, **kwargs)
self._entry.bind(*args, **kwargs)
def configure(self, require_redraw=False, **kwargs):
if "state" in kwargs:
self.state = kwargs.pop("state")
self.entry.configure(state=self.state)
self._state = kwargs.pop("state")
self._entry.configure(state=self._state)
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
if "text_color" in kwargs:
self.text_color = kwargs.pop("text_color")
self._text_color = kwargs.pop("text_color")
require_redraw = True
if "border_color" in kwargs:
self.border_color = kwargs.pop("border_color")
self._border_color = kwargs.pop("border_color")
require_redraw = True
if "corner_radius" in kwargs:
self.corner_radius = kwargs.pop("corner_radius")
self._corner_radius = kwargs.pop("corner_radius")
if self.corner_radius * 2 > self._current_height:
self.corner_radius = self._current_height / 2
elif self.corner_radius * 2 > self._current_width:
self.corner_radius = self._current_width / 2
if self._corner_radius * 2 > self._current_height:
self._corner_radius = self._current_height / 2
elif self._corner_radius * 2 > self._current_width:
self._corner_radius = self._current_width / 2
self.entry.grid(column=0, row=0, sticky="we", padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6))
self._entry.grid(column=0, row=0, sticky="we", padx=self._apply_widget_scaling(self._corner_radius) if self._corner_radius >= 6 else self._apply_widget_scaling(6))
require_redraw = True
if "width" in kwargs:
self.set_dimensions(width=kwargs.pop("width"))
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self.set_dimensions(height=kwargs.pop("height"))
self._set_dimensions(height=kwargs.pop("height"))
if "placeholder_text" in kwargs:
self.placeholder_text = kwargs.pop("placeholder_text")
if self.placeholder_text_active:
self.entry.delete(0, tkinter.END)
self.entry.insert(0, self.placeholder_text)
self._placeholder_text = kwargs.pop("placeholder_text")
if self._placeholder_text_active:
self._entry.delete(0, tkinter.END)
self._entry.insert(0, self._placeholder_text)
else:
self.activate_placeholder()
self._activate_placeholder()
if "placeholder_text_color" in kwargs:
self.placeholder_text_color = kwargs.pop("placeholder_text_color")
self._placeholder_text_color = kwargs.pop("placeholder_text_color")
require_redraw = True
if "textvariable" in kwargs:
self.textvariable = kwargs.pop("textvariable")
self.entry.configure(textvariable=self.textvariable)
self._textvariable = kwargs.pop("textvariable")
self._entry.configure(textvariable=self._textvariable)
if "text_font" in kwargs:
self.text_font = kwargs.pop("text_font")
self.entry.configure(font=self.apply_font_scaling(self.text_font))
self._text_font = kwargs.pop("text_font")
self._entry.configure(font=self._apply_font_scaling(self._text_font))
if "show" in kwargs:
if self.placeholder_text_active:
self.pre_placeholder_arguments["show"] = kwargs.pop("show")
if self._placeholder_text_active:
self._pre_placeholder_arguments["show"] = kwargs.pop("show")
else:
self.entry.configure(show=kwargs.pop("show"))
self._entry.configure(show=kwargs.pop("show"))
if "bg_color" in kwargs:
super().configure(bg_color=kwargs.pop("bg_color"), require_redraw=require_redraw)
if "_bg_color" in kwargs:
super().configure(bg_color=kwargs.pop("_bg_color"), require_redraw=require_redraw)
else:
super().configure(require_redraw=require_redraw)
self.entry.configure(**kwargs) # pass remaining kwargs to entry
self._entry.configure(**kwargs) # pass remaining kwargs to entry
def activate_placeholder(self):
if self.entry.get() == "" and self.placeholder_text is not None and (self.textvariable is None or self.textvariable == ""):
self.placeholder_text_active = True
def _activate_placeholder(self):
if self._entry.get() == "" and self._placeholder_text is not None and (self._textvariable is None or self._textvariable == ""):
self._placeholder_text_active = True
self.pre_placeholder_arguments = {"show": self.entry.cget("show")}
self.entry.config(fg=ThemeManager.single_color(self.placeholder_text_color, self._appearance_mode), show="")
self.entry.delete(0, tkinter.END)
self.entry.insert(0, self.placeholder_text)
self._pre_placeholder_arguments = {"show": self._entry.cget("show")}
self._entry.config(fg=ThemeManager.single_color(self._placeholder_text_color, self._appearance_mode), show="")
self._entry.delete(0, tkinter.END)
self._entry.insert(0, self._placeholder_text)
def deactivate_placeholder(self):
if self.placeholder_text_active:
self.placeholder_text_active = False
def _deactivate_placeholder(self):
if self._placeholder_text_active:
self._placeholder_text_active = False
self.entry.config(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
self.entry.delete(0, tkinter.END)
for argument, value in self.pre_placeholder_arguments.items():
self.entry[argument] = value
self._entry.config(fg=ThemeManager.single_color(self._text_color, self._appearance_mode))
self._entry.delete(0, tkinter.END)
for argument, value in self._pre_placeholder_arguments.items():
self._entry[argument] = value
def entry_focus_out(self, event=None):
self.activate_placeholder()
def _entry_focus_out(self, event=None):
self._activate_placeholder()
self._is_focused = False
def entry_focus_in(self, event=None):
self.deactivate_placeholder()
def _entry_focus_in(self, event=None):
self._deactivate_placeholder()
self._is_focused = True
def delete(self, *args, **kwargs):
self.entry.delete(*args, **kwargs)
self._entry.delete(*args, **kwargs)
if self.entry.get() == "":
self.activate_placeholder()
if not self._is_focused and self._entry.get() == "":
self._activate_placeholder()
def insert(self, *args, **kwargs):
self.deactivate_placeholder()
self._deactivate_placeholder()
return self.entry.insert(*args, **kwargs)
return self._entry.insert(*args, **kwargs)
def get(self):
if self.placeholder_text_active:
if self._placeholder_text_active:
return ""
else:
return self.entry.get()
return self._entry.get()
def focus(self):
self.entry.focus()
self._entry.focus()
def focus_force(self):
self.entry.focus_force()
self._entry.focus_force()

View File

@ -1,3 +1,5 @@
from typing import Union, Tuple, List
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..draw_engine import DrawEngine
@ -5,128 +7,137 @@ from .widget_base_class import CTkBaseClass
class CTkFrame(CTkBaseClass):
"""
Frame with rounded corners and border.
Default foreground colors are set according to theme.
To make the frame transparent set fg_color=None.
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
border_color="default_theme",
border_width="default_theme",
corner_radius="default_theme",
width=200,
height=200,
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
border_color: Union[str, Tuple[str, str]] = "default_theme",
border_width: Union[int, str] = "default_theme",
corner_radius: Union[int, str] = "default_theme",
width: int = 200,
height: int = 200,
overwrite_preferred_drawing_method: str = None,
**kwargs):
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color
self._border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color
# determine fg_color of frame
if fg_color == "default_theme":
if isinstance(self.master, CTkFrame):
if self.master.fg_color == ThemeManager.theme["color"]["frame_low"]:
self.fg_color = ThemeManager.theme["color"]["frame_high"]
if self.master._fg_color == ThemeManager.theme["color"]["frame_low"]:
self._fg_color = ThemeManager.theme["color"]["frame_high"]
else:
self.fg_color = ThemeManager.theme["color"]["frame_low"]
self._fg_color = ThemeManager.theme["color"]["frame_low"]
else:
self.fg_color = ThemeManager.theme["color"]["frame_low"]
self._fg_color = ThemeManager.theme["color"]["frame_low"]
else:
self.fg_color = fg_color
self._fg_color = fg_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width
self._corner_radius = ThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self._current_width),
height=self.apply_widget_scaling(self._current_height))
self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self.draw_engine = DrawEngine(self.canvas)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._canvas.place(x=0, y=0, relwidth=1, relheight=1)
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self._draw_engine = DrawEngine(self._canvas)
self._overwrite_preferred_drawing_method = overwrite_preferred_drawing_method
self.bind('<Configure>', self.update_dimensions_event)
self.bind('<Configure>', self._update_dimensions_event)
self.draw()
self._draw()
def winfo_children(self):
""" winfo_children of CTkFrame without self.canvas widget,
because it's not a child but part of the CTkFrame itself """
def winfo_children(self) -> List[any]:
"""
winfo_children of CTkFrame without self.canvas widget,
because it's not a child but part of the CTkFrame itself
"""
child_widgets = super().winfo_children()
try:
child_widgets.remove(self.canvas)
child_widgets.remove(self._canvas)
return child_widgets
except ValueError:
return child_widgets
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
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()
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)
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()
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw()
def draw(self, no_color_updates=False):
def _draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(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),
overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method)
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(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),
overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method)
if no_color_updates is False or requires_recoloring:
if self.fg_color is None:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
if self._fg_color is None:
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._border_color, self._appearance_mode),
outline=ThemeManager.single_color(self._border_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self.canvas.tag_lower("inner_parts")
self.canvas.tag_lower("border_parts")
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
def configure(self, require_redraw=False, **kwargs):
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
# check if CTk widgets are children of the frame and change their bg_color to new frame fg_color
# check if CTk widgets are children of the frame and change their _bg_color to new frame fg_color
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self.fg_color)
child.configure(bg_color=self._fg_color)
if "border_color" in kwargs:
self.border_color = kwargs.pop("border_color")
self._border_color = kwargs.pop("border_color")
require_redraw = True
if "corner_radius" in kwargs:
self.corner_radius = kwargs.pop("corner_radius")
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "border_width" in kwargs:
self.border_width = kwargs.pop("border_width")
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "width" in kwargs:
self.set_dimensions(width=kwargs.pop("width"))
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self.set_dimensions(height=kwargs.pop("height"))
self._set_dimensions(height=kwargs.pop("height"))
super().configure(require_redraw=require_redraw, **kwargs)

View File

@ -1,5 +1,5 @@
import sys
import tkinter
from typing import Union, Tuple
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -8,152 +8,143 @@ from .widget_base_class import CTkBaseClass
class CTkLabel(CTkBaseClass):
"""
Label with rounded corners. Default is fg_color=None (transparent fg_color).
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
text_color="default_theme",
corner_radius="default_theme",
width=140,
height=28,
text="CTkLabel",
text_font="default_theme",
anchor="center", # label anchor: center, n, e, s, w
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
text_color: Union[str, Tuple[str, str]] = "default_theme",
corner_radius: Union[int, str] = "default_theme",
width: int = 140,
height: int = 28,
text: str = "CTkLabel",
text_font: any = "default_theme",
anchor: str = "center", # label anchor: center, n, e, s, w
**kwargs):
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
if "master" in kwargs:
super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs.pop("master"))
else:
super().__init__(*args, bg_color=bg_color, width=width, height=height)
# color
self.fg_color = ThemeManager.theme["color"]["label"] if fg_color == "default_theme" else fg_color
if self.fg_color is None:
self.fg_color = self.bg_color
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self._fg_color = ThemeManager.theme["color"]["label"] if fg_color == "default_theme" else fg_color
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["label_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._corner_radius = ThemeManager.theme["shape"]["label_corner_radius"] if corner_radius == "default_theme" else corner_radius
# text
self.anchor = anchor
self.text = text
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self._anchor = anchor
self._text = text
self._text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# configure grid system (1x1)
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(row=0, column=0, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas)
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(row=0, column=0, sticky="nswe")
self._draw_engine = DrawEngine(self._canvas)
self.text_label = tkinter.Label(master=self,
highlightthickness=0,
bd=0,
anchor=self.anchor,
text=self.text,
font=self.apply_font_scaling(self.text_font),
**kwargs)
text_label_grid_sticky = self.anchor if self.anchor != "center" else ""
self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius),
sticky=text_label_grid_sticky)
self._text_label = tkinter.Label(master=self,
highlightthickness=0,
bd=0,
anchor=self._anchor,
text=self._text,
font=self._apply_font_scaling(self._text_font),
**kwargs)
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
self._text_label.grid(row=0, column=0, padx=self._apply_widget_scaling(self._corner_radius),
sticky=text_label_grid_sticky)
self.bind('<Configure>', self.update_dimensions_event)
self.draw()
self.bind('<Configure>', self._update_dimensions_event)
self._draw()
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
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.text_label.configure(font=self.apply_font_scaling(self.text_font))
text_label_grid_sticky = self.anchor if self.anchor != "center" else ""
self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius),
sticky=text_label_grid_sticky)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height))
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
self._text_label.grid(row=0, column=0, padx=self._apply_widget_scaling(self._corner_radius),
sticky=text_label_grid_sticky)
self.draw()
self._draw()
def set_dimensions(self, width=None, height=None):
super().set_dimensions(width, height)
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()
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw()
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width),
self.apply_widget_scaling(self._current_height),
self.apply_widget_scaling(self.corner_radius),
0)
def _draw(self, no_color_updates=False):
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._corner_radius),
0)
if no_color_updates is False or requires_recoloring:
if ThemeManager.single_color(self.fg_color, self._appearance_mode) is not None:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
if ThemeManager.single_color(self._fg_color, self._appearance_mode) is not None:
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._text_label.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode),
bg=ThemeManager.single_color(self._fg_color, self._appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._text_label.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode),
bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
def config(self, **kwargs):
sys.stderr.write("Warning: Use .configure() instead of .config()")
self.configure(**kwargs)
return self.configure(**kwargs)
def configure(self, require_redraw=False, **kwargs):
if "anchor" in kwargs:
self.anchor = kwargs.pop("anchor")
text_label_grid_sticky = self.anchor if self.anchor != "center" else ""
self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius),
sticky=text_label_grid_sticky)
self._anchor = kwargs.pop("anchor")
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
self._text_label.grid(row=0, column=0, padx=self._apply_widget_scaling(self._corner_radius),
sticky=text_label_grid_sticky)
if "text" in kwargs:
self.text = kwargs["text"]
self.text_label.configure(text=self.text)
del kwargs["text"]
self._text = kwargs.pop("text")
self._text_label.configure(text=self._text)
if "text_font" in kwargs:
self.text_font = kwargs.pop("text_font")
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self._text_font = kwargs.pop("text_font")
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
del kwargs["fg_color"]
if "text_color" in kwargs:
self.text_color = kwargs["text_color"]
self._text_color = kwargs.pop("text_color")
require_redraw = True
del kwargs["text_color"]
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
self._set_dimensions(height=kwargs.pop("height"))
if "bg_color" in kwargs:
super().configure(bg_color=kwargs.pop("bg_color"), require_redraw=require_redraw)
else:
super().configure(require_redraw=require_redraw)
if "_bg_color" in kwargs:
super().configure(bg_color=kwargs.pop("_bg_color"), require_redraw=require_redraw)
else:
super().configure(require_redraw=require_redraw)
self.text_label.configure(**kwargs) # pass remaining kwargs to label
def set_text(self, text):
""" Will be removed in the next major release """
self.text = text
self.text_label.configure(text=self.text)
self._text_label.configure(**kwargs) # pass remaining kwargs to label

View File

@ -1,104 +1,109 @@
import tkinter
import sys
from .dropdown_menu import DropdownMenu
from typing import Union, Tuple, Callable
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..settings import Settings
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from .dropdown_menu import DropdownMenu
class CTkOptionMenu(CTkBaseClass):
"""
Optionemnu with rounded corners, dropdown menu, variable support, command.
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
button_color="default_theme",
button_hover_color="default_theme",
text_color="default_theme",
text_color_disabled="default_theme",
dropdown_color="default_theme",
dropdown_hover_color="default_theme",
dropdown_text_color="default_theme",
variable=None,
values=None,
command=None,
width=140,
height=28,
corner_radius="default_theme",
text_font="default_theme",
dropdown_text_font="default_theme",
hover=True,
state=tkinter.NORMAL,
dynamic_resizing=True,
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str]] = "default_theme",
button_color: Union[str, Tuple[str, str]] = "default_theme",
button_hover_color: Union[str, Tuple[str, str]] = "default_theme",
text_color: Union[str, Tuple[str, str]] = "default_theme",
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
dropdown_color: Union[str, Tuple[str, str]] = "default_theme",
dropdown_hover_color: Union[str, Tuple[str, str]] = "default_theme",
dropdown_text_color: Union[str, Tuple[str, str]] = "default_theme",
variable: tkinter.Variable = None,
values: list = None,
command: Callable = None,
width: int = 140,
height: int = 28,
corner_radius: Union[int, str] = "default_theme",
text_font: any = "default_theme",
dropdown_text_font: any = "default_theme",
hover: bool = True,
state: str = tkinter.NORMAL,
dynamic_resizing: bool = True,
**kwargs):
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color variables
self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.button_color = ThemeManager.theme["color"]["optionmenu_button"] if button_color == "default_theme" else button_color
self.button_hover_color = ThemeManager.theme["color"]["optionmenu_button_hover"] if button_hover_color == "default_theme" else button_hover_color
self._fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self._button_color = ThemeManager.theme["color"]["optionmenu_button"] if button_color == "default_theme" else button_color
self._button_hover_color = ThemeManager.theme["color"]["optionmenu_button_hover"] if button_hover_color == "default_theme" else button_hover_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
# text and font
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self.dropdown_text_font = dropdown_text_font
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self._text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self._text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self._dropdown_text_font = dropdown_text_font
# callback and hover functionality
self.command = command
self.variable = variable
self.variable_callback_blocked = False
self.variable_callback_name = None
self.state = state
self.hover = hover
self.dynamic_resizing = dynamic_resizing
self._command = command
self._variable = variable
self._variable_callback_blocked: bool = False
self._variable_callback_name: Union[str, None] = None
self._state = state
self._hover = hover
self._dynamic_resizing = dynamic_resizing
if values is None:
self.values = ["CTkOptionMenu"]
self._values = ["CTkOptionMenu"]
else:
self.values = values
self._values = values
if len(self.values) > 0:
self.current_value = self.values[0]
if len(self._values) > 0:
self._current_value = self._values[0]
else:
self.current_value = "CTkOptionMenu"
self._current_value = "CTkOptionMenu"
self.dropdown_menu = DropdownMenu(master=self,
values=self.values,
command=self.dropdown_callback,
fg_color=dropdown_color,
hover_color=dropdown_hover_color,
text_color=dropdown_text_color,
text_font=dropdown_text_font)
self._dropdown_menu = DropdownMenu(master=self,
values=self._values,
command=self._dropdown_callback,
fg_color=dropdown_color,
hover_color=dropdown_hover_color,
text_color=dropdown_text_color,
text_font=dropdown_text_font)
# configure grid system (1x1)
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(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
self.draw_engine = DrawEngine(self.canvas)
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(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
self._draw_engine = DrawEngine(self._canvas)
left_section_width = self._current_width - self._current_height
self.text_label = tkinter.Label(master=self,
font=self.apply_font_scaling(self.text_font),
anchor="w",
text=self.current_value)
self.text_label.grid(row=0, column=0, sticky="w",
padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(3)),
max(self.apply_widget_scaling(self._current_width - left_section_width + 3), self.apply_widget_scaling(3))))
self._text_label = tkinter.Label(master=self,
font=self._apply_font_scaling(self._text_font),
anchor="w",
text=self._current_value)
self._text_label.grid(row=0, column=0, sticky="w",
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)),
max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))))
if not self.dynamic_resizing:
if not self._dynamic_resizing:
self.grid_propagate(0)
if Settings.cursor_manipulation_enabled:
@ -108,204 +113,204 @@ class CTkOptionMenu(CTkBaseClass):
self.configure(cursor="hand2")
# event bindings
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.clicked)
self.canvas.bind("<Button-1>", self.clicked)
self._canvas.bind("<Enter>", self._on_enter)
self._canvas.bind("<Leave>", self._on_leave)
self._canvas.bind("<Button-1>", self._clicked)
self._canvas.bind("<Button-1>", self._clicked)
self.text_label.bind("<Enter>", self.on_enter)
self.text_label.bind("<Leave>", self.on_leave)
self.text_label.bind("<Button-1>", self.clicked)
self.text_label.bind("<Button-1>", self.clicked)
self._text_label.bind("<Enter>", self._on_enter)
self._text_label.bind("<Leave>", self._on_leave)
self._text_label.bind("<Button-1>", self._clicked)
self._text_label.bind("<Button-1>", self._clicked)
self.bind('<Configure>', self.update_dimensions_event)
self.bind('<Configure>', self._update_dimensions_event)
self.draw() # initial draw
self._draw() # initial draw
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.current_value = self.variable.get()
self.text_label.configure(text=self.current_value)
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._current_value = self._variable.get()
self._text_label.configure(text=self._current_value)
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
# change label text size and grid padding
left_section_width = self._current_width - self._current_height
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.text_label.grid(row=0, column=0, sticky="w",
padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(3)),
max(self.apply_widget_scaling(self._current_width - left_section_width + 3), self.apply_widget_scaling(3))))
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
self._text_label.grid(row=0, column=0, sticky="w",
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)),
max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))))
self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
height=self.apply_widget_scaling(self._desired_height))
self.draw()
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: int = None, height: int = None):
super().set_dimensions(width, height)
def _set_dimensions(self, width: int = None, height: int = 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()
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw()
def draw(self, no_color_updates=False):
def _draw(self, no_color_updates=False):
left_section_width = self._current_width - self._current_height
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(self.apply_widget_scaling(self._current_width),
self.apply_widget_scaling(self._current_height),
self.apply_widget_scaling(self.corner_radius),
0,
self.apply_widget_scaling(left_section_width))
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border_vertical_split(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._corner_radius),
0,
self._apply_widget_scaling(left_section_width))
requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self.apply_widget_scaling(self._current_width - (self._current_height / 2)),
self.apply_widget_scaling(self._current_height / 2),
self.apply_widget_scaling(self._current_height / 3))
requires_recoloring_2 = self._draw_engine.draw_dropdown_arrow(self._apply_widget_scaling(self._current_width - (self._current_height / 2)),
self._apply_widget_scaling(self._current_height / 2),
self._apply_widget_scaling(self._current_height / 3))
if no_color_updates is False or requires_recoloring or requires_recoloring_2:
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self.canvas.itemconfig("inner_parts_left",
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self.canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self.button_color, self._appearance_mode),
fill=ThemeManager.single_color(self.button_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts_left",
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self._button_color, self._appearance_mode),
fill=ThemeManager.single_color(self._button_color, self._appearance_mode))
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
self._text_label.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode))
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)))
self.canvas.itemconfig("dropdown_arrow",
fill=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode))
if self._state == tkinter.DISABLED:
self._text_label.configure(fg=(ThemeManager.single_color(self._text_color_disabled, self._appearance_mode)))
self._canvas.itemconfig("dropdown_arrow",
fill=ThemeManager.single_color(self._text_color_disabled, self._appearance_mode))
else:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
self.canvas.itemconfig("dropdown_arrow",
fill=ThemeManager.single_color(self.text_color, self._appearance_mode))
self._text_label.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode))
self._canvas.itemconfig("dropdown_arrow",
fill=ThemeManager.single_color(self._text_color, self._appearance_mode))
self.text_label.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._text_label.configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self.canvas.update_idletasks()
self._canvas.update_idletasks()
def open_dropdown_menu(self):
self.dropdown_menu.open(self.winfo_rootx(),
self.winfo_rooty() + self.apply_widget_scaling(self._current_height + 0))
def _open_dropdown_menu(self):
self._dropdown_menu.open(self.winfo_rootx(),
self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0))
def configure(self, require_redraw=False, **kwargs):
if "state" in kwargs:
self.state = kwargs.pop("state")
self._state = kwargs.pop("state")
require_redraw = True
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
if "button_color" in kwargs:
self.button_color = kwargs.pop("button_color")
self._button_color = kwargs.pop("button_color")
require_redraw = True
if "button_hover_color" in kwargs:
self.button_hover_color = kwargs.pop("button_hover_color")
self._button_hover_color = kwargs.pop("button_hover_color")
require_redraw = True
if "text_color" in kwargs:
self.text_color = kwargs.pop("text_color")
self._text_color = kwargs.pop("text_color")
require_redraw = True
if "text_font" in kwargs:
self.text_font = kwargs.pop("text_font")
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self._text_font = kwargs.pop("text_font")
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
if "command" in kwargs:
self.command = kwargs.pop("command")
self._command = kwargs.pop("command")
if "variable" in kwargs:
if self.variable is not None: # remove old callback
self.variable.trace_remove("write", self.variable_callback_name)
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
self.variable = kwargs.pop("variable")
self._variable = kwargs.pop("variable")
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.current_value = self.variable.get()
self.text_label.configure(text=self.current_value)
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._current_value = self._variable.get()
self._text_label.configure(text=self._current_value)
else:
self.variable = None
self._variable = None
if "width" in kwargs:
self.set_dimensions(width=kwargs.pop("width"))
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self.set_dimensions(height=kwargs.pop("height"))
self._set_dimensions(height=kwargs.pop("height"))
if "values" in kwargs:
self.values = kwargs.pop("values")
self.dropdown_menu.configure(values=self.values)
self._values = kwargs.pop("values")
self._dropdown_menu.configure(values=self._values)
if "dropdown_color" in kwargs:
self.dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color"))
self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color"))
if "dropdown_hover_color" in kwargs:
self.dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
if "dropdown_text_color" in kwargs:
self.dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
if "dropdown_text_font" in kwargs:
self.dropdown_text_font = kwargs.pop("dropdown_text_font")
self.dropdown_menu.configure(text_font=self.dropdown_text_font)
self._dropdown_text_font = kwargs.pop("dropdown_text_font")
self._dropdown_menu.configure(text_font=self._dropdown_text_font)
if "dynamic_resizing" in kwargs:
self.dynamic_resizing = kwargs.pop("dynamic_resizing")
if not self.dynamic_resizing:
self._dynamic_resizing = kwargs.pop("dynamic_resizing")
if not self._dynamic_resizing:
self.grid_propagate(0)
else:
self.grid_propagate(1)
super().configure(require_redraw=require_redraw, **kwargs)
def on_enter(self, event=0):
if self.hover is True and self.state == tkinter.NORMAL and len(self.values) > 0:
def _on_enter(self, event=0):
if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0:
# set color of inner button parts to hover color
self.canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self._button_hover_color, self._appearance_mode),
fill=ThemeManager.single_color(self._button_hover_color, self._appearance_mode))
def on_leave(self, event=0):
if self.hover is True:
def _on_leave(self, event=0):
if self._hover is True:
# set color of inner button parts
self.canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self.button_color, self._appearance_mode),
fill=ThemeManager.single_color(self.button_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self._button_color, self._appearance_mode),
fill=ThemeManager.single_color(self._button_color, self._appearance_mode))
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
self.current_value = self.variable.get()
self.text_label.configure(text=self.current_value)
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self._current_value = self._variable.get()
self._text_label.configure(text=self._current_value)
def dropdown_callback(self, value: str):
self.current_value = value
self.text_label.configure(text=self.current_value)
def _dropdown_callback(self, value: str):
self._current_value = value
self._text_label.configure(text=self._current_value)
if self.variable is not None:
self.variable_callback_blocked = True
self.variable.set(self.current_value)
self.variable_callback_blocked = False
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(self._current_value)
self._variable_callback_blocked = False
if self.command is not None:
self.command(self.current_value)
if self._command is not None:
self._command(self._current_value)
def set(self, value: str):
self.current_value = value
self.text_label.configure(text=self.current_value)
self._current_value = value
self._text_label.configure(text=self._current_value)
if self.variable is not None:
self.variable_callback_blocked = True
self.variable.set(self.current_value)
self.variable_callback_blocked = False
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(self._current_value)
self._variable_callback_blocked = False
def get(self) -> str:
return self.current_value
return self._current_value
def clicked(self, event=0):
if self.state is not tkinter.DISABLED and len(self.values) > 0:
self.open_dropdown_menu()
def _clicked(self, event=0):
if self._state is not tkinter.DISABLED and len(self._values) > 0:
self._open_dropdown_menu()

View File

@ -1,5 +1,6 @@
import tkinter
import math
from typing import Union, Tuple
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -8,250 +9,247 @@ from .widget_base_class import CTkBaseClass
class CTkProgressBar(CTkBaseClass):
""" tkinter custom progressbar, values from 0 to 1 """
"""
Progressbar with rounded corners, border, variable support,
indeterminate mode, vertical orientation.
For detailed information check out the documentation.
"""
def __init__(self, *args,
variable=None,
bg_color=None,
border_color="default_theme",
fg_color="default_theme",
progress_color="default_theme",
corner_radius="default_theme",
width=None,
height=None,
border_width="default_theme",
orient="horizontal",
mode="determinate",
determinate_speed=1,
indeterminate_speed=1,
bg_color: Union[str, Tuple[str, str], None] = None,
border_color: Union[str, Tuple[str, str]] = "default_theme",
fg_color: Union[str, Tuple[str, str]] = "default_theme",
progress_color: Union[str, Tuple[str, str]] = "default_theme",
corner_radius: Union[str, Tuple[str, str]] = "default_theme",
width: Union[int, str] = "default_init",
height: Union[int, str] = "default_init",
border_width: Union[int, str] = "default_theme",
variable: tkinter.Variable = None,
orient: str = "horizontal",
mode: str = "determinate",
determinate_speed: float = 1,
indeterminate_speed: float = 1,
**kwargs):
# set default dimensions according to orientation
if width is None:
if width == "default_init":
if orient.lower() == "vertical":
width = 8
else:
width = 200
if height is None:
if height == "default_init":
if orient.lower() == "vertical":
height = 200
else:
height = 8
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.border_color = ThemeManager.theme["color"]["progressbar_border"] if border_color == "default_theme" else border_color
self.fg_color = ThemeManager.theme["color"]["progressbar"] if fg_color == "default_theme" else fg_color
self.progress_color = ThemeManager.theme["color"]["progressbar_progress"] if progress_color == "default_theme" else progress_color
self._border_color = ThemeManager.theme["color"]["progressbar_border"] if border_color == "default_theme" else border_color
self._fg_color = ThemeManager.theme["color"]["progressbar"] if fg_color == "default_theme" else fg_color
self._progress_color = ThemeManager.theme["color"]["progressbar_progress"] if progress_color == "default_theme" else progress_color
# control variable
self.variable = variable
self.variable_callback_blocked = False
self.variable_callback_name = None
self._variable = variable
self._variable_callback_blocked = False
self._variable_callback_name = None
# shape
self.corner_radius = ThemeManager.theme["shape"]["progressbar_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width
self.determinate_value = 0.5 # range 0-1
self.determinate_speed = determinate_speed # range 0-1
self.indeterminate_value = 0 # range 0-inf
self.indeterminate_width = 0.4 # range 0-1
self.indeterminate_speed = indeterminate_speed # range 0-1 to travel in 50ms
self.loop_running = False
self.orient = orient
self.mode = mode # "determinate" or "indeterminate"
self._corner_radius = ThemeManager.theme["shape"]["progressbar_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._border_width = ThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width
self._determinate_value: float = 0.5 # range 0-1
self._determinate_speed = determinate_speed # range 0-1
self._indeterminate_value: float = 0 # range 0-inf
self._indeterminate_width: float = 0.4 # range 0-1
self._indeterminate_speed = indeterminate_speed # range 0-1 to travel in 50ms
self._loop_running: bool = False
self._orient = orient
self._mode = mode # "determinate" or "indeterminate"
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(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas)
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(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe")
self._draw_engine = DrawEngine(self._canvas)
# Each time an item is resized due to pack position mode, the binding Configure is called on the widget
self.bind('<Configure>', self.update_dimensions_event)
self.bind('<Configure>', self._update_dimensions_event)
self.draw() # initial draw
self._draw() # initial draw
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.variable_callback_blocked = True
self.set(self.variable.get(), from_variable_callback=True)
self.variable_callback_blocked = False
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._variable_callback_blocked = True
self.set(self._variable.get(), from_variable_callback=True)
self._variable_callback_blocked = False
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
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()
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)
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()
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw()
def destroy(self):
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
def draw(self, no_color_updates=False):
if self.orient.lower() == "horizontal":
def _draw(self, no_color_updates=False):
if self._orient.lower() == "horizontal":
orientation = "w"
elif self.orient.lower() == "vertical":
elif self._orient.lower() == "vertical":
orientation = "s"
else:
orientation = "w"
if self.mode == "determinate":
requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(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),
0,
self.determinate_value,
orientation)
if self._mode == "determinate":
requires_recoloring = self._draw_engine.draw_rounded_progress_bar_with_border(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),
0,
self._determinate_value,
orientation)
else: # indeterminate mode
progress_value = (math.sin(self.indeterminate_value * math.pi / 40) + 1) / 2
progress_value_1 = min(1.0, progress_value + (self.indeterminate_width / 2))
progress_value_2 = max(0.0, progress_value - (self.indeterminate_width / 2))
progress_value = (math.sin(self._indeterminate_value * math.pi / 40) + 1) / 2
progress_value_1 = min(1.0, progress_value + (self._indeterminate_width / 2))
progress_value_2 = max(0.0, progress_value - (self._indeterminate_width / 2))
requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(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),
progress_value_1,
progress_value_2,
orientation)
requires_recoloring = self._draw_engine.draw_rounded_progress_bar_with_border(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),
progress_value_1,
progress_value_2,
orientation)
if no_color_updates is False or requires_recoloring:
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self.canvas.itemconfig("progress_parts",
fill=ThemeManager.single_color(self.progress_color, self._appearance_mode),
outline=ThemeManager.single_color(self.progress_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._border_color, self._appearance_mode),
outline=ThemeManager.single_color(self._border_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self._canvas.itemconfig("progress_parts",
fill=ThemeManager.single_color(self._progress_color, self._appearance_mode),
outline=ThemeManager.single_color(self._progress_color, self._appearance_mode))
def configure(self, require_redraw=False, **kwargs):
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
del kwargs["fg_color"]
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
del kwargs["border_color"]
self._border_color = kwargs.pop("border_color")
require_redraw = True
if "progress_color" in kwargs:
self.progress_color = kwargs["progress_color"]
del kwargs["progress_color"]
self._progress_color = kwargs.pop("progress_color")
require_redraw = True
if "border_width" in kwargs:
self.border_width = kwargs["border_width"]
del kwargs["border_width"]
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "variable" in kwargs:
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
self.variable = kwargs["variable"]
self._variable = kwargs.pop("variable")
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.set(self.variable.get(), from_variable_callback=True)
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.set(self._variable.get(), from_variable_callback=True)
else:
self.variable = None
del kwargs["variable"]
self._variable = None
if "mode" in kwargs:
self.mode = kwargs.pop("mode")
self._mode = kwargs.pop("mode")
require_redraw = True
if "determinate_speed" in kwargs:
self.determinate_speed = kwargs.pop("determinate_speed")
self._determinate_speed = kwargs.pop("determinate_speed")
if "indeterminate_speed" in kwargs:
self.indeterminate_speed = kwargs.pop("indeterminate_speed")
self._indeterminate_speed = kwargs.pop("indeterminate_speed")
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
self._set_dimensions(height=kwargs.pop("height"))
super().configure(require_redraw=require_redraw, **kwargs)
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
self.set(self.variable.get(), from_variable_callback=True)
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self.set(self._variable.get(), from_variable_callback=True)
def set(self, value, from_variable_callback=False):
""" set determinate value """
self.determinate_value = value
self._determinate_value = value
if self.determinate_value > 1:
self.determinate_value = 1
elif self.determinate_value < 0:
self.determinate_value = 0
if self._determinate_value > 1:
self._determinate_value = 1
elif self._determinate_value < 0:
self._determinate_value = 0
self.draw(no_color_updates=True)
self._draw(no_color_updates=True)
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(round(self.determinate_value) if isinstance(self.variable, tkinter.IntVar) else self.determinate_value)
self.variable_callback_blocked = False
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(round(self._determinate_value) if isinstance(self._variable, tkinter.IntVar) else self._determinate_value)
self._variable_callback_blocked = False
def get(self):
def get(self) -> float:
""" get determinate value """
return self.determinate_value
return self._determinate_value
def start(self):
""" start indeterminate mode """
if not self.loop_running:
self.loop_running = True
self.internal_loop()
if not self._loop_running:
self._loop_running = True
self._internal_loop()
def stop(self):
""" stop indeterminate mode """
self.loop_running = False
self._loop_running = False
def internal_loop(self):
if self.loop_running:
if self.mode == "determinate":
self.determinate_value += self.determinate_speed / 50
if self.determinate_value > 1:
self.determinate_value -= 1
self.draw()
self.after(20, self.internal_loop)
def _internal_loop(self):
if self._loop_running:
if self._mode == "determinate":
self._determinate_value += self._determinate_speed / 50
if self._determinate_value > 1:
self._determinate_value -= 1
self._draw()
self.after(20, self._internal_loop)
else:
self.indeterminate_value += self.indeterminate_speed
self.draw()
self.after(20, self.internal_loop)
self._indeterminate_value += self._indeterminate_speed
self._draw()
self.after(20, self._internal_loop)
def step(self):
if self.mode == "determinate":
self.determinate_value += self.determinate_speed / 50
if self.determinate_value > 1:
self.determinate_value -= 1
self.draw()
if self._mode == "determinate":
self._determinate_value += self._determinate_speed / 50
if self._determinate_value > 1:
self._determinate_value -= 1
self._draw()
else:
self.indeterminate_value += self.indeterminate_speed
self.draw()
self._indeterminate_value += self._indeterminate_speed
self._draw()

View File

@ -1,6 +1,6 @@
import tkinter
import sys
from typing import Union
from typing import Union, Tuple, Callable
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -10,272 +10,277 @@ from .widget_base_class import CTkBaseClass
class CTkRadioButton(CTkBaseClass):
"""
Radiobutton with rounded corners, border, label, variable support, command.
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
hover_color="default_theme",
border_color="default_theme",
border_width_unchecked="default_theme",
border_width_checked="default_theme",
width=22,
height=22,
corner_radius="default_theme",
text_font="default_theme",
text_color="default_theme",
text="CTkRadioButton",
text_color_disabled="default_theme",
hover=True,
command=None,
state=tkinter.NORMAL,
value=0,
variable=None,
textvariable=None,
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str]] = "default_theme",
hover_color: Union[str, Tuple[str, str]] = "default_theme",
border_color: Union[str, Tuple[str, str]] = "default_theme",
border_width_unchecked: Union[int, str] = "default_theme",
border_width_checked: Union[int, str] = "default_theme",
width: int = 22,
height: int = 22,
corner_radius: Union[int, str] = "default_theme",
text_font: any = "default_theme",
text_color: Union[str, Tuple[str, str]] = "default_theme",
text: str = "CTkRadioButton",
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
hover: bool = True,
command: Callable = None,
state: str = tkinter.NORMAL,
value: Union[int, str] = 0,
variable: tkinter.Variable = None,
textvariable: tkinter.Variable = None,
**kwargs):
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self.border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
self._fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self._hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self._border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["radiobutton_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width_unchecked = ThemeManager.theme["shape"]["radiobutton_border_width_unchecked"] if border_width_unchecked == "default_theme" else border_width_unchecked
self.border_width_checked = ThemeManager.theme["shape"]["radiobutton_border_width_checked"] if border_width_checked == "default_theme" else border_width_checked
self.border_width = self.border_width_unchecked
self._corner_radius = ThemeManager.theme["shape"]["radiobutton_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._border_width_unchecked = ThemeManager.theme["shape"]["radiobutton_border_width_unchecked"] if border_width_unchecked == "default_theme" else border_width_unchecked
self._border_width_checked = ThemeManager.theme["shape"]["radiobutton_border_width_checked"] if border_width_checked == "default_theme" else border_width_checked
self._border_width = self._border_width_unchecked
# text
self.text = text
self.text_label: Union[tkinter.Label, None] = None
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self._text = text
self._text_label: Union[tkinter.Label, None] = None
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self._text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self._text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# callback and control variables
self.command = command
self.state = state
self.hover = hover
self.check_state = False
self.value = value
self.variable: tkinter.Variable = variable
self.variable_callback_blocked = False
self.textvariable = textvariable
self.variable_callback_name = None
self._command = command
self._state = state
self._hover = hover
self._check_state: bool = False
self._value = value
self._variable: tkinter.Variable = variable
self._variable_callback_blocked: bool = False
self._textvariable = textvariable
self._variable_callback_name: Union[str, None] = None
# configure grid system (3x1)
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=1)
self.bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self._current_width),
height=self.apply_widget_scaling(self._current_height))
self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self._bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self._current_width),
height=self.apply_widget_scaling(self._current_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1)
self.draw_engine = DrawEngine(self.canvas)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1)
self._draw_engine = DrawEngine(self._canvas)
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.invoke)
self._canvas.bind("<Enter>", self._on_enter)
self._canvas.bind("<Leave>", self._on_leave)
self._canvas.bind("<Button-1>", self.invoke)
self.text_label = tkinter.Label(master=self,
bd=0,
text=self.text,
justify=tkinter.LEFT,
font=self.apply_font_scaling(self.text_font),
textvariable=self.textvariable)
self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self.text_label["anchor"] = "w"
self._text_label = tkinter.Label(master=self,
bd=0,
text=self._text,
justify=tkinter.LEFT,
font=self._apply_font_scaling(self._text_font),
textvariable=self._textvariable)
self._text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self._text_label["anchor"] = "w"
self.text_label.bind("<Enter>", self.on_enter)
self.text_label.bind("<Leave>", self.on_leave)
self.text_label.bind("<Button-1>", self.invoke)
self._text_label.bind("<Enter>", self._on_enter)
self._text_label.bind("<Leave>", self._on_leave)
self._text_label.bind("<Button-1>", self.invoke)
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.check_state = True if self.variable.get() == self.value else False
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._value else False
self.draw() # initial draw
self.set_cursor()
self._draw() # initial draw
self._set_cursor()
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
self.bg_canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
self.draw()
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_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):
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(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))
def _draw(self, no_color_updates=False):
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(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.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._bg_canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
if self.check_state is False:
self.canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
if self._check_state is False:
self._canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self._border_color, self._appearance_mode),
fill=ThemeManager.single_color(self._border_color, self._appearance_mode))
else:
self.canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode),
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self.canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode),
fill=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode),
fill=ThemeManager.single_color(self._bg_color, self._appearance_mode))
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode))
if self._state == tkinter.DISABLED:
self._text_label.configure(fg=ThemeManager.single_color(self._text_color_disabled, self._appearance_mode))
else:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
self._text_label.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode))
self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._text_label.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
def configure(self, require_redraw=False, **kwargs):
if "text" in kwargs:
self.text = kwargs.pop("text")
self.text_label.configure(text=self.text)
self._text = kwargs.pop("text")
self._text_label.configure(text=self._text)
if "text_font" in kwargs:
self.text_font = kwargs.pop("text_font")
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self._text_font = kwargs.pop("text_font")
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
if "state" in kwargs:
self.state = kwargs.pop("state")
self.set_cursor()
self._state = kwargs.pop("state")
self._set_cursor()
require_redraw = True
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
if "hover_color" in kwargs:
self.hover_color = kwargs.pop("hover_color")
self._hover_color = kwargs.pop("hover_color")
require_redraw = True
if "text_color" in kwargs:
self.text_color = kwargs.pop("text_color")
self._text_color = kwargs.pop("text_color")
require_redraw = True
if "border_color" in kwargs:
self.border_color = kwargs.pop("border_color")
self._border_color = kwargs.pop("border_color")
require_redraw = True
if "border_width" in kwargs:
self.border_width = kwargs.pop("border_width")
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "command" in kwargs:
self.command = kwargs.pop("command")
self._command = kwargs.pop("command")
if "textvariable" in kwargs:
self.textvariable = kwargs.pop("textvariable")
self.text_label.configure(textvariable=self.textvariable)
self._textvariable = kwargs.pop("textvariable")
self._text_label.configure(textvariable=self._textvariable)
if "variable" in kwargs:
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
self.variable = kwargs.pop("variable")
self._variable = kwargs.pop("variable")
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.check_state = True if self.variable.get() == self.value else False
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._value else False
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def set_cursor(self):
def _set_cursor(self):
if Settings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if self._state == tkinter.DISABLED:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
elif self._state == tkinter.NORMAL:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
if self.text_label is not None:
self.text_label.configure(cursor="pointinghand")
self._canvas.configure(cursor="pointinghand")
if self._text_label is not None:
self._text_label.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
if self.text_label is not None:
self.text_label.configure(cursor="hand2")
self._canvas.configure(cursor="hand2")
if self._text_label is not None:
self._text_label.configure(cursor="hand2")
def on_enter(self, event=0):
if self.hover is True and self.state == tkinter.NORMAL:
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self.hover_color, self._appearance_mode))
def _on_enter(self, event=0):
if self._hover is True and self._state == tkinter.NORMAL:
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self._hover_color, self._appearance_mode))
def on_leave(self, event=0):
if self.hover is True:
if self.check_state is True:
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
def _on_leave(self, event=0):
if self._hover is True:
if self._check_state is True:
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
else:
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._border_color, self._appearance_mode),
outline=ThemeManager.single_color(self._border_color, self._appearance_mode))
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
if self.variable.get() == self.value:
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
if self._variable.get() == self._value:
self.select(from_variable_callback=True)
else:
self.deselect(from_variable_callback=True)
def invoke(self, event=0):
if self.state == tkinter.NORMAL:
if self.check_state is False:
self.check_state = True
if self._state == tkinter.NORMAL:
if self._check_state is False:
self._check_state = True
self.select()
if self.command is not None:
self.command()
if self._command is not None:
self._command()
def select(self, from_variable_callback=False):
self.check_state = True
self.border_width = self.border_width_checked
self.draw()
self._check_state = True
self._border_width = self._border_width_checked
self._draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.value)
self.variable_callback_blocked = False
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._value)
self._variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
self.check_state = False
self.border_width = self.border_width_unchecked
self.draw()
self._check_state = False
self._border_width = self._border_width_unchecked
self._draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set("")
self.variable_callback_blocked = False
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set("")
self._variable_callback_blocked = False

View File

@ -1,4 +1,5 @@
import sys
from typing import Union, Tuple, Callable
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -7,219 +8,217 @@ from .widget_base_class import CTkBaseClass
class CTkScrollbar(CTkBaseClass):
"""
Scrollbar with rounded corners, configurable spacing.
Connect to scrollable widget by passing .set() method and set command attribute.
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
scrollbar_color="default_theme",
scrollbar_hover_color="default_theme",
border_spacing="default_theme",
corner_radius="default_theme",
width=None,
height=None,
minimum_pixel_length=20,
orientation="vertical",
command=None,
hover=True,
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
scrollbar_color: Union[str, Tuple[str, str]] = "default_theme",
scrollbar_hover_color: Union[str, Tuple[str, str]] = "default_theme",
border_spacing: Union[int, str] = "default_theme",
corner_radius: Union[int, str] = "default_theme",
width: Union[int, str] = "default_init",
height: Union[int, str] = "default_init",
minimum_pixel_length: int = 20,
orientation: str = "vertical",
command: Callable = None,
hover: bool = True,
**kwargs):
# set default dimensions according to orientation
if width is None:
if width == "default_init":
if orientation.lower() == "vertical":
width = 16
else:
width = 200
if height is None:
if height == "default_init":
if orientation.lower() == "horizontal":
height = 16
else:
height = 200
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.fg_color = ThemeManager.theme["color"]["frame_high"] if fg_color == "default_theme" else fg_color
self.scrollbar_color = ThemeManager.theme["color"]["scrollbar_button"] if scrollbar_color == "default_theme" else scrollbar_color
self.scrollbar_hover_color = ThemeManager.theme["color"]["scrollbar_button_hover"] if scrollbar_hover_color == "default_theme" else scrollbar_hover_color
self._fg_color = ThemeManager.theme["color"]["frame_high"] if fg_color == "default_theme" else fg_color
self._scrollbar_color = ThemeManager.theme["color"]["scrollbar_button"] if scrollbar_color == "default_theme" else scrollbar_color
self._scrollbar_hover_color = ThemeManager.theme["color"]["scrollbar_button_hover"] if scrollbar_hover_color == "default_theme" else scrollbar_hover_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["scrollbar_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_spacing = ThemeManager.theme["shape"]["scrollbar_border_spacing"] if border_spacing == "default_theme" else border_spacing
self._corner_radius = ThemeManager.theme["shape"]["scrollbar_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._border_spacing = ThemeManager.theme["shape"]["scrollbar_border_spacing"] if border_spacing == "default_theme" else border_spacing
self.hover = hover
self.hover_state = False
self.command = command
self.orientation = orientation
self.start_value: float = 0 # 0 to 1
self.end_value: float = 1 # 0 to 1
self.minimum_pixel_length = minimum_pixel_length
self._hover = hover
self._hover_state: bool = False
self._command = command
self._orientation = orientation
self._start_value: float = 0 # 0 to 1
self._end_value: float = 1 # 0 to 1
self._minimum_pixel_length = minimum_pixel_length
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self._current_width),
height=self.apply_widget_scaling(self._current_height))
self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
self.draw_engine = DrawEngine(self.canvas)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._canvas.place(x=0, y=0, relwidth=1, relheight=1)
self._draw_engine = DrawEngine(self._canvas)
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.tag_bind("border_parts", "<Button-1>", self.clicked)
self.canvas.bind("<B1-Motion>", self.clicked)
self.canvas.bind("<MouseWheel>", self.mouse_scroll_event)
self.bind('<Configure>', self.update_dimensions_event)
self._canvas.bind("<Enter>", self._on_enter)
self._canvas.bind("<Leave>", self._on_leave)
self._canvas.tag_bind("border_parts", "<Button-1>", self._clicked)
self._canvas.bind("<B1-Motion>", self._clicked)
self._canvas.bind("<MouseWheel>", self._mouse_scroll_event)
self.bind('<Configure>', self._update_dimensions_event)
self.draw()
self._draw()
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
self.draw(no_color_updates=True)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw(no_color_updates=True)
def set_dimensions(self, width=None, height=None):
super().set_dimensions(width, height)
def _set_dimensions(self, width=None, height=None):
super()._set_dimensions(width, height)
self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
height=self.apply_widget_scaling(self._desired_height))
self.draw(no_color_updates=True)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw(no_color_updates=True)
def get_scrollbar_values_for_minimum_pixel_size(self):
def _get_scrollbar_values_for_minimum_pixel_size(self):
# correct scrollbar float values if scrollbar is too small
if self.orientation == "vertical":
scrollbar_pixel_length = (self.end_value - self.start_value) * self._current_height
if scrollbar_pixel_length < self.minimum_pixel_length and -scrollbar_pixel_length + self._current_height != 0:
if self._orientation == "vertical":
scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_height
if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_height != 0:
# calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
interval_extend_factor = (-scrollbar_pixel_length + self.minimum_pixel_length) / (-scrollbar_pixel_length + self._current_height)
corrected_end_value = self.end_value + (1 - self.end_value) * interval_extend_factor
corrected_start_value = self.start_value - self.start_value * interval_extend_factor
interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_height)
corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor
corrected_start_value = self._start_value - self._start_value * interval_extend_factor
return corrected_start_value, corrected_end_value
else:
return self.start_value, self.end_value
return self._start_value, self._end_value
else:
scrollbar_pixel_length = (self.end_value - self.start_value) * self._current_width
if scrollbar_pixel_length < self.minimum_pixel_length and -scrollbar_pixel_length + self._current_width != 0:
scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_width
if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_width != 0:
# calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
interval_extend_factor = (-scrollbar_pixel_length + self.minimum_pixel_length) / (-scrollbar_pixel_length + self._current_width)
corrected_end_value = self.end_value + (1 - self.end_value) * interval_extend_factor
corrected_start_value = self.start_value - self.start_value * interval_extend_factor
interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_width)
corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor
corrected_start_value = self._start_value - self._start_value * interval_extend_factor
return corrected_start_value, corrected_end_value
else:
return self.start_value, self.end_value
return self._start_value, self._end_value
def draw(self, no_color_updates=False):
corrected_start_value, corrected_end_value = self.get_scrollbar_values_for_minimum_pixel_size()
requires_recoloring = self.draw_engine.draw_rounded_scrollbar(self.apply_widget_scaling(self._current_width),
self.apply_widget_scaling(self._current_height),
self.apply_widget_scaling(self.corner_radius),
self.apply_widget_scaling(self.border_spacing),
corrected_start_value,
corrected_end_value,
self.orientation)
def _draw(self, no_color_updates=False):
corrected_start_value, corrected_end_value = self._get_scrollbar_values_for_minimum_pixel_size()
requires_recoloring = self._draw_engine.draw_rounded_scrollbar(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_spacing),
corrected_start_value,
corrected_end_value,
self._orientation)
if no_color_updates is False or requires_recoloring:
if self.hover_state is True:
self.canvas.itemconfig("scrollbar_parts",
fill=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode))
if self._hover_state is True:
self._canvas.itemconfig("scrollbar_parts",
fill=ThemeManager.single_color(self._scrollbar_hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self._scrollbar_hover_color, self._appearance_mode))
else:
self.canvas.itemconfig("scrollbar_parts",
fill=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode),
outline=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode))
self._canvas.itemconfig("scrollbar_parts",
fill=ThemeManager.single_color(self._scrollbar_color, self._appearance_mode),
outline=ThemeManager.single_color(self._scrollbar_color, self._appearance_mode))
if self.fg_color is None:
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
if self._fg_color is None:
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode))
else:
self.canvas.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self.canvas.update_idletasks()
self._canvas.update_idletasks()
def set(self, start_value: float, end_value: float):
self.start_value = float(start_value)
self.end_value = float(end_value)
self.draw()
self._start_value = float(start_value)
self._end_value = float(end_value)
self._draw()
def get(self):
return self.start_value, self.end_value
return self._start_value, self._end_value
def configure(self, require_redraw=False, **kwargs):
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
del kwargs["fg_color"]
if "scrollbar_color" in kwargs:
self.scrollbar_color = kwargs["scrollbar_color"]
self._scrollbar_color = kwargs.pop("scrollbar_color")
require_redraw = True
del kwargs["scrollbar_color"]
if "scrollbar_hover_color" in kwargs:
self.scrollbar_hover_color = kwargs["scrollbar_hover_color"]
self._scrollbar_hover_color = kwargs.pop("scrollbar_hover_color")
require_redraw = True
del kwargs["scrollbar_hover_color"]
if "command" in kwargs:
self.command = kwargs["command"]
del kwargs["command"]
self._command = kwargs.pop("command")
if "corner_radius" in kwargs:
self.corner_radius = kwargs["corner_radius"]
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
del kwargs["corner_radius"]
if "border_spacing" in kwargs:
self.border_spacing = kwargs["border_spacing"]
self._border_spacing = kwargs.pop("border_spacing")
require_redraw = True
del kwargs["border_spacing"]
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
self._set_dimensions(height=kwargs.pop("height"))
super().configure(require_redraw=require_redraw, **kwargs)
def on_enter(self, event=0):
if self.hover is True:
self.hover_state = True
self.canvas.itemconfig("scrollbar_parts",
outline=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode),
fill=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode))
def _on_enter(self, event=0):
if self._hover is True:
self._hover_state = True
self._canvas.itemconfig("scrollbar_parts",
outline=ThemeManager.single_color(self._scrollbar_hover_color, self._appearance_mode),
fill=ThemeManager.single_color(self._scrollbar_hover_color, self._appearance_mode))
def on_leave(self, event=0):
self.hover_state = False
self.canvas.itemconfig("scrollbar_parts",
outline=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode),
fill=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode))
def _on_leave(self, event=0):
self._hover_state = False
self._canvas.itemconfig("scrollbar_parts",
outline=ThemeManager.single_color(self._scrollbar_color, self._appearance_mode),
fill=ThemeManager.single_color(self._scrollbar_color, self._appearance_mode))
def clicked(self, event):
if self.orientation == "vertical":
value = ((event.y - self.border_spacing) / (self._current_height - 2 * self.border_spacing)) / self._widget_scaling
def _clicked(self, event):
if self._orientation == "vertical":
value = ((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing)) / self._widget_scaling
else:
value = ((event.x - self.border_spacing) / (self._current_width - 2 * self.border_spacing)) / self._widget_scaling
value = ((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing)) / self._widget_scaling
current_scrollbar_length = self.end_value - self.start_value
current_scrollbar_length = self._end_value - self._start_value
value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2)))
self.start_value = value - (current_scrollbar_length / 2)
self.end_value = value + (current_scrollbar_length / 2)
self.draw()
self._start_value = value - (current_scrollbar_length / 2)
self._end_value = value + (current_scrollbar_length / 2)
self._draw()
if self.command is not None:
self.command('moveto', self.start_value)
if self._command is not None:
self._command('moveto', self._start_value)
def mouse_scroll_event(self, event=None):
if self.command is not None:
def _mouse_scroll_event(self, event=None):
if self._command is not None:
if sys.platform.startswith("win"):
self.command('scroll', -int(event.delta/40), 'units')
self._command('scroll', -int(event.delta/40), 'units')
else:
self.command('scroll', -event.delta, 'units')
self._command('scroll', -event.delta, 'units')

View File

@ -1,5 +1,6 @@
import tkinter
import sys
from typing import Union, Tuple, Callable
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -9,331 +10,317 @@ from .widget_base_class import CTkBaseClass
class CTkSlider(CTkBaseClass):
""" tkinter custom slider"""
"""
Slider with rounded corners, border, number of steps, variable support, vertical orientation.
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color=None,
border_color=None,
fg_color="default_theme",
progress_color="default_theme",
button_color="default_theme",
button_hover_color="default_theme",
from_=0,
to=1,
number_of_steps=None,
width=None,
height=None,
corner_radius="default_theme",
button_corner_radius="default_theme",
border_width="default_theme",
button_length="default_theme",
command=None,
variable=None,
orient="horizontal",
state="normal",
bg_color: Union[str, Tuple[str, str], None] = None,
border_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str]] = "default_theme",
progress_color: Union[str, Tuple[str, str], None] = "default_theme",
button_color: Union[str, Tuple[str, str]] = "default_theme",
button_hover_color: Union[str, Tuple[str, str]] = "default_theme",
from_: int = 0,
to: int = 1,
number_of_steps: Union[int, None] = None,
width: Union[int, str] = "default_init",
height: Union[int, str] = "default_init",
corner_radius: Union[int, str] = "default_theme",
button_corner_radius: Union[int, str] = "default_theme",
border_width: Union[int, str] = "default_theme",
button_length: Union[int, str] = "default_theme",
command: Callable = None,
variable: tkinter.Variable = None,
orient: str = "horizontal",
state: str = "normal",
**kwargs):
# set default dimensions according to orientation
if width is None:
if width == "default_init":
if orient.lower() == "vertical":
width = 16
else:
width = 200
if height is None:
if height == "default_init":
if orient.lower() == "vertical":
height = 200
else:
height = 16
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.border_color = border_color
self.fg_color = ThemeManager.theme["color"]["slider"] if fg_color == "default_theme" else fg_color
self.progress_color = ThemeManager.theme["color"]["slider_progress"] if progress_color == "default_theme" else progress_color
self.button_color = ThemeManager.theme["color"]["slider_button"] if button_color == "default_theme" else button_color
self.button_hover_color = ThemeManager.theme["color"]["slider_button_hover"] if button_hover_color == "default_theme" else button_hover_color
self._border_color = border_color
self._fg_color = ThemeManager.theme["color"]["slider"] if fg_color == "default_theme" else fg_color
self._progress_color = ThemeManager.theme["color"]["slider_progress"] if progress_color == "default_theme" else progress_color
self._button_color = ThemeManager.theme["color"]["slider_button"] if button_color == "default_theme" else button_color
self._button_hover_color = ThemeManager.theme["color"]["slider_button_hover"] if button_hover_color == "default_theme" else button_hover_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["slider_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.button_corner_radius = ThemeManager.theme["shape"]["slider_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius
self.border_width = ThemeManager.theme["shape"]["slider_border_width"] if border_width == "default_theme" else border_width
self.button_length = ThemeManager.theme["shape"]["slider_button_length"] if button_length == "default_theme" else button_length
self.value = 0.5 # initial value of slider in percent
self.orientation = orient
self.hover_state = False
self.from_ = from_
self.to = to
self.number_of_steps = number_of_steps
self.output_value = self.from_ + (self.value * (self.to - self.from_))
self._corner_radius = ThemeManager.theme["shape"]["slider_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._button_corner_radius = ThemeManager.theme["shape"]["slider_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius
self._border_width = ThemeManager.theme["shape"]["slider_border_width"] if border_width == "default_theme" else border_width
self._button_length = ThemeManager.theme["shape"]["slider_button_length"] if button_length == "default_theme" else button_length
self._value: float = 0.5 # initial value of slider in percent
self._orientation = orient
self._hover_state: bool = False
self._from_ = from_
self._to = to
self._number_of_steps = number_of_steps
self._output_value = self._from_ + (self._value * (self._to - self._from_))
if self.corner_radius < self.button_corner_radius:
self.corner_radius = self.button_corner_radius
if self._corner_radius < self._button_corner_radius:
self._corner_radius = self._button_corner_radius
# callback and control variables
self.command = command
self.variable: tkinter.Variable = variable
self.variable_callback_blocked = False
self.variable_callback_name = None
self.state = state
self._command = command
self._variable: tkinter.Variable = variable
self._variable_callback_blocked: bool = False
self._variable_callback_name: Union[bool, 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 = DrawEngine(self.canvas)
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 = DrawEngine(self._canvas)
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.clicked)
self.canvas.bind("<B1-Motion>", self.clicked)
self._canvas.bind("<Enter>", self._on_enter)
self._canvas.bind("<Leave>", self._on_leave)
self._canvas.bind("<Button-1>", self._clicked)
self._canvas.bind("<B1-Motion>", self._clicked)
# Each time an item is resized due to pack position mode, the binding Configure is called on the widget
self.bind('<Configure>', self.update_dimensions_event)
self.bind('<Configure>', self._update_dimensions_event)
self.set_cursor()
self.draw() # initial draw
self._set_cursor()
self._draw() # initial draw
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.variable_callback_blocked = True
self.set(self.variable.get(), from_variable_callback=True)
self.variable_callback_blocked = False
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._variable_callback_blocked = True
self.set(self._variable.get(), from_variable_callback=True)
self._variable_callback_blocked = False
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
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()
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)
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()
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.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
def set_cursor(self):
if self.state == "normal" and Settings.cursor_manipulation_enabled:
def _set_cursor(self):
if self._state == "normal" and Settings.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 Settings.cursor_manipulation_enabled:
elif self._state == "disabled" and Settings.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):
if self.orientation.lower() == "horizontal":
def _draw(self, no_color_updates=False):
if self._orientation.lower() == "horizontal":
orientation = "w"
elif self.orientation.lower() == "vertical":
elif self._orientation.lower() == "vertical":
orientation = "s"
else:
orientation = "w"
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_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.value, orientation)
requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_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._value, orientation)
if no_color_updates is False or requires_recoloring:
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
if self.border_color is None:
self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
if self._border_color is None:
self._canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self._bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode))
else:
self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
self._canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self._border_color, self._appearance_mode),
outline=ThemeManager.single_color(self._border_color, self._appearance_mode))
self.canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
if self.progress_color is None:
self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
if self._progress_color is None:
self._canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
else:
self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.progress_color, self._appearance_mode),
outline=ThemeManager.single_color(self.progress_color, self._appearance_mode))
self._canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self._progress_color, self._appearance_mode),
outline=ThemeManager.single_color(self._progress_color, self._appearance_mode))
if self.hover_state is True:
self.canvas.itemconfig("slider_parts",
fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
if self._hover_state is True:
self._canvas.itemconfig("slider_parts",
fill=ThemeManager.single_color(self._button_hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self._button_hover_color, self._appearance_mode))
else:
self.canvas.itemconfig("slider_parts",
fill=ThemeManager.single_color(self.button_color, self._appearance_mode),
outline=ThemeManager.single_color(self.button_color, self._appearance_mode))
def clicked(self, event=None):
if self.state == "normal":
if self.orientation.lower() == "horizontal":
self.value = (event.x / self._current_width) / self._widget_scaling
else:
self.value = 1 - (event.y / self._current_height) / self._widget_scaling
if self.value > 1:
self.value = 1
if self.value < 0:
self.value = 0
self.output_value = self.round_to_step_size(self.from_ + (self.value * (self.to - self.from_)))
self.value = (self.output_value - self.from_) / (self.to - self.from_)
self.draw(no_color_updates=False)
if self.variable is not None:
self.variable_callback_blocked = True
self.variable.set(round(self.output_value) if isinstance(self.variable, tkinter.IntVar) else self.output_value)
self.variable_callback_blocked = False
if self.command is not None:
self.command(self.output_value)
def on_enter(self, event=0):
if self.state == "normal":
self.hover_state = True
self.canvas.itemconfig("slider_parts",
fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
def on_leave(self, event=0):
self.hover_state = False
self.canvas.itemconfig("slider_parts",
fill=ThemeManager.single_color(self.button_color, self._appearance_mode),
outline=ThemeManager.single_color(self.button_color, self._appearance_mode))
def round_to_step_size(self, value):
if self.number_of_steps is not None:
step_size = (self.to - self.from_) / self.number_of_steps
value = self.to - (round((self.to - value) / step_size) * step_size)
return value
else:
return value
def get(self):
return self.output_value
def set(self, output_value, from_variable_callback=False):
if self.from_ < self.to:
if output_value > self.to:
output_value = self.to
elif output_value < self.from_:
output_value = self.from_
else:
if output_value < self.to:
output_value = self.to
elif output_value > self.from_:
output_value = self.from_
self.output_value = self.round_to_step_size(output_value)
self.value = (self.output_value - self.from_) / (self.to - self.from_)
self.draw(no_color_updates=False)
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(round(self.output_value) if isinstance(self.variable, tkinter.IntVar) else self.output_value)
self.variable_callback_blocked = False
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
self.set(self.variable.get(), from_variable_callback=True)
self._canvas.itemconfig("slider_parts",
fill=ThemeManager.single_color(self._button_color, self._appearance_mode),
outline=ThemeManager.single_color(self._button_color, self._appearance_mode))
def configure(self, require_redraw=False, **kwargs):
if "state" in kwargs:
self.state = kwargs["state"]
self.set_cursor()
self._state = kwargs.pop("state")
self._set_cursor()
require_redraw = True
del kwargs["state"]
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
del kwargs["fg_color"]
if "progress_color" in kwargs:
if kwargs["progress_color"] is None:
self.progress_color = self.fg_color
else:
self.progress_color = kwargs["progress_color"]
self._progress_color = kwargs.pop("progress_color")
require_redraw = True
del kwargs["progress_color"]
if "button_color" in kwargs:
self.button_color = kwargs["button_color"]
self._button_color = kwargs.pop("button_color")
require_redraw = True
del kwargs["button_color"]
if "button_hover_color" in kwargs:
self.button_hover_color = kwargs["button_hover_color"]
self._button_hover_color = kwargs.pop("button_hover_color")
require_redraw = True
del kwargs["button_hover_color"]
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
self._border_color = kwargs.pop("border_color")
require_redraw = True
del kwargs["border_color"]
if "border_width" in kwargs:
self.border_width = kwargs["border_width"]
self._border_width = kwargs.pop("border_width")
require_redraw = True
del kwargs["border_width"]
if "from_" in kwargs:
self.from_ = kwargs["from_"]
del kwargs["from_"]
self._from_ = kwargs.pop("from_")
if "to" in kwargs:
self.to = kwargs["to"]
del kwargs["to"]
self._to = kwargs.pop("to")
if "number_of_steps" in kwargs:
self.number_of_steps = kwargs["number_of_steps"]
del kwargs["number_of_steps"]
self._number_of_steps = kwargs.pop("number_of_steps")
if "command" in kwargs:
self.command = kwargs["command"]
del kwargs["command"]
self._command = kwargs.pop("command")
if "variable" in kwargs:
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
self.variable = kwargs["variable"]
self._variable = kwargs.pop("variable")
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.set(self.variable.get(), from_variable_callback=True)
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.set(self._variable.get(), from_variable_callback=True)
else:
self.variable = None
del kwargs["variable"]
self._variable = None
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
self._set_dimensions(height=kwargs.pop("height"))
super().configure(require_redraw=require_redraw, **kwargs)
def _clicked(self, event=None):
if self._state == "normal":
if self._orientation.lower() == "horizontal":
self._value = (event.x / self._current_width) / self._widget_scaling
else:
self._value = 1 - (event.y / self._current_height) / self._widget_scaling
if self._value > 1:
self._value = 1
if self._value < 0:
self._value = 0
self._output_value = self._round_to_step_size(self._from_ + (self._value * (self._to - self._from_)))
self._value = (self._output_value - self._from_) / (self._to - self._from_)
self._draw(no_color_updates=False)
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
self._variable_callback_blocked = False
if self._command is not None:
self._command(self._output_value)
def _on_enter(self, event=0):
if self._state == "normal":
self._hover_state = True
self._canvas.itemconfig("slider_parts",
fill=ThemeManager.single_color(self._button_hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self._button_hover_color, self._appearance_mode))
def _on_leave(self, event=0):
self._hover_state = False
self._canvas.itemconfig("slider_parts",
fill=ThemeManager.single_color(self._button_color, self._appearance_mode),
outline=ThemeManager.single_color(self._button_color, self._appearance_mode))
def _round_to_step_size(self, value) -> float:
if self._number_of_steps is not None:
step_size = (self._to - self._from_) / self._number_of_steps
value = self._to - (round((self._to - value) / step_size) * step_size)
return value
else:
return value
def get(self) -> float:
return self._output_value
def set(self, output_value, from_variable_callback=False):
if self._from_ < self._to:
if output_value > self._to:
output_value = self._to
elif output_value < self._from_:
output_value = self._from_
else:
if output_value < self._to:
output_value = self._to
elif output_value > self._from_:
output_value = self._from_
self._output_value = self._round_to_step_size(output_value)
self._value = (self._output_value - self._from_) / (self._to - self._from_)
self._draw(no_color_updates=False)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
self._variable_callback_blocked = False
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self.set(self._variable.get(), from_variable_callback=True)

View File

@ -1,5 +1,6 @@
import tkinter
import sys
from typing import Union, Tuple, Callable
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -9,316 +10,322 @@ from .widget_base_class import CTkBaseClass
class CTkSwitch(CTkBaseClass):
"""
Switch with rounded corners, border, label, command, variable support.
For detailed information check out the documentation.
"""
def __init__(self, *args,
text="CTkSwitch",
text_font="default_theme",
text_color="default_theme",
text_color_disabled="default_theme",
bg_color=None,
border_color=None,
fg_color="default_theme",
progress_color="default_theme",
button_color="default_theme",
button_hover_color="default_theme",
width=36,
height=18,
corner_radius="default_theme",
# button_corner_radius="default_theme",
border_width="default_theme",
button_length="default_theme",
command=None,
onvalue=1,
offvalue=0,
variable=None,
textvariable=None,
state=tkinter.NORMAL,
bg_color: Union[str, Tuple[str, str], None] = None,
border_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str]] = "default_theme",
progress_color: Union[str, Tuple[str, str]] = "default_theme",
button_color: Union[str, Tuple[str, str]] = "default_theme",
button_hover_color: Union[str, Tuple[str, str]] = "default_theme",
width: int = 36,
height: int = 18,
text: str = "CTkSwitch",
text_font: any = "default_theme",
text_color: Union[str, Tuple[str, str]] = "default_theme",
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
corner_radius: Union[int, str] = "default_theme",
border_width: Union[int, str] = "default_theme",
button_length: Union[int, str] = "default_theme",
command: Callable = None,
onvalue: Union[int, str] = 1,
offvalue: Union[int, str] = 0,
variable: tkinter.Variable = None,
textvariable: tkinter.Variable = None,
state: str = tkinter.NORMAL,
**kwargs):
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self.border_color = border_color
self.fg_color = ThemeManager.theme["color"]["switch"] if fg_color == "default_theme" else fg_color
self.progress_color = ThemeManager.theme["color"]["switch_progress"] if progress_color == "default_theme" else progress_color
self.button_color = ThemeManager.theme["color"]["switch_button"] if button_color == "default_theme" else button_color
self.button_hover_color = ThemeManager.theme["color"]["switch_button_hover"] if button_hover_color == "default_theme" else button_hover_color
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self._border_color = border_color
self._fg_color = ThemeManager.theme["color"]["switch"] if fg_color == "default_theme" else fg_color
self._progress_color = ThemeManager.theme["color"]["switch_progress"] if progress_color == "default_theme" else progress_color
self._button_color = ThemeManager.theme["color"]["switch_button"] if button_color == "default_theme" else button_color
self._button_hover_color = ThemeManager.theme["color"]["switch_button_hover"] if button_hover_color == "default_theme" else button_hover_color
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self._text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
# text
self.text = text
self.text_label = None
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self._text = text
self._text_label = None
self._text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# shape
self.corner_radius = ThemeManager.theme["shape"]["switch_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._corner_radius = ThemeManager.theme["shape"]["switch_corner_radius"] if corner_radius == "default_theme" else corner_radius
# self.button_corner_radius = ThemeManager.theme["shape"]["switch_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius
self.border_width = ThemeManager.theme["shape"]["switch_border_width"] if border_width == "default_theme" else border_width
self.button_length = ThemeManager.theme["shape"]["switch_button_length"] if button_length == "default_theme" else button_length
self.hover_state = False
self.check_state = False # True if switch is activated
self.state = state
self.onvalue = onvalue
self.offvalue = offvalue
self._border_width = ThemeManager.theme["shape"]["switch_border_width"] if border_width == "default_theme" else border_width
self._button_length = ThemeManager.theme["shape"]["switch_button_length"] if button_length == "default_theme" else button_length
self._hover_state: bool = False
self._check_state: bool = False # True if switch is activated
self._state = state
self._onvalue = onvalue
self._offvalue = offvalue
# callback and control variables
self.command = command
self.variable: tkinter.Variable = variable
self.variable_callback_blocked = False
self.variable_callback_name = None
self.textvariable = textvariable
self._command = command
self._variable: tkinter.Variable = variable
self._variable_callback_blocked = False
self._variable_callback_name = None
self._textvariable = textvariable
# configure grid system (3x1)
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=0)
self.bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self._current_width),
height=self.apply_widget_scaling(self._current_height))
self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self._bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self._current_width),
height=self.apply_widget_scaling(self._current_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, sticky="nswe")
self._draw_engine = DrawEngine(self._canvas)
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.toggle)
self._canvas.bind("<Enter>", self._on_enter)
self._canvas.bind("<Leave>", self._on_leave)
self._canvas.bind("<Button-1>", self.toggle)
self.text_label = tkinter.Label(master=self,
bd=0,
text=self.text,
justify=tkinter.LEFT,
font=self.apply_font_scaling(self.text_font),
textvariable=self.textvariable)
self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self.text_label["anchor"] = "w"
self._text_label = tkinter.Label(master=self,
bd=0,
text=self._text,
justify=tkinter.LEFT,
font=self._apply_font_scaling(self._text_font),
textvariable=self._textvariable)
self._text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self._text_label["anchor"] = "w"
self.text_label.bind("<Enter>", self.on_enter)
self.text_label.bind("<Leave>", self.on_leave)
self.text_label.bind("<Button-1>", self.toggle)
self._text_label.bind("<Enter>", self._on_enter)
self._text_label.bind("<Leave>", self._on_leave)
self._text_label.bind("<Button-1>", self.toggle)
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.check_state = True if self.variable.get() == self.onvalue else False
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.c_heck_state = True if self._variable.get() == self._onvalue else False
self.draw() # initial draw
self.set_cursor()
self._draw() # initial draw
self._set_cursor()
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
self.bg_canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
self.draw()
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_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.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
def set_cursor(self):
def _set_cursor(self):
if Settings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if self._state == tkinter.DISABLED:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
elif self._state == tkinter.NORMAL:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
if self.text_label is not None:
self.text_label.configure(cursor="pointinghand")
self._canvas.configure(cursor="pointinghand")
if self._text_label is not None:
self._text_label.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
if self.text_label is not None:
self.text_label.configure(cursor="hand2")
self._canvas.configure(cursor="hand2")
if self._text_label is not None:
self._text_label.configure(cursor="hand2")
def draw(self, no_color_updates=False):
def _draw(self, no_color_updates=False):
if self.check_state is True:
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_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.corner_radius),
1, "w")
if self._check_state is True:
requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_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._corner_radius),
1, "w")
else:
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_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.corner_radius),
0, "w")
requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_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._corner_radius),
0, "w")
if no_color_updates is False or requires_recoloring:
self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._bg_canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
if self.border_color is None:
self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
if self._border_color is None:
self._canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self._bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode))
else:
self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
self._canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self._border_color, self._appearance_mode),
outline=ThemeManager.single_color(self._border_color, self._appearance_mode))
self.canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
if self.progress_color is None:
self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
if self._progress_color is None:
self._canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
else:
self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.progress_color, self._appearance_mode),
outline=ThemeManager.single_color(self.progress_color, self._appearance_mode))
self._canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self._progress_color, self._appearance_mode),
outline=ThemeManager.single_color(self._progress_color, self._appearance_mode))
self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_color, self._appearance_mode),
outline=ThemeManager.single_color(self.button_color, self._appearance_mode))
self._canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self._button_color, self._appearance_mode),
outline=ThemeManager.single_color(self._button_color, self._appearance_mode))
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)))
if self._state == tkinter.DISABLED:
self._text_label.configure(fg=(ThemeManager.single_color(self._text_color_disabled, self._appearance_mode)))
else:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
self._text_label.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode))
self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._text_label.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
def toggle(self, event=None):
if self.state is not tkinter.DISABLED:
if self.check_state is True:
self.check_state = False
if self._state is not tkinter.DISABLED:
if self._check_state is True:
self._check_state = False
else:
self.check_state = True
self._check_state = True
self.draw(no_color_updates=True)
self._draw(no_color_updates=True)
if self.variable is not None:
self.variable_callback_blocked = True
self.variable.set(self.onvalue if self.check_state is True else self.offvalue)
self.variable_callback_blocked = False
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
self._variable_callback_blocked = False
if self.command is not None:
self.command()
if self._command is not None:
self._command()
def select(self, from_variable_callback=False):
if self.state is not tkinter.DISABLED or from_variable_callback:
self.check_state = True
if self._state is not tkinter.DISABLED or from_variable_callback:
self._check_state = True
self.draw(no_color_updates=True)
self._draw(no_color_updates=True)
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.onvalue)
self.variable_callback_blocked = False
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._onvalue)
self._variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
if self.state is not tkinter.DISABLED or from_variable_callback:
self.check_state = False
if self._state is not tkinter.DISABLED or from_variable_callback:
self._check_state = False
self.draw(no_color_updates=True)
self._draw(no_color_updates=True)
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.offvalue)
self.variable_callback_blocked = False
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._offvalue)
self._variable_callback_blocked = False
def get(self):
return self.onvalue if self.check_state is True else self.offvalue
def get(self) -> Union[int, str]:
return self._onvalue if self._check_state is True else self._offvalue
def on_enter(self, event=0):
self.hover_state = True
def _on_enter(self, event=0):
self._hover_state = True
if self.state is not tkinter.DISABLED:
self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
if self._state is not tkinter.DISABLED:
self._canvas.itemconfig("slider_parts",
fill=ThemeManager.single_color(self._button_hover_color, self._appearance_mode),
outline=ThemeManager.single_color(self._button_hover_color, self._appearance_mode))
def on_leave(self, event=0):
self.hover_state = False
self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_color, self._appearance_mode),
outline=ThemeManager.single_color(self.button_color, self._appearance_mode))
def _on_leave(self, event=0):
self._hover_state = False
self._canvas.itemconfig("slider_parts",
fill=ThemeManager.single_color(self._button_color, self._appearance_mode),
outline=ThemeManager.single_color(self._button_color, self._appearance_mode))
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
if self.variable.get() == self.onvalue:
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
if self._variable.get() == self._onvalue:
self.select(from_variable_callback=True)
elif self.variable.get() == self.offvalue:
elif self._variable.get() == self._offvalue:
self.deselect(from_variable_callback=True)
def configure(self, require_redraw=False, **kwargs):
if "text" in kwargs:
self.text = kwargs.pop("text")
self.text_label.configure(text=self.text)
self._text = kwargs.pop("text")
self._text_label.configure(text=self._text)
if "text_font" in kwargs:
self.text_font = kwargs.pop("text_font")
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self._text_font = kwargs.pop("text_font")
self._text_label.configure(font=self._apply_font_scaling(self._text_font))
if "state" in kwargs:
self.state = kwargs.pop("state")
self.set_cursor()
self._state = kwargs.pop("state")
self._set_cursor()
require_redraw = True
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
if "progress_color" in kwargs:
new_progress_color = kwargs.pop("progress_color")
if new_progress_color is None:
self.progress_color = self.fg_color
self._progress_color = self._fg_color
else:
self.progress_color = new_progress_color
self._progress_color = new_progress_color
require_redraw = True
if "button_color" in kwargs:
self.button_color = kwargs.pop("button_color")
self._button_color = kwargs.pop("button_color")
require_redraw = True
if "button_hover_color" in kwargs:
self.button_hover_color = kwargs.pop("button_hover_color")
self._button_hover_color = kwargs.pop("button_hover_color")
require_redraw = True
if "border_color" in kwargs:
self.border_color = kwargs.pop("border_color")
self._border_color = kwargs.pop("border_color")
require_redraw = True
if "border_width" in kwargs:
self.border_width = kwargs.pop("border_width")
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "command" in kwargs:
self.command = kwargs.pop("command")
self._command = kwargs.pop("command")
if "textvariable" in kwargs:
self.textvariable = kwargs.pop("textvariable")
self.text_label.configure(textvariable=self.textvariable)
self._textvariable = kwargs.pop("textvariable")
self._text_label.configure(textvariable=self._textvariable)
if "variable" in kwargs:
if self.variable is not None and self.variable != "":
self.variable.trace_remove("write", self.variable_callback_name)
if self._variable is not None and self._variable != "":
self._variable.trace_remove("write", self._variable_callback_name)
self.variable = kwargs.pop("variable")
self._variable = kwargs.pop("variable")
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.check_state = True if self.variable.get() == self.onvalue else False
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._onvalue else False
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)

View File

@ -1,4 +1,5 @@
import tkinter
from typing import Union, Tuple, Callable
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -7,163 +8,169 @@ from .widget_base_class import CTkBaseClass
class CTkTextbox(CTkBaseClass):
"""
Textbox with rounded corners, and all text features of tkinter Text widget.
For detailed information check out the documentation.
"""
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
border_color="default_theme",
border_width="default_theme",
corner_radius="default_theme",
text_font="default_theme",
text_color="default_theme",
width=200,
height=200,
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
border_color: Union[str, Tuple[str, str]] = "default_theme",
border_width: Union[str, str] = "default_theme",
corner_radius: Union[str, str] = "default_theme",
text_font: any = "default_theme",
text_color: Union[str, str] = "default_theme",
width: int = 200,
height: int = 200,
**kwargs):
# transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
if "master" in kwargs:
super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs.pop("master"))
else:
super().__init__(*args, bg_color=bg_color, width=width, height=height)
# color
self.fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
self.border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self._fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
self._border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width
self._corner_radius = ThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width
# text
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self._text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# configure 1x1 grid
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._current_width),
height=self.apply_widget_scaling(self._current_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, rowspan=1, columnspan=1, sticky="nsew")
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self.draw_engine = DrawEngine(self.canvas)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._canvas.grid(row=0, column=0, padx=0, pady=0, rowspan=1, columnspan=1, sticky="nsew")
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self._draw_engine = DrawEngine(self._canvas)
for arg in ["highlightthickness", "fg", "bg", "font", "width", "height"]:
kwargs.pop(arg, None)
self.textbox = tkinter.Text(self,
fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
width=0,
height=0,
font=self.text_font,
highlightthickness=0,
relief="flat",
insertbackground=ThemeManager.single_color(("black", "white"), self._appearance_mode),
bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
**kwargs)
self.textbox.grid(row=0, column=0, padx=self.corner_radius, pady=self.corner_radius, rowspan=1, columnspan=1, sticky="nsew")
self._textbox = tkinter.Text(self,
fg=ThemeManager.single_color(self._text_color, self._appearance_mode),
width=0,
height=0,
font=self._text_font,
highlightthickness=0,
relief="flat",
insertbackground=ThemeManager.single_color(("black", "white"), self._appearance_mode),
bg=ThemeManager.single_color(self._fg_color, self._appearance_mode),
**kwargs)
self._textbox.grid(row=0, column=0, padx=self._corner_radius, pady=self._corner_radius, rowspan=1, columnspan=1, sticky="nsew")
self.bind('<Configure>', self.update_dimensions_event)
self.draw()
self.bind('<Configure>', self._update_dimensions_event)
self._draw()
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self.textbox.configure(font=self.apply_font_scaling(self.text_font))
self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
self.draw()
self._textbox.configure(font=self._apply_font_scaling(self.text_font))
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)
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()
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw()
def draw(self, no_color_updates=False):
def _draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(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))
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(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))
if no_color_updates is False or requires_recoloring:
if self.fg_color is None:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
if self._fg_color is None:
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._bg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._bg_color, self._appearance_mode))
else:
self.canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._canvas.itemconfig("inner_parts",
fill=ThemeManager.single_color(self._fg_color, self._appearance_mode),
outline=ThemeManager.single_color(self._fg_color, self._appearance_mode))
self.canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
self._canvas.itemconfig("border_parts",
fill=ThemeManager.single_color(self._border_color, self._appearance_mode),
outline=ThemeManager.single_color(self._border_color, self._appearance_mode))
self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
self.textbox.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
insertbackground=ThemeManager.single_color(("black", "white"), self._appearance_mode))
self._textbox.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode),
bg=ThemeManager.single_color(self._fg_color, self._appearance_mode),
insertbackground=ThemeManager.single_color(("black", "white"), self._appearance_mode))
self.canvas.tag_lower("inner_parts")
self.canvas.tag_lower("border_parts")
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
def yview(self, *args):
return self.textbox.yview(*args)
return self._textbox.yview(*args)
def xview(self, *args):
return self.textbox.xview(*args)
return self._textbox.xview(*args)
def insert(self, *args, **kwargs):
return self.textbox.insert(*args, **kwargs)
return self._textbox.insert(*args, **kwargs)
def focus(self):
return self.textbox.focus()
return self._textbox.focus()
def tag_add(self, *args, **kwargs):
return self.textbox.tag_add(*args, **kwargs)
return self._textbox.tag_add(*args, **kwargs)
def tag_config(self, *args, **kwargs):
return self.textbox.tag_config(*args, **kwargs)
return self._textbox.tag_config(*args, **kwargs)
def tag_configure(self, *args, **kwargs):
return self.textbox.tag_configure(*args, **kwargs)
return self._textbox.tag_configure(*args, **kwargs)
def tag_remove(self, *args, **kwargs):
return self.textbox.tag_remove(*args, **kwargs)
return self._textbox.tag_remove(*args, **kwargs)
def configure(self, require_redraw=False, **kwargs):
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
self._fg_color = kwargs.pop("fg_color")
require_redraw = True
# check if CTk widgets are children of the frame and change their bg_color to new frame fg_color
# check if CTk widgets are children of the frame and change their _bg_color to new frame fg_color
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self.fg_color)
if isinstance(child, CTkBaseClass) and hasattr(child, "_fg_color"):
child.configure(bg_color=self._fg_color)
if "border_color" in kwargs:
self.border_color = kwargs.pop("border_color")
self._border_color = kwargs.pop("border_color")
require_redraw = True
if "corner_radius" in kwargs:
self.corner_radius = kwargs.pop("corner_radius")
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "border_width" in kwargs:
self.border_width = kwargs.pop("border_width")
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "width" in kwargs:
self.set_dimensions(width=kwargs.pop("width"))
self._set_dimensions(width=kwargs.pop("width"))
if "height" in kwargs:
self.set_dimensions(height=kwargs.pop("height"))
self._set_dimensions(height=kwargs.pop("height"))
if "text_font" in kwargs:
self.text_font = kwargs.pop("text_font")
self.textbox.configure(font=self.apply_font_scaling(self.text_font))
self._text_font = kwargs.pop("text_font")
self._textbox.configure(font=self._apply_font_scaling(self._text_font))
if "font" in kwargs:
raise ValueError("No attribute named font. Use text_font instead of font for CTk widgets")
@ -173,4 +180,4 @@ class CTkTextbox(CTkBaseClass):
else:
super().configure(require_redraw=require_redraw)
self.textbox.configure(**kwargs)
self._textbox.configure(**kwargs)

View File

@ -2,7 +2,7 @@ import tkinter
import sys
import copy
import re
from typing import Union
from typing import Union, Tuple, Callable, List
from ..theme_manager import ThemeManager
from ..appearance_mode_tracker import AppearanceModeTracker
@ -11,124 +11,127 @@ from ..scaling_tracker import ScalingTracker
class DropdownMenu(tkinter.Menu):
def __init__(self, *args,
min_character_width=18,
fg_color="default_theme",
hover_color="default_theme",
text_color="default_theme",
text_font="default_theme",
command=None,
values=None,
min_character_width: int = 18,
fg_color: Union[str, Tuple[str, str]] = "default_theme",
hover_color: Union[str, Tuple[str, str]] = "default_theme",
text_color: Union[str, Tuple[str, str]] = "default_theme",
text_font: Union[str, Tuple[str, str]] = "default_theme",
command: Callable = None,
values: List[str] = None,
**kwargs):
super().__init__(*args, **kwargs)
ScalingTracker.add_widget(self.set_scaling, self)
ScalingTracker.add_widget(self._set_scaling, self)
self._widget_scaling = ScalingTracker.get_widget_scaling(self)
self._spacing_scaling = ScalingTracker.get_spacing_scaling(self)
AppearanceModeTracker.add(self.set_appearance_mode, self)
AppearanceModeTracker.add(self._set_appearance_mode, self)
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
self.min_character_width = min_character_width
self.fg_color = ThemeManager.theme["color"]["dropdown_color"] if fg_color == "default_theme" else fg_color
self.hover_color = ThemeManager.theme["color"]["dropdown_hover"] if hover_color == "default_theme" else hover_color
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self._min_character_width = min_character_width
self._fg_color = ThemeManager.theme["color"]["dropdown_color"] if fg_color == "default_theme" else fg_color
self._hover_color = ThemeManager.theme["color"]["dropdown_hover"] if hover_color == "default_theme" else hover_color
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self._text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self.configure_menu_for_platforms()
self._configure_menu_for_platforms()
self.values = values
self.command = command
self._values = values
self._command = command
self.add_menu_commands()
self._add_menu_commands()
def configure_menu_for_platforms(self):
""" apply platform specific appearance attributes """
def _configure_menu_for_platforms(self):
""" apply platform specific appearance attributes, configure all colors """
if sys.platform == "darwin":
self.configure(tearoff=False,
font=self.apply_font_scaling(self.text_font))
font=self._apply_font_scaling(self._text_font))
elif sys.platform.startswith("win"):
self.configure(tearoff=False,
relief="flat",
activebackground=ThemeManager.single_color(self.hover_color, self._appearance_mode),
activebackground=ThemeManager.single_color(self._hover_color, self._appearance_mode),
borderwidth=0,
activeborderwidth=self.apply_widget_scaling(4),
bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
activeforeground=ThemeManager.single_color(self.text_color, self._appearance_mode),
font=self.apply_font_scaling(self.text_font),
activeborderwidth=self._apply_widget_scaling(4),
bg=ThemeManager.single_color(self._fg_color, self._appearance_mode),
fg=ThemeManager.single_color(self._text_color, self._appearance_mode),
activeforeground=ThemeManager.single_color(self._text_color, self._appearance_mode),
font=self._apply_font_scaling(self._text_font),
cursor="hand2")
else:
self.configure(tearoff=False,
relief="flat",
activebackground=ThemeManager.single_color(self.hover_color, self._appearance_mode),
activebackground=ThemeManager.single_color(self._hover_color, self._appearance_mode),
borderwidth=0,
activeborderwidth=0,
bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
activeforeground=ThemeManager.single_color(self.text_color, self._appearance_mode),
font=self.apply_font_scaling(self.text_font))
bg=ThemeManager.single_color(self._fg_color, self._appearance_mode),
fg=ThemeManager.single_color(self._text_color, self._appearance_mode),
activeforeground=ThemeManager.single_color(self._text_color, self._appearance_mode),
font=self._apply_font_scaling(self._text_font))
def _add_menu_commands(self):
""" delete existing menu labels and createe new labels with command according to values list """
self.delete(0, "end") # delete all old commands
def add_menu_commands(self):
if sys.platform.startswith("linux"):
for value in self.values:
self.add_command(label=" " + value.ljust(self.min_character_width) + " ",
command=lambda v=value: self.button_callback(v),
for value in self._values:
self.add_command(label=" " + value.ljust(self._min_character_width) + " ",
command=lambda v=value: self._button_callback(v),
compound="left")
else:
for value in self.values:
self.add_command(label=value.ljust(self.min_character_width),
command=lambda v=value: self.button_callback(v),
for value in self._values:
self.add_command(label=value.ljust(self._min_character_width),
command=lambda v=value: self._button_callback(v),
compound="left")
def _button_callback(self, value):
if self._command is not None:
self._command(value)
def open(self, x: Union[int, float], y: Union[int, float]):
if sys.platform == "darwin":
y += self.apply_widget_scaling(8)
y += self._apply_widget_scaling(8)
else:
y += self.apply_widget_scaling(3)
y += self._apply_widget_scaling(3)
if sys.platform == "darwin" or sys.platform.startswith("win"):
self.post(int(x), int(y))
else: # Linux
self.tk_popup(int(x), int(y))
def button_callback(self, value):
if self.command is not None:
self.command(value)
def configure(self, **kwargs):
if "values" in kwargs:
self.values = kwargs.pop("values")
self.delete(0, "end") # delete all old commands
self.add_menu_commands()
self._values = kwargs.pop("values")
self._add_menu_commands()
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
self.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
self._fg_color = kwargs.pop("fg_color")
self.configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode))
if "hover_color" in kwargs:
self.hover_color = kwargs.pop("hover_color")
self.configure(activebackground=ThemeManager.single_color(self.hover_color, self._appearance_mode))
self._hover_color = kwargs.pop("hover_color")
self.configure(activebackground=ThemeManager.single_color(self._hover_color, self._appearance_mode))
if "text_color" in kwargs:
self.text_color = kwargs.pop("text_color")
self.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
self._text_color = kwargs.pop("text_color")
self.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode))
if "text_font" in kwargs:
self.text_font = kwargs.pop("text_font")
self.configure(font=self.apply_font_scaling(self.text_font))
self._text_font = kwargs.pop("text_font")
self.configure(font=self._apply_font_scaling(self._text_font))
super().configure(**kwargs)
def apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
def _apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
if isinstance(value, (int, float)):
return value * self._widget_scaling
else:
return value
def apply_font_scaling(self, font):
def _apply_font_scaling(self, font):
if type(font) == tuple or type(font) == list:
font_list = list(font)
for i in range(len(font_list)):
@ -150,16 +153,16 @@ class DropdownMenu(tkinter.Menu):
else:
return font
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
def _set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self._widget_scaling = new_widget_scaling
self._spacing_scaling = new_spacing_scaling
self.configure(font=self.apply_font_scaling(self.text_font))
self.configure(font=self._apply_font_scaling(self._text_font))
if sys.platform.startswith("win"):
self.configure(activeborderwidth=self.apply_widget_scaling(4))
self.configure(activeborderwidth=self._apply_widget_scaling(4))
def set_appearance_mode(self, mode_string):
def _set_appearance_mode(self, mode_string):
""" colors won't update on appearance mode change when dropdown is open, because it's not necessary """
if mode_string.lower() == "dark":
@ -167,4 +170,4 @@ class DropdownMenu(tkinter.Menu):
elif mode_string.lower() == "light":
self._appearance_mode = 0
self.configure_menu_for_platforms()
self._configure_menu_for_platforms()

View File

@ -2,7 +2,7 @@ import tkinter
import tkinter.ttk as ttk
import copy
import re
from typing import Callable, Union
from typing import Union, Callable
try:
from typing import TypedDict
@ -17,7 +17,7 @@ from ..theme_manager import ThemeManager
class CTkBaseClass(tkinter.Frame):
""" Base class of every CTk widget, handles the dimensions, bg_color,
""" Base class of every CTk widget, handles the dimensions, _bg_color,
appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """
def __init__(self,
@ -36,12 +36,12 @@ class CTkBaseClass(tkinter.Frame):
self._desired_height = height
# scaling
ScalingTracker.add_widget(self.set_scaling, self) # add callback for automatic scaling changes
ScalingTracker.add_widget(self._set_scaling, self) # add callback for automatic scaling changes
self._widget_scaling = ScalingTracker.get_widget_scaling(self)
self._spacing_scaling = ScalingTracker.get_spacing_scaling(self)
super().configure(width=self.apply_widget_scaling(self._desired_width),
height=self.apply_widget_scaling(self._desired_height))
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
# save latest geometry function and kwargs
class GeometryCallDict(TypedDict):
@ -51,13 +51,13 @@ class CTkBaseClass(tkinter.Frame):
self._last_geometry_manager_call: Union[GeometryCallDict, None] = None
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self.set_appearance_mode, self)
AppearanceModeTracker.add(self._set_appearance_mode, self)
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
# background color
self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color
self._bg_color = self._detect_color_of_master() if bg_color is None else bg_color
super().configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
super().configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode))
# overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well
if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame)) and not isinstance(self.master, (CTkBaseClass, CTk, CTkToplevel)):
@ -81,79 +81,79 @@ class CTkBaseClass(tkinter.Frame):
self.master.configure = new_configure
def destroy(self):
AppearanceModeTracker.remove(self.set_appearance_mode)
AppearanceModeTracker.remove(self._set_appearance_mode)
super().destroy()
def place(self, **kwargs):
self._last_geometry_manager_call = {"function": super().place, "kwargs": kwargs}
super().place(**self.apply_argument_scaling(kwargs))
super().place(**self._apply_argument_scaling(kwargs))
def pack(self, **kwargs):
self._last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs}
super().pack(**self.apply_argument_scaling(kwargs))
super().pack(**self._apply_argument_scaling(kwargs))
def grid(self, **kwargs):
self._last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs}
super().grid(**self.apply_argument_scaling(kwargs))
super().grid(**self._apply_argument_scaling(kwargs))
def apply_argument_scaling(self, kwargs: dict) -> dict:
def _apply_argument_scaling(self, kwargs: dict) -> dict:
scaled_kwargs = copy.copy(kwargs)
if "pady" in scaled_kwargs:
if isinstance(scaled_kwargs["pady"], (int, float, str)):
scaled_kwargs["pady"] = self.apply_spacing_scaling(scaled_kwargs["pady"])
scaled_kwargs["pady"] = self._apply_spacing_scaling(scaled_kwargs["pady"])
elif isinstance(scaled_kwargs["pady"], tuple):
scaled_kwargs["pady"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["pady"]])
scaled_kwargs["pady"] = tuple([self._apply_spacing_scaling(v) for v in scaled_kwargs["pady"]])
if "padx" in kwargs:
if isinstance(scaled_kwargs["padx"], (int, float, str)):
scaled_kwargs["padx"] = self.apply_spacing_scaling(scaled_kwargs["padx"])
scaled_kwargs["padx"] = self._apply_spacing_scaling(scaled_kwargs["padx"])
elif isinstance(scaled_kwargs["padx"], tuple):
scaled_kwargs["padx"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["padx"]])
scaled_kwargs["padx"] = tuple([self._apply_spacing_scaling(v) for v in scaled_kwargs["padx"]])
if "x" in scaled_kwargs:
scaled_kwargs["x"] = self.apply_spacing_scaling(scaled_kwargs["x"])
scaled_kwargs["x"] = self._apply_spacing_scaling(scaled_kwargs["x"])
if "y" in scaled_kwargs:
scaled_kwargs["y"] = self.apply_spacing_scaling(scaled_kwargs["y"])
scaled_kwargs["y"] = self._apply_spacing_scaling(scaled_kwargs["y"])
return scaled_kwargs
def configure(self, require_redraw=False, **kwargs):
""" basic configure with bg_color support, to be overridden """
""" basic configure with _bg_color support, to be overridden """
if "bg_color" in kwargs:
new_bg_color = kwargs.pop("bg_color")
if new_bg_color is None:
self.bg_color = self.detect_color_of_master()
self._bg_color = self._detect_color_of_master()
else:
self.bg_color = new_bg_color
self._bg_color = new_bg_color
require_redraw = True
super().configure(**kwargs)
if require_redraw:
self.draw()
self._draw()
def update_dimensions_event(self, event):
def _update_dimensions_event(self, event):
# only redraw if dimensions changed (for performance), independent of scaling
if round(self._current_width) != round(event.width / self._widget_scaling) or round(self._current_height) != round(event.height / self._widget_scaling):
self._current_width = (event.width / self._widget_scaling) # adjust current size according to new size given by event
self._current_height = (event.height / self._widget_scaling) # _current_width and _current_height are independent of the scale
self.draw(no_color_updates=True) # faster drawing without color changes
self._draw(no_color_updates=True) # faster drawing without color changes
def detect_color_of_master(self, master_widget=None):
""" detect color of self.master widget to set correct bg_color """
def _detect_color_of_master(self, master_widget=None):
""" detect color of self.master widget to set correct _bg_color """
if master_widget is None:
master_widget = self.master
if isinstance(master_widget, (CTkBaseClass, CTk, CTkToplevel)) and hasattr(master_widget, "fg_color"):
if master_widget.fg_color is not None:
return master_widget.fg_color
if isinstance(master_widget, (CTkBaseClass, CTk, CTkToplevel)) and hasattr(master_widget, "_fg_color"):
if master_widget._fg_color is not None:
return master_widget._fg_color
# if fg_color of master is None, try to retrieve fg_color from master of master
elif hasattr(master_widget.master, "master"):
return self.detect_color_of_master(master_widget.master)
return self._detect_color_of_master(master_widget.master)
elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget
try:
@ -168,46 +168,46 @@ class CTkBaseClass(tkinter.Frame):
except Exception:
return "#FFFFFF", "#000000"
def set_appearance_mode(self, mode_string):
def _set_appearance_mode(self, mode_string):
if mode_string.lower() == "dark":
self._appearance_mode = 1
elif mode_string.lower() == "light":
self._appearance_mode = 0
self.draw()
self._draw()
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
def _set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self._widget_scaling = new_widget_scaling
self._spacing_scaling = new_spacing_scaling
super().configure(width=self.apply_widget_scaling(self._desired_width),
height=self.apply_widget_scaling(self._desired_height))
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
if self._last_geometry_manager_call is not None:
self._last_geometry_manager_call["function"](**self.apply_argument_scaling(self._last_geometry_manager_call["kwargs"]))
self._last_geometry_manager_call["function"](**self._apply_argument_scaling(self._last_geometry_manager_call["kwargs"]))
def set_dimensions(self, width=None, height=None):
def _set_dimensions(self, width=None, height=None):
if width is not None:
self._desired_width = width
if height is not None:
self._desired_height = height
super().configure(width=self.apply_widget_scaling(self._desired_width),
height=self.apply_widget_scaling(self._desired_height))
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
def apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
def _apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
if isinstance(value, (int, float)):
return value * self._widget_scaling
else:
return value
def apply_spacing_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
def _apply_spacing_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
if isinstance(value, (int, float)):
return value * self._spacing_scaling
else:
return value
def apply_font_scaling(self, font):
def _apply_font_scaling(self, font):
if type(font) == tuple or type(font) == list:
font_list = list(font)
for i in range(len(font_list)):
@ -229,6 +229,6 @@ class CTkBaseClass(tkinter.Frame):
else:
return font
def draw(self, no_color_updates: bool = False):
def _draw(self, no_color_updates: bool = False):
""" abstract of draw method to be overridden """
pass

View File

@ -1,5 +1,6 @@
import tkinter
import time
from typing import Union, Tuple
from ..widgets.ctk_label import CTkLabel
from ..widgets.ctk_entry import CTkEntry
@ -11,109 +12,111 @@ from ..theme_manager import ThemeManager
class CTkInputDialog:
def __init__(self,
master=None,
title="CTkDialog",
text="CTkDialog",
fg_color="default_theme",
hover_color="default_theme",
border_color="default_theme"):
"""
Dialog with extra window, message, entry widget, cancel and ok button.
For detailed information check out the documentation.
"""
self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
def __init__(self,
master: any = None,
title: str = "CTkDialog",
text: str = "CTkDialog",
fg_color: Union[str, Tuple[str, str]] = "default_theme",
hover_color: Union[str, Tuple[str, str]] = "default_theme",
border_color: Union[str, Tuple[str, str]] = "default_theme"):
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
self.master = master
self.user_input = None
self.running = False
self._window_bg_color = ThemeManager.theme["color"]["window_bg_color"]
self._fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self._hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self._border_color = ThemeManager.theme["color"]["button_hover"] if border_color == "default_theme" else border_color
self.height = len(text.split("\n"))*20 + 150
self._user_input: Union[str, None] = None
self._running: bool = False
self._height: int = len(text.split("\n")) * 20 + 150
self._text = text
self.text = text
self.window_bg_color = ThemeManager.theme["color"]["window_bg_color"]
self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self.border_color = ThemeManager.theme["color"]["button_hover"] if border_color == "default_theme" else border_color
self._toplevel_window = CTkToplevel()
self._toplevel_window.geometry(f"{280}x{self._height}")
self._toplevel_window.minsize(280, self._height)
self._toplevel_window.maxsize(280, self._height)
self._toplevel_window.title(title)
self._toplevel_window.lift()
self._toplevel_window.focus_force()
self._toplevel_window.grab_set()
self._toplevel_window.protocol("WM_DELETE_WINDOW", self._on_closing)
self._toplevel_window.after(10, self._create_widgets) # create widgets with slight delay, to avoid white flickering of background
self.top = CTkToplevel()
self.top.geometry(f"{280}x{self.height}")
self.top.minsize(280, self.height)
self.top.maxsize(280, self.height)
self.top.title(title)
self.top.lift()
self.top.focus_force()
self.top.grab_set()
def _create_widgets(self):
self._label_frame = CTkFrame(master=self._toplevel_window,
corner_radius=0,
fg_color=self._window_bg_color,
width=300,
height=self._height-100)
self._label_frame.place(relx=0.5, rely=0, anchor=tkinter.N)
self.top.protocol("WM_DELETE_WINDOW", self.on_closing)
self._button_and_entry_frame = CTkFrame(master=self._toplevel_window,
corner_radius=0,
fg_color=self._window_bg_color,
width=300,
height=100)
self._button_and_entry_frame.place(relx=0.5, rely=1, anchor=tkinter.S)
self.top.after(10, self.create_widgets) # create widgets with slight delay, to avoid white flickering of background
self._myLabel = CTkLabel(master=self._label_frame,
text=self._text,
width=300,
fg_color=None,
height=self._height-100)
self._myLabel.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
def create_widgets(self):
self.label_frame = CTkFrame(master=self.top,
corner_radius=0,
fg_color=self.window_bg_color,
width=300,
height=self.height-100)
self.label_frame.place(relx=0.5, rely=0, anchor=tkinter.N)
self._entry = CTkEntry(master=self._button_and_entry_frame,
width=230)
self._entry.place(relx=0.5, rely=0.15, anchor=tkinter.CENTER)
self.button_and_entry_frame = CTkFrame(master=self.top,
corner_radius=0,
fg_color=self.window_bg_color,
width=300,
height=100)
self.button_and_entry_frame.place(relx=0.5, rely=1, anchor=tkinter.S)
self._ok_button = CTkButton(master=self._button_and_entry_frame,
text='Ok',
width=100,
command=self._ok_event,
fg_color=self._fg_color,
hover_color=self._hover_color,
border_color=self._border_color)
self._ok_button.place(relx=0.28, rely=0.65, anchor=tkinter.CENTER)
self.myLabel = CTkLabel(master=self.label_frame,
text=self.text,
width=300,
fg_color=None,
height=self.height-100)
self.myLabel.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
self._cancel_button = CTkButton(master=self._button_and_entry_frame,
text='Cancel',
width=100,
command=self._cancel_event,
fg_color=self._fg_color,
hover_color=self._hover_color,
border_color=self._border_color)
self._cancel_button.place(relx=0.72, rely=0.65, anchor=tkinter.CENTER)
self.entry = CTkEntry(master=self.button_and_entry_frame,
width=230)
self.entry.place(relx=0.5, rely=0.15, anchor=tkinter.CENTER)
self._entry.focus_force()
self._entry.bind("<Return>", self._ok_event)
self.ok_button = CTkButton(master=self.button_and_entry_frame,
text='Ok',
width=100,
command=self.ok_event,
fg_color=self.fg_color,
hover_color=self.hover_color,
border_color=self.border_color)
self.ok_button.place(relx=0.28, rely=0.65, anchor=tkinter.CENTER)
def _ok_event(self, event=None):
self._user_input = self._entry.get()
self._running = False
self.cancel_button = CTkButton(master=self.button_and_entry_frame,
text='Cancel',
width=100,
command=self.cancel_event,
fg_color=self.fg_color,
hover_color=self.hover_color,
border_color=self.border_color)
self.cancel_button.place(relx=0.72, rely=0.65, anchor=tkinter.CENTER)
def _on_closing(self):
self._running = False
self.entry.entry.focus_force()
self.entry.bind("<Return>", self.ok_event)
def ok_event(self, event=None):
self.user_input = self.entry.get()
self.running = False
def on_closing(self):
self.running = False
def cancel_event(self):
self.running = False
def _cancel_event(self):
self._running = False
def get_input(self):
self.running = True
self._running = True
while self.running:
while self._running:
try:
self.top.update()
self._toplevel_window.update()
except Exception:
return self.user_input
return self._user_input
finally:
time.sleep(0.01)
time.sleep(0.05)
self.top.destroy()
return self.user_input
self._toplevel_window.destroy()
return self._user_input

View File

@ -14,126 +14,129 @@ from ..settings import Settings
class CTk(tkinter.Tk):
"""
Main app window with dark titlebar on Windows and macOS.
For detailed information check out the documentation.
"""
def __init__(self, *args,
fg_color="default_theme",
fg_color: Union[str, Tuple[str, str]] = "default_theme",
**kwargs):
ScalingTracker.activate_high_dpi_awareness() # make process DPI aware
self.enable_macos_dark_title_bar()
self._enable_macos_dark_title_bar()
super().__init__(*args, **kwargs)
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self.set_appearance_mode, self)
self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
AppearanceModeTracker.add(self._set_appearance_mode, self)
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
ScalingTracker.add_widget(self.set_scaling, self)
self.window_scaling = ScalingTracker.get_window_scaling(self)
ScalingTracker.add_widget(self._set_scaling, self)
self._window_scaling = ScalingTracker.get_window_scaling(self)
self.current_width = 600 # initial window size, always without scaling
self.current_height = 500
self.min_width: int = 0
self.min_height: int = 0
self.max_width: int = 1_000_000
self.max_height: int = 1_000_000
self.last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
self._current_width = 600 # initial window size, always without scaling
self._current_height = 500
self._min_width: int = 0
self._min_height: int = 0
self._max_width: int = 1_000_000
self._max_height: int = 1_000_000
self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
self.fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
self._fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
if "bg" in kwargs:
self.fg_color = kwargs["bg"]
del kwargs["bg"]
self._fg_color = kwargs.pop("bg")
elif "background" in kwargs:
self.fg_color = kwargs["background"]
del kwargs["background"]
self._fg_color = kwargs.pop("background")
super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
super().configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode))
super().title("CTk")
self.geometry(f"{self.current_width}x{self.current_height}")
self.geometry(f"{self._current_width}x{self._current_height}")
self.state_before_windows_set_titlebar_color = None
self.window_exists = False # indicates if the window is already shown through update() or mainloop() after init
self.withdraw_called_before_window_exists = False # indicates if withdraw() was called before window is first shown through update() or mainloop()
self.iconify_called_before_window_exists = False # indicates if iconify() was called before window is first shown through update() or mainloop()
self._state_before_windows_set_titlebar_color = None
self._window_exists = False # indicates if the window is already shown through update() or mainloop() after init
self._withdraw_called_before_window_exists = False # indicates if withdraw() was called before window is first shown through update() or mainloop()
self._iconify_called_before_window_exists = False # indicates if iconify() was called before window is first shown through update() or mainloop()
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
if self._appearance_mode == 1:
self._windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self._windows_set_titlebar_color("light")
self.bind('<Configure>', self.update_dimensions_event)
self.bind('<Configure>', self._update_dimensions_event)
self.block_update_dimensions_event = False
self._block_update_dimensions_event = False
def update_dimensions_event(self, event=None):
if not self.block_update_dimensions_event:
def _update_dimensions_event(self, event=None):
if not self._block_update_dimensions_event:
detected_width = self.winfo_width() # detect current window size
detected_height = self.winfo_height()
if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling):
self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event
self.current_height = round(detected_height / self.window_scaling) # _current_width and _current_height are independent of the scale
if self._current_width != round(detected_width / self._window_scaling) or self._current_height != round(detected_height / self._window_scaling):
self._current_width = round(detected_width / self._window_scaling) # adjust current size according to new size given by event
self._current_height = round(detected_height / self._window_scaling) # _current_width and _current_height are independent of the scale
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self.window_scaling = new_window_scaling
def _set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self._window_scaling = new_window_scaling
# block update_dimensions_event to prevent current_width and current_height to get updated
self.block_update_dimensions_event = True
self._block_update_dimensions_event = True
# force new dimensions on window by using min, max, and geometry
super().minsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
super().maxsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
super().geometry(f"{self.apply_window_scaling(self.current_width)}x"+f"{self.apply_window_scaling(self.current_height)}")
super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
super().geometry(f"{self._apply_window_scaling(self._current_width)}x" + f"{self._apply_window_scaling(self._current_height)}")
# set new scaled min and max with 400ms delay (otherwise it won't work for some reason)
self.after(400, self.set_scaled_min_max)
self.after(400, self._set_scaled_min_max)
# release the blocking of update_dimensions_event after a small amount of time (slight delay is necessary)
def set_block_update_dimensions_event_false():
self.block_update_dimensions_event = False
self._block_update_dimensions_event = False
self.after(100, lambda: set_block_update_dimensions_event_false())
def set_scaled_min_max(self):
if self.min_width is not None or self.min_height is not None:
super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
if self.max_width is not None or self.max_height is not None:
super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
def _set_scaled_min_max(self):
if self._min_width is not None or self._min_height is not None:
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
if self._max_width is not None or self._max_height is not None:
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def destroy(self):
AppearanceModeTracker.remove(self.set_appearance_mode)
ScalingTracker.remove_window(self.set_scaling, self)
self.disable_macos_dark_title_bar()
AppearanceModeTracker.remove(self._set_appearance_mode)
ScalingTracker.remove_window(self._set_scaling, self)
self._disable_macos_dark_title_bar()
super().destroy()
def withdraw(self):
if self.window_exists is False:
self.withdraw_called_before_window_exists = True
if self._window_exists is False:
self._withdraw_called_before_window_exists = True
super().withdraw()
def iconify(self):
if self.window_exists is False:
self.iconify_called_before_window_exists = True
if self._window_exists is False:
self._iconify_called_before_window_exists = True
super().iconify()
def update(self):
if self.window_exists is False:
self.window_exists = True
if self._window_exists is False:
self._window_exists = True
if sys.platform.startswith("win"):
if not self.withdraw_called_before_window_exists and not self.iconify_called_before_window_exists:
if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists:
# print("window dont exists -> deiconify in update")
self.deiconify()
super().update()
def mainloop(self, *args, **kwargs):
if not self.window_exists:
self.window_exists = True
if not self._window_exists:
self._window_exists = True
if sys.platform.startswith("win"):
if not self.withdraw_called_before_window_exists and not self.iconify_called_before_window_exists:
if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists:
# print("window dont exists -> deiconify in mainloop")
self.deiconify()
@ -141,42 +144,46 @@ class CTk(tkinter.Tk):
def resizable(self, *args, **kwargs):
super().resizable(*args, **kwargs)
self.last_resizable_args = (args, kwargs)
self._last_resizable_args = (args, kwargs)
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
if self._appearance_mode == 1:
self._windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self._windows_set_titlebar_color("light")
def minsize(self, width=None, height=None):
self.min_width = width
self.min_height = height
if self.current_width < width: self.current_width = width
if self.current_height < height: self.current_height = height
super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
self._min_width = width
self._min_height = height
if self._current_width < width:
self._current_width = width
if self._current_height < height:
self._current_height = height
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
def maxsize(self, width=None, height=None):
self.max_width = width
self.max_height = height
if self.current_width > width: self.current_width = width
if self.current_height > height: self.current_height = height
super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
self._max_width = width
self._max_height = height
if self._current_width > width:
self._current_width = width
if self._current_height > height:
self._current_height = height
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def geometry(self, geometry_string: str = None):
if geometry_string is not None:
super().geometry(self.apply_geometry_scaling(geometry_string))
super().geometry(self._apply_geometry_scaling(geometry_string))
# update width and height attributes
width, height, x, y = self.parse_geometry_string(geometry_string)
width, height, x, y = self._parse_geometry_string(geometry_string)
if width is not None and height is not None:
self.current_width = max(self.min_width, min(width, self.max_width)) # bound value between min and max
self.current_height = max(self.min_height, min(height, self.max_height))
self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max
self._current_height = max(self._min_height, min(height, self._max_height))
else:
return self.reverse_geometry_scaling(super().geometry())
@staticmethod
def parse_geometry_string(geometry_string: str) -> tuple:
def _parse_geometry_string(geometry_string: str) -> tuple:
# index: 1 2 3 4 5 6
# regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
@ -188,33 +195,33 @@ class CTk(tkinter.Tk):
return width, height, x, y
def apply_geometry_scaling(self, geometry_string: str) -> str:
width, height, x, y = self.parse_geometry_string(geometry_string)
def _apply_geometry_scaling(self, geometry_string: str) -> str:
width, height, x, y = self._parse_geometry_string(geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width * self.window_scaling)}x{round(height * self.window_scaling)}"
return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width * self.window_scaling)}x{round(height * self.window_scaling)}+{x}+{y}"
return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}+{x}+{y}"
def reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
width, height, x, y = self.parse_geometry_string(scaled_geometry_string)
def _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
width, height, x, y = self._parse_geometry_string(scaled_geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width / self.window_scaling)}x{round(height / self.window_scaling)}"
return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width / self.window_scaling)}x{round(height / self.window_scaling)}+{x}+{y}"
return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}+{x}+{y}"
def apply_window_scaling(self, value):
def _apply_window_scaling(self, value):
if isinstance(value, (int, float)):
return int(value * self.window_scaling)
return int(value * self._window_scaling)
else:
return value
@ -225,26 +232,25 @@ class CTk(tkinter.Tk):
bg_changed = False
if "bg" in kwargs:
self.fg_color = kwargs["bg"]
self._fg_color = kwargs["bg"]
bg_changed = True
kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
kwargs["bg"] = ThemeManager.single_color(self._fg_color, self._appearance_mode)
elif "background" in kwargs:
self.fg_color = kwargs["background"]
self._fg_color = kwargs["background"]
bg_changed = True
kwargs["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
kwargs["background"] = ThemeManager.single_color(self._fg_color, self._appearance_mode)
elif "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
del kwargs["fg_color"]
self._fg_color = kwargs.pop("fg_color")
kwargs["bg"] = ThemeManager.single_color(self._fg_color, self._appearance_mode)
bg_changed = True
elif len(args) > 0 and type(args[0]) == dict:
if "bg" in args[0]:
self.fg_color=args[0]["bg"]
self._fg_color = args[0]["bg"]
bg_changed = True
args[0]["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
elif "background" in args[0]:
self.fg_color=args[0]["background"]
self._fg_color = args[0]["background"]
bg_changed = True
args[0]["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
@ -252,13 +258,13 @@ class CTk(tkinter.Tk):
from ..widgets.widget_base_class import CTkBaseClass
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self.fg_color)
if isinstance(child, CTkBaseClass) and hasattr(child, "_fg_color"):
child.configure(bg_color=self._fg_color)
super().configure(*args, **kwargs)
@staticmethod
def enable_macos_dark_title_bar():
def _enable_macos_dark_title_bar():
if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
@ -266,14 +272,14 @@ class CTk(tkinter.Tk):
# This command allows dark-mode for all programs
@staticmethod
def disable_macos_dark_title_bar():
def _disable_macos_dark_title_bar():
if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
# This command reverts the dark-mode setting for all programs.
def windows_set_titlebar_color(self, color_mode: str):
def _windows_set_titlebar_color(self, color_mode: str):
"""
Set the titlebar color of the window to light or dark theme on Microsoft Windows.
@ -286,11 +292,11 @@ class CTk(tkinter.Tk):
if sys.platform.startswith("win") and not Settings.deactivate_windows_window_header_manipulation:
if self.window_exists:
self.state_before_windows_set_titlebar_color = self.state()
if self._window_exists:
self._state_before_windows_set_titlebar_color = self.state()
# print("window_exists -> state_before_windows_set_titlebar_color: ", self.state_before_windows_set_titlebar_color)
if self.state_before_windows_set_titlebar_color != "iconic" or self.state_before_windows_set_titlebar_color != "withdrawn":
if self._state_before_windows_set_titlebar_color != "iconic" or self._state_before_windows_set_titlebar_color != "withdrawn":
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
else:
# print("window dont exists -> withdraw and update")
@ -322,29 +328,29 @@ class CTk(tkinter.Tk):
except Exception as err:
print(err)
if self.window_exists:
if self._window_exists:
# print("window_exists -> return to original state: ", self.state_before_windows_set_titlebar_color)
if self.state_before_windows_set_titlebar_color == "normal":
if self._state_before_windows_set_titlebar_color == "normal":
self.deiconify()
elif self.state_before_windows_set_titlebar_color == "iconic":
elif self._state_before_windows_set_titlebar_color == "iconic":
self.iconify()
elif self.state_before_windows_set_titlebar_color == "zoomed":
elif self._state_before_windows_set_titlebar_color == "zoomed":
self.state("zoomed")
else:
self.state(self.state_before_windows_set_titlebar_color) # other states
self.state(self._state_before_windows_set_titlebar_color) # other states
else:
pass # wait for update or mainloop to be called
def set_appearance_mode(self, mode_string):
def _set_appearance_mode(self, mode_string):
if mode_string.lower() == "dark":
self.appearance_mode = 1
self._appearance_mode = 1
elif mode_string.lower() == "light":
self.appearance_mode = 0
self._appearance_mode = 0
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
if self._appearance_mode == 1:
self._windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self._windows_set_titlebar_color("light")
super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
super().configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode))

View File

@ -14,93 +14,96 @@ from ..scaling_tracker import ScalingTracker
class CTkToplevel(tkinter.Toplevel):
"""
Toplevel window with dark titlebar on Windows and macOS.
For detailed information check out the documentation.
"""
def __init__(self, *args,
fg_color="default_theme",
fg_color: Union[str, Tuple[str, str]] = "default_theme",
**kwargs):
self.enable_macos_dark_title_bar()
self._enable_macos_dark_title_bar()
super().__init__(*args, **kwargs)
self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
ScalingTracker.add_widget(self.set_scaling, self)
self.window_scaling = ScalingTracker.get_window_scaling(self)
ScalingTracker.add_widget(self._set_scaling, self)
self._window_scaling = ScalingTracker.get_window_scaling(self)
self.current_width = 200 # initial window size, always without scaling
self.current_height = 200
self.min_width: int = 0
self.min_height: int = 0
self.max_width: int = 1_000_000
self.max_height: int = 1_000_000
self.last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
self._current_width = 200 # initial window size, always without scaling
self._current_height = 200
self._min_width: int = 0
self._min_height: int = 0
self._max_width: int = 1_000_000
self._max_height: int = 1_000_000
self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
self.fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
self._fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
if "bg" in kwargs:
self.fg_color = kwargs["bg"]
del kwargs["bg"]
self._fg_color = kwargs.pop("bg")
elif "background" in kwargs:
self.fg_color = kwargs["background"]
del kwargs["background"]
self.fg_color = kwargs.pop("background")
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self.set_appearance_mode, self)
super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
AppearanceModeTracker.add(self._set_appearance_mode, self)
super().configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode))
super().title("CTkToplevel")
self.state_before_windows_set_titlebar_color = None
self.windows_set_titlebar_color_called = False # indicates if windows_set_titlebar_color was called, stays True until revert_withdraw_after_windows_set_titlebar_color is called
self.withdraw_called_after_windows_set_titlebar_color = False # indicates if withdraw() was called after windows_set_titlebar_color
self.iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color
self._state_before_windows_set_titlebar_color = None
self._windows_set_titlebar_color_called = False # indicates if windows_set_titlebar_color was called, stays True until revert_withdraw_after_windows_set_titlebar_color is called
self._withdraw_called_after_windows_set_titlebar_color = False # indicates if withdraw() was called after windows_set_titlebar_color
self._iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
if self._appearance_mode == 1:
self._windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self._windows_set_titlebar_color("light")
self.bind('<Configure>', self.update_dimensions_event)
self.bind('<Configure>', self._update_dimensions_event)
def update_dimensions_event(self, event=None):
def _update_dimensions_event(self, event=None):
detected_width = self.winfo_width() # detect current window size
detected_height = self.winfo_height()
if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling):
self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event
self.current_height = round(detected_height / self.window_scaling) # _current_width and _current_height are independent of the scale
if self._current_width != round(detected_width / self._window_scaling) or self._current_height != round(detected_height / self._window_scaling):
self._current_width = round(detected_width / self._window_scaling) # adjust current size according to new size given by event
self._current_height = round(detected_height / self._window_scaling) # _current_width and _current_height are independent of the scale
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self.window_scaling = new_window_scaling
def _set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self._window_scaling = new_window_scaling
# force new dimensions on window by using min, max, and geometry
super().minsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
super().maxsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
super().geometry(
f"{self.apply_window_scaling(self.current_width)}x" + f"{self.apply_window_scaling(self.current_height)}")
f"{self._apply_window_scaling(self._current_width)}x" + f"{self._apply_window_scaling(self._current_height)}")
# set new scaled min and max with 400ms delay (otherwise it won't work for some reason)
self.after(400, self.set_scaled_min_max)
self.after(400, self._set_scaled_min_max)
def set_scaled_min_max(self):
if self.min_width is not None or self.min_height is not None:
super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
if self.max_width is not None or self.max_height is not None:
super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
def _set_scaled_min_max(self):
if self._min_width is not None or self._min_height is not None:
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
if self._max_width is not None or self._max_height is not None:
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def geometry(self, geometry_string: str = None):
if geometry_string is not None:
super().geometry(self.apply_geometry_scaling(geometry_string))
super().geometry(self._apply_geometry_scaling(geometry_string))
# update width and height attributes
width, height, x, y = self.parse_geometry_string(geometry_string)
width, height, x, y = self._parse_geometry_string(geometry_string)
if width is not None and height is not None:
self.current_width = max(self.min_width, min(width, self.max_width)) # bound value between min and max
self.current_height = max(self.min_height, min(height, self.max_height))
self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max
self._current_height = max(self._min_height, min(height, self._max_height))
else:
return self.reverse_geometry_scaling(super().geometry())
return self._reverse_geometry_scaling(super().geometry())
@staticmethod
def parse_geometry_string(geometry_string: str) -> tuple:
def _parse_geometry_string(geometry_string: str) -> tuple:
# index: 1 2 3 4 5 6
# regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
@ -112,75 +115,79 @@ class CTkToplevel(tkinter.Toplevel):
return width, height, x, y
def apply_geometry_scaling(self, geometry_string: str) -> str:
width, height, x, y = self.parse_geometry_string(geometry_string)
def _apply_geometry_scaling(self, geometry_string: str) -> str:
width, height, x, y = self._parse_geometry_string(geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width * self.window_scaling)}x{round(height * self.window_scaling)}"
return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width * self.window_scaling)}x{round(height * self.window_scaling)}+{x}+{y}"
return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}+{x}+{y}"
def reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
width, height, x, y = self.parse_geometry_string(scaled_geometry_string)
def _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
width, height, x, y = self._parse_geometry_string(scaled_geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width / self.window_scaling)}x{round(height / self.window_scaling)}"
return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width / self.window_scaling)}x{round(height / self.window_scaling)}+{x}+{y}"
return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}+{x}+{y}"
def apply_window_scaling(self, value):
def _apply_window_scaling(self, value):
if isinstance(value, (int, float)):
return int(value * self.window_scaling)
return int(value * self._window_scaling)
else:
return value
def destroy(self):
AppearanceModeTracker.remove(self.set_appearance_mode)
ScalingTracker.remove_window(self.set_scaling, self)
self.disable_macos_dark_title_bar()
AppearanceModeTracker.remove(self._set_appearance_mode)
ScalingTracker.remove_window(self._set_scaling, self)
self._disable_macos_dark_title_bar()
super().destroy()
def withdraw(self):
if self.windows_set_titlebar_color_called:
self.withdraw_called_after_windows_set_titlebar_color = True
if self._windows_set_titlebar_color_called:
self._withdraw_called_after_windows_set_titlebar_color = True
super().withdraw()
def iconify(self):
if self.windows_set_titlebar_color_called:
self.iconify_called_after_windows_set_titlebar_color = True
if self._windows_set_titlebar_color_called:
self._iconify_called_after_windows_set_titlebar_color = True
super().iconify()
def resizable(self, *args, **kwargs):
super().resizable(*args, **kwargs)
self.last_resizable_args = (args, kwargs)
self._last_resizable_args = (args, kwargs)
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.after(10, lambda: self.windows_set_titlebar_color("dark"))
if self._appearance_mode == 1:
self.after(10, lambda: self._windows_set_titlebar_color("dark"))
else:
self.after(10, lambda: self.windows_set_titlebar_color("light"))
self.after(10, lambda: self._windows_set_titlebar_color("light"))
def minsize(self, width=None, height=None):
self.min_width = width
self.min_height = height
if self.current_width < width: self.current_width = width
if self.current_height < height: self.current_height = height
super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
self._min_width = width
self._min_height = height
if self._current_width < width:
self._current_width = width
if self._current_height < height:
self._current_height = height
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
def maxsize(self, width=None, height=None):
self.max_width = width
self.max_height = height
if self.current_width > width: self.current_width = width
if self.current_height > height: self.current_height = height
super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
self._max_width = width
self._max_height = height
if self._current_width > width:
self._current_width = width
if self._current_height > height:
self._current_height = height
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def config(self, *args, **kwargs):
self.configure(*args, **kwargs)
@ -189,54 +196,53 @@ class CTkToplevel(tkinter.Toplevel):
bg_changed = False
if "bg" in kwargs:
self.fg_color = kwargs["bg"]
self._fg_color = kwargs["bg"]
bg_changed = True
kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
kwargs["bg"] = ThemeManager.single_color(self._fg_color, self._appearance_mode)
elif "background" in kwargs:
self.fg_color = kwargs["background"]
self._fg_color = kwargs["background"]
bg_changed = True
kwargs["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
kwargs["background"] = ThemeManager.single_color(self._fg_color, self._appearance_mode)
elif "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
del kwargs["fg_color"]
self._fg_color = kwargs.pop("fg_color")
kwargs["bg"] = ThemeManager.single_color(self._fg_color, self._appearance_mode)
bg_changed = True
elif len(args) > 0 and type(args[0]) == dict:
if "bg" in args[0]:
self.fg_color=args[0]["bg"]
self._fg_color = args[0]["bg"]
bg_changed = True
args[0]["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
args[0]["bg"] = ThemeManager.single_color(self._fg_color, self._appearance_mode)
elif "background" in args[0]:
self.fg_color=args[0]["background"]
self._fg_color = args[0]["background"]
bg_changed = True
args[0]["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
args[0]["background"] = ThemeManager.single_color(self._fg_color, self._appearance_mode)
if bg_changed:
from ..widgets.widget_base_class import CTkBaseClass
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self.fg_color)
if isinstance(child, CTkBaseClass) and hasattr(child, "_fg_color"):
child.configure(bg_color=self._fg_color)
super().configure(*args, **kwargs)
@staticmethod
def enable_macos_dark_title_bar():
def _enable_macos_dark_title_bar():
if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
@staticmethod
def disable_macos_dark_title_bar():
def _disable_macos_dark_title_bar():
if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
# This command reverts the dark-mode setting for all programs.
def windows_set_titlebar_color(self, color_mode: str):
def _windows_set_titlebar_color(self, color_mode: str):
"""
Set the titlebar color of the window to light or dark theme on Microsoft Windows.
@ -249,7 +255,7 @@ class CTkToplevel(tkinter.Toplevel):
if sys.platform.startswith("win") and not Settings.deactivate_windows_window_header_manipulation:
self.state_before_windows_set_titlebar_color = self.state()
self._state_before_windows_set_titlebar_color = self.state()
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
super().update()
@ -277,41 +283,41 @@ class CTkToplevel(tkinter.Toplevel):
except Exception as err:
print(err)
self.windows_set_titlebar_color_called = True
self.after(5, self.revert_withdraw_after_windows_set_titlebar_color)
self._windows_set_titlebar_color_called = True
self.after(5, self._revert_withdraw_after_windows_set_titlebar_color)
def revert_withdraw_after_windows_set_titlebar_color(self):
def _revert_withdraw_after_windows_set_titlebar_color(self):
""" if in a short time (5ms) after """
if self.windows_set_titlebar_color_called:
if self._windows_set_titlebar_color_called:
if self.withdraw_called_after_windows_set_titlebar_color:
if self._withdraw_called_after_windows_set_titlebar_color:
pass # leave it withdrawed
elif self.iconify_called_after_windows_set_titlebar_color:
elif self._iconify_called_after_windows_set_titlebar_color:
super().iconify()
else:
if self.state_before_windows_set_titlebar_color == "normal":
if self._state_before_windows_set_titlebar_color == "normal":
self.deiconify()
elif self.state_before_windows_set_titlebar_color == "iconic":
elif self._state_before_windows_set_titlebar_color == "iconic":
self.iconify()
elif self.state_before_windows_set_titlebar_color == "zoomed":
elif self._state_before_windows_set_titlebar_color == "zoomed":
self.state("zoomed")
else:
self.state(self.state_before_windows_set_titlebar_color) # other states
self.state(self._state_before_windows_set_titlebar_color) # other states
self.windows_set_titlebar_color_called = False
self.withdraw_called_after_windows_set_titlebar_color = False
self.iconify_called_after_windows_set_titlebar_color = False
self._windows_set_titlebar_color_called = False
self._withdraw_called_after_windows_set_titlebar_color = False
self._iconify_called_after_windows_set_titlebar_color = False
def set_appearance_mode(self, mode_string):
def _set_appearance_mode(self, mode_string):
if mode_string.lower() == "dark":
self.appearance_mode = 1
self._appearance_mode = 1
elif mode_string.lower() == "light":
self.appearance_mode = 0
self._appearance_mode = 0
if sys.platform.startswith("win"):
if self.appearance_mode == 1:
self.windows_set_titlebar_color("dark")
if self._appearance_mode == 1:
self._windows_set_titlebar_color("dark")
else:
self.windows_set_titlebar_color("light")
self._windows_set_titlebar_color("light")
super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
super().configure(bg=ThemeManager.single_color(self._fg_color, self._appearance_mode))

View File

@ -140,6 +140,9 @@ class App(customtkinter.CTk):
def sidebar_button_callback(self):
print("sidebar_button click")
self.entry.delete(0, tkinter.END)
def on_closing(self, event=0):
self.destroy()