finished CTkSegmentedButton, created test_segmented_button.py

This commit is contained in:
Tom Schimansky
2022-10-08 01:50:09 +02:00
parent 1696016d54
commit 327957e97a
12 changed files with 571 additions and 33 deletions

View File

@ -29,6 +29,10 @@ class CTkButton(CTkBaseClass):
text_color: Union[str, Tuple[str, str]] = "default_theme",
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
background_corner_colors: Tuple[Union[str, Tuple[str, str]]] = None,
round_width_to_even_numbers: bool = True,
round_height_to_even_numbers: bool = True,
text: str = "CTkButton",
font: any = "default_theme",
textvariable: tkinter.Variable = None,
@ -46,12 +50,17 @@ class CTkButton(CTkBaseClass):
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 = ThemeManager.theme["color"]["text_button"] 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._background_corner_colors = background_corner_colors # rendering options for DrawEngine
# 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._round_width_to_even_numbers = round_width_to_even_numbers # rendering options for DrawEngine
self._round_height_to_even_numbers = round_height_to_even_numbers # rendering options for DrawEngine
self._corner_radius = min(self._corner_radius, round(self._current_height/2))
# text, font, image
self._image = image
@ -81,6 +90,7 @@ class CTkButton(CTkBaseClass):
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._draw_engine.set_round_to_even_numbers(self._round_width_to_even_numbers, self._round_height_to_even_numbers) # rendering options
# canvas event bindings
self._canvas.bind("<Enter>", self._on_enter)
@ -115,6 +125,16 @@ class CTkButton(CTkBaseClass):
self._draw()
def _draw(self, no_color_updates=False):
if self._background_corner_colors is not None:
self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height))
self._canvas.itemconfig("background_corner_top_left", fill=ThemeManager.single_color(self._background_corner_colors[0], self._appearance_mode))
self._canvas.itemconfig("background_corner_top_right", fill=ThemeManager.single_color(self._background_corner_colors[1], self._appearance_mode))
self._canvas.itemconfig("background_corner_bottom_right", fill=ThemeManager.single_color(self._background_corner_colors[2], self._appearance_mode))
self._canvas.itemconfig("background_corner_bottom_left", fill=ThemeManager.single_color(self._background_corner_colors[3], self._appearance_mode))
else:
self._canvas.delete("background_parts")
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),
@ -267,6 +287,10 @@ class CTkButton(CTkBaseClass):
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "compound" in kwargs:
self._compound = kwargs.pop("compound")
require_redraw = True
@ -301,6 +325,10 @@ class CTkButton(CTkBaseClass):
if "height" in kwargs:
self._set_dimensions(height=kwargs.pop("height"))
if "background_corner_colors" in kwargs:
self._background_corner_colors = kwargs.pop("background_corner_colors")
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
@ -319,6 +347,8 @@ class CTkButton(CTkBaseClass):
return self._text_color
elif attribute_name == "text_color_disabled":
return self._text_color_disabled
elif attribute_name == "background_corner_colors":
return self._background_corner_colors
elif attribute_name == "text":
return self._text

View File

@ -60,7 +60,7 @@ class CTkComboBox(CTkBaseClass):
# 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_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self._font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if font == "default_theme" else font
# callback and hover functionality

View File

@ -128,6 +128,12 @@ class CTkFrame(CTkBaseClass):
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self._fg_color)
# only workaround, to enable one layer of passing new bg_color for children with fg_color=None,
# but needs to be abstracted to n-layers somehow
if isinstance(child, CTkFrame) and child.cget("fg_color") is None:
for childrens_child in child.winfo_children():
childrens_child.configure(bg_color=self._fg_color)
if "border_color" in kwargs:
self._border_color = kwargs.pop("border_color")
require_redraw = True

View File

@ -70,7 +70,7 @@ class CTkLabel(CTkBaseClass):
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
self._text_label.grid(row=0, column=0, sticky=text_label_grid_sticky,
padx=min(self._apply_widget_scaling(self._corner_radius), round(self._current_height/2)))
padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height/2))))
self._check_kwargs_empty(kwargs, raise_error=True)
@ -85,7 +85,7 @@ class CTkLabel(CTkBaseClass):
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
self._text_label.grid(row=0, column=0, sticky=text_label_grid_sticky,
padx=min(self._apply_widget_scaling(self._corner_radius), round(self._current_height/2)))
padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height/2))))
self._draw()
@ -125,7 +125,7 @@ class CTkLabel(CTkBaseClass):
self._anchor = kwargs.pop("anchor")
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
self._text_label.grid(row=0, column=0, sticky=text_label_grid_sticky,
padx=min(self._apply_widget_scaling(self._corner_radius), round(self._current_height/2)))
padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height/2))))
if "text" in kwargs:
self._text = kwargs.pop("text")
@ -153,7 +153,7 @@ class CTkLabel(CTkBaseClass):
self._corner_radius = kwargs.pop("corner_radius")
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
self._text_label.grid(row=0, column=0, sticky=text_label_grid_sticky,
padx=min(self._apply_widget_scaling(self._corner_radius), round(self._current_height/2)))
padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height/2))))
require_redraw = True
self._text_label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes)) # configure tkinter.Label

View File

@ -54,7 +54,7 @@ class CTkOptionMenu(CTkBaseClass):
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 = ThemeManager.theme["color"]["text_button"] 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._font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if font == "default_theme" else font
self._dropdown_text_font = dropdown_text_font
@ -156,6 +156,12 @@ class CTkOptionMenu(CTkBaseClass):
height=self._apply_widget_scaling(self._desired_height))
self._draw()
def destroy(self):
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
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),

View File

@ -1,19 +1,332 @@
import tkinter
from typing import Union, Tuple
from typing import Union, Tuple, List, Dict
from .widget_base_class import CTkBaseClass
from ..theme_manager import ThemeManager
from .ctk_button import CTkButton
from .ctk_frame import CTkFrame
class CTkSegmentedButton(CTkBaseClass):
class CTkSegmentedButton(CTkFrame):
"""
Segmented button with corner radius, border width, variable support.
For detailed information check out the documentation.
"""
def __init__(self,
master: any = None,
width: int = 140,
height: int = 28,
corner_radius: Union[int, str] = "default_theme",
border_width: Union[int, str] = 3,
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
hover_color: Union[str, Tuple[str, str]] = "default_theme",
selected_color: Union[str, Tuple[str, str]] = "default_theme",
selected_hover_color: Union[str, Tuple[str, str]] = "default_theme",
unselected_color: Union[str, Tuple[str, str]] = "default_theme",
unselected_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",
values: list = None):
super().__init__(master=master, )
values: list = None,
variable: tkinter.Variable = None,
dynamic_resizing: bool = True,
**kwargs):
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
self._sb_fg_color = ThemeManager.theme["color"]["segmented_button"] if fg_color == "default_theme" else fg_color
self._sb_selected_color = ThemeManager.theme["color"]["button"] if selected_color == "default_theme" else selected_color
self._sb_selected_hover_color = ThemeManager.theme["color"]["button_hover"] if selected_hover_color == "default_theme" else selected_hover_color
self._sb_unselected_color = ThemeManager.theme["color"]["segmented_button_unselected"] if unselected_color == "default_theme" else unselected_color
self._sb_unselected_hover_color = ThemeManager.theme["color"]["segmented_button_unselected_hover"] if unselected_hover_color == "default_theme" else unselected_hover_color
self._sb_text_color = ThemeManager.theme["color"]["text_button"] if text_color == "default_theme" else text_color
self._sb_text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self._sb_corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
self._sb_border_width = ThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width
self._buttons_dict: Dict[str, CTkButton] = {} # mapped from value to button object
if values is None:
self._value_list: List[str] = ["CTkSegmentedButton"]
elif len(values) == 0:
raise ValueError("values of CTkSegmentedButton can not be empty")
else:
self._value_list: List[str] = values # Values ordered like buttons rendered on widget
self._dynamic_resizing = dynamic_resizing
if not self._dynamic_resizing:
self.grid_propagate(0)
self._check_unique_values(self._value_list)
self._current_value: str = ""
self._create_buttons_from_values()
self._create_button_grid()
self._variable = variable
self._variable_callback_blocked: bool = False
self._variable_callback_name: Union[str, None] = None
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.set(self._variable.get(), from_variable_callback=True)
super().configure(corner_radius=self._sb_corner_radius)
def destroy(self):
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self.set(self._variable.get(), from_variable_callback=True)
def _get_index_by_value(self, value: str):
for index, value_from_list in enumerate(self._value_list):
if value_from_list == value:
return index
raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
def _configure_button_corners_for_index(self, index: int):
if index == 0 and len(self._value_list) == 1:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
elif index == 0:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._sb_fg_color, self._sb_fg_color, self._bg_color))
elif index == len(self._value_list) - 1:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._bg_color, self._bg_color, self._sb_fg_color))
else:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._sb_fg_color, self._sb_fg_color, self._sb_fg_color))
def _unselect_button_by_value(self, value: str):
if value in self._buttons_dict:
self._buttons_dict[value].configure(fg_color=self._sb_unselected_color,
hover_color=self._sb_unselected_hover_color)
def _select_button_by_value(self, value: str):
if self._current_value is not None and self._current_value != "":
self._unselect_button_by_value(self._current_value)
self._current_value = value
self._buttons_dict[value].configure(fg_color=self._sb_selected_color,
hover_color=self._sb_selected_hover_color)
def _create_button(self, index: int, value: str) -> CTkButton:
new_button = CTkButton(self,
height=self._current_height,
width=0,
corner_radius=self._sb_corner_radius,
text=value,
border_width=self._sb_border_width,
border_color=self._sb_fg_color,
fg_color=self._sb_unselected_color,
hover_color=self._sb_unselected_hover_color,
text_color=self._sb_text_color,
text_color_disabled=self._sb_text_color_disabled,
command=lambda v=value: self.set(v),
background_corner_colors=None,
round_width_to_even_numbers=False) # DrawEngine rendering option (so that theres no gap between buttons)
return new_button
@staticmethod
def _check_unique_values(values: List[str]):
""" raises exception if values are not unique """
if len(values) != len(set(values)):
raise ValueError("CTkSegmentedButton values are not unique")
def _create_button_grid(self):
# remove minsize from every grid cell in the first row
number_of_columns, _ = self.grid_size()
for n in range(number_of_columns):
self.grid_columnconfigure(n, weight=1, minsize=0)
self.grid_rowconfigure(0, weight=1)
for index, value in enumerate(self._value_list):
self.grid_columnconfigure(index, weight=1, minsize=self._current_height)
self._buttons_dict[value].grid(row=0, column=index, sticky="ew")
def _create_buttons_from_values(self):
assert len(self._buttons_dict) == 0
assert len(self._value_list) > 0
for index, value in enumerate(self._value_list):
self._buttons_dict[value] = self._create_button(index, value)
self._configure_button_corners_for_index(index)
def configure(self, **kwargs):
if "height" in kwargs:
for button in self._buttons_dict.values():
button.configure(height=kwargs["height"])
if "bg_color" in kwargs:
super().configure(bg_color=kwargs.pop("bg_color"))
if len(self._buttons_dict) > 0:
self._configure_button_corners_for_index(0)
if len(self._buttons_dict) > 1:
max_index = len(self._buttons_dict) - 1
self._configure_button_corners_for_index(max_index)
if "fg_color" in kwargs:
self._sb_fg_color = kwargs.pop("fg_color")
for index, button in enumerate(self._buttons_dict.values()):
button.configure(border_color=self._sb_fg_color)
self._configure_button_corners_for_index(index)
if "selected_color" in kwargs:
self._sb_selected_color = kwargs.pop("selected_color")
if self._current_value in self._buttons_dict:
self._buttons_dict[self._current_value].configure(fg_color=self._sb_selected_color)
if "selected_hover_color" in kwargs:
self._sb_selected_hover_color = kwargs.pop("selected_hover_color")
if self._current_value in self._buttons_dict:
self._buttons_dict[self._current_value].configure(hover_color=self._sb_selected_hover_color)
if "unselected_color" in kwargs:
self._sb_unselected_color = kwargs.pop("unselected_color")
for value, button in self._buttons_dict.items():
if value != self._current_value:
button.configure(fg_color=self._sb_unselected_color)
if "unselected_hover_color" in kwargs:
self._sb_unselected_hover_color = kwargs.pop("unselected_hover_color")
for value, button in self._buttons_dict.items():
if value != self._current_value:
button.configure(hover_color=self._sb_unselected_hover_color)
if "text_color" in kwargs:
self._sb_text_color = kwargs.pop("text_color")
for button in self._buttons_dict.values():
button.configure(text_color=self._sb_text_color)
if "text_color_disabled" in kwargs:
self._sb_text_color_disabled = kwargs.pop("text_color_disabled")
for button in self._buttons_dict.values():
button.configure(text_color_disabled=self._sb_text_color_disabled)
if "values" in kwargs:
for button in self._buttons_dict.values():
button.destroy()
self._buttons_dict.clear()
self._value_list = kwargs.pop("values")
self._current_value = ""
if len(self._value_list) == 0:
raise ValueError("len() of values of CTkSegmentedButton can not be zero")
self._check_unique_values(self._value_list)
self._create_buttons_from_values()
self._create_button_grid()
if "variable" in kwargs:
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
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)
else:
self._variable = None
if "dynamic_resizing" in kwargs:
self._dynamic_resizing = kwargs.pop("dynamic_resizing")
if not self._dynamic_resizing:
self.grid_propagate(0)
else:
self.grid_propagate(1)
super().configure(**kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._sb_corner_radius
elif attribute_name == "border_width":
return self._sb_border_width
elif attribute_name == "fg_color":
return self._sb_fg_color
elif attribute_name == "selected_color":
return self._sb_selected_color
elif attribute_name == "selected_hover_color":
return self._sb_selected_hover_color
elif attribute_name == "unselected_color":
return self._sb_unselected_color
elif attribute_name == "unselected_hover_color":
return self._sb_unselected_hover_color
elif attribute_name == "text_color":
return self._sb_text_color
elif attribute_name == "text_color_disabled":
return self._sb_text_color_disabled
elif attribute_name == "values":
return self._value_list
elif attribute_name == "variable":
return self._variable
elif attribute_name == "dynamic_resizing":
return self._dynamic_resizing
else:
return super().cget(attribute_name)
def set(self, value: str, from_variable_callback: bool = False):
if value == self._current_value:
print("value == self._current_value")
elif value in self._buttons_dict:
self._select_button_by_value(value)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(value)
self._variable_callback_blocked = False
else:
if self._current_value in self._buttons_dict:
self._unselect_button_by_value(self._current_value)
self._current_value = value
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(value)
self._variable_callback_blocked = False
def get(self) -> str:
return self._current_value
def insert_value(self, index: int, value: str):
if value not in self._buttons_dict:
self._value_list.insert(index, value)
self._buttons_dict[value] = self._create_button(index, value)
self._configure_button_corners_for_index(index)
if index > 0:
self._configure_button_corners_for_index(index - 1)
if index < len(self._buttons_dict) - 1:
self._configure_button_corners_for_index(index + 1)
self._create_button_grid()
if value == self._current_value:
self._select_button_by_value(self._current_value)
else:
raise ValueError(f"CTkSegmentedButton can not insert value '{value}', already part of the values")
def remove_value(self, value: str):
if value in self._buttons_dict:
self._buttons_dict[value].destroy()
self._buttons_dict.pop(value)
index_to_remove = self._get_index_by_value(value)
self._value_list.pop(index_to_remove)
if index_to_remove <= len(self._buttons_dict) - 1:
self._configure_button_corners_for_index(index_to_remove)
self._create_button_grid()
else:
raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")

View File

@ -102,30 +102,78 @@ class CTkBaseClass(tkinter.Frame):
return False
def destroy(self):
""" Destroy this and all descendants widgets. """
AppearanceModeTracker.remove(self._set_appearance_mode)
super().destroy()
def place(self, **kwargs):
"""
Place a widget in the parent widget. Use as options:
in=master - master relative to which the widget is placed
in_=master - see 'in' option description
x=amount - locate anchor of this widget at position x of master
y=amount - locate anchor of this widget at position y of master
relx=amount - locate anchor of this widget between 0.0 and 1.0 relative to width of master (1.0 is right edge)
rely=amount - locate anchor of this widget between 0.0 and 1.0 relative to height of master (1.0 is bottom edge)
anchor=NSEW (or subset) - position anchor according to given direction
width=amount - width of this widget in pixel
height=amount - height of this widget in pixel
relwidth=amount - width of this widget between 0.0 and 1.0 relative to width of master (1.0 is the same width as the master)
relheight=amount - height of this widget between 0.0 and 1.0 relative to height of master (1.0 is the same height as the master)
bordermode="inside" or "outside" - whether to take border width of master widget into account
"""
self._last_geometry_manager_call = {"function": super().place, "kwargs": kwargs}
return super().place(**self._apply_argument_scaling(kwargs))
def place_forget(self):
""" Unmap this widget. """
self._last_geometry_manager_call = None
return super().place_forget()
def pack(self, **kwargs):
"""
Pack a widget in the parent widget. Use as options:
after=widget - pack it after you have packed widget
anchor=NSEW (or subset) - position widget according to given direction
before=widget - pack it before you will pack widget
expand=bool - expand widget if parent size grows
fill=NONE or X or Y or BOTH - fill widget if widget grows
in=master - use master to contain this widget
in_=master - see 'in' option description
ipadx=amount - add internal padding in x direction
ipady=amount - add internal padding in y direction
padx=amount - add padding in x direction
pady=amount - add padding in y direction
side=TOP or BOTTOM or LEFT or RIGHT - where to add this widget.
"""
self._last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs}
return super().pack(**self._apply_argument_scaling(kwargs))
def pack_forget(self):
""" Unmap this widget and do not use it for the packing order. """
self._last_geometry_manager_call = None
return super().pack_forget()
def grid(self, **kwargs):
"""
Position a widget in the parent widget in a grid. Use as options:
column=number - use cell identified with given column (starting with 0)
columnspan=number - this widget will span several columns
in=master - use master to contain this widget
in_=master - see 'in' option description
ipadx=amount - add internal padding in x direction
ipady=amount - add internal padding in y direction
padx=amount - add padding in x direction
pady=amount - add padding in y direction
row=number - use cell identified with given row (starting with 0)
rowspan=number - this widget will span several rows
sticky=NSEW - if cell is larger on which sides will this widget stick to the cell boundary
"""
self._last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs}
return super().grid(**self._apply_argument_scaling(kwargs))
def grid_forget(self):
""" Unmap this widget. """
self._last_geometry_manager_call = None
return super().grid_forget()
@ -158,7 +206,7 @@ class CTkBaseClass(tkinter.Frame):
raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.")
def configure(self, require_redraw=False, **kwargs):
""" basic configure with bg_color support, calls configure of tkinter.Frame, calls draw() in the end """
""" basic configure with bg_color support, calls configure of tkinter.Frame, updates in the end """
if "bg_color" in kwargs:
new_bg_color = kwargs.pop("bg_color")