diff --git a/.gitignore b/.gitignore
index 3b44e68a..1af685c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -164,3 +164,4 @@ cython_debug/
documentation_images/customtkinter_design.afdesign
images/
Readme_pypi.md
+temp.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d432f85..f5541e7a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,26 @@ ToDo:
- set icon (self.call("wm", "iconphoto", self._w, tkinter.PhotoImage(file="test_images/CustomTkinter_logo_single.png")))
- add option to change label position for checkbox, switch, radiobutton #628
+## [5.3.0] - 2026-01-21
+### Added
+ - Showroom App, immediately available with the library installation
+ - Gold theme
+ - set(), index(), len() methods for those widgets that were suitable to use them
+ - Attribute to CTkSegmentedButton to make it vertically instead of horizontally
+ - Attribute to CTkTabview to configure the font for its CTkSegmentedButton
+ - Possibility to add a border to CTkLabel
+ - Mouse Wheel detection to CTkSlider and improved it on CTkScrollbar
+
+### Changed
+ - CTkButton triggers the command when the Mouse Button is released
+ - CTkEntry and CTkTextbox lose focus when you click somewhere else
+ - Clicking the Dropdown button again closes the menu for CTkComboBox and CTkOptionMenu
+ - Improved/fixed configure() and cget() methods for all widgets
+ - Improved Tab renaming for CTkTabview
+ - Improved drag behavior for CTkScrollbar
+ - Properly managed borders for CTkScrollableFrame
+ - Fixed a bug that prevented setting a custom Icon for CTkToplevel
+ - Fixed many bugs related to missing invocations or wrong names
## [5.2.0] - 2022-05-02
### Added
diff --git a/Readme.md b/Readme.md
index d42ad15e..d798596c 100644
--- a/Readme.md
+++ b/Readme.md
@@ -6,27 +6,33 @@
-
+




-
-
+
---
-
+| Current Developers | Contact |
+|----------------------------------------------|-|
+| Tom Schimansky | contact@customtkinter.tomschimansky.com |
+| Federico Spada | www.linkedin.com/in/federicospada13 |
+

-
-| Massive Thanks to all the People who Donated to help this Project 😇 |
-|----------------------------------------------|
+
-
+---
+
+
+
+
Official website: https://customtkinter.tomschimansky.com/
-
+
+
CustomTkinter is a python UI-library based on Tkinter, which provides new, modern and
@@ -88,6 +94,11 @@ can find more example programs and in the [Documentation](https://github.com/Tom
you can find further information on the appearance mode, scaling, themes and all widgets.
## More Examples and Showcase
+You can run the following code to show a simple App that displays all available widgets:
+```python
+import customtkinter as ctk
+ctk.run_showroom()
+```
### Appearance mode change and scaling change
diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py
index d9144ceb..91642edf 100644
--- a/customtkinter/__init__.py
+++ b/customtkinter/__init__.py
@@ -1,5 +1,6 @@
-__version__ = "5.2.2"
+__version__ = "5.3.0"
+from typing import Optional
import os
import sys
from tkinter import Variable, StringVar, IntVar, DoubleVar, BooleanVar
@@ -86,3 +87,228 @@ def deactivate_automatic_dpi_awareness():
def set_ctk_parent_class(ctk_parent_class):
ctk_tk.CTK_PARENT_CLASS = ctk_parent_class
+
+
+def run_showroom() -> None:
+ set_appearance_mode("Light")
+ set_default_color_theme("blue")
+
+ new_instance: bool = True
+ while new_instance:
+ app = _Showroom()
+ app.mainloop()
+ new_instance = app.new_instance_requested
+
+
+class _Showroom(CTk):
+ SPACING = 20
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ # configure window
+ self.title("CustomTkinter Showroom")
+
+ self.new_instance_requested: bool = False
+
+ # create sidebar frame with widgets
+ self.sidebar_frame = CTkFrame(self, width=140, corner_radius=0)
+ self.logo_label = CTkLabel(self.sidebar_frame, text="CustomTkinter", font=CTkFont(size=20, weight="bold"))
+ self.theme_label = CTkLabel(self.sidebar_frame, text="Theme:", anchor="w")
+ self.theme_optionmenu = CTkOptionMenu(self.sidebar_frame, values=ThemeManager._built_in_themes,
+ command=self._change_theme_event)
+ self.theme_optionmenu.set(ThemeManager._currently_loaded_theme)
+ self.appearance_mode_label = CTkLabel(self.sidebar_frame, text="Appearance Mode:", anchor="w")
+ self.appearance_mode_optionemenu = CTkOptionMenu(self.sidebar_frame, values=["Light", "Dark", "System"],
+ command=self._change_appearance_mode_event)
+ self.appearance_mode_optionemenu.set(get_appearance_mode())
+ self.scaling_label = CTkLabel(self.sidebar_frame, text="UI Scaling:", anchor="w")
+ self.scaling_optionmenu = CTkOptionMenu(self.sidebar_frame, values=["80%", "90%", "100%", "110%", "120%"],
+ command=self._change_scaling_event)
+ widget_scaling = round(ScalingTracker.widget_scaling*100)
+ self.scaling_optionmenu.set(f"{widget_scaling}%")
+ self.drawing_label = CTkLabel(self.sidebar_frame, text="Drawing method:", anchor="w")
+ self.drawing_optionmenu = CTkOptionMenu(self.sidebar_frame, values=DrawEngine.DRAWING_METHODS,
+ command=self._change_drawing_event)
+ self.drawing_optionmenu.set(DrawEngine.preferred_drawing_method)
+
+ self.sidebar_frame.pack(side="left", fill="y")
+ self.logo_label.pack(side="top", fill="x", padx=5, pady=5)
+ self.theme_label.pack(side="top", fill="x", padx=20, pady=(20, 5))
+ self.theme_optionmenu.pack(side="top", fill="x", padx=20, pady=(0, 10))
+ self.appearance_mode_label.pack(side="top", fill="x", padx=20, pady=(20, 5))
+ self.appearance_mode_optionemenu.pack(side="top", fill="x", padx=20, pady=(0, 10))
+ self.scaling_label.pack(side="top", fill="x", padx=20, pady=(20, 5))
+ self.scaling_optionmenu.pack(side="top", fill="x", padx=20, pady=(0, 10))
+ self.drawing_label.pack(side="top", fill="x", padx=20, pady=(20, 5))
+ self.drawing_optionmenu.pack(side="top", fill="x", padx=20, pady=(0, 10))
+
+ # create main tabview
+ self.main_tabview = CTkTabview(self)
+
+ self.main_tabview.pack(side="left", fill="both", expand=True, padx=5, pady=(0, 5))
+
+ # buttons
+ self.buttons_frame = self.main_tabview.add("Buttons")
+
+ self.button_1 = CTkButton(self.buttons_frame)
+ self.button_2 = CTkButton(self.buttons_frame, hover=False, text="No Hover")
+ self.button_3 = CTkButton(self.buttons_frame, state="disabled", text="disabled")
+
+ self.button_1.pack(padx=20, pady=(self.SPACING, 5))
+ self.button_2.pack(padx=20, pady=(0, 5))
+ self.button_3.pack(padx=20, pady=(0, 5))
+
+ # choices
+ self.choices_frame = self.main_tabview.add("Choices")
+ self.combobox_1 = CTkComboBox(self.choices_frame,
+ values=["CTkComboBox", "Value 2", "Value 3", "User can also", "write any text"])
+ self.combobox_1.set("CTkComboBox")
+ self.combobox_2 = CTkComboBox(self.choices_frame, state="readonly",
+ values=["readonly", "Value 2", "Value 3", "User can only", "choose a value"])
+ self.combobox_2.set("readonly")
+ self.optionmenu = CTkOptionMenu(self.choices_frame, dynamic_resizing=False,
+ values=["CTkOptionMenu", "Value 2", "Value 3"])
+ self.seg_button1 = CTkSegmentedButton(self.choices_frame, values=["CTkSegmentedButton", "Value 2", "Value 3"])
+ self.seg_button1.set("CTkSegmentedButton")
+ self.seg_button2 = CTkSegmentedButton(self.choices_frame, values=["vertical", "Value 2", "Value 3"], orientation="vertical")
+ self.seg_button2.set("vertical")
+
+ self.combobox_1.pack(padx=20, pady=(self.SPACING, 5))
+ self.combobox_2.pack(padx=20, pady=(0, 5))
+ self.optionmenu.pack(padx=20, pady=(self.SPACING, 5))
+ self.seg_button1.pack(padx=20, pady=(self.SPACING, 5))
+ self.seg_button2.pack(padx=20, pady=(0, 5))
+
+ # text
+ self.text_frame = self.main_tabview.add("Text")
+ self.label = CTkLabel(self.text_frame, text="CTkLabel", height=1)
+ self.entry = CTkEntry(self.text_frame, placeholder_text="CTkEntry")
+ self.textbox = CTkTextbox(self.text_frame, width=320)
+ self.textbox.insert("0.0", "CTkTextbox\n\n" + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20)
+
+ self.label.pack(padx=20, pady=(self.SPACING, 5))
+ self.entry.pack(padx=20, pady=(self.SPACING, 5))
+ self.textbox.pack(padx=20, pady=(self.SPACING, 5))
+
+ # boolean
+ self.boolean_frame = self.main_tabview.add("Boolean")
+ self.radio_var = IntVar(value=0)
+ self.radio_button_1 = CTkRadioButton(self.boolean_frame, variable=self.radio_var, value=0, width=130)
+ self.radio_button_2 = CTkRadioButton(self.boolean_frame, variable=self.radio_var, value=1, hover=False, text="No Hover", width=130)
+ self.radio_button_3 = CTkRadioButton(self.boolean_frame, variable=self.radio_var, value=2, state="disabled", text="Disabled", width=130)
+ self.checkbox_var = BooleanVar(value=True)
+ self.checkbox_1 = CTkCheckBox(self.boolean_frame, variable=self.checkbox_var, width=130)
+ self.checkbox_2 = CTkCheckBox(self.boolean_frame, hover=False, text="No Hover", width=130)
+ self.checkbox_3 = CTkCheckBox(self.boolean_frame, state="disabled", text="Disabled", width=130)
+ self.switch_var = BooleanVar(value=True)
+ self.switch_1 = CTkSwitch(self.boolean_frame, variable=self.switch_var, width=130)
+ self.switch_2 = CTkSwitch(self.boolean_frame, hover=False, text="No Hover", width=130)
+ self.switch_3 = CTkSwitch(self.boolean_frame, state="disabled", text="Disabled", width=130)
+
+ self.radio_button_1.pack(padx=20, pady=(self.SPACING, 5))
+ self.radio_button_2.pack(padx=20, pady=(0, 5))
+ self.radio_button_3.pack(padx=20, pady=(0, 5))
+ self.checkbox_1.pack(padx=20, pady=(self.SPACING, 5))
+ self.checkbox_2.pack(padx=20, pady=(0, 5))
+ self.checkbox_3.pack(padx=20, pady=(0, 5))
+ self.switch_1.pack(padx=20, pady=(self.SPACING, 5))
+ self.switch_2.pack(padx=20, pady=(0, 5))
+ self.switch_3.pack(padx=20, pady=(0, 5))
+
+ # bars
+ self.bars_frame = self.main_tabview.add("Bars")
+ self.label_progbar_1 = CTkLabel(self.bars_frame, text="CTkProgressBar - determinate", height=1)
+ self.progressbar_1 = CTkProgressBar(self.bars_frame, mode="determinate", determinate_speed=0.5)
+ self.label_progbar_2 = CTkLabel(self.bars_frame, text="CTkProgressBar - indeterminate", height=1)
+ self.progressbar_2 = CTkProgressBar(self.bars_frame, mode="indeterminate", indeterminate_speed=0.5)
+ self.label_slider_1 = CTkLabel(self.bars_frame, text="CTkSlider - with steps", height=1)
+ self.slider_1 = CTkSlider(self.bars_frame, from_=0, to=1, number_of_steps=4)
+ self.label_slider_2 = CTkLabel(self.bars_frame, text="CTkSlider - continuous", height=1)
+ self.slider_2 = CTkSlider(self.bars_frame, from_=10, to=100)
+ self.label_scrollbar_1 = CTkLabel(self.bars_frame, text="CTkScrollbar", height=1)
+ self.scrollbar_1 = CTkScrollbar(self.bars_frame, orientation="horizontal")
+ self.scrollbar_1.set(0, 0.3)
+
+ self.label_vertical = CTkLabel(self.bars_frame, text="vertical", height=1)
+ self.frame_vertical = CTkFrame(self.bars_frame)
+ self.progressbar_3 = CTkProgressBar(self.frame_vertical, orientation="vertical")
+ self.slider_3 = CTkSlider(self.frame_vertical, orientation="vertical")
+ self.scrollbar_2 = CTkScrollbar(self.frame_vertical, orientation="vertical")
+ self.scrollbar_2.set(0, 0.3)
+
+ self.progressbar_1.start()
+ self.progressbar_2.start()
+ self.slider_3.configure(command = self.progressbar_3.set)
+
+ self.label_progbar_1.pack(padx=20, pady=(self.SPACING, 5))
+ self.progressbar_1.pack(padx=20, pady=(0, 5))
+ self.label_progbar_2.pack(padx=20, pady=(0, 5))
+ self.progressbar_2.pack(padx=20, pady=(0, 5))
+ self.label_slider_1.pack(padx=20, pady=(self.SPACING, 5))
+ self.slider_1.pack(padx=20, pady=(0, 5))
+ self.label_slider_2.pack(padx=20, pady=(0, 5))
+ self.slider_2.pack(padx=20, pady=(0, 5))
+ self.label_scrollbar_1.pack(padx=20, pady=(self.SPACING, 5))
+ self.scrollbar_1.pack(padx=20, pady=(0, 5))
+
+ self.label_vertical.pack(padx=20, pady=(self.SPACING, 5))
+ self.frame_vertical.pack(padx=20, pady=(0, 5))
+ self.progressbar_3.pack(side="left", padx=20)
+ self.slider_3.pack(side="left", padx=20)
+ self.scrollbar_2.pack(side="left", padx=20)
+
+ # frames
+ self.frames_frame = self.main_tabview.add("Frames")
+ self.scrollable_frame = CTkScrollableFrame(self.frames_frame, label_text="CTkScrollableFrame",
+ fg_color=ThemeManager.theme["CTk"]["fg_color"])
+ self.tabview = CTkTabview(self.frames_frame, fg_color=ThemeManager.theme["CTk"]["fg_color"])
+ tab1 = self.tabview.add("CTkTabview")
+ tab2 = self.tabview.add("Tab 2")
+ tab3 = self.tabview.add("Tab 3")
+ CTkButton(tab1, text="Widget on 1st Tab").pack()
+ CTkCheckBox(tab2, text="Widget on 2nd Tab").pack()
+ CTkSwitch(tab3, text="Widget on 3rd Tab").pack()
+
+ for i in range(100):
+ switch = CTkSwitch(self.scrollable_frame, text=f"CTkSwitch {i+1}")
+ switch.pack(padx=20, pady=5)
+
+ self.scrollable_frame.pack(side=LEFT, padx=20, pady=(self.SPACING, 5))
+ self.tabview.pack(side=LEFT, padx=20, pady=(self.SPACING, 5))
+
+ # windows
+ self.windows_frame = self.main_tabview.add("Windows")
+ self.open_toplevel = CTkButton(self.windows_frame, text="Open CTkToplevel", command=self._open_ctktoplevel_event)
+ self.open_dialog = CTkButton(self.windows_frame, text="Open CTkInputDialog", command=self._open_input_dialog_event)
+
+ self.open_toplevel.pack(padx=20, pady=(self.SPACING, 5))
+ self.open_dialog.pack(padx=20, pady=(self.SPACING, 5))
+
+
+ def _open_ctktoplevel_event(self) -> None:
+ toplevel = CTkToplevel(self)
+ toplevel.geometry(f"{500}x{250}")
+ toplevel.resizable(True, True)
+ toplevel.title("CTkToplevel")
+
+ def _open_input_dialog_event(self) -> None:
+ dialog = CTkInputDialog(title="CTkInputDialog", text="Description of requested input")
+ dialog.get_input()
+
+ def _change_appearance_mode_event(self, new_appearance_mode: str) -> None:
+ set_appearance_mode(new_appearance_mode)
+
+ def _change_scaling_event(self, new_scaling: str) -> None:
+ new_scaling_float = int(new_scaling.replace("%", "")) / 100
+ set_widget_scaling(new_scaling_float)
+
+ def _change_theme_event(self, new_theme: str) -> None:
+ set_default_color_theme(new_theme)
+ self.new_instance_requested = True
+ self.destroy()
+
+ def _change_drawing_event(self, new_drawing_method: str) -> None:
+ DrawEngine.preferred_drawing_method = new_drawing_method
+ self.new_instance_requested = True
+ self.destroy()
diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json
index 192c1362..938557d9 100644
--- a/customtkinter/assets/themes/blue.json
+++ b/customtkinter/assets/themes/blue.json
@@ -23,7 +23,9 @@
},
"CTkLabel": {
"corner_radius": 0,
+ "border_width": 0,
"fg_color": "transparent",
+ "border_color": ["#979DA2", "#565B5E"],
"text_color": ["gray10", "#DCE4EE"]
},
"CTkEntry": {
@@ -31,7 +33,7 @@
"border_width": 2,
"fg_color": ["#F9F9FA", "#343638"],
"border_color": ["#979DA2", "#565B5E"],
- "text_color":["gray10", "#DCE4EE"],
+ "text_color": ["gray10", "#DCE4EE"],
"placeholder_text_color": ["gray52", "gray62"]
},
"CTkCheckBox": {
@@ -39,7 +41,7 @@
"border_width": 3,
"fg_color": ["#3B8ED0", "#1F6AA5"],
"border_color": ["#3E454A", "#949A9F"],
- "hover_color": ["#3B8ED0", "#1F6AA5"],
+ "hover_color": ["#36719F", "#144870"],
"checkmark_color": ["#DCE4EE", "gray90"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray60", "gray45"]
@@ -123,7 +125,7 @@
"border_width": 0,
"fg_color": ["#F9F9FA", "#1D1E1E"],
"border_color": ["#979DA2", "#565B5E"],
- "text_color":["gray10", "#DCE4EE"],
+ "text_color": ["gray10", "#DCE4EE"],
"scrollbar_button_color": ["gray55", "gray41"],
"scrollbar_button_hover_color": ["gray40", "gray53"]
},
diff --git a/customtkinter/assets/themes/dark-blue.json b/customtkinter/assets/themes/dark-blue.json
index 54ff211d..5d1547c2 100644
--- a/customtkinter/assets/themes/dark-blue.json
+++ b/customtkinter/assets/themes/dark-blue.json
@@ -15,15 +15,17 @@
"CTkButton": {
"corner_radius": 6,
"border_width": 0,
- "fg_color": ["#3a7ebf", "#1f538d"],
- "hover_color": ["#325882", "#14375e"],
+ "fg_color": ["#3A7EBF", "#1F538D"],
+ "hover_color": ["#325882", "#14375E"],
"border_color": ["#3E454A", "#949A9F"],
"text_color": ["#DCE4EE", "#DCE4EE"],
"text_color_disabled": ["gray74", "gray60"]
},
"CTkLabel": {
"corner_radius": 0,
+ "border_width": 0,
"fg_color": "transparent",
+ "border_color": ["#979DA2", "#565B5E"],
"text_color": ["gray14", "gray84"]
},
"CTkEntry": {
@@ -37,9 +39,9 @@
"CTkCheckBox": {
"corner_radius": 6,
"border_width": 3,
- "fg_color": ["#3a7ebf", "#1f538d"],
+ "fg_color": ["#3A7EBF", "#1F538D"],
"border_color": ["#3E454A", "#949A9F"],
- "hover_color": ["#325882", "#14375e"],
+ "hover_color": ["#325882", "#14375E"],
"checkmark_color": ["#DCE4EE", "gray90"],
"text_color": ["gray14", "gray84"],
"text_color_disabled": ["gray60", "gray45"]
@@ -49,7 +51,7 @@
"border_width": 3,
"button_length": 0,
"fg_color": ["#939BA2", "#4A4D50"],
- "progress_color": ["#3a7ebf", "#1f538d"],
+ "progress_color": ["#3A7EBF", "#1F538D"],
"button_color": ["gray36", "#D5D9DE"],
"button_hover_color": ["gray20", "gray100"],
"text_color": ["gray14", "gray84"],
@@ -59,9 +61,9 @@
"corner_radius": 1000,
"border_width_checked": 6,
"border_width_unchecked": 3,
- "fg_color": ["#3a7ebf", "#1f538d"],
+ "fg_color": ["#3A7EBF", "#1F538D"],
"border_color": ["#3E454A", "#949A9F"],
- "hover_color": ["#325882", "#14375e"],
+ "hover_color": ["#325882", "#14375E"],
"text_color": ["gray14", "gray84"],
"text_color_disabled": ["gray60", "gray45"]
},
@@ -69,7 +71,7 @@
"corner_radius": 1000,
"border_width": 0,
"fg_color": ["#939BA2", "#4A4D50"],
- "progress_color": ["#3a7ebf", "#1f538d"],
+ "progress_color": ["#3A7EBF", "#1F538D"],
"border_color": ["gray", "gray"]
},
"CTkSlider": {
@@ -79,14 +81,14 @@
"button_length": 0,
"fg_color": ["#939BA2", "#4A4D50"],
"progress_color": ["gray40", "#AAB0B5"],
- "button_color": ["#3a7ebf", "#1f538d"],
- "button_hover_color": ["#325882", "#14375e"]
+ "button_color": ["#3A7EBF", "#1F538D"],
+ "button_hover_color": ["#325882", "#14375E"]
},
"CTkOptionMenu": {
"corner_radius": 6,
- "fg_color": ["#3a7ebf", "#1f538d"],
- "button_color": ["#325882", "#14375e"],
- "button_hover_color": ["#234567", "#1e2c40"],
+ "fg_color": ["#3A7EBF", "#1F538D"],
+ "button_color": ["#325882", "#14375E"],
+ "button_hover_color": ["#234567", "#1E2C40"],
"text_color": ["#DCE4EE", "#DCE4EE"],
"text_color_disabled": ["gray74", "gray60"]
},
@@ -111,8 +113,8 @@
"corner_radius": 6,
"border_width": 2,
"fg_color": ["#979DA2", "gray29"],
- "selected_color": ["#3a7ebf", "#1f538d"],
- "selected_hover_color": ["#325882", "#14375e"],
+ "selected_color": ["#3A7EBF", "#1F538D"],
+ "selected_hover_color": ["#325882", "#14375E"],
"unselected_color": ["#979DA2", "gray29"],
"unselected_hover_color": ["gray70", "gray41"],
"text_color": ["#DCE4EE", "#DCE4EE"],
diff --git a/customtkinter/assets/themes/gold.json b/customtkinter/assets/themes/gold.json
new file mode 100644
index 00000000..4d9d8ecf
--- /dev/null
+++ b/customtkinter/assets/themes/gold.json
@@ -0,0 +1,157 @@
+{
+ "CTk": {
+ "fg_color": ["gray92", "gray14"]
+ },
+ "CTkToplevel": {
+ "fg_color": ["gray92", "gray14"]
+ },
+ "CTkFrame": {
+ "corner_radius": 6,
+ "border_width": 0,
+ "fg_color": ["gray86", "gray17"],
+ "top_fg_color": ["gray81", "gray20"],
+ "border_color": ["gray65", "gray28"]
+ },
+ "CTkButton": {
+ "corner_radius": 6,
+ "border_width": 0,
+ "fg_color": ["#EFB951", "#ECA607"],
+ "hover_color": ["#DBA220", "#B57F05"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "text_color": ["gray98", "#DCE4EE"],
+ "text_color_disabled": ["gray78", "gray68"]
+ },
+ "CTkLabel": {
+ "corner_radius": 0,
+ "border_width": 0,
+ "fg_color": "transparent",
+ "border_color": ["#979DA2", "#565B5E"],
+ "text_color": ["gray10", "#DCE4EE"]
+ },
+ "CTkEntry": {
+ "corner_radius": 6,
+ "border_width": 2,
+ "fg_color": ["#F9F9FA", "#343638"],
+ "border_color": ["#979DA2", "#565B5E"],
+ "text_color": ["gray10", "#DCE4EE"],
+ "placeholder_text_color": ["gray52", "gray62"]
+ },
+ "CTkCheckBox": {
+ "corner_radius": 6,
+ "border_width": 3,
+ "fg_color": ["#EFB951", "#ECA607"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "hover_color": ["#DBA220", "#B57F05"],
+ "checkmark_color": ["#DCE4EE", "gray90"],
+ "text_color": ["gray10", "#DCE4EE"],
+ "text_color_disabled": ["gray60", "gray45"]
+ },
+ "CTkSwitch": {
+ "corner_radius": 1000,
+ "border_width": 3,
+ "button_length": 0,
+ "fg_color": ["#939BA2", "#4A4D50"],
+ "progress_color": ["#EFB951", "#ECA607"],
+ "button_color": ["gray36", "#D5D9DE"],
+ "button_hover_color": ["gray20", "gray100"],
+ "text_color": ["gray10", "#DCE4EE"],
+ "text_color_disabled": ["gray60", "gray45"]
+ },
+ "CTkRadioButton": {
+ "corner_radius": 1000,
+ "border_width_checked": 6,
+ "border_width_unchecked": 3,
+ "fg_color": ["#EFB951", "#ECA607"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "hover_color": ["#DBA220", "#B57F05"],
+ "text_color": ["gray10", "#DCE4EE"],
+ "text_color_disabled": ["gray60", "gray45"]
+ },
+ "CTkProgressBar": {
+ "corner_radius": 1000,
+ "border_width": 0,
+ "fg_color": ["#939BA2", "#4A4D50"],
+ "progress_color": ["#EFB951", "#ECA607"],
+ "border_color": ["gray", "gray"]
+ },
+ "CTkSlider": {
+ "corner_radius": 1000,
+ "button_corner_radius": 1000,
+ "border_width": 6,
+ "button_length": 0,
+ "fg_color": ["#939BA2", "#4A4D50"],
+ "progress_color": ["gray40", "#AAB0B5"],
+ "button_color": ["#EFB951", "#ECA607"],
+ "button_hover_color": ["#DBA220", "#B57F05"]
+ },
+ "CTkOptionMenu": {
+ "corner_radius": 6,
+ "fg_color": ["#EFB951", "#ECA607"],
+ "button_color": ["#DBA220", "#B57F05"],
+ "button_hover_color": ["#BC8416", "#896F1C"],
+ "text_color": ["gray98", "#DCE4EE"],
+ "text_color_disabled": ["gray78", "gray68"]
+ },
+ "CTkComboBox": {
+ "corner_radius": 6,
+ "border_width": 2,
+ "fg_color": ["#F9F9FA", "#343638"],
+ "border_color": ["#979DA2", "#565B5E"],
+ "button_color": ["#979DA2", "#565B5E"],
+ "button_hover_color": ["#6E7174", "#7A848D"],
+ "text_color": ["gray10", "#DCE4EE"],
+ "text_color_disabled": ["gray50", "gray45"]
+ },
+ "CTkScrollbar": {
+ "corner_radius": 1000,
+ "border_spacing": 4,
+ "fg_color": "transparent",
+ "button_color": ["gray55", "gray41"],
+ "button_hover_color": ["gray40", "gray53"]
+ },
+ "CTkSegmentedButton": {
+ "corner_radius": 6,
+ "border_width": 2,
+ "fg_color": ["#979DA2", "gray29"],
+ "selected_color": ["#EFB951", "#ECA607"],
+ "selected_hover_color": ["#DBA220", "#B57F05"],
+ "unselected_color": ["#979DA2", "gray29"],
+ "unselected_hover_color": ["gray70", "gray41"],
+ "text_color": ["gray98", "#DCE4EE"],
+ "text_color_disabled": ["gray78", "gray68"]
+ },
+ "CTkTextbox": {
+ "corner_radius": 6,
+ "border_width": 0,
+ "fg_color": ["#F9F9FA", "#1D1E1E"],
+ "border_color": ["#979DA2", "#565B5E"],
+ "text_color": ["gray10", "#DCE4EE"],
+ "scrollbar_button_color": ["gray55", "gray41"],
+ "scrollbar_button_hover_color": ["gray40", "gray53"]
+ },
+ "CTkScrollableFrame": {
+ "label_fg_color": ["gray78", "gray23"]
+ },
+ "DropdownMenu": {
+ "fg_color": ["gray90", "gray20"],
+ "hover_color": ["gray75", "gray28"],
+ "text_color": ["gray10", "gray90"]
+ },
+ "CTkFont": {
+ "macOS": {
+ "family": "SF Display",
+ "size": 13,
+ "weight": "normal"
+ },
+ "Windows": {
+ "family": "Roboto",
+ "size": 13,
+ "weight": "normal"
+ },
+ "Linux": {
+ "family": "Roboto",
+ "size": 13,
+ "weight": "normal"
+ }
+ }
+}
diff --git a/customtkinter/assets/themes/green.json b/customtkinter/assets/themes/green.json
index 200012f7..174747c7 100644
--- a/customtkinter/assets/themes/green.json
+++ b/customtkinter/assets/themes/green.json
@@ -23,7 +23,9 @@
},
"CTkLabel": {
"corner_radius": 0,
+ "border_width": 0,
"fg_color": "transparent",
+ "border_color": ["#979DA2", "#565B5E"],
"text_color": ["gray10", "#DCE4EE"]
},
"CTkEntry": {
@@ -31,7 +33,7 @@
"border_width": 2,
"fg_color": ["#F9F9FA", "#343638"],
"border_color": ["#979DA2", "#565B5E"],
- "text_color":["gray10", "#DCE4EE"],
+ "text_color": ["gray10", "#DCE4EE"],
"placeholder_text_color": ["gray52", "gray62"]
},
"CTkCheckBox": {
@@ -61,7 +63,7 @@
"border_width_unchecked": 3,
"fg_color": ["#2CC985", "#2FA572"],
"border_color": ["#3E454A", "#949A9F"],
- "hover_color":["#0C955A", "#106A43"],
+ "hover_color": ["#0C955A", "#106A43"],
"text_color": ["gray10", "#DCE4EE"],
"text_color_disabled": ["gray60", "gray45"]
},
@@ -84,9 +86,9 @@
},
"CTkOptionMenu": {
"corner_radius": 6,
- "fg_color": ["#2cbe79", "#2FA572"],
+ "fg_color": ["#2CBE79", "#2FA572"],
"button_color": ["#0C955A", "#106A43"],
- "button_hover_color": ["#0b6e3d", "#17472e"],
+ "button_hover_color": ["#0B6E3D", "#17472E"],
"text_color": ["gray98", "#DCE4EE"],
"text_color_disabled": ["gray78", "gray68"]
},
@@ -123,7 +125,7 @@
"border_width": 0,
"fg_color": ["#F9F9FA", "gray23"],
"border_color": ["#979DA2", "#565B5E"],
- "text_color":["gray10", "#DCE4EE"],
+ "text_color": ["gray10", "#DCE4EE"],
"scrollbar_button_color": ["gray55", "gray41"],
"scrollbar_button_hover_color": ["gray40", "gray53"]
},
diff --git a/customtkinter/windows/ctk_input_dialog.py b/customtkinter/windows/ctk_input_dialog.py
index 6c4669ae..efae43f6 100644
--- a/customtkinter/windows/ctk_input_dialog.py
+++ b/customtkinter/windows/ctk_input_dialog.py
@@ -31,7 +31,7 @@ def __init__(self,
super().__init__(fg_color=fg_color)
self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
- self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color)
+ self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color)
self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color)
diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py
index e137dc3c..b8a313a6 100644
--- a/customtkinter/windows/ctk_tk.py
+++ b/customtkinter/windows/ctk_tk.py
@@ -79,6 +79,11 @@ def __init__(self,
self.bind('', self._update_dimensions_event)
self.bind('', self._focus_in_event)
+ #allows CTkEntry and CTkTextbox to lose focus
+ def set_focus(event: tkinter.Event):
+ if hasattr(event.widget, "focus_set"):
+ event.widget.focus_set()
+ self.bind_all("", set_focus, add=True)
def destroy(self):
self._disable_macos_dark_title_bar()
@@ -220,6 +225,7 @@ def configure(self, **kwargs):
def cget(self, attribute_name: str) -> any:
if attribute_name == "fg_color":
return self._fg_color
+
else:
return super().cget(attribute_name)
diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py
index 9780380b..f87bec00 100644
--- a/customtkinter/windows/ctk_toplevel.py
+++ b/customtkinter/windows/ctk_toplevel.py
@@ -38,14 +38,6 @@ def __init__(self, *args,
CTkScalingBaseClass.__init__(self, scaling_type="window")
check_kwargs_empty(kwargs, raise_error=True)
- try:
- # Set Windows titlebar icon
- if sys.platform.startswith("win"):
- customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- self.after(200, lambda: self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico")))
- except Exception:
- pass
-
self._current_width = 200 # initial window size, always without scaling
self._current_height = 200
self._min_width: int = 0
@@ -63,7 +55,7 @@ def __init__(self, *args,
super().title("CTkToplevel")
# indicator variables
- self._iconbitmap_method_called = True
+ self._iconbitmap_method_called = False
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
@@ -83,6 +75,11 @@ def __init__(self, *args,
self.bind('', self._update_dimensions_event)
self.bind('', self._focus_in_event)
+ #allows CTkEntry and CTkTextbox to lose focus
+ def set_focus(event: tkinter.Event):
+ if hasattr(event.widget, "focus_set"):
+ event.widget.focus_set()
+ self.bind_all("", set_focus, add=True)
def destroy(self):
self._disable_macos_dark_title_bar()
@@ -196,6 +193,7 @@ def configure(self, **kwargs):
def cget(self, attribute_name: str) -> any:
if attribute_name == "fg_color":
return self._fg_color
+
else:
return super().cget(attribute_name)
@@ -203,6 +201,10 @@ def wm_iconbitmap(self, bitmap=None, default=None):
self._iconbitmap_method_called = True
super().wm_iconbitmap(bitmap, default)
+ def iconbitmap(self, bitmap=None, default=None):
+ self._iconbitmap_method_called = True
+ super().wm_iconbitmap(bitmap, default)
+
def _windows_set_titlebar_icon(self):
try:
# if not the user already called iconbitmap method, set icon
diff --git a/customtkinter/windows/widgets/core_rendering/ctk_canvas.py b/customtkinter/windows/widgets/core_rendering/ctk_canvas.py
index f291e2cb..30e85e55 100644
--- a/customtkinter/windows/widgets/core_rendering/ctk_canvas.py
+++ b/customtkinter/windows/widgets/core_rendering/ctk_canvas.py
@@ -1,6 +1,6 @@
import tkinter
import sys
-from typing import Union, Tuple
+from typing import Union, Tuple, List
class CTkCanvas(tkinter.Canvas):
@@ -80,23 +80,25 @@ def create_aa_circle(self, x_pos: int, y_pos: int, radius: int, angle: int = 0,
return circle_1
- def coords(self, tag_or_id, *args):
+ def coords(self, tag_or_id, *args) -> List[float]:
if type(tag_or_id) == str and "ctk_aa_circle_font_element" in self.gettags(tag_or_id):
coords_id = self.find_withtag(tag_or_id)[0] # take the lowest id for the given tag
- super().coords(coords_id, *args[:2])
+ coords = 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]))
elif type(tag_or_id) == int and tag_or_id in self._aa_circle_canvas_ids:
- super().coords(tag_or_id, *args[:2])
+ coords = 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]))
else:
- super().coords(tag_or_id, *args)
+ coords = super().coords(tag_or_id, *args)
+
+ return coords
def itemconfig(self, tag_or_id, *args, **kwargs):
kwargs_except_outline = kwargs.copy()
diff --git a/customtkinter/windows/widgets/core_rendering/draw_engine.py b/customtkinter/windows/widgets/core_rendering/draw_engine.py
index 5acea560..894cc6fc 100644
--- a/customtkinter/windows/widgets/core_rendering/draw_engine.py
+++ b/customtkinter/windows/widgets/core_rendering/draw_engine.py
@@ -26,7 +26,8 @@ class DrawEngine:
"""
- preferred_drawing_method: str = None # 'polygon_shapes', 'font_shapes', 'circle_shapes'
+ DRAWING_METHODS: list[str] = ["polygon_shapes", "font_shapes", "circle_shapes"]
+ preferred_drawing_method: str = None
def __init__(self, canvas: CTkCanvas):
self._canvas = canvas
@@ -651,12 +652,12 @@ def __draw_rounded_rect_with_border_vertical_split_font_shapes(self, width: int,
self._canvas.delete("inner_corner_part") # delete inner corner parts if not needed
# create canvas inner rectangle parts if not already created
- if not self._canvas.find_withtag("inner_rectangle_1"):
+ if not self._canvas.find_withtag("inner_rectangle_left_1"):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_left_1", "inner_rectangle_part", "inner_parts_left", "inner_parts", "left_parts"), width=0)
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_right_1", "inner_rectangle_part", "inner_parts_right", "inner_parts", "right_parts"), width=0)
requires_recoloring = True
- if not self._canvas.find_withtag("inner_rectangle_2") and inner_corner_radius * 2 < height - (border_width * 2):
+ if not self._canvas.find_withtag("inner_rectangle_left_2") and inner_corner_radius * 2 < height - (border_width * 2):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_left_2", "inner_rectangle_part", "inner_parts_left", "inner_parts", "left_parts"), width=0)
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_right_2", "inner_rectangle_part", "inner_parts_right", "inner_parts", "right_parts"), width=0)
requires_recoloring = True
diff --git a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py
index afd9431d..4177c8a7 100644
--- a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py
+++ b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py
@@ -171,7 +171,7 @@ def _check_font_type(self, font: any):
def _check_image_type(self, image: any):
""" check image type when passed to widget """
- if image is None:
+ if image is None or image == "":
return image
elif isinstance(image, CTkImage):
return image
diff --git a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py
index a6b8186f..7c4648b9 100644
--- a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py
+++ b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py
@@ -1,4 +1,5 @@
import tkinter
+import copy
import sys
from typing import Union, Tuple, Callable, List, Optional
@@ -50,6 +51,7 @@ def destroy(self):
# call destroy methods of super classes
tkinter.Menu.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
+ CTkScalingBaseClass.destroy(self)
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling """
@@ -117,7 +119,17 @@ def open(self, x: Union[int, float], y: Union[int, float]):
else: # Linux
self.tk_popup(int(x), int(y))
+ def close(self):
+ self.unpost()
+
+ def is_open(self) -> bool:
+ return bool(self.winfo_viewable())
+
def configure(self, **kwargs):
+ if "min_character_width" in kwargs:
+ self._min_character_width = kwargs.pop("min_character_width")
+ self._add_menu_commands()
+
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
super().configure(bg=self._apply_appearance_mode(self._fg_color))
@@ -136,7 +148,6 @@ def configure(self, **kwargs):
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
-
self._update_font()
if "command" in kwargs:
@@ -164,7 +175,7 @@ def cget(self, attribute_name: str) -> any:
elif attribute_name == "command":
return self._command
elif attribute_name == "values":
- return self._values
+ return copy.copy(self._values)
else:
return super().cget(attribute_name)
diff --git a/customtkinter/windows/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py
index e9f78391..578025c6 100644
--- a/customtkinter/windows/widgets/ctk_button.py
+++ b/customtkinter/windows/widgets/ctk_button.py
@@ -90,6 +90,7 @@ def __init__(self,
self._compound: str = compound
self._anchor: str = anchor
self._click_animation_running: bool = False
+ self._mouse_inside: bool = False
# canvas and draw engine
self._canvas = CTkCanvas(master=self,
@@ -124,13 +125,13 @@ def _create_bindings(self, sequence: Optional[str] = None):
if self._image_label is not None:
self._image_label.bind("", self._on_leave)
- if sequence is None or sequence == "":
- self._canvas.bind("", self._clicked)
+ if sequence is None or sequence == "":
+ self._canvas.bind("", self._on_release)
if self._text_label is not None:
- self._text_label.bind("", self._clicked)
+ self._text_label.bind("", self._on_release)
if self._image_label is not None:
- self._image_label.bind("", self._clicked)
+ self._image_label.bind("", self._on_release)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
@@ -224,6 +225,7 @@ def _draw(self, no_color_updates=False):
self._text_label = tkinter.Label(master=self,
font=self._apply_font_scaling(self._font),
text=self._text,
+ anchor=self._anchor,
padx=0,
pady=0,
borderwidth=1,
@@ -232,8 +234,8 @@ def _draw(self, no_color_updates=False):
self._text_label.bind("", self._on_enter)
self._text_label.bind("", self._on_leave)
- self._text_label.bind("", self._clicked)
- self._text_label.bind("", self._clicked)
+ self._text_label.bind("", self._on_release)
+ self._text_label.bind("", self._on_release)
if no_color_updates is False:
# set text_label fg color (text color)
@@ -260,14 +262,14 @@ def _draw(self, no_color_updates=False):
if self._image is not None:
if self._image_label is None:
- self._image_label = tkinter.Label(master=self)
+ self._image_label = tkinter.Label(master=self, anchor=self._anchor)
self._update_image() # set image
self._create_grid()
self._image_label.bind("", self._on_enter)
self._image_label.bind("", self._on_leave)
- self._image_label.bind("", self._clicked)
- self._image_label.bind("", self._clicked)
+ self._image_label.bind("", self._on_release)
+ self._image_label.bind("", self._on_release)
if no_color_updates is False:
# set image_label bg color (background color of label)
@@ -314,7 +316,7 @@ def _create_grid(self):
if self._image_label is not None and self._text_label is not None:
self.grid_columnconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._image_label_spacing))
else:
- self.grid_columnconfigure(2, weight=0)
+ self.grid_columnconfigure(2, weight=0, minsize=0)
self.grid_rowconfigure((1, 3), weight=0)
self.grid_columnconfigure((1, 3), weight=1)
@@ -323,7 +325,7 @@ def _create_grid(self):
if self._image_label is not None and self._text_label is not None:
self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._image_label_spacing))
else:
- self.grid_rowconfigure(2, weight=0)
+ self.grid_rowconfigure(2, weight=0, minsize=0)
self.grid_columnconfigure((1, 3), weight=0)
self.grid_rowconfigure((1, 3), weight=1)
@@ -402,7 +404,6 @@ def configure(self, require_redraw=False, **kwargs):
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
-
self._update_font()
if "textvariable" in kwargs:
@@ -416,7 +417,10 @@ def configure(self, require_redraw=False, **kwargs):
self._image = self._check_image_type(kwargs.pop("image"))
if isinstance(self._image, CTkImage):
self._image.add_configure_callback(self._update_image)
- self._update_image()
+ if self._image_label is not None:
+ self._update_image()
+ else:
+ require_redraw = True
if "state" in kwargs:
self._state = kwargs.pop("state")
@@ -436,6 +440,10 @@ def configure(self, require_redraw=False, **kwargs):
if "anchor" in kwargs:
self._anchor = kwargs.pop("anchor")
+ if self._text_label is not None:
+ self._text_label.configure(anchor=self._anchor)
+ if self._image_label is not None:
+ self._image_label.configure(anchor=self._anchor)
self._create_grid()
require_redraw = True
@@ -480,6 +488,7 @@ def cget(self, attribute_name: str) -> any:
return self._compound
elif attribute_name == "anchor":
return self._anchor
+
else:
return super().cget(attribute_name)
@@ -498,6 +507,7 @@ def _set_cursor(self):
self.configure(cursor="hand2")
def _on_enter(self, event=None):
+ self._mouse_inside = True
if self._hover is True and self._state == "normal":
if self._hover_color is None:
inner_parts_color = self._fg_color
@@ -518,6 +528,7 @@ def _on_enter(self, event=None):
self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
def _on_leave(self, event=None):
+ self._mouse_inside = False
self._click_animation_running = False
if self._fg_color == "transparent":
@@ -542,9 +553,8 @@ def _click_animation(self):
if self._click_animation_running:
self._on_enter()
- def _clicked(self, event=None):
- if self._state != tkinter.DISABLED:
-
+ def _on_release(self, event=None):
+ if self._mouse_inside and 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
diff --git a/customtkinter/windows/widgets/ctk_checkbox.py b/customtkinter/windows/widgets/ctk_checkbox.py
index 42f04f5e..33cd3a7f 100644
--- a/customtkinter/windows/widgets/ctk_checkbox.py
+++ b/customtkinter/windows/widgets/ctk_checkbox.py
@@ -221,13 +221,7 @@ def _draw(self, no_color_updates=False):
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
def configure(self, require_redraw=False, **kwargs):
- if "corner_radius" in kwargs:
- 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
+ require_new_state = False
if "checkbox_width" in kwargs:
self._checkbox_width = kwargs.pop("checkbox_width")
@@ -239,22 +233,12 @@ def configure(self, require_redraw=False, **kwargs):
self._canvas.configure(height=self._apply_widget_scaling(self._checkbox_height))
require_redraw = True
- if "text" in kwargs:
- self._text = kwargs.pop("text")
- self._text_label.configure(text=self._text)
-
- if "font" in kwargs:
- if isinstance(self._font, CTkFont):
- self._font.remove_size_configure_callback(self._update_font)
- self._font = self._check_font_type(kwargs.pop("font"))
- if isinstance(self._font, CTkFont):
- self._font.add_size_configure_callback(self._update_font)
-
- self._update_font()
+ if "corner_radius" in kwargs:
+ self._corner_radius = kwargs.pop("corner_radius")
+ require_redraw = True
- if "state" in kwargs:
- self._state = kwargs.pop("state")
- self._set_cursor()
+ if "border_width" in kwargs:
+ self._border_width = kwargs.pop("border_width")
require_redraw = True
if "fg_color" in kwargs:
@@ -281,38 +265,63 @@ def configure(self, require_redraw=False, **kwargs):
self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
require_redraw = True
+ if "text" in kwargs:
+ self._text = kwargs.pop("text")
+ self._text_label.configure(text=self._text)
+
+ if "font" in kwargs:
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+ self._font = self._check_font_type(kwargs.pop("font"))
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+ self._update_font()
+
+ if "textvariable" in kwargs:
+ self._textvariable = kwargs.pop("textvariable")
+ self._text_label.configure(textvariable=self._textvariable)
+
+ if "state" in kwargs:
+ self._state = kwargs.pop("state")
+ self._set_cursor()
+ require_redraw = True
+
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "command" in kwargs:
self._command = kwargs.pop("command")
- if "textvariable" in kwargs:
- self._textvariable = kwargs.pop("textvariable")
- self._text_label.configure(textvariable=self._textvariable)
+ if "onvalue" in kwargs:
+ self._onvalue = kwargs.pop("onvalue")
+ require_new_state = True
+
+ if "offvalue" in kwargs:
+ self._offvalue = kwargs.pop("offvalue")
+ require_new_state = True
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
-
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
- require_redraw = True
+ require_new_state = True
+ if require_new_state and self._variable is not None and self._variable != "":
+ self._check_state = True if self._variable.get() == self._onvalue else False
+ require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
- if attribute_name == "corner_radius":
- return self._corner_radius
- elif attribute_name == "border_width":
- return self._border_width
- elif attribute_name == "checkbox_width":
+ if attribute_name == "checkbox_width":
return self._checkbox_width
elif attribute_name == "checkbox_height":
return self._checkbox_height
+ elif attribute_name == "corner_radius":
+ return self._corner_radius
+ elif attribute_name == "border_width":
+ return self._border_width
elif attribute_name == "fg_color":
return self._fg_color
@@ -337,12 +346,15 @@ def cget(self, attribute_name: str) -> any:
return self._state
elif attribute_name == "hover":
return self._hover
+ elif attribute_name == "command":
+ return self._command
elif attribute_name == "onvalue":
return self._onvalue
elif attribute_name == "offvalue":
return self._offvalue
elif attribute_name == "variable":
return self._variable
+
else:
return super().cget(attribute_name)
@@ -404,41 +416,28 @@ def _variable_callback(self, var_name, index, mode):
self.select(from_variable_callback=True)
elif self._variable.get() == self._offvalue:
self.deselect(from_variable_callback=True)
+
+ def set(self, state: bool, from_variable_callback=False):
+ self._check_state = state
+ self._draw()
+
+ if self._variable is not None and not from_variable_callback:
+ self._variable_callback_blocked = True
+ self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
+ self._variable_callback_blocked = False
def toggle(self, event=0):
if self._state == tkinter.NORMAL:
- if self._check_state is True:
- self._check_state = False
- self._draw()
- else:
- 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
+ self.set(not self._check_state)
if self._command is not None:
self._command()
def select(self, from_variable_callback=False):
- 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
+ self.set(True, from_variable_callback)
def deselect(self, from_variable_callback=False):
- 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
+ self.set(False, from_variable_callback)
def get(self) -> Union[int, str]:
return self._onvalue if self._check_state is True else self._offvalue
diff --git a/customtkinter/windows/widgets/ctk_combobox.py b/customtkinter/windows/widgets/ctk_combobox.py
index 99495641..823a1c4b 100644
--- a/customtkinter/windows/widgets/ctk_combobox.py
+++ b/customtkinter/windows/widgets/ctk_combobox.py
@@ -83,6 +83,7 @@ def __init__(self,
hover_color=dropdown_hover_color,
text_color=dropdown_text_color,
font=dropdown_font)
+ self._close_on_next_click: bool = False
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
@@ -217,6 +218,7 @@ def _draw(self, no_color_updates=False):
def _open_dropdown_menu(self):
self._dropdown_menu.open(self.winfo_rootx(),
self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0))
+ self._close_on_next_click = True
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
@@ -267,7 +269,6 @@ def configure(self, require_redraw=False, **kwargs):
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
-
self._update_font()
if "dropdown_font" in kwargs:
@@ -338,10 +339,12 @@ def cget(self, attribute_name: str) -> any:
return self._command
elif attribute_name == "justify":
return self._entry.cget("justify")
+
else:
return super().cget(attribute_name)
def _on_enter(self, event=0):
+ self._close_on_next_click = self._dropdown_menu.is_open()
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 self._cursor_manipulation_enabled:
self._canvas.configure(cursor="pointinghand")
@@ -396,8 +399,19 @@ def set(self, value: str):
def get(self) -> str:
return self._entry.get()
+ def index(self, value: Optional[Any] = None) -> int:
+ """ returns index of selected value, raises ValueError if the value is missing
+ if the parameter is provided, returns the associated index or raises ValueError if no value is found """
+ if value is None:
+ return self._values.index(self.get())
+ else:
+ return self._values.index(value)
+
def _clicked(self, event=None):
- if self._state is not tkinter.DISABLED and len(self._values) > 0:
+ if self._close_on_next_click:
+ self._dropdown_menu.close()
+ self._close_on_next_click = False
+ elif self._state is not tkinter.DISABLED and len(self._values) > 0:
self._open_dropdown_menu()
def bind(self, sequence=None, command=None, add=True):
diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py
index cdc0220b..f2c9f749 100644
--- a/customtkinter/windows/widgets/ctk_entry.py
+++ b/customtkinter/windows/widgets/ctk_entry.py
@@ -72,7 +72,7 @@ def __init__(self,
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
- if not (self._textvariable is None or self._textvariable == ""):
+ if self._textvariable is not None and self._textvariable != "":
self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback)
self._canvas = CTkCanvas(master=self,
@@ -145,6 +145,9 @@ def _update_font(self):
self._canvas.grid(column=0, row=0, sticky="nswe")
def destroy(self):
+ if self._textvariable is not None:
+ self._textvariable.trace_remove("write", self._textvariable_callback_name)
+
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
@@ -192,36 +195,40 @@ def _draw(self, no_color_updates=False):
insertbackground=self._apply_appearance_mode(self._text_color))
def configure(self, require_redraw=False, **kwargs):
- if "state" in kwargs:
- self._state = kwargs.pop("state")
- self._entry.configure(state=self._state)
-
- if "fg_color" in kwargs:
- self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
+ if "corner_radius" in kwargs:
+ self._corner_radius = kwargs.pop("corner_radius")
+ self._create_grid()
require_redraw = True
- if "text_color" in kwargs:
- self._text_color = self._check_color_type(kwargs.pop("text_color"))
+ if "border_width" in kwargs:
+ self._border_width = kwargs.pop("border_width")
+ self._create_grid()
require_redraw = True
- if "placeholder_text_color" in kwargs:
- self._placeholder_text_color = self._check_color_type(kwargs.pop("placeholder_text_color"))
+ if "fg_color" in kwargs:
+ self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
- if "border_width" in kwargs:
- self._border_width = kwargs.pop("border_width")
- self._create_grid()
+ if "text_color" in kwargs:
+ self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
- if "corner_radius" in kwargs:
- self._corner_radius = kwargs.pop("corner_radius")
- self._create_grid()
+ if "placeholder_text_color" in kwargs:
+ self._placeholder_text_color = self._check_color_type(kwargs.pop("placeholder_text_color"))
require_redraw = True
+ if "textvariable" in kwargs:
+ if self._textvariable is not None and self._textvariable != "":
+ self._textvariable.trace_remove("write", self._textvariable_callback_name) # remove old variable callback
+ self._textvariable = kwargs.pop("textvariable")
+ self._entry.configure(textvariable=self._textvariable)
+ if self._textvariable is not None and self._textvariable != "":
+ self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback)
+
if "placeholder_text" in kwargs:
self._placeholder_text = kwargs.pop("placeholder_text")
if self._placeholder_text_active:
@@ -230,19 +237,18 @@ def configure(self, require_redraw=False, **kwargs):
else:
self._activate_placeholder()
- if "textvariable" in kwargs:
- self._textvariable = kwargs.pop("textvariable")
- self._entry.configure(textvariable=self._textvariable)
-
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
-
self._update_font()
+ if "state" in kwargs:
+ self._state = kwargs.pop("state")
+ self._entry.configure(state=self._state)
+
if "show" in kwargs:
if self._placeholder_text_active:
self._pre_placeholder_arguments["show"] = kwargs.pop("show") # remember show argument for when placeholder gets deactivated
@@ -335,6 +341,10 @@ def insert(self, index, string):
return self._entry.insert(index, string)
+ def set(self, string: str):
+ self._entry.delete(0, tkinter.END)
+ self.insert(0, string)
+
def get(self):
if self._placeholder_text_active:
return ""
diff --git a/customtkinter/windows/widgets/ctk_frame.py b/customtkinter/windows/widgets/ctk_frame.py
index 7bddf3c2..c1367adb 100644
--- a/customtkinter/windows/widgets/ctk_frame.py
+++ b/customtkinter/windows/widgets/ctk_frame.py
@@ -132,6 +132,14 @@ def _draw(self, no_color_updates=False):
# self._canvas.tag_lower("border_parts")
def configure(self, require_redraw=False, **kwargs):
+ if "corner_radius" in kwargs:
+ 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 "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
@@ -156,14 +164,6 @@ def configure(self, require_redraw=False, **kwargs):
self._background_corner_colors = kwargs.pop("background_corner_colors")
require_redraw = True
- if "corner_radius" in kwargs:
- 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
-
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
diff --git a/customtkinter/windows/widgets/ctk_label.py b/customtkinter/windows/widgets/ctk_label.py
index 7e59ad76..ca303231 100644
--- a/customtkinter/windows/widgets/ctk_label.py
+++ b/customtkinter/windows/widgets/ctk_label.py
@@ -27,9 +27,11 @@ def __init__(self,
width: int = 0,
height: int = 28,
corner_radius: Optional[int] = None,
+ border_width: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ border_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
@@ -46,11 +48,12 @@ def __init__(self,
# color
self._fg_color = ThemeManager.theme["CTkLabel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
+ self._border_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkLabel"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(text_color)
if text_color_disabled is None:
if "text_color_disabled" in ThemeManager.theme["CTkLabel"]:
- self._text_color_disabled = ThemeManager.theme["CTkLabel"]["text_color"]
+ self._text_color_disabled = ThemeManager.theme["CTkLabel"]["text_color_disabled"]
else:
self._text_color_disabled = self._text_color
else:
@@ -58,6 +61,7 @@ def __init__(self,
# shape
self._corner_radius = ThemeManager.theme["CTkLabel"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_width: int = ThemeManager.theme["CTkLabel"]["border_width"] if border_width is None else border_width
# text
self._anchor = anchor
@@ -161,38 +165,54 @@ 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)
+ self._apply_widget_scaling(self._border_width))
if no_color_updates is False or requires_recoloring:
- if self._apply_appearance_mode(self._fg_color) == "transparent":
+
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ # set color for the button border parts (outline)
+ self._canvas.itemconfig("border_parts",
+ outline=self._apply_appearance_mode(self._border_color),
+ fill=self._apply_appearance_mode(self._border_color))
+
+ # set color for inner parts
+ if self._fg_color == "transparent":
self._canvas.itemconfig("inner_parts",
- fill=self._apply_appearance_mode(self._bg_color),
- outline=self._apply_appearance_mode(self._bg_color))
+ outline=self._apply_appearance_mode(self._bg_color),
+ fill=self._apply_appearance_mode(self._bg_color))
self._label.configure(fg=self._apply_appearance_mode(self._text_color),
disabledforeground=self._apply_appearance_mode(self._text_color_disabled),
bg=self._apply_appearance_mode(self._bg_color))
else:
self._canvas.itemconfig("inner_parts",
- fill=self._apply_appearance_mode(self._fg_color),
- outline=self._apply_appearance_mode(self._fg_color))
+ outline=self._apply_appearance_mode(self._fg_color),
+ fill=self._apply_appearance_mode(self._fg_color))
self._label.configure(fg=self._apply_appearance_mode(self._text_color),
disabledforeground=self._apply_appearance_mode(self._text_color_disabled),
bg=self._apply_appearance_mode(self._fg_color))
- self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
-
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
self._create_grid()
require_redraw = True
+ if "border_width" in kwargs:
+ self._border_width = kwargs.pop("border_width")
+ self._create_grid()
+ require_redraw = True
+
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
+ if "border_color" in kwargs:
+ self._border_color = self._check_color_type(kwargs.pop("border_color"))
+ require_redraw = True
+
if "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
@@ -240,9 +260,13 @@ def configure(self, require_redraw=False, **kwargs):
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
+ elif attribute_name == "border_width":
+ return self._border_width
elif attribute_name == "fg_color":
return self._fg_color
+ elif attribute_name == "border_color":
+ return self._border_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text_color_disabled":
diff --git a/customtkinter/windows/widgets/ctk_optionmenu.py b/customtkinter/windows/widgets/ctk_optionmenu.py
index 491027b1..0fbd38bb 100644
--- a/customtkinter/windows/widgets/ctk_optionmenu.py
+++ b/customtkinter/windows/widgets/ctk_optionmenu.py
@@ -90,6 +90,7 @@ def __init__(self,
hover_color=dropdown_hover_color,
text_color=dropdown_text_color,
font=dropdown_font)
+ self._close_on_next_click: bool = False
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
@@ -262,7 +263,6 @@ def configure(self, require_redraw=False, **kwargs):
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
-
self._update_font()
if "dropdown_font" in kwargs:
@@ -275,15 +275,11 @@ def configure(self, require_redraw=False, **kwargs):
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._current_value = self._variable.get()
self._text_label.configure(text=self._current_value)
- else:
- self._variable = None
if "state" in kwargs:
self._state = kwargs.pop("state")
@@ -353,8 +349,10 @@ def cget(self, attribute_name: str) -> any:
def _open_dropdown_menu(self):
self._dropdown_menu.open(self.winfo_rootx(),
self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0))
+ self._close_on_next_click = True
def _on_enter(self, event=0):
+ self._close_on_next_click = self._dropdown_menu.is_open()
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",
@@ -396,8 +394,19 @@ def set(self, value: str):
def get(self) -> str:
return self._current_value
+ def index(self, value: Optional[Any] = None) -> int:
+ """ returns index of selected value, raises ValueError if the value is missing
+ if the parameter is provided, returns the associated index or raises ValueError if no value is found """
+ if value is None:
+ return self._values.index(self._current_value)
+ else:
+ return self._values.index(value)
+
def _clicked(self, event=0):
- if self._state is not tkinter.DISABLED and len(self._values) > 0:
+ if self._close_on_next_click:
+ self._dropdown_menu.close()
+ self._close_on_next_click = False
+ elif self._state is not tkinter.DISABLED and len(self._values) > 0:
self._open_dropdown_menu()
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
diff --git a/customtkinter/windows/widgets/ctk_progressbar.py b/customtkinter/windows/widgets/ctk_progressbar.py
index 2d6ce59b..e2df078b 100644
--- a/customtkinter/windows/widgets/ctk_progressbar.py
+++ b/customtkinter/windows/widgets/ctk_progressbar.py
@@ -181,14 +181,10 @@ def configure(self, require_redraw=False, **kwargs):
if "variable" in kwargs:
if self._variable is not None:
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 "mode" in kwargs:
self._mode = kwargs.pop("mode")
diff --git a/customtkinter/windows/widgets/ctk_radiobutton.py b/customtkinter/windows/widgets/ctk_radiobutton.py
index c07cd1f0..5c4ce845 100644
--- a/customtkinter/windows/widgets/ctk_radiobutton.py
+++ b/customtkinter/windows/widgets/ctk_radiobutton.py
@@ -206,17 +206,7 @@ def _draw(self, no_color_updates=False):
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
def configure(self, require_redraw=False, **kwargs):
- if "corner_radius" in kwargs:
- self._corner_radius = kwargs.pop("corner_radius")
- require_redraw = True
-
- if "border_width_unchecked" in kwargs:
- self._border_width_unchecked = kwargs.pop("border_width_unchecked")
- require_redraw = True
-
- if "border_width_checked" in kwargs:
- self._border_width_checked = kwargs.pop("border_width_checked")
- require_redraw = True
+ require_new_state = False
if "radiobutton_width" in kwargs:
self._radiobutton_width = kwargs.pop("radiobutton_width")
@@ -228,22 +218,16 @@ def configure(self, require_redraw=False, **kwargs):
self._canvas.configure(height=self._apply_widget_scaling(self._radiobutton_height))
require_redraw = True
- if "text" in kwargs:
- self._text = kwargs.pop("text")
- self._text_label.configure(text=self._text)
-
- if "font" in kwargs:
- if isinstance(self._font, CTkFont):
- self._font.remove_size_configure_callback(self._update_font)
- self._font = self._check_font_type(kwargs.pop("font"))
- if isinstance(self._font, CTkFont):
- self._font.add_size_configure_callback(self._update_font)
+ if "corner_radius" in kwargs:
+ self._corner_radius = kwargs.pop("corner_radius")
+ require_redraw = True
- self._update_font()
+ if "border_width_unchecked" in kwargs:
+ self._border_width_unchecked = kwargs.pop("border_width_unchecked")
+ require_redraw = True
- if "state" in kwargs:
- self._state = kwargs.pop("state")
- self._set_cursor()
+ if "border_width_checked" in kwargs:
+ self._border_width_checked = kwargs.pop("border_width_checked")
require_redraw = True
if "fg_color" in kwargs:
@@ -254,6 +238,10 @@ def configure(self, require_redraw=False, **kwargs):
self._hover_color = self._check_color_type(kwargs.pop("hover_color"))
require_redraw = True
+ if "border_color" in kwargs:
+ self._border_color = self._check_color_type(kwargs.pop("border_color"))
+ require_redraw = True
+
if "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
@@ -262,15 +250,17 @@ def configure(self, require_redraw=False, **kwargs):
self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
require_redraw = True
- if "border_color" in kwargs:
- self._border_color = self._check_color_type(kwargs.pop("border_color"))
- require_redraw = True
-
- if "hover" in kwargs:
- self._hover = kwargs.pop("hover")
+ if "text" in kwargs:
+ self._text = kwargs.pop("text")
+ self._text_label.configure(text=self._text)
- if "command" in kwargs:
- self._command = kwargs.pop("command")
+ if "font" in kwargs:
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+ self._font = self._check_font_type(kwargs.pop("font"))
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+ self._update_font()
if "textvariable" in kwargs:
self._textvariable = kwargs.pop("textvariable")
@@ -279,27 +269,42 @@ def configure(self, require_redraw=False, **kwargs):
if "variable" in kwargs:
if self._variable is not None:
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._check_state = True if self._variable.get() == self._value else False
- require_redraw = True
+ require_new_state = True
+
+ if "value" in kwargs:
+ self._value = kwargs.pop("value")
+ require_new_state = True
+ if "state" in kwargs:
+ self._state = kwargs.pop("state")
+ self._set_cursor()
+ require_redraw = True
+
+ if "hover" in kwargs:
+ self._hover = kwargs.pop("hover")
+
+ if "command" in kwargs:
+ self._command = kwargs.pop("command")
+
+ if require_new_state and self._variable is not None and self._variable != "":
+ self._check_state = True if self._variable.get() == self._value else False
+ require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
- if attribute_name == "corner_radius":
+ if attribute_name == "radiobutton_width":
+ return self._radiobutton_width
+ elif attribute_name == "radiobutton_height":
+ return self._radiobutton_height
+ elif attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width_unchecked":
return self._border_width_unchecked
elif attribute_name == "border_width_checked":
return self._border_width_checked
- elif attribute_name == "radiobutton_width":
- return self._radiobutton_width
- elif attribute_name == "radiobutton_height":
- return self._radiobutton_height
elif attribute_name == "fg_color":
return self._fg_color
@@ -377,32 +382,28 @@ def _variable_callback(self, var_name, index, mode):
else:
self.deselect(from_variable_callback=True)
+ def set(self, state: bool, from_variable_callback=False):
+ self._check_state = state
+ self._draw()
+
+ if self._variable is not None and not from_variable_callback:
+ self._variable_callback_blocked = True
+ self._variable.set(self._value if self._check_state else "")
+ self._variable_callback_blocked = False
+
def invoke(self, event=0):
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()
def select(self, from_variable_callback=False):
- 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._value)
- self._variable_callback_blocked = False
+ self.set(True, from_variable_callback)
def deselect(self, from_variable_callback=False):
- 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._variable_callback_blocked = False
+ self.set(False, from_variable_callback)
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
""" called on the tkinter.Canvas """
diff --git a/customtkinter/windows/widgets/ctk_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py
index b727bba4..d29a97b4 100644
--- a/customtkinter/windows/widgets/ctk_scrollable_frame.py
+++ b/customtkinter/windows/widgets/ctk_scrollable_frame.py
@@ -8,6 +8,8 @@
from .ctk_frame import CTkFrame
from .ctk_scrollbar import CTkScrollbar
+from .ctk_slider import CTkSlider
+from .ctk_textbox import CTkTextbox
from .appearance_mode import CTkAppearanceModeBaseClass
from .scaling import CTkScalingBaseClass
from .core_widget_classes import CTkBaseClass
@@ -75,11 +77,17 @@ def __init__(self,
self.bind("", lambda e: self._parent_canvas.configure(scrollregion=self._parent_canvas.bbox("all")))
self._parent_canvas.bind("", self._fit_frame_dimensions_to_canvas)
- self.bind_all("", self._mouse_wheel_all, add="+")
- self.bind_all("", self._keyboard_shift_press_all, add="+")
- self.bind_all("", self._keyboard_shift_press_all, add="+")
- self.bind_all("", self._keyboard_shift_release_all, add="+")
- self.bind_all("", self._keyboard_shift_release_all, add="+")
+
+ if "linux" in sys.platform:
+ self.bind_all("", self._mouse_wheel_all, add=True)
+ self.bind_all("", self._mouse_wheel_all, add=True)
+ else:
+ self.bind_all("", self._mouse_wheel_all, add=True)
+
+ self.bind_all("", self._keyboard_shift_press_all, add=True)
+ self.bind_all("", self._keyboard_shift_press_all, add=True)
+ self.bind_all("", self._keyboard_shift_release_all, add=True)
+ self.bind_all("", self._keyboard_shift_release_all, add=True)
self._create_window_id = self._parent_canvas.create_window(0, 0, window=self, anchor="nw")
if self._parent_frame.cget("fg_color") == "transparent":
@@ -93,6 +101,7 @@ def __init__(self,
def destroy(self):
tkinter.Frame.destroy(self)
+ self._parent_frame.destroy()
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
@@ -190,6 +199,12 @@ def configure(self, **kwargs):
if "scrollbar_button_hover_color" in kwargs:
self._scrollbar.configure(button_hover_color=kwargs.pop("scrollbar_button_hover_color"))
+ if "label_fg_color" in kwargs:
+ self._label.configure(fg_color=kwargs.pop("label_fg_color"))
+
+ if "label_text_color" in kwargs:
+ self._label.configure(text_color=kwargs.pop("label_text_color"))
+
if "label_text" in kwargs:
self._label_text = kwargs.pop("label_text")
self._label.configure(text=self._label_text)
@@ -198,12 +213,6 @@ def configure(self, **kwargs):
if "label_font" in kwargs:
self._label.configure(font=kwargs.pop("label_font"))
- if "label_text_color" in kwargs:
- self._label.configure(text_color=kwargs.pop("label_text_color"))
-
- if "label_fg_color" in kwargs:
- self._label.configure(fg_color=kwargs.pop("label_fg_color"))
-
if "label_anchor" in kwargs:
self._label.configure(anchor=kwargs.pop("label_anchor"))
@@ -215,23 +224,25 @@ def cget(self, attribute_name: str):
elif attribute_name == "height":
return self._desired_height
- elif attribute_name == "label_text":
- return self._label_text
- elif attribute_name == "label_font":
- return self._label.cget("font")
- elif attribute_name == "label_text_color":
- return self._label.cget("_text_color")
- elif attribute_name == "label_fg_color":
- return self._label.cget("fg_color")
- elif attribute_name == "label_anchor":
- return self._label.cget("anchor")
-
elif attribute_name.startswith("scrollbar_fg_color"):
return self._scrollbar.cget("fg_color")
elif attribute_name.startswith("scrollbar_button_color"):
return self._scrollbar.cget("button_color")
elif attribute_name.startswith("scrollbar_button_hover_color"):
return self._scrollbar.cget("button_hover_color")
+ elif attribute_name == "label_fg_color":
+ return self._label.cget("fg_color")
+ elif attribute_name == "label_text_color":
+ return self._label.cget("_text_color")
+
+ elif attribute_name == "label_text":
+ return self._label_text
+ elif attribute_name == "label_font":
+ return self._label.cget("font")
+ elif attribute_name == "label_anchor":
+ return self._label.cget("anchor")
+ elif attribute_name == "orientation":
+ return self._orientation
else:
return self._parent_frame.cget(attribute_name)
@@ -247,9 +258,11 @@ def _set_scroll_increments(self):
self._parent_canvas.configure(xscrollincrement=1, yscrollincrement=1)
elif sys.platform == "darwin":
self._parent_canvas.configure(xscrollincrement=4, yscrollincrement=8)
+ else:
+ self._parent_canvas.configure(xscrollincrement=30, yscrollincrement=30)
def _mouse_wheel_all(self, event):
- if self.check_if_master_is_canvas(event.widget):
+ if self._check_if_valid_scroll(event.widget):
if sys.platform.startswith("win"):
if self._shift_pressed:
if self._parent_canvas.xview() != (0.0, 1.0):
@@ -267,10 +280,11 @@ def _mouse_wheel_all(self, event):
else:
if self._shift_pressed:
if self._parent_canvas.xview() != (0.0, 1.0):
- self._parent_canvas.xview("scroll", -event.delta, "units")
+ self._parent_canvas.xview_scroll(-1 if event.num == 4 else 1, "units")
else:
if self._parent_canvas.yview() != (0.0, 1.0):
- self._parent_canvas.yview("scroll", -event.delta, "units")
+ self._parent_canvas.yview_scroll(-1 if event.num == 4 else 1, "units")
+
def _keyboard_shift_press_all(self, event):
self._shift_pressed = True
@@ -278,11 +292,15 @@ def _keyboard_shift_press_all(self, event):
def _keyboard_shift_release_all(self, event):
self._shift_pressed = False
- def check_if_master_is_canvas(self, widget):
+ def _check_if_valid_scroll(self, widget):
if widget == self._parent_canvas:
return True
+ elif isinstance(widget, (CTkScrollbar, CTkSlider, CTkTextbox)):
+ return False
+ elif isinstance(widget, CTkScrollableFrame):
+ return widget._parent_canvas == self._parent_canvas
elif widget.master is not None:
- return self.check_if_master_is_canvas(widget.master)
+ return self._check_if_valid_scroll(widget.master)
else:
return False
diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py
index 8e962215..7e0a9582 100644
--- a/customtkinter/windows/widgets/ctk_scrollbar.py
+++ b/customtkinter/windows/widgets/ctk_scrollbar.py
@@ -78,14 +78,21 @@ def _create_bindings(self, sequence: Optional[str] = None):
""" set necessary bindings for functionality of widget, will overwrite other bindings """
if sequence is None:
self._canvas.tag_bind("border_parts", "", self._clicked)
+ self._canvas.tag_bind("scrollbar_parts", "", self._clicked_scrollbar)
if sequence is None or sequence == "":
self._canvas.bind("", self._on_enter)
if sequence is None or sequence == "":
self._canvas.bind("", self._on_leave)
if sequence is None or sequence == "":
- self._canvas.bind("", self._clicked)
- if sequence is None or sequence == "":
- self._canvas.bind("", self._mouse_scroll_event)
+ self._canvas.bind("", self._on_motion)
+ if "linux" in sys.platform:
+ if sequence is None or sequence == "":
+ self._canvas.bind("", self._mouse_scroll_event)
+ if sequence is None or sequence == "":
+ self._canvas.bind("", self._mouse_scroll_event)
+ else:
+ if sequence is None or sequence == "":
+ self._canvas.bind("", self._mouse_scroll_event)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
@@ -161,6 +168,14 @@ def _draw(self, no_color_updates=False):
self._canvas.update_idletasks()
def configure(self, require_redraw=False, **kwargs):
+ if "corner_radius" in kwargs:
+ self._corner_radius = kwargs.pop("corner_radius")
+ require_redraw = True
+
+ if "border_spacing" in kwargs:
+ self._border_spacing = kwargs.pop("border_spacing")
+ require_redraw = True
+
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
@@ -179,14 +194,6 @@ def configure(self, require_redraw=False, **kwargs):
if "command" in kwargs:
self._command = kwargs.pop("command")
- if "corner_radius" in kwargs:
- self._corner_radius = kwargs.pop("corner_radius")
- require_redraw = True
-
- if "border_spacing" in kwargs:
- self._border_spacing = kwargs.pop("border_spacing")
- require_redraw = True
-
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
@@ -199,9 +206,9 @@ def cget(self, attribute_name: str) -> any:
elif attribute_name == "fg_color":
return self._fg_color
- elif attribute_name == "scrollbar_color":
+ elif attribute_name == "button_color":
return self._button_color
- elif attribute_name == "scrollbar_hover_color":
+ elif attribute_name == "button_hover_color":
return self._button_hover_color
elif attribute_name == "hover":
@@ -228,10 +235,22 @@ def _on_leave(self, event=0):
fill=self._apply_appearance_mode(self._button_color))
def _clicked(self, event):
+ self._motion_center_offset = 0
+ self._on_motion(event)
+
+ def _clicked_scrollbar(self,event):
if self._orientation == "vertical":
value = self._reverse_widget_scaling(((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing)))
else:
value = self._reverse_widget_scaling(((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing)))
+ center = self._start_value + ((self._end_value - self._start_value) * 0.5)
+ self._motion_center_offset = center - value
+
+ def _on_motion(self, event):
+ if self._orientation == "vertical":
+ value = self._reverse_widget_scaling(((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing)))+self._motion_center_offset
+ else:
+ value = self._reverse_widget_scaling(((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing)))+self._motion_center_offset
current_scrollbar_length = self._end_value - self._start_value
value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2)))
@@ -245,9 +264,30 @@ def _clicked(self, event):
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')
+ delta = -int(event.delta/40)
+ elif sys.platform == "darwin":
+ delta = -event.delta
+ else:
+ delta = -1 if event.num == 4 else 1
+ self._command('scroll', delta, 'units')
+ else:
+ #empty space is divided in 20 steps
+ delta = (1 - self._end_value + self._start_value) / 20
+ #condition for both Linux and others OS
+ if event.delta > 0 or event.num == 4:
+ delta = -delta
+
+ if self._start_value + delta < 0.0:
+ self._end_value = self._end_value - self._start_value
+ self._start_value = 0.0
+ elif self._end_value + delta > 1.0:
+ self._start_value = 1 - self._end_value + self._start_value
+ self._end_value = 1.0
else:
- self._command('scroll', -event.delta, 'units')
+ self._start_value += delta
+ self._end_value += delta
+ self._draw()
+
def set(self, start_value: float, end_value: float):
self._start_value = float(start_value)
diff --git a/customtkinter/windows/widgets/ctk_segmented_button.py b/customtkinter/windows/widgets/ctk_segmented_button.py
index b8de1e79..ed575666 100644
--- a/customtkinter/windows/widgets/ctk_segmented_button.py
+++ b/customtkinter/windows/widgets/ctk_segmented_button.py
@@ -41,7 +41,8 @@ def __init__(self,
variable: Union[tkinter.Variable, None] = None,
dynamic_resizing: bool = True,
command: Union[Callable[[str], Any], None] = None,
- state: str = "normal"):
+ state: str = "normal",
+ orientation: Literal["horizontal", "vertical"] = "horizontal"):
super().__init__(master=master, bg_color=bg_color, width=width, height=height)
@@ -64,6 +65,7 @@ def __init__(self,
self._command: Callable[[str], None] = command
self._font = CTkFont() if font is None else font
self._state = state
+ self._orientation = orientation
self._buttons_dict: Dict[str, CTkButton] = {} # mapped from value to button object
if values is None:
@@ -123,15 +125,27 @@ def _configure_button_corners_for_index(self, index: int):
elif index == 0:
if self._background_corner_colors is None:
- 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))
+ if self._orientation == "vertical":
+ self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._bg_color, self._sb_fg_color, self._sb_fg_color))
+ else:
+ 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))
else:
- self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._background_corner_colors[0], self._sb_fg_color, self._sb_fg_color, self._background_corner_colors[3]))
+ if self._orientation == "vertical":
+ self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._background_corner_colors[0], self._background_corner_colors[1], self._sb_fg_color, self._sb_fg_color))
+ else:
+ self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._background_corner_colors[0], self._sb_fg_color, self._sb_fg_color, self._background_corner_colors[3]))
elif index == len(self._value_list) - 1:
if self._background_corner_colors is None:
- 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))
+ if self._orientation == "vertical":
+ self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._sb_fg_color, self._bg_color, self._bg_color))
+ else:
+ 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._background_corner_colors[1], self._background_corner_colors[2], self._sb_fg_color))
+ if self._orientation == "vertical":
+ self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._sb_fg_color, self._background_corner_colors[2], self._background_corner_colors[3]))
+ else:
+ self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._background_corner_colors[1], self._background_corner_colors[2], 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))
@@ -178,15 +192,25 @@ def _check_unique_values(values: List[str]):
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)
+ number_of_columns, number_of_rows = self.grid_size()
+ if self._orientation == "vertical":
+ # remove minsize from every grid cell in the first column
+ for n in range(number_of_rows):
+ self.grid_rowconfigure(n, weight=1, minsize=0)
+ self.grid_columnconfigure(0, weight=1)
+
+ for index, value in enumerate(self._value_list):
+ self.grid_rowconfigure(index, weight=1, minsize=self._current_height)
+ self._buttons_dict[value].grid(row=index, column=0, sticky="nsew")
+ else:
+ # remove minsize from every grid cell in the first row
+ 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="nsew")
+ 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="nsew")
def _create_buttons_from_values(self):
assert len(self._buttons_dict) == 0
@@ -216,7 +240,6 @@ def configure(self, **kwargs):
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:
@@ -289,14 +312,10 @@ def configure(self, **kwargs):
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")
@@ -316,17 +335,11 @@ def configure(self, **kwargs):
check_kwargs_empty(kwargs, raise_error=True)
def cget(self, attribute_name: str) -> any:
- if attribute_name == "width":
- return super().cget(attribute_name)
- elif attribute_name == "height":
- return super().cget(attribute_name)
- elif attribute_name == "corner_radius":
+ if attribute_name == "corner_radius":
return self._sb_corner_radius
elif attribute_name == "border_width":
return self._sb_border_width
- elif attribute_name == "bg_color":
- return super().cget(attribute_name)
elif attribute_name == "fg_color":
return self._sb_fg_color
elif attribute_name == "selected_color":
@@ -341,6 +354,8 @@ def cget(self, attribute_name: str) -> any:
return self._sb_text_color
elif attribute_name == "text_color_disabled":
return self._sb_text_color_disabled
+ elif attribute_name == "background_corner_colors":
+ return self._background_corner_colors
elif attribute_name == "font":
return self._font
@@ -352,9 +367,13 @@ def cget(self, attribute_name: str) -> any:
return self._dynamic_resizing
elif attribute_name == "command":
return self._command
+ elif attribute_name == "state":
+ return self._state
+ elif attribute_name == "orientation":
+ return self._orientation
else:
- raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.")
+ return super().cget(attribute_name)
def set(self, value: str, from_variable_callback: bool = False, from_button_callback: bool = False):
if value == self._current_value:
@@ -383,8 +402,13 @@ def set(self, value: str, from_variable_callback: bool = False, from_button_call
def get(self) -> str:
return self._current_value
- def index(self, value: str) -> int:
- return self._value_list.index(value)
+ def index(self, value: Optional[str] = None) -> int:
+ """ returns index of selected value, raises ValueError if the value is missing
+ if the parameter is provided, returns the associated index or raises ValueError if no value is found """
+ if value is None:
+ return self._value_list.index(self._current_value)
+ else:
+ return self._value_list.index(value)
def insert(self, index: int, value: str):
if value not in self._buttons_dict:
@@ -406,6 +430,10 @@ def insert(self, index: int, value: str):
raise ValueError(f"CTkSegmentedButton can not insert value ''")
else:
raise ValueError(f"CTkSegmentedButton can not insert value '{value}', already part of the values")
+
+ def len(self) -> int:
+ """ returns the number of defined buttons """
+ return len(self._value_list)
def move(self, new_index: int, value: str):
if 0 <= new_index < len(self._value_list):
diff --git a/customtkinter/windows/widgets/ctk_slider.py b/customtkinter/windows/widgets/ctk_slider.py
index 7aa03eeb..5e2f8f8b 100644
--- a/customtkinter/windows/widgets/ctk_slider.py
+++ b/customtkinter/windows/widgets/ctk_slider.py
@@ -34,6 +34,7 @@ def __init__(self,
to: int = 1,
state: str = "normal",
number_of_steps: Union[int, None] = None,
+ scroll_step: Optional[Union[int, float]] = None,
hover: bool = True,
command: Union[Callable[[float], Any], None] = None,
variable: Union[tkinter.Variable, None] = None,
@@ -74,6 +75,7 @@ def __init__(self,
self._from_ = from_
self._to = to
self._number_of_steps = number_of_steps
+ self._scroll_step = (1 / (20 if number_of_steps is None else number_of_steps)) if scroll_step is None else scroll_step
self._output_value = self._from_ + (self._value * (self._to - self._from_))
if self._corner_radius < self._button_corner_radius:
@@ -116,6 +118,14 @@ def _create_bindings(self, sequence: Optional[str] = None):
self._canvas.bind("", self._clicked)
if sequence is None or sequence == "":
self._canvas.bind("", self._clicked)
+ if "linux" in sys.platform:
+ if sequence is None or sequence == "":
+ self._canvas.bind("", self._mouse_scroll_event)
+ if sequence is None or sequence == "":
+ self._canvas.bind("", self._mouse_scroll_event)
+ else:
+ if sequence is None or sequence == "":
+ self._canvas.bind("", self._mouse_scroll_event)
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
@@ -249,6 +259,9 @@ def configure(self, require_redraw=False, **kwargs):
if "number_of_steps" in kwargs:
self._number_of_steps = kwargs.pop("number_of_steps")
+ if "scroll_step" in kwargs:
+ self._scroll_step = kwargs.pop("scroll_step")
+
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
@@ -258,14 +271,10 @@ def configure(self, require_redraw=False, **kwargs):
if "variable" in kwargs:
if self._variable is not None:
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 "orientation" in kwargs:
self._orientation = kwargs.pop("orientation")
@@ -302,6 +311,8 @@ def cget(self, attribute_name: str) -> any:
return self._state
elif attribute_name == "number_of_steps":
return self._number_of_steps
+ elif attribute_name == "scroll_step":
+ return self._scroll_step
elif attribute_name == "hover":
return self._hover
elif attribute_name == "command":
@@ -313,31 +324,39 @@ def cget(self, attribute_name: str) -> any:
else:
return super().cget(attribute_name)
+
+ def _update_value(self, value: float):
+ self._value = max(0.0, min(1.0, value))
+
+ 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 _clicked(self, event=None):
if self._state == "normal":
if self._orientation.lower() == "horizontal":
- self._value = self._reverse_widget_scaling(event.x / self._current_width)
+ value = self._reverse_widget_scaling(event.x / self._current_width)
else:
- self._value = 1 - self._reverse_widget_scaling(event.y / self._current_height)
+ value = 1.0 - self._reverse_widget_scaling(event.y / self._current_height)
- if self._value > 1:
- self._value = 1
- if self._value < 0:
- self._value = 0
+ self._update_value(value)
- 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
+ def _mouse_scroll_event(self, event):
+ delta = self._scroll_step
+ #condition for both Linux and others OS
+ if event.delta < 0 or event.num == 5:
+ delta = -delta
- if self._command is not None:
- self._command(self._output_value)
+ self._update_value(self._value + delta)
def _on_enter(self, event=0):
if self._hover is True and self._state == "normal":
@@ -360,9 +379,6 @@ def _round_to_step_size(self, value) -> float:
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:
@@ -385,6 +401,9 @@ def set(self, output_value, from_variable_callback=False):
self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
self._variable_callback_blocked = False
+ def get(self) -> float:
+ return self._output_value
+
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self.set(self._variable.get(), from_variable_callback=True)
diff --git a/customtkinter/windows/widgets/ctk_switch.py b/customtkinter/windows/widgets/ctk_switch.py
index 155c1746..5deeb5d6 100644
--- a/customtkinter/windows/widgets/ctk_switch.py
+++ b/customtkinter/windows/widgets/ctk_switch.py
@@ -255,17 +255,7 @@ def _draw(self, no_color_updates=False):
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
def configure(self, require_redraw=False, **kwargs):
- if "corner_radius" in kwargs:
- 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 "button_length" in kwargs:
- self._button_length = kwargs.pop("button_length")
- require_redraw = True
+ require_new_state = False
if "switch_width" in kwargs:
self._switch_width = kwargs.pop("switch_width")
@@ -277,22 +267,16 @@ def configure(self, require_redraw=False, **kwargs):
self._canvas.configure(height=self._apply_widget_scaling(self._switch_height))
require_redraw = True
- if "text" in kwargs:
- self._text = kwargs.pop("text")
- self._text_label.configure(text=self._text)
-
- if "font" in kwargs:
- if isinstance(self._font, CTkFont):
- self._font.remove_size_configure_callback(self._update_font)
- self._font = self._check_font_type(kwargs.pop("font"))
- if isinstance(self._font, CTkFont):
- self._font.add_size_configure_callback(self._update_font)
+ if "corner_radius" in kwargs:
+ self._corner_radius = kwargs.pop("corner_radius")
+ require_redraw = True
- self._update_font()
+ if "border_width" in kwargs:
+ self._border_width = kwargs.pop("border_width")
+ require_redraw = True
- if "state" in kwargs:
- self._state = kwargs.pop("state")
- self._set_cursor()
+ if "button_length" in kwargs:
+ self._button_length = kwargs.pop("button_length")
require_redraw = True
if "fg_color" in kwargs:
@@ -323,40 +307,65 @@ def configure(self, require_redraw=False, **kwargs):
self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
require_redraw = True
- if "hover" in kwargs:
- self._hover = kwargs.pop("hover")
+ if "text" in kwargs:
+ self._text = kwargs.pop("text")
+ self._text_label.configure(text=self._text)
- if "command" in kwargs:
- self._command = kwargs.pop("command")
+ if "font" in kwargs:
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+ self._font = self._check_font_type(kwargs.pop("font"))
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+ self._update_font()
if "textvariable" in kwargs:
self._textvariable = kwargs.pop("textvariable")
self._text_label.configure(textvariable=self._textvariable)
+ if "onvalue" in kwargs:
+ self._onvalue = kwargs.pop("onvalue")
+ require_new_state = True
+
+ if "offvalue" in kwargs:
+ self._offvalue = kwargs.pop("offvalue")
+ require_new_state = True
+
if "variable" in kwargs:
if self._variable is not None and self._variable != "":
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._check_state = True if self._variable.get() == self._onvalue else False
- require_redraw = True
+ require_new_state = True
+
+ if "hover" in kwargs:
+ self._hover = kwargs.pop("hover")
+
+ if "command" in kwargs:
+ self._command = kwargs.pop("command")
+ if "state" in kwargs:
+ self._state = kwargs.pop("state")
+ self._set_cursor()
+ require_redraw = True
+
+ if require_new_state and self._variable is not None and self._variable != "":
+ self._check_state = True if self._variable.get() == self._onvalue else False
+ require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
- if attribute_name == "corner_radius":
+ if attribute_name == "switch_width":
+ return self._switch_width
+ elif attribute_name == "switch_height":
+ return self._switch_height
+ elif attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "button_length":
return self._button_length
- elif attribute_name == "switch_width":
- return self._switch_width
- elif attribute_name == "switch_height":
- return self._switch_height
elif attribute_name == "fg_color":
return self._fg_color
@@ -394,45 +403,28 @@ def cget(self, attribute_name: str) -> any:
else:
return super().cget(attribute_name)
+
+ def set(self, state: bool, from_variable_callback=False):
+ self._check_state = state
+ self._draw(no_color_updates=True)
- def toggle(self, event=None):
- if self._state is not tkinter.DISABLED:
- if self._check_state is True:
- self._check_state = False
- else:
- self._check_state = 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 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
+ def toggle(self, event=None):
+ if self._state == tkinter.NORMAL:
+ self.set(not self._check_state)
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
-
- 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
+ self.set(True, from_variable_callback)
def deselect(self, from_variable_callback=False):
- if self._state is not tkinter.DISABLED or from_variable_callback:
- self._check_state = False
-
- 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
+ self.set(False, from_variable_callback)
def get(self) -> Union[int, str]:
return self._onvalue if self._check_state is True else self._offvalue
diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py
index 3b2ea5bb..4b805601 100644
--- a/customtkinter/windows/widgets/ctk_tabview.py
+++ b/customtkinter/windows/widgets/ctk_tabview.py
@@ -7,6 +7,7 @@
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .ctk_segmented_button import CTkSegmentedButton
+from .font import CTkFont
class CTkTabview(CTkBaseClass):
@@ -36,6 +37,7 @@ def __init__(self,
segmented_button_selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_unselected_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_unselected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+ segmented_button_font: Optional[Union[tuple, CTkFont]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
@@ -75,6 +77,9 @@ def __init__(self,
height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang))
self._draw_engine = DrawEngine(self._canvas)
+ # segmented_button_font font
+ self._segmented_button_font = CTkFont() if segmented_button_font is None else segmented_button_font
+
self._segmented_button = CTkSegmentedButton(self,
values=[],
height=self._button_height,
@@ -88,6 +93,7 @@ def __init__(self,
corner_radius=corner_radius,
border_width=self._segmented_button_border_width,
command=self._segmented_button_callback,
+ font=self._segmented_button_font,
state=state)
self._configure_segmented_button_background_corners()
self._configure_grid()
@@ -253,37 +259,53 @@ def configure(self, require_redraw=False, **kwargs):
self._set_grid_canvas()
self._configure_segmented_button_background_corners()
self._segmented_button.configure(corner_radius=self._corner_radius)
+
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
require_redraw = True
+
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
self._configure_segmented_button_background_corners()
require_redraw = True
+
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
+
if "segmented_button_fg_color" in kwargs:
self._segmented_button.configure(fg_color=kwargs.pop("segmented_button_fg_color"))
+
if "segmented_button_selected_color" in kwargs:
self._segmented_button.configure(selected_color=kwargs.pop("segmented_button_selected_color"))
+
if "segmented_button_selected_hover_color" in kwargs:
self._segmented_button.configure(selected_hover_color=kwargs.pop("segmented_button_selected_hover_color"))
+
if "segmented_button_unselected_color" in kwargs:
self._segmented_button.configure(unselected_color=kwargs.pop("segmented_button_unselected_color"))
+
if "segmented_button_unselected_hover_color" in kwargs:
self._segmented_button.configure(unselected_hover_color=kwargs.pop("segmented_button_unselected_hover_color"))
+
+ if "segmented_button_font" in kwargs:
+ self._segmented_button_font = kwargs.pop("segmented_button_font")
+ self._segmented_button.configure(font=self._segmented_button_font)
+
if "text_color" in kwargs:
self._segmented_button.configure(text_color=kwargs.pop("text_color"))
+
if "text_color_disabled" in kwargs:
self._segmented_button.configure(text_color_disabled=kwargs.pop("text_color_disabled"))
if "command" in kwargs:
self._command = kwargs.pop("command")
+
if "anchor" in kwargs:
self._anchor = kwargs.pop("anchor")
self._configure_grid()
self._set_grid_segmented_button()
+
if "state" in kwargs:
self._segmented_button.configure(state=kwargs.pop("state"))
@@ -299,6 +321,7 @@ def cget(self, attribute_name: str):
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
+
elif attribute_name == "segmented_button_fg_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "segmented_button_selected_color":
@@ -309,6 +332,9 @@ def cget(self, attribute_name: str):
return self._segmented_button.cget(attribute_name)
elif attribute_name == "segmented_button_unselected_hover_color":
return self._segmented_button.cget(attribute_name)
+ elif attribute_name == "segmented_button_font":
+ return self._segmented_button_font
+
elif attribute_name == "text_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "text_color_disabled":
@@ -359,10 +385,6 @@ def add(self, name: str) -> CTkFrame:
""" appends new tab with given name """
return self.insert(len(self._tab_dict), name)
- def index(self, name) -> int:
- """ get index of tab with given name """
- return self._segmented_button.index(name)
-
def move(self, new_index: int, name: str):
if 0 <= new_index < len(self._name_list):
if name in self._tab_dict:
@@ -382,18 +404,21 @@ def rename(self, old_name: str, new_name: str):
self._segmented_button.insert(old_index, new_name)
# name list
- self._name_list.remove(old_name)
- self._name_list.append(new_name)
+ self._name_list[self._name_list.index(old_name)] = new_name
# tab dictionary
self._tab_dict[new_name] = self._tab_dict.pop(old_name)
+ # update current_name so we don't loose the connection to the frame
+ if self._current_name == old_name:
+ self._current_name = new_name
+
def delete(self, name: str):
""" delete tab by name """
if name in self._tab_dict:
self._name_list.remove(name)
- self._tab_dict[name].grid_forget()
+ self._tab_dict[name].destroy()
self._tab_dict.pop(name)
self._segmented_button.delete(name)
@@ -428,6 +453,22 @@ def set(self, name: str):
else:
raise ValueError(f"CTkTabview has no tab named '{name}'")
- def get(self) -> str:
- """ returns name of selected tab, returns empty string if no tab selected """
- return self._current_name
+ def get(self, index: Optional[int] = None) -> str:
+ """ returns name of selected tab, returns empty string if no tab selected.\n
+ if an index is provided, returns the tab name in that position """
+ if index is None:
+ return self._current_name
+ else:
+ return self._name_list[index]
+
+ def index(self, name: Optional[str] = None) -> int:
+ """ returns index of selected tab, raises ValueError if the tab is missing
+ if the parameter is provided, returns the associated index or raises ValueError if no tab is found """
+ if name is None:
+ return self._name_list.index(self._current_name)
+ else:
+ return self._name_list.index(name)
+
+ def len(self) -> int:
+ """ returns the number of defined tabs """
+ return len(self._name_list)
diff --git a/customtkinter/windows/widgets/ctk_textbox.py b/customtkinter/windows/widgets/ctk_textbox.py
index 4b3a165f..82905287 100644
--- a/customtkinter/windows/widgets/ctk_textbox.py
+++ b/customtkinter/windows/widgets/ctk_textbox.py
@@ -251,6 +251,21 @@ def _draw(self, no_color_updates=False):
self._canvas.tag_lower("border_parts")
def configure(self, require_redraw=False, **kwargs):
+ if "corner_radius" in kwargs:
+ self._corner_radius = kwargs.pop("corner_radius")
+ self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
+ require_redraw = True
+
+ if "border_width" in kwargs:
+ self._border_width = kwargs.pop("border_width")
+ self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
+ require_redraw = True
+
+ if "border_spacing" in kwargs:
+ self._border_spacing = kwargs.pop("border_spacing")
+ self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
+ require_redraw = True
+
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
@@ -278,28 +293,12 @@ def configure(self, require_redraw=False, **kwargs):
self._x_scrollbar.configure(button_hover_color=self._scrollbar_button_hover_color)
self._y_scrollbar.configure(button_hover_color=self._scrollbar_button_hover_color)
- if "corner_radius" in kwargs:
- self._corner_radius = kwargs.pop("corner_radius")
- self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
- require_redraw = True
-
- if "border_width" in kwargs:
- self._border_width = kwargs.pop("border_width")
- self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
- require_redraw = True
-
- if "border_spacing" in kwargs:
- self._border_spacing = kwargs.pop("border_spacing")
- self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
- require_redraw = True
-
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
-
self._update_font()
self._textbox.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes))
@@ -319,10 +318,18 @@ def cget(self, attribute_name: str) -> any:
return self._border_color
elif attribute_name == "text_color":
return self._text_color
+ elif attribute_name == "scrollbar_button_color":
+ return self._scrollbar_button_color
+ elif attribute_name == "scrollbar_button_hover_color":
+ return self._scrollbar_button_hover_color
elif attribute_name == "font":
return self._font
+ elif attribute_name == "activate_scrollbars":
+ return self._scrollbars_activated
+ elif attribute_name in self._valid_tk_text_attributes:
+ return self._textbox.cget(attribute_name) # cget of tkinter.Text
else:
return super().cget(attribute_name)
diff --git a/customtkinter/windows/widgets/font/ctk_font.py b/customtkinter/windows/widgets/font/ctk_font.py
index e0eca0d2..a02ce0a0 100644
--- a/customtkinter/windows/widgets/font/ctk_font.py
+++ b/customtkinter/windows/widgets/font/ctk_font.py
@@ -65,14 +65,14 @@ def config(self, *args, **kwargs):
raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.")
def configure(self, **kwargs):
- if "size" in kwargs:
- self._size = kwargs.pop("size")
- super().configure(size=-abs(self._size))
-
if "family" in kwargs:
super().configure(family=kwargs.pop("family"))
self._family = super().cget("family")
+ if "size" in kwargs:
+ self._size = kwargs.pop("size")
+ super().configure(size=-abs(self._size))
+
super().configure(**kwargs)
# update style string for create_scaled_tuple() method
@@ -85,8 +85,7 @@ def configure(self, **kwargs):
def cget(self, attribute_name: str) -> any:
if attribute_name == "size":
return self._size
- if attribute_name == "family":
- return self._family
+
else:
return super().cget(attribute_name)
diff --git a/customtkinter/windows/widgets/image/ctk_image.py b/customtkinter/windows/widgets/image/ctk_image.py
index 0247cdd3..60a05f82 100644
--- a/customtkinter/windows/widgets/image/ctk_image.py
+++ b/customtkinter/windows/widgets/image/ctk_image.py
@@ -55,10 +55,12 @@ def configure(self, **kwargs):
self._light_image = kwargs.pop("light_image")
self._scaled_light_photo_images = {}
self._check_images()
+
if "dark_image" in kwargs:
self._dark_image = kwargs.pop("dark_image")
self._scaled_dark_photo_images = {}
self._check_images()
+
if "size" in kwargs:
self._size = kwargs.pop("size")
@@ -69,9 +71,9 @@ def configure(self, **kwargs):
def cget(self, attribute_name: str) -> any:
if attribute_name == "light_image":
return self._light_image
- if attribute_name == "dark_image":
+ elif attribute_name == "dark_image":
return self._dark_image
- if attribute_name == "size":
+ elif attribute_name == "size":
return self._size
def _check_images(self):
diff --git a/customtkinter/windows/widgets/scaling/scaling_base_class.py b/customtkinter/windows/widgets/scaling/scaling_base_class.py
index 0d7b29b3..1be0f558 100644
--- a/customtkinter/windows/widgets/scaling/scaling_base_class.py
+++ b/customtkinter/windows/widgets/scaling/scaling_base_class.py
@@ -57,13 +57,24 @@ def _get_widget_scaling(self) -> float:
def _get_window_scaling(self) -> float:
return self.__window_scaling
- def _apply_widget_scaling(self, value: Union[int, float]) -> Union[float]:
+ # Some parts of Tk - notably canvas - are very buggy with floats, because they use locale-dependent parsing
+ # (and thus might not recognize "." as the decimal point)
+ # https://wiki.tcl-lang.org/page/locale
+ # https://github.com/python/cpython/issues/56767
+ # Hence, we must ensure any integer value stays that way
+ def _apply_widget_scaling(self, value: Union[int, float]) -> Union[int, float]:
assert self.__scaling_type == "widget"
- return value * self.__widget_scaling
+ if isinstance(value, float):
+ return value * self.__widget_scaling
+ else:
+ return int(value * self.__widget_scaling)
- def _reverse_widget_scaling(self, value: Union[int, float]) -> Union[float]:
+ def _reverse_widget_scaling(self, value: Union[int, float]) -> Union[int, float]:
assert self.__scaling_type == "widget"
- return value / self.__widget_scaling
+ if isinstance(value, float):
+ return value / self.__widget_scaling
+ else:
+ return int(value / self.__widget_scaling)
def _apply_window_scaling(self, value: Union[int, float]) -> int:
assert self.__scaling_type == "window"
diff --git a/customtkinter/windows/widgets/theme/theme_manager.py b/customtkinter/windows/widgets/theme/theme_manager.py
index cf22858b..3469de96 100644
--- a/customtkinter/windows/widgets/theme/theme_manager.py
+++ b/customtkinter/windows/widgets/theme/theme_manager.py
@@ -8,7 +8,7 @@
class ThemeManager:
theme: dict = {} # contains all the theme data
- _built_in_themes: List[str] = ["blue", "green", "dark-blue", "sweetkind"]
+ _built_in_themes: List[str] = ["blue", "green", "gold", "dark-blue"]
_currently_loaded_theme: Union[str, None] = None
@classmethod
@@ -42,6 +42,11 @@ def load_theme(cls, theme_name_or_path: str):
cls.theme["CTkCheckBox"] = cls.theme.pop("CTkCheckbox")
if "CTkRadiobutton" in cls.theme.keys():
cls.theme["CTkRadioButton"] = cls.theme.pop("CTkRadiobutton")
+ if "CTkLabel" in cls.theme.keys():
+ if "border_width" not in cls.theme["CTkLabel"].keys():
+ cls.theme["CTkLabel"]["border_width"] = 0
+ if "border_color" not in cls.theme["CTkLabel"].keys():
+ cls.theme["CTkLabel"]["border_color"] = ["black", "white"]
@classmethod
def save_theme(cls):
diff --git a/dev-proces.md b/dev-proces.md
new file mode 100644
index 00000000..78a40862
--- /dev/null
+++ b/dev-proces.md
@@ -0,0 +1,28 @@
+Development on `/develop` branch.
+- create Pull Requests following these guidelines:
+ - 1 feature <=> 1 PR: do not condese in a single Pull Requests different changes
+ - do not leave commented code: if you need to remove an entire section, just delete it
+ - do not add comments that refer to how the code was: "changed", "fixed", "it wasn't present" do not provide useful info and must be avoided
+- merge external pull requests into `/develop`
+- implement features, fix bugs on `/develop`
+- update changelog in `CHANGELOG.md`
+- test on all platforms for new graphical features
+
+When ready: Bump version using `tbump`:
+```
+tbump 5.2.3
+```
+
+Create pull request to merge `/develop` into `/master` branch on Github.
+- approval by owner (Tom), merge to `/master`
+
+
+Publish new version to PyPI (Tom):
+```
+python -m pip install --upgrade build
+rm -r dist
+python -m build
+python -m twine upload dist/*
+```
+Finally: Update documentation for new features.
+- upload to website by Tom
diff --git a/examples/complex_example.py b/examples/complex_example.py
index 072bc646..3ac9cb8c 100644
--- a/examples/complex_example.py
+++ b/examples/complex_example.py
@@ -132,7 +132,7 @@ def __init__(self):
self.scrollable_frame_switches[0].select()
self.scrollable_frame_switches[4].select()
self.radio_button_3.configure(state="disabled")
- self.appearance_mode_optionemenu.set("Dark")
+ self.appearance_mode_optionemenu.set("System")
self.scaling_optionemenu.set("100%")
self.optionmenu_1.set("CTkOptionmenu")
self.combobox_1.set("CTkComboBox")
diff --git a/pyproject.toml b/pyproject.toml
index a2ad10d2..1642e743 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
github_url = "https://github.com/TomSchimansky/CustomTkinter"
[tool.tbump.version]
-current = "5.2.2"
+current = "5.3.0"
# Example of a semver regexp.
# Make sure this matches current_version before
diff --git a/requirements.txt b/requirements.txt
index c03edc25..c915da35 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-darkdetect~=0.3.1
+darkdetect~=0.7.0
typing-extensions~=4.4.0
packaging
setuptools
diff --git a/setup.cfg b/setup.cfg
index 5f95b7f9..2bc7b963 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = customtkinter
-version = 5.2.2
+version = 5.3.0
description = Create modern looking GUIs with Python
long_description = A modern and customizable python UI-library based on Tkinter: https://customtkinter.tomschimansky.com
long_description_content_type = text/markdown
diff --git a/test/manual_integration_tests/test_scaling/test_scaling_simple_place.py b/test/manual_integration_tests/test_scaling/test_scaling_simple_place.py
index 90dcbffb..8a62b751 100644
--- a/test/manual_integration_tests/test_scaling/test_scaling_simple_place.py
+++ b/test/manual_integration_tests/test_scaling/test_scaling_simple_place.py
@@ -1,6 +1,8 @@
+import locale
import tkinter
import customtkinter # <- import the CustomTkinter module
+locale.setlocale(locale.LC_NUMERIC, 'de_DE') # to verify that the canvas float argument bug is properly averted
customtkinter.ScalingTracker.set_window_scaling(0.5)
customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light"
diff --git a/test/manual_integration_tests/test_scaling/test_scaling_toplevel_pack.py b/test/manual_integration_tests/test_scaling/test_scaling_toplevel_pack.py
index 3c0d8f7d..498ff5f3 100644
--- a/test/manual_integration_tests/test_scaling/test_scaling_toplevel_pack.py
+++ b/test/manual_integration_tests/test_scaling/test_scaling_toplevel_pack.py
@@ -1,6 +1,8 @@
+import locale
import tkinter
import customtkinter # <- import the CustomTkinter module
+locale.setlocale(locale.LC_NUMERIC, 'de_DE') # to verify that the canvas float argument bug is properly averted
customtkinter.ScalingTracker.set_window_scaling(0.5)
customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light"
diff --git a/test/manual_integration_tests/test_scrollable_frame.py b/test/manual_integration_tests/test_scrollable_frame.py
index 808a4ca2..3929244f 100644
--- a/test/manual_integration_tests/test_scrollable_frame.py
+++ b/test/manual_integration_tests/test_scrollable_frame.py
@@ -28,7 +28,7 @@
frame_5 = customtkinter.CTkScrollableFrame(app, orientation="vertical", label_text="CTkScrollableFrame", corner_radius=0)
frame_5.grid(row=0, column=2, rowspan=2, sticky="nsew")
-for i in range(100):
+for i in range(20):
customtkinter.CTkCheckBox(frame_1).grid(row=i, padx=10, pady=10)
customtkinter.CTkCheckBox(frame_2).grid(row=i, padx=10, pady=10)
customtkinter.CTkCheckBox(frame_3).grid(row=0, column=i, padx=10, pady=10)
diff --git a/test/manual_integration_tests/test_segmented_button.py b/test/manual_integration_tests/test_segmented_button.py
index 79ef7ce4..0c3111d7 100644
--- a/test/manual_integration_tests/test_segmented_button.py
+++ b/test/manual_integration_tests/test_segmented_button.py
@@ -55,27 +55,35 @@
label_seg_5 = customtkinter.CTkLabel(app, textvariable=seg_5_var)
label_seg_5.pack(padx=20, pady=20)
-seg_6_var = customtkinter.StringVar(value="kfasjkfdklaj")
-seg_6 = customtkinter.CTkSegmentedButton(app, width=300)
-seg_6.pack(padx=20, pady=20)
-entry_6 = customtkinter.CTkEntry(app)
-entry_6.pack(padx=20, pady=(0, 20))
-button_6 = customtkinter.CTkButton(app, text="set", command=lambda: seg_6.set(entry_6.get()))
-button_6.pack(padx=20, pady=(0, 20))
-button_6 = customtkinter.CTkButton(app, text="insert value", command=lambda: seg_6.insert(0, entry_6.get()))
-button_6.pack(padx=20, pady=(0, 20))
-label_6 = customtkinter.CTkLabel(app, textvariable=seg_6_var)
-label_6.pack(padx=20, pady=(0, 20))
-
-seg_6.configure(height=50, variable=seg_6_var)
-seg_6.delete("CTkSegmentedButton")
-
-seg_7 = customtkinter.CTkSegmentedButton(app, values=["disabled seg button", "2", "3"])
-seg_7.pack(padx=20, pady=20)
-seg_7.configure(state="disabled")
-seg_7.set("2")
-
-seg_7.configure(height=40, width=400,
+seg_6 = customtkinter.CTkSegmentedButton(app, corner_radius=20, values=["value 1", "value 2", "value 3"], background_corner_colors=["red", "orange", "green", "blue"], orientation="vertical")
+seg_6.set("value 2")
+seg_6.pack(side="left", padx=40)
+
+seg_7 = customtkinter.CTkSegmentedButton(app, corner_radius=40, values=["value 4", "value 5", "value 6"], orientation="vertical")
+seg_7.set("value 6")
+seg_7.pack(side="left")
+
+seg_8_var = customtkinter.StringVar(value="kfasjkfdklaj")
+seg_8 = customtkinter.CTkSegmentedButton(app, width=300)
+seg_8.pack(padx=20, pady=20)
+entry_8 = customtkinter.CTkEntry(app)
+entry_8.pack(padx=20, pady=(0, 20))
+button_8 = customtkinter.CTkButton(app, text="set", command=lambda: seg_8.set(entry_8.get()))
+button_8.pack(padx=20, pady=(0, 20))
+button_8 = customtkinter.CTkButton(app, text="insert value", command=lambda: seg_8.insert(0, entry_8.get()))
+button_8.pack(padx=20, pady=(0, 20))
+label_8 = customtkinter.CTkLabel(app, textvariable=seg_8_var)
+label_8.pack(padx=20, pady=(0, 20))
+
+seg_8.configure(height=50, variable=seg_8_var)
+seg_8.delete("CTkSegmentedButton")
+
+seg_9 = customtkinter.CTkSegmentedButton(app, values=["disabled seg button", "2", "3"])
+seg_9.pack(padx=20, pady=20)
+seg_9.configure(state="disabled")
+seg_9.set("2")
+
+seg_9.configure(height=40, width=400,
dynamic_resizing=False, font=("Times", -20))
app.mainloop()