From 344b30e68420c80dc2f2de97c44e87710864bb6e Mon Sep 17 00:00:00 2001 From: tschiman Date: Tue, 11 Jul 2023 14:15:06 +0200 Subject: [PATCH 01/33] fixed scrollable frame mouse wheel on linux #1356 --- .../windows/widgets/ctk_scrollable_frame.py | 15 ++++++++++++--- .../test_scrollable_frame.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/customtkinter/windows/widgets/ctk_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py index 685deb2f..20b3a68e 100644 --- a/customtkinter/windows/widgets/ctk_scrollable_frame.py +++ b/customtkinter/windows/widgets/ctk_scrollable_frame.py @@ -74,7 +74,13 @@ 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="+") + + if "linux" in sys.platform: + self.bind_all("", self._mouse_wheel_all, add="+") + self.bind_all("", self._mouse_wheel_all, add="+") + else: + 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="+") @@ -243,6 +249,8 @@ 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): @@ -263,10 +271,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 diff --git a/test/manual_integration_tests/test_scrollable_frame.py b/test/manual_integration_tests/test_scrollable_frame.py index c7c0127a..a5a377ad 100644 --- a/test/manual_integration_tests/test_scrollable_frame.py +++ b/test/manual_integration_tests/test_scrollable_frame.py @@ -26,7 +26,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) From 9420ac371aea3efe545c1596345cecb6b1ab39d2 Mon Sep 17 00:00:00 2001 From: Tom Schimansky <66446067+TomSchimansky@users.noreply.github.com> Date: Sun, 28 Dec 2025 15:10:09 +0100 Subject: [PATCH 02/33] Enhance README with developer info and formatting Updated the README to include developer contact information and improved formatting. --- Readme.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Readme.md b/Readme.md index d42ad15e..1589973a 100644 --- a/Readme.md +++ b/Readme.md @@ -6,27 +6,32 @@

- + ![PyPI](https://img.shields.io/pypi/v/customtkinter) ![PyPI - Downloads](https://img.shields.io/pypi/dm/customtkinter?color=green&label=downloads) ![Downloads last 6 month](https://static.pepy.tech/personalized-badge/customtkinter?period=total&units=international_system&left_color=grey&right_color=green&left_text=total%20downloads) ![PyPI - License](https://img.shields.io/badge/license-MIT-blue) -
- --- -
+| Current Developers | Contact | +|----------------------------------------------|-| +| Tom Schimansky | contact@customtkinter.tomschimansky.com | +| Federico Spada | | + Paypal donation button - -| 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 From 9a9453ab787285594d9ca135e6d03784cc712bcb Mon Sep 17 00:00:00 2001 From: Tom Schimansky Date: Sun, 28 Dec 2025 15:33:03 +0100 Subject: [PATCH 03/33] add dev notes --- dev-proces.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 dev-proces.md diff --git a/dev-proces.md b/dev-proces.md new file mode 100644 index 00000000..294ee07f --- /dev/null +++ b/dev-proces.md @@ -0,0 +1,24 @@ +Development on `/develop` branch. +- 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 From a6e07228004f729ba2473c51cddac252d1f3f1f2 Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Thu, 1 Jan 2026 20:56:32 +0100 Subject: [PATCH 04/33] Created Showroom App Added a predefined App to display all available widgets, similar to complex_example.py, but directly available in the downloaded version of the library, so that any user can see it without the need to download a separate file. --- .gitignore | 1 + Readme.md | 7 +- customtkinter/__init__.py | 208 ++++++++++++++++++++++++++++++++++++ examples/complex_example.py | 2 +- 4 files changed, 216 insertions(+), 2 deletions(-) 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/Readme.md b/Readme.md index 1589973a..eb2cabaf 100644 --- a/Readme.md +++ b/Readme.md @@ -17,7 +17,7 @@ | Current Developers | Contact | |----------------------------------------------|-| | Tom Schimansky | contact@customtkinter.tomschimansky.com | -| Federico Spada | | +| Federico Spada | linkedin.com/in/federicospada13 | Paypal donation button @@ -93,6 +93,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..9b5fcd54 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -1,5 +1,6 @@ __version__ = "5.2.2" +from typing import Optional import os import sys from tkinter import Variable, StringVar, IntVar, DoubleVar, BooleanVar @@ -86,3 +87,210 @@ 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") + + new_theme: Optional[str] = "blue" + while new_theme: + set_default_color_theme(new_theme) + + app = _Showroom() + app.mainloop() + new_theme = app.new_theme + + +class _Showroom(CTk): + SPACING = 20 + + def __init__(self) -> None: + super().__init__() + + # configure window + self.title("CustomTkinter complex_example.py") + self.geometry(f"{1100}x{580}") + + self.new_theme: Optional[str] = None + + # 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) + self.scaling_optionmenu.set("100%") + + 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)) + + # 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_button = CTkSegmentedButton(self.choices_frame, values=["CTkSegmentedButton", "Value 2", "Value 3"]) + self.seg_button.set("CTkSegmentedButton") + + 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_button.pack(padx=20, pady=(self.SPACING, 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=400) + 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"]) + self.tabview.add("CTkTabview") + self.tabview.add("Tab 2") + self.tabview.add("Tab 3") + + 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(padx=20, pady=(self.SPACING, 5)) + self.tabview.pack(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: + self.new_theme = new_theme + self.destroy() 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") From ebd3b71f81ac02875216acabfdb648be3edf67a9 Mon Sep 17 00:00:00 2001 From: QwertyZen Date: Fri, 2 Jan 2026 02:35:49 +0530 Subject: [PATCH 05/33] fix: Add missing scaling base class destroy in dropdown_menu. (#2772) Co-authored-by: Anustuv Pal --- .../windows/widgets/core_widget_classes/dropdown_menu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py index a6b8186f..6c0cce12 100644 --- a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py +++ b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py @@ -50,6 +50,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 """ From 2e11a1e25403ef97262b042d610d2112376a7ec0 Mon Sep 17 00:00:00 2001 From: Luca Heyworth <162014348+lucaheyworth@users.noreply.github.com> Date: Fri, 2 Jan 2026 13:32:58 +0000 Subject: [PATCH 06/33] Update Readme.md to answer issues/2668 (#2705) Co-authored-by: lucaheyworth --- Readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index eb2cabaf..d798596c 100644 --- a/Readme.md +++ b/Readme.md @@ -11,13 +11,14 @@ ![PyPI - Downloads](https://img.shields.io/pypi/dm/customtkinter?color=green&label=downloads) ![Downloads last 6 month](https://static.pepy.tech/personalized-badge/customtkinter?period=total&units=international_system&left_color=grey&right_color=green&left_text=total%20downloads) ![PyPI - License](https://img.shields.io/badge/license-MIT-blue) +![Minimum Python Version](https://shields.io/badge/Minimum_Python-3.7-blue) --- | Current Developers | Contact | |----------------------------------------------|-| | Tom Schimansky | contact@customtkinter.tomschimansky.com | -| Federico Spada | linkedin.com/in/federicospada13 | +| Federico Spada | www.linkedin.com/in/federicospada13 | Paypal donation button From 764a30288411b55bdfcd2714825d125cfd3e1101 Mon Sep 17 00:00:00 2001 From: Yimin Liu <122608577+HelloWorld-er@users.noreply.github.com> Date: Fri, 2 Jan 2026 15:32:17 -0500 Subject: [PATCH 07/33] update outdated requirements.txt (#2571) Co-authored-by: LYMwyh <122608577+LYMwyh@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 76a10bd1cb0b18c829e535b39a504183d965482b Mon Sep 17 00:00:00 2001 From: Bruno Marques Date: Sat, 3 Jan 2026 20:30:06 +0000 Subject: [PATCH 08/33] Widget-scaled int values are cast back to int (#2468) Prevents triggering a bug on Tk canvas, where float parameters use locally-aware parsing and thus might expect a different decimal point character Fixes #571 --- .../widgets/scaling/scaling_base_class.py | 19 +++++++++++++++---- .../test_scaling/test_scaling_simple_place.py | 2 ++ .../test_scaling_toplevel_pack.py | 2 ++ 3 files changed, 19 insertions(+), 4 deletions(-) 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/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" From 5c77c99c3c214036d8626b20e0111c1f70ff946d Mon Sep 17 00:00:00 2001 From: Jeremiah Date: Sat, 3 Jan 2026 16:34:52 -0500 Subject: [PATCH 09/33] fix scrollable frame destroy() (#2352) CTkScrollableFrame was not fully removed when its destroy() method was invoked. --- customtkinter/windows/widgets/ctk_scrollable_frame.py | 1 + 1 file changed, 1 insertion(+) diff --git a/customtkinter/windows/widgets/ctk_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py index 83b7c4f7..85f335e8 100644 --- a/customtkinter/windows/widgets/ctk_scrollable_frame.py +++ b/customtkinter/windows/widgets/ctk_scrollable_frame.py @@ -98,6 +98,7 @@ def __init__(self, def destroy(self): tkinter.Frame.destroy(self) + self._parent_frame.destroy() CTkAppearanceModeBaseClass.destroy(self) CTkScalingBaseClass.destroy(self) From 27db1bd58308bb6eff474179121679993fca7ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20G=C3=B6rl?= Date: Sat, 3 Jan 2026 23:09:05 +0100 Subject: [PATCH 10/33] Improved renaming method in the CTkTabview class (#2256) Changes: 1. The renaming method in the `CTkTabview` class has been altered. Instead of removing the old name from the `_name_list` and appending the new name at the end, the new name now replaces the old name at the same index in the `_name_list`. This change makes the renaming process more efficient. 2. Code has been added to update the `_current_name` attribute if the old name matches the current name. This is a crucial change as it ensures that the connection to the frame is not lost when a tab is renamed. --- customtkinter/windows/widgets/ctk_tabview.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py index 3b2ea5bb..c69e36d6 100644 --- a/customtkinter/windows/widgets/ctk_tabview.py +++ b/customtkinter/windows/widgets/ctk_tabview.py @@ -382,12 +382,15 @@ 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 """ From cea11a0b70a23a131251fa02882fa28f33a74d0d Mon Sep 17 00:00:00 2001 From: bibo Date: Sun, 4 Jan 2026 11:33:08 +0800 Subject: [PATCH 11/33] Add segmented_button_font option to CTkTabview --- customtkinter/windows/widgets/ctk_tabview.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py index 3b2ea5bb..18e16123 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() @@ -278,6 +284,10 @@ def configure(self, require_redraw=False, **kwargs): if "text_color_disabled" in kwargs: self._segmented_button.configure(text_color_disabled=kwargs.pop("text_color_disabled")) + 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 "command" in kwargs: self._command = kwargs.pop("command") if "anchor" in kwargs: @@ -314,6 +324,9 @@ def cget(self, attribute_name: str): elif attribute_name == "text_color_disabled": return self._segmented_button.cget(attribute_name) + elif attribute_name == "segmented_button_font": + return self._segmented_button_font + elif attribute_name == "command": return self._command elif attribute_name == "anchor": From 9ad9495e0c05901a7664ec1aba406b06094bf705 Mon Sep 17 00:00:00 2001 From: Nerogar Date: Sun, 4 Jan 2026 23:26:10 +0100 Subject: [PATCH 12/33] fixed an exception when destroying a dropdown menu, then rescaling the screen (#2246) From 8ff5d94abddcd1e25ad491b1039e87bdcff0b863 Mon Sep 17 00:00:00 2001 From: DerSchinken <53398996+DerSchinken@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:34:27 +0100 Subject: [PATCH 13/33] CtkCanvas.coords add return values (fixes #1419) (#2240) --- .../windows/widgets/core_rendering/ctk_canvas.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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() From 84222abe997aa8daee5c24d3dfedfe4ed6e17e90 Mon Sep 17 00:00:00 2001 From: timgdx <39223220+timgdx@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:22:59 +0000 Subject: [PATCH 14/33] Fix #1511 and #2160: Toplevel icon overriding on init (#2162) Co-authored-by: gdx --- customtkinter/windows/ctk_toplevel.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index 9780380b..d8083111 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 @@ -203,6 +195,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 From 907300ed1294d99d91b14941b3540fce589a364c Mon Sep 17 00:00:00 2001 From: timgdx <39223220+timgdx@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:51:32 +0000 Subject: [PATCH 15/33] Added Scrollbar drag offset (#2158) Co-authored-by: gdx --- customtkinter/windows/widgets/ctk_scrollbar.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py index 8e962215..60e31262 100644 --- a/customtkinter/windows/widgets/ctk_scrollbar.py +++ b/customtkinter/windows/widgets/ctk_scrollbar.py @@ -78,12 +78,13 @@ 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) + self._canvas.bind("", self._on_motion) if sequence is None or sequence == "": self._canvas.bind("", self._mouse_scroll_event) @@ -228,10 +229,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))) From 207027795053b379193620e30846fc6058494877 Mon Sep 17 00:00:00 2001 From: Alex McPherson Date: Mon, 5 Jan 2026 16:53:49 -0400 Subject: [PATCH 16/33] Quick fix of set text_color operation (#2078) Looks like there was a leftover variable waiting to be replaced from a copy-paste. --- customtkinter/windows/ctk_input_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From b2bb1e0626d596929f309f2d15843c5ba6670045 Mon Sep 17 00:00:00 2001 From: Soli Como Date: Mon, 5 Jan 2026 16:15:01 -0500 Subject: [PATCH 17/33] Fix `text_color_disabled` of `CTkLabel`. (#2063) --- customtkinter/windows/widgets/ctk_label.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customtkinter/windows/widgets/ctk_label.py b/customtkinter/windows/widgets/ctk_label.py index 7e59ad76..c046f5d0 100644 --- a/customtkinter/windows/widgets/ctk_label.py +++ b/customtkinter/windows/widgets/ctk_label.py @@ -50,7 +50,7 @@ def __init__(self, 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: From f81cd8d34c54414eecb53645d52f396fb3531bb8 Mon Sep 17 00:00:00 2001 From: Logan Cederlof <68718280+DragonOfShuu@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:59:21 -0700 Subject: [PATCH 18/33] Reset Minsize for CTkButton (#1931) Fixed issue #1899 by resetting the minsize --- customtkinter/windows/widgets/ctk_button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/customtkinter/windows/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py index e9f78391..f1045f98 100644 --- a/customtkinter/windows/widgets/ctk_button.py +++ b/customtkinter/windows/widgets/ctk_button.py @@ -314,7 +314,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 +323,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) From f159a251925ac4ca2b5485760ad7855226cf6dd2 Mon Sep 17 00:00:00 2001 From: Philip Nelson Date: Tue, 6 Jan 2026 13:11:04 -0700 Subject: [PATCH 19/33] Add an orientation option to CTkSegmentedButton (#2333) --- CHANGELOG.md | 3 ++ .../windows/widgets/ctk_segmented_button.py | 52 ++++++++++++++----- .../test_segmented_button.py | 50 ++++++++++-------- 3 files changed, 71 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d432f85..f1b31518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ 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 +## [Unreleased] +### Added + - Added an option to CTkSegmentedButton to make it layout vertically instead of horizontally ## [5.2.0] - 2022-05-02 ### Added diff --git a/customtkinter/windows/widgets/ctk_segmented_button.py b/customtkinter/windows/widgets/ctk_segmented_button.py index b8de1e79..fa909dfb 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 @@ -352,6 +376,8 @@ def cget(self, attribute_name: str) -> any: return self._dynamic_resizing elif attribute_name == "command": return self._command + 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.") 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() From 7bb56101ebcfea127faef3e542a5568bc6866cfc Mon Sep 17 00:00:00 2001 From: fgosdrauka <62940501+fgosdrauka@users.noreply.github.com> Date: Tue, 6 Jan 2026 21:56:09 +0100 Subject: [PATCH 20/33] Added new methods to ctk_tabview.py (#1428) Adding two methods to the class CTkTabview": "index" to return the index (position) of the current tab or the tab entered as an attribute. "len" method to return the number of defined tabs Modifying the "get" method: it now returns the name of the selected tab, or an empty string if no tab is selected. If an index is defined, it returns the name associated with that index. --- customtkinter/windows/widgets/ctk_tabview.py | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py index 2acdb536..9ebf51cb 100644 --- a/customtkinter/windows/widgets/ctk_tabview.py +++ b/customtkinter/windows/widgets/ctk_tabview.py @@ -444,6 +444,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: str = "") -> int: + """ returns index of selected tab, raises ValueError if no tab selected\n + if a name is provided, returns the associated index or raises ValueError if no tab is found """ + if name == "": + 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) From f605aba388a2060f51179b5772bb766279b48aee Mon Sep 17 00:00:00 2001 From: darkManPi Date: Sat, 10 Jan 2026 04:00:37 +0800 Subject: [PATCH 21/33] Fix typo bug causing app slowdown on multiple refreshes by preventing redundant object creation (#2680) Co-authored-by: Dariusz Klofik --- customtkinter/windows/widgets/core_rendering/draw_engine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/customtkinter/windows/widgets/core_rendering/draw_engine.py b/customtkinter/windows/widgets/core_rendering/draw_engine.py index 5acea560..a213289b 100644 --- a/customtkinter/windows/widgets/core_rendering/draw_engine.py +++ b/customtkinter/windows/widgets/core_rendering/draw_engine.py @@ -651,12 +651,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 From a0a6496859c50ff5a1ce6e6f477d0b0ad6eca537 Mon Sep 17 00:00:00 2001 From: ElectricCandlelight <53838695+ElectricCandlelight@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:08:31 +0000 Subject: [PATCH 22/33] Fix .delete() to destroy ctkframe fixes#1046 (#1083) Co-authored-by: ElectricCandlelight --- customtkinter/windows/widgets/ctk_tabview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py index 9ebf51cb..e9590a83 100644 --- a/customtkinter/windows/widgets/ctk_tabview.py +++ b/customtkinter/windows/widgets/ctk_tabview.py @@ -409,7 +409,7 @@ def delete(self, name: str): 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) From 8d62feb92d9e89a114c06f660e70dff1192236eb Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Sun, 11 Jan 2026 16:23:20 +0100 Subject: [PATCH 23/33] Improved/fixed cget() method for all widgets Added missing attributes to the cget() method for all widgets. Fixed some attribute names and positions within the method. Fixes #1212, #2615, #2756 --- customtkinter/windows/ctk_tk.py | 1 + customtkinter/windows/ctk_toplevel.py | 1 + .../core_widget_classes/dropdown_menu.py | 3 ++- customtkinter/windows/widgets/ctk_button.py | 1 + customtkinter/windows/widgets/ctk_checkbox.py | 13 ++++++---- customtkinter/windows/widgets/ctk_combobox.py | 1 + .../windows/widgets/ctk_radiobutton.py | 10 ++++---- .../windows/widgets/ctk_scrollable_frame.py | 24 ++++++++++--------- .../windows/widgets/ctk_scrollbar.py | 4 ++-- .../windows/widgets/ctk_segmented_button.py | 14 +++++------ customtkinter/windows/widgets/ctk_switch.py | 10 ++++---- customtkinter/windows/widgets/ctk_tabview.py | 7 +++--- customtkinter/windows/widgets/ctk_textbox.py | 8 +++++++ .../windows/widgets/font/ctk_font.py | 3 +-- .../windows/widgets/image/ctk_image.py | 4 ++-- 15 files changed, 60 insertions(+), 44 deletions(-) diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index e137dc3c..e6238521 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -220,6 +220,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 d8083111..1817950d 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -188,6 +188,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/widgets/core_widget_classes/dropdown_menu.py b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py index 6c0cce12..1104cb55 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 @@ -165,7 +166,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 f1045f98..1ce5484c 100644 --- a/customtkinter/windows/widgets/ctk_button.py +++ b/customtkinter/windows/widgets/ctk_button.py @@ -480,6 +480,7 @@ def cget(self, attribute_name: str) -> any: return self._compound elif attribute_name == "anchor": return self._anchor + else: return super().cget(attribute_name) diff --git a/customtkinter/windows/widgets/ctk_checkbox.py b/customtkinter/windows/widgets/ctk_checkbox.py index 42f04f5e..00372e12 100644 --- a/customtkinter/windows/widgets/ctk_checkbox.py +++ b/customtkinter/windows/widgets/ctk_checkbox.py @@ -305,14 +305,14 @@ def configure(self, require_redraw=False, **kwargs): 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 +337,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) diff --git a/customtkinter/windows/widgets/ctk_combobox.py b/customtkinter/windows/widgets/ctk_combobox.py index 99495641..f1fa759a 100644 --- a/customtkinter/windows/widgets/ctk_combobox.py +++ b/customtkinter/windows/widgets/ctk_combobox.py @@ -338,6 +338,7 @@ 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) diff --git a/customtkinter/windows/widgets/ctk_radiobutton.py b/customtkinter/windows/widgets/ctk_radiobutton.py index c07cd1f0..4f440f52 100644 --- a/customtkinter/windows/widgets/ctk_radiobutton.py +++ b/customtkinter/windows/widgets/ctk_radiobutton.py @@ -290,16 +290,16 @@ def configure(self, require_redraw=False, **kwargs): 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 diff --git a/customtkinter/windows/widgets/ctk_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py index 2c486501..bbe2e040 100644 --- a/customtkinter/windows/widgets/ctk_scrollable_frame.py +++ b/customtkinter/windows/widgets/ctk_scrollable_frame.py @@ -222,23 +222,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) diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py index 60e31262..9ef84cea 100644 --- a/customtkinter/windows/widgets/ctk_scrollbar.py +++ b/customtkinter/windows/widgets/ctk_scrollbar.py @@ -200,9 +200,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": diff --git a/customtkinter/windows/widgets/ctk_segmented_button.py b/customtkinter/windows/widgets/ctk_segmented_button.py index fa909dfb..de80f676 100644 --- a/customtkinter/windows/widgets/ctk_segmented_button.py +++ b/customtkinter/windows/widgets/ctk_segmented_button.py @@ -340,17 +340,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": @@ -365,6 +359,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 @@ -376,11 +372,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: diff --git a/customtkinter/windows/widgets/ctk_switch.py b/customtkinter/windows/widgets/ctk_switch.py index 155c1746..1cbf524e 100644 --- a/customtkinter/windows/widgets/ctk_switch.py +++ b/customtkinter/windows/widgets/ctk_switch.py @@ -347,16 +347,16 @@ def configure(self, require_redraw=False, **kwargs): 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 diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py index e9590a83..5a60445d 100644 --- a/customtkinter/windows/widgets/ctk_tabview.py +++ b/customtkinter/windows/widgets/ctk_tabview.py @@ -309,6 +309,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": @@ -319,14 +320,14 @@ 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": return self._segmented_button.cget(attribute_name) - elif attribute_name == "segmented_button_font": - return self._segmented_button_font - elif attribute_name == "command": return self._command elif attribute_name == "anchor": diff --git a/customtkinter/windows/widgets/ctk_textbox.py b/customtkinter/windows/widgets/ctk_textbox.py index 4b3a165f..83d93f32 100644 --- a/customtkinter/windows/widgets/ctk_textbox.py +++ b/customtkinter/windows/widgets/ctk_textbox.py @@ -319,10 +319,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..405f6d84 100644 --- a/customtkinter/windows/widgets/font/ctk_font.py +++ b/customtkinter/windows/widgets/font/ctk_font.py @@ -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..93e10ec7 100644 --- a/customtkinter/windows/widgets/image/ctk_image.py +++ b/customtkinter/windows/widgets/image/ctk_image.py @@ -69,9 +69,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): From cb7347bd22226224e853f650cb992ef68cffb1bd Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Sun, 11 Jan 2026 17:12:33 +0100 Subject: [PATCH 24/33] Added border to CTkLabel Added the possibility to specify border_width and border_color to the CTkLabel, so that there is no need to use CTkButton as a Static Label if a border is needed. By default, it is still hidden as before. Fixes #2612 --- customtkinter/assets/themes/blue.json | 2 ++ customtkinter/assets/themes/dark-blue.json | 2 ++ customtkinter/assets/themes/green.json | 2 ++ customtkinter/windows/widgets/ctk_label.py | 40 +++++++++++++++++----- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json index 192c1362..a1c2451e 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": { diff --git a/customtkinter/assets/themes/dark-blue.json b/customtkinter/assets/themes/dark-blue.json index 54ff211d..9cc5e441 100644 --- a/customtkinter/assets/themes/dark-blue.json +++ b/customtkinter/assets/themes/dark-blue.json @@ -23,7 +23,9 @@ }, "CTkLabel": { "corner_radius": 0, + "border_width": 0, "fg_color": "transparent", + "border_color": ["#979DA2", "#565B5E"], "text_color": ["gray14", "gray84"] }, "CTkEntry": { diff --git a/customtkinter/assets/themes/green.json b/customtkinter/assets/themes/green.json index 200012f7..85a8d234 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": { diff --git a/customtkinter/windows/widgets/ctk_label.py b/customtkinter/windows/widgets/ctk_label.py index c046f5d0..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,6 +48,7 @@ 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: @@ -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": From 6a5460b967d1745855a95a6665af42c919941b82 Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Sun, 11 Jan 2026 22:01:26 +0100 Subject: [PATCH 25/33] Added MouseWheel detection to CTkSlider and fixed it on CTkScrollbar Implemented the MouseWheel scroll detection for the CTkSlider to update its value based on the new attribute scroll_step. Added the management of MouseWheel scroll detection to CTkScrollbar, even when the command function is not provided. In this way, at least the widget is responsive. Fixed MouseWheel scroll detection on CTkScrollbar that didn't work on Linux systems. Fixes #2388, #2777 and replace #2365 Co-Authored-By: Saul Velazquez <141290146+save-27@users.noreply.github.com> --- .../windows/widgets/ctk_scrollbar.py | 37 +++++++++++-- customtkinter/windows/widgets/ctk_slider.py | 52 +++++++++++++------ 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py index 9ef84cea..d0147267 100644 --- a/customtkinter/windows/widgets/ctk_scrollbar.py +++ b/customtkinter/windows/widgets/ctk_scrollbar.py @@ -85,8 +85,14 @@ def _create_bindings(self, sequence: Optional[str] = None): self._canvas.bind("", self._on_leave) if sequence is None or sequence == "": self._canvas.bind("", self._on_motion) - if sequence is None or sequence == "": - self._canvas.bind("", self._mouse_scroll_event) + 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) @@ -258,9 +264,32 @@ def _on_motion(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 == 5: + delta = -delta + if self._orientation == "vertical": + 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_slider.py b/customtkinter/windows/widgets/ctk_slider.py index 7aa03eeb..6081ef48 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) @@ -313,31 +323,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": From db08925ce9b0c8382f3a83e4e46326c758f18d0a Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Tue, 13 Jan 2026 22:56:01 +0100 Subject: [PATCH 26/33] Improved/fixed configure() method for all widgets - Added missing attributes to the configure() method for all widgets. - Allowed changing a CTkButton image using configure even if the widget was created without any image. - Prevented warning when an image is set to "" with configure() to delete it. - Fixed CTkCheckbox hover_color for the blue theme, which was hiding the hover effect when checked. Fixes #1215, #1750, #2494 and replaces #2412, #2719 Co-Authored-By: Shubham25dec <107737999+shubham25dec@users.noreply.github.com> Co-Authored-By: fred Jose Diaz <33442727+fredmain@users.noreply.github.com> --- customtkinter/assets/themes/blue.json | 2 +- .../core_widget_classes/ctk_base_class.py | 2 +- .../core_widget_classes/dropdown_menu.py | 5 +- customtkinter/windows/widgets/ctk_button.py | 6 +- customtkinter/windows/widgets/ctk_checkbox.py | 67 +++++++++------- customtkinter/windows/widgets/ctk_combobox.py | 1 - customtkinter/windows/widgets/ctk_entry.py | 41 +++++----- customtkinter/windows/widgets/ctk_frame.py | 16 ++-- .../windows/widgets/ctk_optionmenu.py | 5 -- .../windows/widgets/ctk_progressbar.py | 4 - .../windows/widgets/ctk_radiobutton.py | 79 ++++++++++--------- .../windows/widgets/ctk_scrollable_frame.py | 12 +-- .../windows/widgets/ctk_scrollbar.py | 16 ++-- .../windows/widgets/ctk_segmented_button.py | 5 -- customtkinter/windows/widgets/ctk_slider.py | 9 ++- customtkinter/windows/widgets/ctk_switch.py | 75 ++++++++++-------- customtkinter/windows/widgets/ctk_tabview.py | 20 ++++- customtkinter/windows/widgets/ctk_textbox.py | 31 ++++---- .../windows/widgets/font/ctk_font.py | 8 +- .../windows/widgets/image/ctk_image.py | 2 + 20 files changed, 216 insertions(+), 190 deletions(-) diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json index a1c2451e..4be97469 100644 --- a/customtkinter/assets/themes/blue.json +++ b/customtkinter/assets/themes/blue.json @@ -41,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"] 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 1104cb55..1d04a983 100644 --- a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py +++ b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py @@ -120,6 +120,10 @@ def open(self, x: Union[int, float], y: Union[int, float]): self.tk_popup(int(x), int(y)) 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)) @@ -138,7 +142,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: diff --git a/customtkinter/windows/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py index 1ce5484c..bf8b405b 100644 --- a/customtkinter/windows/widgets/ctk_button.py +++ b/customtkinter/windows/widgets/ctk_button.py @@ -402,7 +402,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 +415,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") diff --git a/customtkinter/windows/widgets/ctk_checkbox.py b/customtkinter/windows/widgets/ctk_checkbox.py index 00372e12..7d0bd5ea 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,27 +265,52 @@ 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: diff --git a/customtkinter/windows/widgets/ctk_combobox.py b/customtkinter/windows/widgets/ctk_combobox.py index f1fa759a..6cfd246e 100644 --- a/customtkinter/windows/widgets/ctk_combobox.py +++ b/customtkinter/windows/widgets/ctk_combobox.py @@ -267,7 +267,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: diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py index cdc0220b..e2376fe5 100644 --- a/customtkinter/windows/widgets/ctk_entry.py +++ b/customtkinter/windows/widgets/ctk_entry.py @@ -192,36 +192,36 @@ 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: + self._textvariable = kwargs.pop("textvariable") + self._entry.configure(textvariable=self._textvariable) + if "placeholder_text" in kwargs: self._placeholder_text = kwargs.pop("placeholder_text") if self._placeholder_text_active: @@ -230,19 +230,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 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_optionmenu.py b/customtkinter/windows/widgets/ctk_optionmenu.py index 491027b1..f37af0e3 100644 --- a/customtkinter/windows/widgets/ctk_optionmenu.py +++ b/customtkinter/windows/widgets/ctk_optionmenu.py @@ -262,7 +262,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 +274,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") 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 4f440f52..3426c621 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,14 +269,29 @@ 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: diff --git a/customtkinter/windows/widgets/ctk_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py index bbe2e040..7505471a 100644 --- a/customtkinter/windows/widgets/ctk_scrollable_frame.py +++ b/customtkinter/windows/widgets/ctk_scrollable_frame.py @@ -197,6 +197,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) @@ -205,12 +211,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")) diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py index d0147267..fa21c3a9 100644 --- a/customtkinter/windows/widgets/ctk_scrollbar.py +++ b/customtkinter/windows/widgets/ctk_scrollbar.py @@ -168,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 @@ -186,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: diff --git a/customtkinter/windows/widgets/ctk_segmented_button.py b/customtkinter/windows/widgets/ctk_segmented_button.py index de80f676..4d1e2510 100644 --- a/customtkinter/windows/widgets/ctk_segmented_button.py +++ b/customtkinter/windows/widgets/ctk_segmented_button.py @@ -240,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: @@ -313,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") diff --git a/customtkinter/windows/widgets/ctk_slider.py b/customtkinter/windows/widgets/ctk_slider.py index 6081ef48..e22a06e3 100644 --- a/customtkinter/windows/widgets/ctk_slider.py +++ b/customtkinter/windows/widgets/ctk_slider.py @@ -259,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") @@ -268,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") @@ -312,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": diff --git a/customtkinter/windows/widgets/ctk_switch.py b/customtkinter/windows/widgets/ctk_switch.py index 1cbf524e..43e31170 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,27 +307,52 @@ 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: diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py index 5a60445d..40d7433b 100644 --- a/customtkinter/windows/widgets/ctk_tabview.py +++ b/customtkinter/windows/widgets/ctk_tabview.py @@ -259,41 +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 "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 "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")) diff --git a/customtkinter/windows/widgets/ctk_textbox.py b/customtkinter/windows/widgets/ctk_textbox.py index 83d93f32..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)) diff --git a/customtkinter/windows/widgets/font/ctk_font.py b/customtkinter/windows/widgets/font/ctk_font.py index 405f6d84..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 diff --git a/customtkinter/windows/widgets/image/ctk_image.py b/customtkinter/windows/widgets/image/ctk_image.py index 93e10ec7..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") From b33e220b9855b78dbc3dd832f033692a167a3e16 Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Thu, 15 Jan 2026 22:24:17 +0100 Subject: [PATCH 27/33] Added new utility methods to all widgets Added set(), index(), len() methods for those widgets that were suitable to use them. Fixes #1862 --- customtkinter/__init__.py | 2 + customtkinter/windows/widgets/ctk_checkbox.py | 37 ++++++---------- customtkinter/windows/widgets/ctk_combobox.py | 8 ++++ customtkinter/windows/widgets/ctk_entry.py | 4 ++ .../windows/widgets/ctk_optionmenu.py | 8 ++++ .../windows/widgets/ctk_radiobutton.py | 26 +++++------ .../windows/widgets/ctk_segmented_button.py | 13 +++++- customtkinter/windows/widgets/ctk_slider.py | 6 +-- customtkinter/windows/widgets/ctk_switch.py | 43 ++++++------------- customtkinter/windows/widgets/ctk_tabview.py | 12 ++---- 10 files changed, 76 insertions(+), 83 deletions(-) diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index 9b5fcd54..27392868 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -87,6 +87,8 @@ 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") diff --git a/customtkinter/windows/widgets/ctk_checkbox.py b/customtkinter/windows/widgets/ctk_checkbox.py index 7d0bd5ea..33cd3a7f 100644 --- a/customtkinter/windows/widgets/ctk_checkbox.py +++ b/customtkinter/windows/widgets/ctk_checkbox.py @@ -416,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 6cfd246e..aae60680 100644 --- a/customtkinter/windows/widgets/ctk_combobox.py +++ b/customtkinter/windows/widgets/ctk_combobox.py @@ -396,6 +396,14 @@ 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: self._open_dropdown_menu() diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py index e2376fe5..d6d8a4b6 100644 --- a/customtkinter/windows/widgets/ctk_entry.py +++ b/customtkinter/windows/widgets/ctk_entry.py @@ -334,6 +334,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_optionmenu.py b/customtkinter/windows/widgets/ctk_optionmenu.py index f37af0e3..d1b9afca 100644 --- a/customtkinter/windows/widgets/ctk_optionmenu.py +++ b/customtkinter/windows/widgets/ctk_optionmenu.py @@ -391,6 +391,14 @@ 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: self._open_dropdown_menu() diff --git a/customtkinter/windows/widgets/ctk_radiobutton.py b/customtkinter/windows/widgets/ctk_radiobutton.py index 3426c621..5c4ce845 100644 --- a/customtkinter/windows/widgets/ctk_radiobutton.py +++ b/customtkinter/windows/widgets/ctk_radiobutton.py @@ -382,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_segmented_button.py b/customtkinter/windows/widgets/ctk_segmented_button.py index 4d1e2510..ed575666 100644 --- a/customtkinter/windows/widgets/ctk_segmented_button.py +++ b/customtkinter/windows/widgets/ctk_segmented_button.py @@ -402,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: @@ -425,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 e22a06e3..5e2f8f8b 100644 --- a/customtkinter/windows/widgets/ctk_slider.py +++ b/customtkinter/windows/widgets/ctk_slider.py @@ -379,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: @@ -404,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 43e31170..5deeb5d6 100644 --- a/customtkinter/windows/widgets/ctk_switch.py +++ b/customtkinter/windows/widgets/ctk_switch.py @@ -403,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 40d7433b..4b805601 100644 --- a/customtkinter/windows/widgets/ctk_tabview.py +++ b/customtkinter/windows/widgets/ctk_tabview.py @@ -385,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: @@ -465,10 +461,10 @@ def get(self, index: Optional[int] = None) -> str: else: return self._name_list[index] - def index(self, name: str = "") -> int: - """ returns index of selected tab, raises ValueError if no tab selected\n - if a name is provided, returns the associated index or raises ValueError if no tab is found """ - if name == "": + 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) From 73ca84f0530b1ec84cee3f97c96d35431d8e3a0a Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Sat, 17 Jan 2026 19:33:39 +0100 Subject: [PATCH 28/33] Improved user experience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CTkButton triggers the command when the Mouse Button is released, allowing the user to cancel the click by releasing the button outside the widget - CTkEntry and CTkTextbox lose focus when you click somewhere else - Clicking the Dropdown button again closes the menu for CTkComboBox and CTkOptionMenu Fixes #2126, #2257, #2386, #2722 and replaces #2251, #2736 Co-Authored-By: Jan Görl Co-Authored-By: Rivka Sternbuch <210291359+riki-sternbuch@users.noreply.github.com> --- customtkinter/__init__.py | 3 +-- customtkinter/windows/ctk_tk.py | 2 ++ customtkinter/windows/ctk_toplevel.py | 2 ++ .../core_widget_classes/dropdown_menu.py | 6 +++++ customtkinter/windows/widgets/ctk_button.py | 24 ++++++++++--------- customtkinter/windows/widgets/ctk_combobox.py | 8 ++++++- .../windows/widgets/ctk_optionmenu.py | 8 ++++++- .../windows/widgets/ctk_scrollable_frame.py | 14 +++++------ 8 files changed, 45 insertions(+), 22 deletions(-) diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index 27392868..af1ef963 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -108,8 +108,7 @@ def __init__(self) -> None: super().__init__() # configure window - self.title("CustomTkinter complex_example.py") - self.geometry(f"{1100}x{580}") + self.title("CustomTkinter Showroom") self.new_theme: Optional[str] = None diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index e6238521..cdb70823 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -79,6 +79,8 @@ def __init__(self, self.bind('', self._update_dimensions_event) self.bind('', self._focus_in_event) + #allows CTkEntry and CTkTextbox to lose focus + self.bind_all("", lambda event: event.widget.focus_set(), add=True) def destroy(self): self._disable_macos_dark_title_bar() diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index 1817950d..2d36586a 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -75,6 +75,8 @@ def __init__(self, *args, self.bind('', self._update_dimensions_event) self.bind('', self._focus_in_event) + #allows CTkEntry and CTkTextbox to lose focus + self.bind_all("", lambda event: event.widget.focus_set(), add=True) def destroy(self): self._disable_macos_dark_title_bar() diff --git a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py index 1d04a983..7c4648b9 100644 --- a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py +++ b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py @@ -119,6 +119,12 @@ 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") diff --git a/customtkinter/windows/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py index bf8b405b..0e1b28e9 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) @@ -232,8 +233,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) @@ -266,8 +267,8 @@ def _draw(self, no_color_updates=False): 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) @@ -501,6 +502,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 @@ -521,6 +523,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": @@ -545,9 +548,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_combobox.py b/customtkinter/windows/widgets/ctk_combobox.py index aae60680..e5d146eb 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) @@ -215,6 +216,7 @@ def _draw(self, no_color_updates=False): fill=self._apply_appearance_mode(self._text_color)) def _open_dropdown_menu(self): + self._close_on_next_click = True self._dropdown_menu.open(self.winfo_rootx(), self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0)) @@ -342,6 +344,7 @@ def cget(self, attribute_name: str) -> any: 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") @@ -405,7 +408,10 @@ def index(self, value: Optional[Any] = None) -> int: 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_optionmenu.py b/customtkinter/windows/widgets/ctk_optionmenu.py index d1b9afca..8c66d1b4 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) @@ -346,10 +347,12 @@ def cget(self, attribute_name: str) -> any: return super().cget(attribute_name) def _open_dropdown_menu(self): + self._close_on_next_click = True self._dropdown_menu.open(self.winfo_rootx(), self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0)) 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", @@ -400,7 +403,10 @@ def index(self, value: Optional[Any] = None) -> int: 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_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py index 7505471a..5dcd30ff 100644 --- a/customtkinter/windows/widgets/ctk_scrollable_frame.py +++ b/customtkinter/windows/widgets/ctk_scrollable_frame.py @@ -77,15 +77,15 @@ def __init__(self, self._parent_canvas.bind("", self._fit_frame_dimensions_to_canvas) if "linux" in sys.platform: - self.bind_all("", self._mouse_wheel_all, add="+") - self.bind_all("", self._mouse_wheel_all, add="+") + 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="+") + self.bind_all("", self._mouse_wheel_all, add=True) - 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="+") + 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": From 73bc7ad33523a370859f908b9a31231cf03faa94 Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Sun, 18 Jan 2026 19:23:39 +0100 Subject: [PATCH 29/33] Recreated 3 Pull Requests that weren't perfect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Properly managed textvariable attribute for CTkEntry - Properly justified Label and Image inside CTkButton - Added gold theme and standardized spacing and case for the others Fixes #1981, #2743 and replaces #2077, #2173, #2741 Co-Authored-By: federicomassi <5929004+federicomassi@users.noreply.github.com> Co-Authored-By: Pedro Perdigão <91698253+p3rdigas@users.noreply.github.com> Co-Authored-By: Nerogar <3390934+nerogar@users.noreply.github.com> --- customtkinter/assets/themes/blue.json | 4 +- customtkinter/assets/themes/dark-blue.json | 30 ++-- customtkinter/assets/themes/gold.json | 157 ++++++++++++++++++ customtkinter/assets/themes/green.json | 10 +- customtkinter/windows/widgets/ctk_button.py | 7 +- customtkinter/windows/widgets/ctk_entry.py | 9 +- .../windows/widgets/theme/theme_manager.py | 2 +- 7 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 customtkinter/assets/themes/gold.json diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json index 4be97469..938557d9 100644 --- a/customtkinter/assets/themes/blue.json +++ b/customtkinter/assets/themes/blue.json @@ -33,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": { @@ -125,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 9cc5e441..5d1547c2 100644 --- a/customtkinter/assets/themes/dark-blue.json +++ b/customtkinter/assets/themes/dark-blue.json @@ -15,8 +15,8 @@ "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"] @@ -39,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"] @@ -51,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"], @@ -61,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"] }, @@ -71,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": { @@ -81,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"] }, @@ -113,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 85a8d234..174747c7 100644 --- a/customtkinter/assets/themes/green.json +++ b/customtkinter/assets/themes/green.json @@ -33,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": { @@ -63,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"] }, @@ -86,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"] }, @@ -125,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/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py index 0e1b28e9..578025c6 100644 --- a/customtkinter/windows/widgets/ctk_button.py +++ b/customtkinter/windows/widgets/ctk_button.py @@ -225,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, @@ -261,7 +262,7 @@ 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() @@ -439,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 diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py index d6d8a4b6..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) @@ -219,8 +222,12 @@ def configure(self, require_redraw=False, **kwargs): 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") diff --git a/customtkinter/windows/widgets/theme/theme_manager.py b/customtkinter/windows/widgets/theme/theme_manager.py index cf22858b..988c084b 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 From 51b34358e1474d05a1d07ac62acee7ae798e6a9c Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Sun, 18 Jan 2026 22:25:31 +0100 Subject: [PATCH 30/33] Updated CHANGELOG in preparation for the release - Reported all previous changes in the CHANGELOG.md file - Added guidelines to dev-proces.md file - Little improvements to the Showroom App --- CHANGELOG.md | 19 ++++++++++++++++++- customtkinter/__init__.py | 27 ++++++++++++++++----------- dev-proces.md | 4 ++++ 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1b31518..c58af6d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,24 @@ ToDo: ## [Unreleased] ### Added - - Added an option to CTkSegmentedButton to make it layout vertically instead of horizontally + - 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/customtkinter/__init__.py b/customtkinter/__init__.py index af1ef963..a7fecf14 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -163,19 +163,22 @@ def __init__(self) -> None: self.combobox_2.set("readonly") self.optionmenu = CTkOptionMenu(self.choices_frame, dynamic_resizing=False, values=["CTkOptionMenu", "Value 2", "Value 3"]) - self.seg_button = CTkSegmentedButton(self.choices_frame, values=["CTkSegmentedButton", "Value 2", "Value 3"]) - self.seg_button.set("CTkSegmentedButton") + 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_button.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=400) + 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)) @@ -253,18 +256,20 @@ def __init__(self) -> None: 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"]) - self.tabview.add("CTkTabview") - self.tabview.add("Tab 2") - self.tabview.add("Tab 3") + 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(padx=20, pady=(self.SPACING, 5)) - self.tabview.pack(padx=20, pady=(self.SPACING, 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") diff --git a/dev-proces.md b/dev-proces.md index 294ee07f..78a40862 100644 --- a/dev-proces.md +++ b/dev-proces.md @@ -1,4 +1,8 @@ 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` From 8c85d9be528bfb56db67a62216d9757bb6cac173 Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Wed, 21 Jan 2026 22:31:26 +0100 Subject: [PATCH 31/33] Fixed some bugs with the previous changes After additional testing, some special cases have emerged that break some of the new features that have been introduced with the last commits. These changes should solve most of the problems. --- CHANGELOG.md | 2 +- customtkinter/__init__.py | 28 +++++++++++++------ customtkinter/windows/ctk_tk.py | 5 +++- customtkinter/windows/ctk_toplevel.py | 5 +++- .../widgets/core_rendering/draw_engine.py | 3 +- customtkinter/windows/widgets/ctk_combobox.py | 2 +- .../windows/widgets/ctk_optionmenu.py | 2 +- .../windows/widgets/ctk_scrollbar.py | 4 +-- .../windows/widgets/theme/theme_manager.py | 5 ++++ 9 files changed, 39 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c58af6d4..f5541e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ 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 -## [Unreleased] +## [5.3.0] - 2026-01-21 ### Added - Showroom App, immediately available with the library installation - Gold theme diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index a7fecf14..f167e2af 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -91,14 +91,13 @@ def set_ctk_parent_class(ctk_parent_class): def run_showroom() -> None: set_appearance_mode("Light") + set_default_color_theme("blue") - new_theme: Optional[str] = "blue" - while new_theme: - set_default_color_theme(new_theme) - + new_instance: bool = True + while new_instance: app = _Showroom() app.mainloop() - new_theme = app.new_theme + new_instance = app.new_instance_requested class _Showroom(CTk): @@ -110,7 +109,7 @@ def __init__(self) -> None: # configure window self.title("CustomTkinter Showroom") - self.new_theme: Optional[str] = None + self.new_instance_requested: bool = False # create sidebar frame with widgets self.sidebar_frame = CTkFrame(self, width=140, corner_radius=0) @@ -126,7 +125,12 @@ def __init__(self) -> None: 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) - self.scaling_optionmenu.set("100%") + 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) @@ -136,6 +140,8 @@ def __init__(self) -> None: 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) @@ -298,5 +304,11 @@ def _change_scaling_event(self, new_scaling: str) -> None: set_widget_scaling(new_scaling_float) def _change_theme_event(self, new_theme: str) -> None: - self.new_theme = new_theme + 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/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index cdb70823..b8a313a6 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -80,7 +80,10 @@ def __init__(self, self.bind('', self._update_dimensions_event) self.bind('', self._focus_in_event) #allows CTkEntry and CTkTextbox to lose focus - self.bind_all("", lambda event: event.widget.focus_set(), add=True) + 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() diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index 2d36586a..f87bec00 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -76,7 +76,10 @@ def __init__(self, *args, self.bind('', self._update_dimensions_event) self.bind('', self._focus_in_event) #allows CTkEntry and CTkTextbox to lose focus - self.bind_all("", lambda event: event.widget.focus_set(), add=True) + 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() diff --git a/customtkinter/windows/widgets/core_rendering/draw_engine.py b/customtkinter/windows/widgets/core_rendering/draw_engine.py index a213289b..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 diff --git a/customtkinter/windows/widgets/ctk_combobox.py b/customtkinter/windows/widgets/ctk_combobox.py index e5d146eb..823a1c4b 100644 --- a/customtkinter/windows/widgets/ctk_combobox.py +++ b/customtkinter/windows/widgets/ctk_combobox.py @@ -216,9 +216,9 @@ def _draw(self, no_color_updates=False): fill=self._apply_appearance_mode(self._text_color)) def _open_dropdown_menu(self): - self._close_on_next_click = True 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: diff --git a/customtkinter/windows/widgets/ctk_optionmenu.py b/customtkinter/windows/widgets/ctk_optionmenu.py index 8c66d1b4..0fbd38bb 100644 --- a/customtkinter/windows/widgets/ctk_optionmenu.py +++ b/customtkinter/windows/widgets/ctk_optionmenu.py @@ -347,9 +347,9 @@ def cget(self, attribute_name: str) -> any: return super().cget(attribute_name) def _open_dropdown_menu(self): - self._close_on_next_click = True 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() diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py index fa21c3a9..7e0a9582 100644 --- a/customtkinter/windows/widgets/ctk_scrollbar.py +++ b/customtkinter/windows/widgets/ctk_scrollbar.py @@ -274,9 +274,7 @@ def _mouse_scroll_event(self, event=None): #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 == 5: - delta = -delta - if self._orientation == "vertical": + if event.delta > 0 or event.num == 4: delta = -delta if self._start_value + delta < 0.0: diff --git a/customtkinter/windows/widgets/theme/theme_manager.py b/customtkinter/windows/widgets/theme/theme_manager.py index 988c084b..3469de96 100644 --- a/customtkinter/windows/widgets/theme/theme_manager.py +++ b/customtkinter/windows/widgets/theme/theme_manager.py @@ -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): From a796f6e72413f2287ff903184281c9f1372f2772 Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Wed, 21 Jan 2026 22:36:23 +0100 Subject: [PATCH 32/33] Bump to 5.3.0 --- customtkinter/__init__.py | 2 +- pyproject.toml | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index f167e2af..91642edf 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -1,4 +1,4 @@ -__version__ = "5.2.2" +__version__ = "5.3.0" from typing import Optional import os 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/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 From c12c9ab6874ad26b525bad19d912a9ff93f3206d Mon Sep 17 00:00:00 2001 From: Federico Spada Date: Tue, 24 Feb 2026 22:08:06 +0100 Subject: [PATCH 33/33] Improved scroll behavior for CTkScrollableFrame If other scrollable widgets are placed inside CTkScrollableFrame, the MouseWheel event would be triggered on all of them, making them move at the same time. With these improvement, the CTkScrollableFrame doesn't change its view if it detects that the mouse is over a child scrollable widget. --- .../windows/widgets/ctk_scrollable_frame.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/customtkinter/windows/widgets/ctk_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py index 5dcd30ff..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 @@ -260,7 +262,7 @@ def _set_scroll_increments(self): 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): @@ -290,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