diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0fa87b4..d5e4824 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,45 @@ All notable changes to this project will be documented in this file!
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+ToDo:
+ - change font attribute in wiki
+ - add new button attributes to wiki
+
+ - create grayscale theme file
+ - cursor configuring
+ - overwrite winfo methods
+
+
+## [5.0.0] - 2022-11-13
+### Added
+ - Added CTkTextbox with automatic x and y scrollbars, corner_radius, border_width, border_spacing
+ - Added CTkSegmentedButton
+ - Added CTkTabview
+ - Added .cget() method to all widgets and windows
+ - Added .bind() and .focus() methods to almost all widgets
+ - Added 'anchor' option to CTkButton to position image and text inside the button
+ - Added 'anchor' option to CTkOptionMenu and 'justify' option to CTkComboBox
+ - Added CTkFont class
+ - Added CTkImage class to replace PIL.ImageTk.PhotoImage, supports scaling and two images for appearance mode, supports configuring
+ - Added missing configure options for multiple widgets
+
+### Changed
+ - Changed value for transparent colors (same as background) from None to 'transparent'
+ - Changed 'text_font' attribute to 'font' in all widgets, changed 'dropdown_text_font' to 'dropdown_font'
+ - Changed 'dropdown_color' attribute to 'dropdown_fg_color' for combobox, optionmenu
+ - Changed 'orient' attribute of CTkProgressBar and CTkSlider to 'orientation'
+ - Width and height attributes of CTkCheckBox, CTkRadioButton, CTkSwitch now describe the outer dimensions of the whole widget. The button/switch size is described by separate attributes like checkbox_width, checkbox_height
+ - font attribute must be tuple or CTkFont now, all size values are measured in pixel now
+ - Changed dictionary key 'window_bg_color' to 'window' in theme files
+ - CTkInputDialog attributes completely changed
+ - CTkScrollbar attributes scrollbar_color, scrollbar_hover_color changed to button_color, button_hover_color
+
+### Removed
+ - Removed setter and getter functions like set_text in CTkButton
+ - Removed bg and background attribute from CTk and CTkToplevel, always use fg_color
+ - Removed Settings class and moved settings to widget and window classes
+ - removed customtkinter.set_spacing_scaling(), now set_widget_scaling() is used for spacing too
+
## [4.6.0] - 2022-09-17
### Added
- CTkProgressBar indeterminate mode, automatic progress loop with .start() and .stop()
diff --git a/Readme.md b/Readme.md
index fa018ea..f2936b1 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,16 +1,21 @@
+
+
+
+
+
+
+
+
+

-
+
+


-# CustomTkinter UI-Library
+
-
-| _`complex_example.py` on Windows 11 with dark mode and 'dark-blue' theme_
-
-
-| _`complex_example.py` on macOS in light mode and standard 'blue' theme_
-###
+---
CustomTkinter is a python UI-library based on Tkinter, which provides new, modern and
fully customizable widgets. They are created and used like normal Tkinter widgets and
@@ -20,6 +25,13 @@ and the window colors either adapt to the system appearance or the manually set
(Windows, macOS). With CustomTkinter you'll get a consistent and modern look across all
desktop platforms (Windows, macOS, Linux).
+
+| _`complex_example.py` on Windows 11 with dark mode and 'blue' theme_
+
+
+| _`complex_example.py` on macOS in light mode and standard 'blue' theme_
+###
+
## Installation
Install the module with pip:
@@ -38,6 +50,7 @@ The **official** documentation can be found in the Wiki Tab here:
## Example Program
To test customtkinter you can try this simple example with only a single button:
```python
+import tkinter
import customtkinter
customtkinter.set_appearance_mode("System") # Modes: system (default), light, dark
@@ -51,34 +64,35 @@ def button_function():
# Use CTkButton instead of tkinter Button
button = customtkinter.CTkButton(master=app, text="CTkButton", command=button_function)
-button.place(relx=0.5, rely=0.5, anchor=customtkinter.CENTER)
+button.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
app.mainloop()
```
-which gives the following (macOS dark mode on):
+which results in the following window on macOS:
-
+
In the [examples folder](https://github.com/TomSchimansky/CustomTkinter/tree/master/examples), you
can find more example programs and in the [Documentation](https://github.com/TomSchimansky/CustomTkinter/wiki)
-you can find further information on the appearance mode, the themes and all widgets.
+you can find further information on the appearance mode, scaling, themes and all widgets.
## More Examples and Showcase
-### Appearance mode change
+### Appearance mode change and scaling change
-On Windows 10/11 you get a dark window header, which changes with set
-appearance mode or the system, when you use `customtkinter.CTk()`
-to create the window, and it works with all python versions:
+CustomTkinter can adapt to the Windows 10/11 light or dark mode:
+https://user-images.githubusercontent.com/66446067/204672968-6584f360-4c52-434f-9c16-25761341368b.mp4
-| _`complex_example.py` on Windows 11 with system mode change and standard 'blue' theme_
+| _`complex_example.py` on Windows 11 with system appearance mode change and standard 'blue' theme_
###
-On macOS however you either need python3.10 or higher or the anaconda python
-version to get a dark window header at all (Tcl/Tk >= 8.6.9 required).
+On macOS you either need python3.10 or higher or the anaconda python
+version to get a dark window header (Tcl/Tk >= 8.6.9 required):
-| _`complex_example.py` on macOS with system mode change and standard 'blue' theme_
+https://user-images.githubusercontent.com/66446067/204673854-b6cbcfda-d9a1-4425-92a3-5b57d7f2fd6b.mp4
+
+| _`complex_example.py` on macOS with system appearance mode change, user-scaling change and standard 'blue' theme_
###
### Button with images
@@ -87,8 +101,8 @@ pass a PhotoImage object to the CTkButton with the ``image`` argument.
If you want no text at all you have to set ``text=""`` or you specify
how to position the text and image at once with the ``compound`` option:
-
-| _`example_button_images.py` on macOS_
+
+| _`image_example.py` on Windows 11_
###
### Integration of TkinterMapView widget
@@ -96,8 +110,9 @@ In the following example I used a TkinterMapView which integrates
well with a CustomTkinter program. It's a tile based map widget which displays
OpenStreetMap or other tile based maps:
-
-| _`examples/map_with_customtkinter.py` from TkinterMapView repository on macOS_
+https://user-images.githubusercontent.com/66446067/204675835-1584a8da-5acc-4993-b4a9-e70f06fa14b0.mp4
-You can find the TkinterMapView library and the example program here:
+| _`examples/map_with_customtkinter.py` from TkinterMapView repository on Windows 11_
+
+You can find the TkinterMapView library and example program here:
https://github.com/TomSchimansky/TkinterMapView
diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py
index f760bd6..df5dae0 100644
--- a/customtkinter/__init__.py
+++ b/customtkinter/__init__.py
@@ -2,73 +2,46 @@ __version__ = "4.6.3"
import os
import sys
+from tkinter import Variable, StringVar, IntVar, DoubleVar, BooleanVar
from tkinter.constants import *
-from tkinter import StringVar, IntVar, DoubleVar, BooleanVar
+import tkinter.filedialog as filedialog
# import manager classes
-from .settings import Settings
-from .appearance_mode_tracker import AppearanceModeTracker
-from .theme_manager import ThemeManager
-from .scaling_tracker import ScalingTracker
-from .font_manager import FontManager
-from .draw_engine import DrawEngine
-
-AppearanceModeTracker.init_appearance_mode()
-
-# load default blue theme
-try:
- ThemeManager.load_theme("blue")
-except FileNotFoundError as err:
- raise FileNotFoundError(f"{err}\n\nThe .json theme file for CustomTkinter could not be found.\n" +
- f"If packaging with pyinstaller was used, have a look at the wiki:\n" +
- f"https://github.com/TomSchimansky/CustomTkinter/wiki/Packaging#windows-pyinstaller-auto-py-to-exe")
-
-FontManager.init_font_manager()
-
-# determine draw method based on current platform
-if sys.platform == "darwin":
- DrawEngine.preferred_drawing_method = "polygon_shapes"
-else:
- DrawEngine.preferred_drawing_method = "font_shapes"
-
-if sys.platform.startswith("win") and sys.getwindowsversion().build < 9000: # No automatic scaling on Windows < 8.1
- ScalingTracker.deactivate_automatic_dpi_awareness = True
-
-# load Roboto fonts (used on Windows/Linux)
-script_directory = os.path.dirname(os.path.abspath(__file__))
-FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf"))
-FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Medium.ttf"))
-
-# load font necessary for rendering the widgets (used on Windows/Linux)
-if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "CustomTkinter_shapes_font.otf")) is False:
- # change draw method if font loading failed
- if DrawEngine.preferred_drawing_method == "font_shapes":
- sys.stderr.write("customtkinter.__init__ warning: " +
- "Preferred drawing method 'font_shapes' can not be used because the font file could not be loaded.\n" +
- "Using 'circle_shapes' instead. The rendering quality will be bad!")
- DrawEngine.preferred_drawing_method = "circle_shapes"
+from .windows.widgets.appearance_mode import AppearanceModeTracker
+from .windows.widgets.font import FontManager
+from .windows.widgets.scaling import ScalingTracker
+from .windows.widgets.theme import ThemeManager
+from .windows.widgets.core_rendering import DrawEngine
# import widgets
-from .widgets.widget_base_class import CTkBaseClass
-from .widgets.ctk_button import CTkButton
-from .widgets.ctk_checkbox import CTkCheckBox
-from .widgets.ctk_entry import CTkEntry
-from .widgets.ctk_slider import CTkSlider
-from .widgets.ctk_frame import CTkFrame
-from .widgets.ctk_progressbar import CTkProgressBar
-from .widgets.ctk_label import CTkLabel
-from .widgets.ctk_radiobutton import CTkRadioButton
-from .widgets.ctk_canvas import CTkCanvas
-from .widgets.ctk_switch import CTkSwitch
-from .widgets.ctk_optionmenu import CTkOptionMenu
-from .widgets.ctk_combobox import CTkComboBox
-from .widgets.ctk_scrollbar import CTkScrollbar
-from .widgets.ctk_textbox import CTkTextbox
+from .windows.widgets import CTkButton
+from .windows.widgets import CTkCheckBox
+from .windows.widgets import CTkComboBox
+from .windows.widgets import CTkEntry
+from .windows.widgets import CTkFrame
+from .windows.widgets import CTkLabel
+from .windows.widgets import CTkOptionMenu
+from .windows.widgets import CTkProgressBar
+from .windows.widgets import CTkRadioButton
+from .windows.widgets import CTkScrollbar
+from .windows.widgets import CTkSegmentedButton
+from .windows.widgets import CTkSlider
+from .windows.widgets import CTkSwitch
+from .windows.widgets import CTkTabview
+from .windows.widgets import CTkTextbox
# import windows
-from .windows.ctk_tk import CTk
-from .windows.ctk_toplevel import CTkToplevel
-from .windows.ctk_input_dialog import CTkInputDialog
+from .windows import CTk
+from .windows import CTkToplevel
+from .windows import CTkInputDialog
+
+# import font classes
+from .windows.widgets.font import CTkFont
+
+# import image classes
+from .windows.widgets.image import CTkImage
+
+_ = Variable, StringVar, IntVar, DoubleVar, BooleanVar, CENTER, filedialog # prevent IDE from removing unused imports
def set_appearance_mode(mode_string: str):
@@ -94,11 +67,6 @@ def set_widget_scaling(scaling_value: float):
ScalingTracker.set_widget_scaling(scaling_value)
-def set_spacing_scaling(scaling_value: float):
- """ set scaling for geometry manager calls (place, pack, grid)"""
- ScalingTracker.set_spacing_scaling(scaling_value)
-
-
def set_window_scaling(scaling_value: float):
""" set scaling for window dimensions """
ScalingTracker.set_window_scaling(scaling_value)
diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json
index 82de860..bca0b2b 100644
--- a/customtkinter/assets/themes/blue.json
+++ b/customtkinter/assets/themes/blue.json
@@ -1,79 +1,152 @@
{
- "color": {
- "window_bg_color": ["#EBEBEC", "#212325"],
- "button": ["#3B8ED0", "#1F6AA5"],
- "button_hover": ["#36719F", "#144870"],
- "button_border": ["#3E454A", "#949A9F"],
- "checkbox_border": ["#3E454A", "#949A9F"],
- "checkmark": ["white", "gray90"],
- "entry": ["#F9F9FA", "#343638"],
- "entry_border": ["#979DA2", "#565B5E"],
- "entry_placeholder_text": ["gray52", "gray62"],
- "frame_border": ["#979DA2", "#1F2122"],
- "frame_low": ["#D1D5D8", "#2A2D2E"],
- "frame_high": ["#C0C2C5", "#343638"],
- "label": [null, null],
- "text": ["gray10", "#DCE4EE"],
- "text_disabled": ["gray60", "#777B80"],
- "text_button_disabled": ["gray40", "gray74"],
- "progressbar": ["#939BA2", "#4A4D50"],
- "progressbar_progress": ["#3B8ED0", "#1F6AA5"],
- "progressbar_border": ["gray", "gray"],
- "slider": ["#939BA2", "#4A4D50"],
- "slider_progress": ["gray40", "#AAB0B5"],
- "slider_button": ["#3B8ED0", "#1F6AA5"],
- "slider_button_hover": ["#36719F", "#144870"],
- "switch": ["#939BA2", "#4A4D50"],
- "switch_progress": ["#3B8ED0", "#1F6AA5"],
- "switch_button": ["gray36", "#D5D9DE"],
- "switch_button_hover": ["gray20", "gray100"],
- "optionmenu_button": ["#36719F", "#144870"],
- "optionmenu_button_hover": ["#27577D", "#203A4F"],
- "combobox_border": ["#979DA2", "#565B5E"],
- "combobox_button_hover": ["#6E7174", "#7A848D"],
- "dropdown_color": ["gray90", "gray20"],
- "dropdown_hover": ["gray75", "gray28"],
- "dropdown_text": ["gray10", "#DCE4EE"],
- "scrollbar_button": ["gray55", "gray41"],
- "scrollbar_button_hover": ["gray40", "gray53"]
+ "CTk": {
+ "fg_color": ["gray92", "gray14"]
},
- "text": {
+ "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": ["#3B8ED0", "#1F6AA5"],
+ "hover_color": ["#36719F", "#144870"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "text_color": ["#DCE4EE", "#DCE4EE"],
+ "text_color_disabled": ["gray74", "gray60"]
+ },
+ "CTkLabel": {
+ "corner_radius": 0,
+ "fg_color": "transparent",
+ "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": ["#3B8ED0", "#1F6AA5"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "hover_color": ["#3B8ED0", "#1F6AA5"],
+ "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": ["#3B8ED0", "#1F6AA5"],
+ "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": ["#3B8ED0", "#1F6AA5"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "hover_color": ["#36719F", "#144870"],
+ "text_color": ["gray10", "#DCE4EE"],
+ "text_color_disabled": ["gray60", "gray45"]
+ },
+ "CTkProgressBar": {
+ "corner_radius": 1000,
+ "border_width": 0,
+ "fg_color": ["#939BA2", "#4A4D50"],
+ "progress_color": ["#3B8ED0", "#1F6AA5"],
+ "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": ["#3B8ED0", "#1F6AA5"],
+ "button_hover_color": ["#36719F", "#144870"]
+ },
+ "CTkOptionMenu": {
+ "corner_radius": 6,
+ "fg_color": ["#3B8ED0", "#1F6AA5"],
+ "button_color": ["#36719F", "#144870"],
+ "button_hover_color": ["#27577D", "#203A4F"],
+ "text_color": ["#DCE4EE", "#DCE4EE"],
+ "text_color_disabled": ["gray74", "gray60"]
+ },
+ "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": ["#3B8ED0", "#1F6AA5"],
+ "selected_hover_color": ["#36719F", "#144870"],
+ "unselected_color": ["#979DA2", "gray29"],
+ "unselected_hover_color": ["gray70", "gray41"],
+ "text_color": ["#DCE4EE", "#DCE4EE"],
+ "text_color_disabled": ["gray74", "gray60"]
+ },
+ "CTkTextbox": {
+ "corner_radius": 6,
+ "border_width": 0,
+ "fg_color": ["#F9F9FA", "gray23"],
+ "border_color": ["#979DA2", "#565B5E"],
+ "text_color":["gray10", "#DCE4EE"],
+ "scrollbar_button_color": ["gray55", "gray41"],
+ "scrollbar_button_hover_color": ["gray40", "gray53"]
+ },
+ "DropdownMenu": {
+ "fg_color": ["gray90", "gray20"],
+ "hover_color": ["gray75", "gray28"],
+ "text_color": ["gray10", "gray90"]
+ },
+ "CTkFont": {
"macOS": {
- "font": "SF Display",
- "size": -13
+ "family": "SF Display",
+ "size": 13,
+ "weight": "normal"
},
"Windows": {
- "font": "Roboto",
- "size": -13
+ "family": "Roboto",
+ "size": 13,
+ "weight": "normal"
},
"Linux": {
- "font": "Roboto",
- "size": -13
+ "family": "Roboto",
+ "size": 13,
+ "weight": "normal"
}
- },
- "shape": {
- "button_corner_radius": 6,
- "button_border_width": 0,
- "checkbox_corner_radius": 6,
- "checkbox_border_width": 3,
- "radiobutton_corner_radius": 1000,
- "radiobutton_border_width_unchecked": 3,
- "radiobutton_border_width_checked": 6,
- "entry_border_width": 2,
- "frame_corner_radius": 6,
- "frame_border_width": 0,
- "label_corner_radius": 0,
- "progressbar_border_width": 0,
- "progressbar_corner_radius": 1000,
- "slider_border_width": 6,
- "slider_corner_radius": 1000,
- "slider_button_length": 0,
- "slider_button_corner_radius": 1000,
- "switch_border_width": 3,
- "switch_corner_radius": 1000,
- "switch_button_corner_radius": 1000,
- "switch_button_length": 0,
- "scrollbar_corner_radius": 1000,
- "scrollbar_border_spacing": 4
}
}
diff --git a/customtkinter/assets/themes/dark-blue.json b/customtkinter/assets/themes/dark-blue.json
index 0b1a33f..1ecf8ab 100644
--- a/customtkinter/assets/themes/dark-blue.json
+++ b/customtkinter/assets/themes/dark-blue.json
@@ -1,79 +1,152 @@
{
- "color": {
- "window_bg_color": ["gray98", "gray10"],
- "button": ["#608BD5", "#395E9C"],
- "button_hover": ["#A4BDE6", "#748BB3"],
- "button_border": ["gray40", "gray70"],
- "checkbox_border": ["gray40", "gray60"],
- "checkmark": ["white", "gray90"],
- "entry": ["white", "gray24"],
- "entry_border": ["gray70", "gray32"],
- "entry_placeholder_text": ["gray52", "gray62"],
- "frame_border": ["#A7C2E0", "#5FB4DD"],
- "frame_low": ["gray92", "gray16"],
- "frame_high": ["gray86", "gray20"],
- "label": [null, null],
- "text": ["gray12", "gray90"],
- "text_disabled": ["gray60", "gray50"],
- "text_button_disabled": ["gray40", "gray74"],
- "progressbar": ["#6B6B6B", "gray0"],
- "progressbar_progress": ["#608BD5", "#395E9C"],
- "progressbar_border": ["gray", "gray"],
- "slider": ["#6B6B6B", "gray6"],
- "slider_progress": ["gray70", "gray30"],
- "slider_button": ["#608BD5", "#395E9C"],
- "slider_button_hover": ["#A4BDE6", "#748BB3"],
- "switch": ["gray70", "gray35"],
- "switch_progress": ["#608BD5", "#395E9C"],
- "switch_button": ["gray38", "gray70"],
- "switch_button_hover": ["gray30", "gray90"],
- "optionmenu_button": ["#36719F", "#144870"],
- "optionmenu_button_hover": ["#27577D", "#203A4F"],
- "combobox_border": ["gray70", "gray32"],
- "combobox_button_hover": ["#6E7174", "#7A848D"],
- "dropdown_color": ["gray90", "gray20"],
- "dropdown_hover": ["gray75", "gray28"],
- "dropdown_text": ["gray10", "#DCE4EE"],
- "scrollbar_button": ["gray55", "gray41"],
- "scrollbar_button_hover": ["gray40", "gray53"]
+ "CTk": {
+ "fg_color": ["gray95", "gray10"]
},
- "text": {
+ "CTkToplevel": {
+ "fg_color": ["gray95", "gray10"]
+ },
+ "CTkFrame": {
+ "corner_radius": 6,
+ "border_width": 0,
+ "fg_color": ["gray90", "gray13"],
+ "top_fg_color": ["gray85", "gray16"],
+ "border_color": ["gray65", "gray28"]
+ },
+ "CTkButton": {
+ "corner_radius": 6,
+ "border_width": 0,
+ "fg_color": ["#3a7ebf", "#1f538d"],
+ "hover_color": ["#325882", "#14375e"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "text_color": ["#DCE4EE", "#DCE4EE"],
+ "text_color_disabled": ["gray74", "gray60"]
+ },
+ "CTkLabel": {
+ "corner_radius": 0,
+ "fg_color": "transparent",
+ "text_color": ["gray14", "gray84"]
+ },
+ "CTkEntry": {
+ "corner_radius": 6,
+ "border_width": 2,
+ "fg_color": ["#F9F9FA", "#343638"],
+ "border_color": ["#979DA2", "#565B5E"],
+ "text_color": ["gray14", "gray84"],
+ "placeholder_text_color": ["gray52", "gray62"]
+ },
+ "CTkCheckbox": {
+ "corner_radius": 6,
+ "border_width": 3,
+ "fg_color": ["#3a7ebf", "#1f538d"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "hover_color": ["#325882", "#14375e"],
+ "checkmark_color": ["#DCE4EE", "gray90"],
+ "text_color": ["gray14", "gray84"],
+ "text_color_disabled": ["gray60", "gray45"]
+ },
+ "CTkSwitch": {
+ "corner_radius": 1000,
+ "border_width": 3,
+ "button_length": 0,
+ "fg_Color": ["#939BA2", "#4A4D50"],
+ "progress_color": ["#3a7ebf", "#1f538d"],
+ "button_color": ["gray36", "#D5D9DE"],
+ "button_hover_color": ["gray20", "gray100"],
+ "text_color": ["gray14", "gray84"],
+ "text_color_disabled": ["gray60", "gray45"]
+ },
+ "CTkRadiobutton": {
+ "corner_radius": 1000,
+ "border_width_checked": 6,
+ "border_width_unchecked": 3,
+ "fg_color": ["#3a7ebf", "#1f538d"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "hover_color": ["#325882", "#14375e"],
+ "text_color": ["gray14", "gray84"],
+ "text_color_disabled": ["gray60", "gray45"]
+ },
+ "CTkProgressBar": {
+ "corner_radius": 1000,
+ "border_width": 0,
+ "fg_color": ["#939BA2", "#4A4D50"],
+ "progress_color": ["#3a7ebf", "#1f538d"],
+ "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": ["#3a7ebf", "#1f538d"],
+ "button_hover_color": ["#325882", "#14375e"]
+ },
+ "CTkOptionMenu": {
+ "corner_radius": 6,
+ "fg_color": ["#3a7ebf", "#1f538d"],
+ "button_color": ["#325882", "#14375e"],
+ "button_hover_color": ["#234567", "#1e2c40"],
+ "text_color": ["#DCE4EE", "#DCE4EE"],
+ "text_color_disabled": ["gray74", "gray60"]
+ },
+ "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": ["gray14", "gray84"],
+ "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": ["#3a7ebf", "#1f538d"],
+ "selected_hover_color": ["#325882", "#14375e"],
+ "unselected_color": ["#979DA2", "gray29"],
+ "unselected_hover_color": ["gray70", "gray41"],
+ "text_color": ["#DCE4EE", "#DCE4EE"],
+ "text_color_disabled": ["gray74", "gray60"]
+ },
+ "CTkTextbox": {
+ "corner_radius": 6,
+ "border_width": 0,
+ "fg_color": ["gray100", "gray20"],
+ "border_color": ["#979DA2", "#565B5E"],
+ "text_color": ["gray14", "gray84"],
+ "scrollbar_button_color": ["gray55", "gray41"],
+ "scrollbar_button_hover_color": ["gray40", "gray53"]
+ },
+ "DropdownMenu": {
+ "fg_color": ["gray90", "gray20"],
+ "hover_color": ["gray75", "gray28"],
+ "text_color": ["gray14", "gray84"]
+ },
+ "CTkFont": {
"macOS": {
- "font": "SF Display",
- "size": -13
+ "family": "SF Display",
+ "size": 13,
+ "weight": "normal"
},
"Windows": {
- "font": "Roboto",
- "size": -13
+ "family": "Roboto",
+ "size": 13,
+ "weight": "normal"
},
"Linux": {
- "font": "Roboto",
- "size": -13
+ "family": "Roboto",
+ "size": 13,
+ "weight": "normal"
}
- },
- "shape": {
- "button_corner_radius": 8,
- "button_border_width": 0,
- "checkbox_corner_radius": 7,
- "checkbox_border_width": 3,
- "radiobutton_corner_radius": 1000,
- "radiobutton_border_width_unchecked": 3,
- "radiobutton_border_width_checked": 6,
- "entry_border_width": 2,
- "frame_corner_radius": 10,
- "frame_border_width": 0,
- "label_corner_radius": 0,
- "progressbar_border_width": 0,
- "progressbar_corner_radius": 1000,
- "slider_border_width": 6,
- "slider_corner_radius": 8,
- "slider_button_length": 0,
- "slider_button_corner_radius": 1000,
- "switch_border_width": 3,
- "switch_corner_radius": 1000,
- "switch_button_corner_radius": 1000,
- "switch_button_length": 0,
- "scrollbar_corner_radius": 1000,
- "scrollbar_border_spacing": 4
}
}
diff --git a/customtkinter/assets/themes/green.json b/customtkinter/assets/themes/green.json
index d3e9442..14cd8c6 100644
--- a/customtkinter/assets/themes/green.json
+++ b/customtkinter/assets/themes/green.json
@@ -1,79 +1,152 @@
{
- "color": {
- "window_bg_color": ["gray92", "gray12"],
- "button": ["#72CF9F", "#11B384"],
- "button_hover": ["#0E9670", "#0D8A66"],
- "button_border": ["gray40", "gray70"],
- "checkbox_border": ["gray40", "gray60"],
- "checkmark": ["white", "gray90"],
- "entry": ["white", "gray24"],
- "entry_border": ["gray70", "gray32"],
- "entry_placeholder_text": ["gray52", "gray62"],
- "frame_border": ["#A7C2E0", "#5FB4DD"],
- "frame_low": ["gray87", "gray18"],
- "frame_high": ["gray82", "gray22"],
- "label": [null, null],
- "text": ["gray20", "gray90"],
- "text_disabled": ["gray60", "gray50"],
- "text_button_disabled": ["gray40", "gray74"],
- "progressbar": ["#6B6B6B", "#222222"],
- "progressbar_progress": ["#72CF9F", "#11B384"],
- "progressbar_border": ["gray", "gray"],
- "slider": ["#6B6B6B", "#222222"],
- "slider_progress": ["white", "#555555"],
- "slider_button": ["#72CF9F", "#11B384"],
- "slider_button_hover": ["#0E9670", "#0D8A66"],
- "switch": ["gray70", "gray35"],
- "switch_progress": ["#72CF9F", "#11B384"],
- "switch_button": ["gray38", "gray70"],
- "switch_button_hover": ["gray30", "gray90"],
- "optionmenu_button": ["#0E9670", "#0D8A66"],
- "optionmenu_button_hover":["gray40", "gray70"],
- "combobox_border": ["gray70", "gray32"],
- "combobox_button_hover": ["#6E7174", "#7A848D"],
- "dropdown_color": ["gray90", "gray20"],
- "dropdown_hover": ["gray75", "gray28"],
- "dropdown_text": ["gray10", "#DCE4EE"],
- "scrollbar_button": ["gray55", "gray41"],
- "scrollbar_button_hover": ["gray40", "gray53"]
+ "CTk": {
+ "fg_color": ["gray92", "gray14"]
},
- "text": {
+ "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": ["#2CC985", "#2FA572"],
+ "hover_color": ["#0C955A", "#106A43"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "text_color": ["gray98", "#DCE4EE"],
+ "text_color_disabled": ["gray78", "gray68"]
+ },
+ "CTkLabel": {
+ "corner_radius": 0,
+ "fg_color": "transparent",
+ "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": ["#2CC985", "#2FA572"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "hover_color": ["#0C955A", "#106A43"],
+ "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": ["#2CC985", "#2FA572"],
+ "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": ["#2CC985", "#2FA572"],
+ "border_color": ["#3E454A", "#949A9F"],
+ "hover_color":["#0C955A", "#106A43"],
+ "text_color": ["gray10", "#DCE4EE"],
+ "text_color_disabled": ["gray60", "gray45"]
+ },
+ "CTkProgressBar": {
+ "corner_radius": 1000,
+ "border_width": 0,
+ "fg_color": ["#939BA2", "#4A4D50"],
+ "progress_color": ["#2CC985", "#2FA572"],
+ "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": ["#2CC985", "#2FA572"],
+ "button_hover_color": ["#0C955A", "#106A43"]
+ },
+ "CTkOptionMenu": {
+ "corner_radius": 6,
+ "fg_color": ["#2cbe79", "#2FA572"],
+ "button_color": ["#0C955A", "#106A43"],
+ "button_hover_color": ["#0b6e3d", "#17472e"],
+ "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": ["#2CC985", "#2FA572"],
+ "selected_hover_color": ["#0C955A", "#106A43"],
+ "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", "gray23"],
+ "border_color": ["#979DA2", "#565B5E"],
+ "text_color":["gray10", "#DCE4EE"],
+ "scrollbar_button_color": ["gray55", "gray41"],
+ "scrollbar_button_hover_color": ["gray40", "gray53"]
+ },
+ "DropdownMenu": {
+ "fg_color": ["gray90", "gray20"],
+ "hover_color": ["gray75", "gray28"],
+ "text_color": ["gray10", "gray90"]
+ },
+ "CTkFont": {
"macOS": {
- "font": "SF Display",
- "size": -13
+ "family": "SF Display",
+ "size": 13,
+ "weight": "normal"
},
"Windows": {
- "font": "Roboto",
- "size": -13
+ "family": "Roboto",
+ "size": 13,
+ "weight": "normal"
},
"Linux": {
- "font": "Roboto",
- "size": -13
+ "family": "Roboto",
+ "size": 13,
+ "weight": "normal"
}
- },
- "shape": {
- "button_corner_radius": 6,
- "button_border_width": 0,
- "checkbox_corner_radius": 7,
- "checkbox_border_width": 3,
- "radiobutton_corner_radius": 1000,
- "radiobutton_border_width_unchecked": 3,
- "radiobutton_border_width_checked": 6,
- "entry_border_width": 2,
- "frame_corner_radius": 10,
- "frame_border_width": 0,
- "label_corner_radius": 0,
- "progressbar_border_width": 0,
- "progressbar_corner_radius": 1000,
- "slider_border_width": 6,
- "slider_corner_radius": 8,
- "slider_button_length": 0,
- "slider_button_corner_radius": 1000,
- "switch_border_width": 3,
- "switch_corner_radius": 1000,
- "switch_button_corner_radius": 1000,
- "switch_button_length": 0,
- "scrollbar_corner_radius": 1000,
- "scrollbar_border_spacing": 4
}
}
diff --git a/customtkinter/assets/themes/sweetkind.json b/customtkinter/assets/themes/sweetkind.json
deleted file mode 100644
index 178fec1..0000000
--- a/customtkinter/assets/themes/sweetkind.json
+++ /dev/null
@@ -1,79 +0,0 @@
-{
- "color": {
- "window_bg_color": ["#ebf0f5", "#181b28"],
- "button": ["#e46bff", "#212435"],
- "button_hover": ["#8593d6", "#171926"],
- "button_border": ["#525983", "#080b12"],
- "checkbox_border": ["#01e9c4", "#01e9c4"],
- "checkmark": ["#01e9c4", "#01e9c4"],
- "entry": ["#dee2e7", "#212435"],
- "entry_border": ["#fa00d0", "#080b12"],
- "entry_placeholder_text": ["#cdc8ce", "#cdc8ce"],
- "frame_border": ["#525983", "#10121f"],
- "frame_low": ["#dee2e7", "#181b28"],
- "frame_high": ["#dee2e7", "#1b1e2d"],
- "label": [null, null],
- "text": ["#0c0e14", "#cdc8ce"],
- "text_disabled": ["#5e6062", "#7a8894"],
- "text_button_disabled": ["#7a8894", "#7a8894"],
- "progressbar": ["#fa00d0", "#fa00d0"],
- "progressbar_progress": ["#363844", "#363844"],
- "progressbar_border": ["#fa00d0", "#0d101f"],
- "slider": ["#fa00d0", "#fa00d0"],
- "slider_progress": ["#0d101f", "#0d101f"],
- "slider_button": ["#fa00d0", "#fa00d0"],
- "slider_button_hover": ["#e46bff", "#fa00d0"],
- "switch": ["#7681be", "#1f2233"],
- "switch_progress": ["#00e6c3", "#00e6c3"],
- "switch_button": ["#525983", "#2e324a"],
- "switch_button_hover": ["#fa00d0", "#2e324a"],
- "optionmenu_button": ["#525983", "#080b12"],
- "optionmenu_button_hover": ["#fa00d0", "#080b12"],
- "combobox_border": ["#525983", "#080b12"],
- "combobox_button_hover": ["#fa00d0", "#fa00d0"],
- "dropdown_color": ["#dee2e7", "#212435"],
- "dropdown_hover": ["#fa00d0", "#fa00d0"],
- "dropdown_text": ["#0c0e14", "#cdc8ce"],
- "scrollbar_button": ["#fa00d0", "#fa00d0"],
- "scrollbar_button_hover": ["#9b45ff", "#9b45ff"]
- },
- "text": {
- "macOS": {
- "font": "SF Display",
- "size": -13
- },
- "Windows": {
- "font": "Roboto",
- "size": -13
- },
- "Linux": {
- "font": "Roboto",
- "size": -13
- }
- },
- "shape": {
- "button_corner_radius": 8,
- "button_border_width": 1,
- "checkbox_corner_radius": 7,
- "checkbox_border_width": 1,
- "radiobutton_corner_radius": 1000,
- "radiobutton_border_width_unchecked": 2,
- "radiobutton_border_width_checked": 6,
- "entry_border_width": 1,
- "frame_corner_radius": 10,
- "frame_border_width": 1,
- "label_corner_radius": 3,
- "progressbar_border_width": 2,
- "progressbar_corner_radius": 1000,
- "slider_border_width": 6,
- "slider_corner_radius": 8,
- "slider_button_length": 0,
- "slider_button_corner_radius": 1000,
- "switch_border_width": 3,
- "switch_corner_radius": 1000,
- "switch_button_corner_radius": 1000,
- "switch_button_length": 2,
- "scrollbar_corner_radius": 1000,
- "scrollbar_border_spacing": 4
- }
-}
diff --git a/customtkinter/settings.py b/customtkinter/settings.py
deleted file mode 100644
index a93f800..0000000
--- a/customtkinter/settings.py
+++ /dev/null
@@ -1,6 +0,0 @@
-
-class Settings:
- cursor_manipulation_enabled = True
- deactivate_macos_window_header_manipulation = False
- deactivate_windows_window_header_manipulation = False
- use_dropdown_fallback = True
diff --git a/customtkinter/theme_manager.py b/customtkinter/theme_manager.py
deleted file mode 100644
index 7c70c8a..0000000
--- a/customtkinter/theme_manager.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import sys
-import os
-import json
-
-
-class ThemeManager:
-
- theme = {} # contains all the theme data
- built_in_themes = ["blue", "green", "dark-blue", "sweetkind"]
-
- @classmethod
- def load_theme(cls, theme_name_or_path: str):
- script_directory = os.path.dirname(os.path.abspath(__file__))
-
- if theme_name_or_path in cls.built_in_themes:
- with open(os.path.join(script_directory, "assets", "themes", f"{theme_name_or_path}.json"), "r") as f:
- cls.theme = json.load(f)
- else:
- with open(theme_name_or_path, "r") as f:
- cls.theme = json.load(f)
-
- if sys.platform == "darwin":
- cls.theme["text"] = cls.theme["text"]["macOS"]
- elif sys.platform.startswith("win"):
- cls.theme["text"] = cls.theme["text"]["Windows"]
- else:
- cls.theme["text"] = cls.theme["text"]["Linux"]
-
- @staticmethod
- def single_color(color, appearance_mode: int) -> str:
- """ color can be either a single hex color string or a color name or it can be a
- tuple color with (light_color, dark_color). The functions then returns
- always a single color string """
-
- if type(color) == tuple or type(color) == list:
- return color[appearance_mode]
- else:
- return color
-
- @staticmethod
- def rgb2hex(rgb_color: tuple) -> str:
- return "#{:02x}{:02x}{:02x}".format(round(rgb_color[0]), round(rgb_color[1]), round(rgb_color[2]))
-
- @staticmethod
- def hex2rgb(hex_color: str) -> tuple:
- return tuple(int(hex_color.strip("#")[i:i+2], 16) for i in (0, 2, 4))
-
- @classmethod
- def linear_blend(cls, color_1: str, color_2: str, blend_factor: float) -> str:
- """ Blends two hex colors linear, where blend_factor of 0
- results in color_1 and blend_factor of 1 results in color_2. """
-
- if color_1 is None or color_2 is None:
- return None
-
- rgb_1 = cls.hex2rgb(color_1)
- rgb_2 = cls.hex2rgb(color_2)
-
- new_rgb = (rgb_1[0] + (rgb_2[0] - rgb_1[0]) * blend_factor,
- rgb_1[1] + (rgb_2[1] - rgb_1[1]) * blend_factor,
- rgb_1[2] + (rgb_2[2] - rgb_1[2]) * blend_factor)
-
- return cls.rgb2hex(new_rgb)
-
- @classmethod
- def get_minimal_darker(cls, color: str) -> str:
- if color.startswith("#"):
- color_rgb = cls.hex2rgb(color)
- if color_rgb[0] > 0:
- return cls.rgb2hex((color_rgb[0] - 1, color_rgb[1], color_rgb[2]))
- elif color_rgb[1] > 0:
- return cls.rgb2hex((color_rgb[0], color_rgb[1] - 1, color_rgb[2]))
- elif color_rgb[2] > 0:
- return cls.rgb2hex((color_rgb[0], color_rgb[1], color_rgb[2] - 1))
- else:
- return cls.rgb2hex((color_rgb[0] + 1, color_rgb[1], color_rgb[2] - 1)) # otherwise slightly lighter
-
- @classmethod
- def multiply_hex_color(cls, hex_color: str, factor: float = 1.0) -> str:
- try:
- rgb_color = ThemeManager.hex2rgb(hex_color)
- dark_rgb_color = (min(255, rgb_color[0] * factor),
- min(255, rgb_color[1] * factor),
- min(255, rgb_color[2] * factor))
- return ThemeManager.rgb2hex(dark_rgb_color)
- except Exception as err:
- # sys.stderr.write("ERROR (CTkColorManager): failed to darken the following color: " + str(hex_color) + " " + str(err))
- return hex_color
-
- @classmethod
- def set_main_color(cls, main_color, main_color_hover):
- cls.MAIN_COLOR = main_color
- cls.MAIN_HOVER_COLOR = main_color_hover
diff --git a/customtkinter/widgets/__init__.py b/customtkinter/widgets/__init__.py
deleted file mode 100644
index 62acbf5..0000000
--- a/customtkinter/widgets/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .ctk_canvas import CTkCanvas
-
-CTkCanvas.init_font_character_mapping()
diff --git a/customtkinter/widgets/ctk_button.py b/customtkinter/widgets/ctk_button.py
deleted file mode 100644
index 09acdc8..0000000
--- a/customtkinter/widgets/ctk_button.py
+++ /dev/null
@@ -1,377 +0,0 @@
-import tkinter
-import sys
-from typing import Union, Tuple, Callable
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..settings import Settings
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkButton(CTkBaseClass):
- """ button with border, rounded corners, hover effect, image support """
-
- def __init__(self, *args,
- bg_color: Union[str, Tuple[str, str], None] = None,
- fg_color: Union[str, Tuple[str, str], None] = "default_theme",
- hover_color: Union[str, Tuple[str, str]] = "default_theme",
- border_color: Union[str, Tuple[str, str]] = "default_theme",
- text_color: Union[str, Tuple[str, str]] = "default_theme",
- text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
- width: int = 140,
- height: int = 28,
- corner_radius: Union[int, str] = "default_theme",
- border_width: Union[int, str] = "default_theme",
- text: str = "CTkButton",
- textvariable: tkinter.Variable = None,
- text_font: any = "default_theme",
- image: tkinter.PhotoImage = None,
- hover: bool = True,
- compound: str = "left",
- state: str = "normal",
- command: Callable = None,
- **kwargs):
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
-
- # color
- self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
- self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
- self.border_color = ThemeManager.theme["color"]["button_border"] if border_color == "default_theme" else border_color
- self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
- self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
- self.border_width = ThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width
-
- # text, font, image
- self.image = image
- self.image_label = None
- self.text = text
- self.text_label = None
- self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
-
- # callback and hover functionality
- self.command = command
- self.textvariable = textvariable
- self.state = state
- self.hover = hover
- self.compound = compound
- self.click_animation_running = False
-
- # configure grid system (2x2)
- self.grid_rowconfigure(0, weight=1)
- self.grid_columnconfigure(0, weight=1)
- self.grid_rowconfigure(1, weight=1)
- self.grid_columnconfigure(1, weight=1)
-
- # canvas
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
- self.draw_engine = DrawEngine(self.canvas)
-
- # canvas event bindings
- self.canvas.bind("", self.on_enter)
- self.canvas.bind("", self.on_leave)
- self.canvas.bind("", self.clicked)
- self.canvas.bind("", self.clicked)
- self.bind('', self.update_dimensions_event)
-
- # configure cursor and initial draw
- self.set_cursor()
- self.draw()
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- if self.text_label is not None:
- self.text_label.destroy()
- self.text_label = None
- if self.image_label is not None:
- self.image_label.destroy()
- self.image_label = None
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def set_dimensions(self, width: int = None, height: int = None):
- super().set_dimensions(width, height)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def draw(self, no_color_updates=False):
- requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width))
-
- if no_color_updates is False or requires_recoloring:
-
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- # set color for the button border parts (outline)
- self.canvas.itemconfig("border_parts",
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
-
- # set color for inner button parts
- if self.fg_color is None:
- self.canvas.itemconfig("inner_parts",
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("inner_parts",
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- # create text label if text given
- if self.text is not None and self.text != "":
-
- if self.text_label is None:
- self.text_label = tkinter.Label(master=self,
- font=self.apply_font_scaling(self.text_font),
- text=self.text,
- textvariable=self.textvariable)
-
- 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)
-
- if no_color_updates is False:
- # set text_label fg color (text color)
- self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
-
- if self.state == tkinter.DISABLED:
- self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)))
- else:
- self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
-
- if self.fg_color is None:
- self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- else:
- self.text_label.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- else:
- # delete text_label if no text given
- if self.text_label is not None:
- self.text_label.destroy()
- self.text_label = None
-
- # create image label if image given
- if self.image is not None:
-
- if self.image_label is None:
- self.image_label = tkinter.Label(master=self)
-
- 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)
-
- if no_color_updates is False:
- # set image_label bg color (background color of label)
- if self.fg_color is None:
- self.image_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- else:
- self.image_label.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- self.image_label.configure(image=self.image) # set image
-
- else:
- # delete text_label if no text given
- if self.image_label is not None:
- self.image_label.destroy()
- self.image_label = None
-
- # create grid layout with just an image given
- if self.image_label is not None and self.text_label is None:
- self.image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
- pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) # bottom pady with +1 for rounding to even
-
- # create grid layout with just text given
- if self.image_label is None and self.text_label is not None:
- self.text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
- padx=self.apply_widget_scaling(self.corner_radius),
- pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) # bottom pady with +1 for rounding to even
-
- # create grid layout of image and text label in 2x2 grid system with given compound
- if self.image_label is not None and self.text_label is not None:
- if self.compound == tkinter.LEFT or self.compound == "left":
- self.image_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
- padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2),
- pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
- self.text_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
- padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))),
- pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
- elif self.compound == tkinter.TOP or self.compound == "top":
- self.image_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
- padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
- pady=(self.apply_widget_scaling(self.border_width), 2))
- self.text_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
- padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
- pady=(2, self.apply_widget_scaling(self.border_width)))
- elif self.compound == tkinter.RIGHT or self.compound == "right":
- self.image_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
- padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))),
- pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
- self.text_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
- padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2),
- pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1))
- elif self.compound == tkinter.BOTTOM or self.compound == "bottom":
- self.image_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
- padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
- pady=(2, self.apply_widget_scaling(self.border_width)))
- self.text_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
- padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)),
- pady=(self.apply_widget_scaling(self.border_width), 2))
-
- def configure(self, require_redraw=False, **kwargs):
- if "text" in kwargs:
- self.text = kwargs.pop("text")
- if self.text_label is None:
- require_redraw = True # text_label will be created in .draw()
- else:
- self.text_label.configure(text=self.text)
-
- if "text_font" in kwargs:
- self.text_font = kwargs.pop("text_font")
- if self.text_label is not None:
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
-
- if "state" in kwargs:
- self.state = kwargs.pop("state")
- self.set_cursor()
- require_redraw = True
-
- if "image" in kwargs:
- self.image = kwargs.pop("image")
- require_redraw = True
-
- if "corner_radius" in kwargs:
- self.corner_radius = kwargs.pop("corner_radius")
- require_redraw = True
-
- if "compound" in kwargs:
- self.compound = kwargs.pop("compound")
- require_redraw = True
-
- if "fg_color" in kwargs:
- self.fg_color = kwargs.pop("fg_color")
- require_redraw = True
-
- if "border_color" in kwargs:
- self.border_color = kwargs.pop("border_color")
- require_redraw = True
-
- if "hover_color" in kwargs:
- self.hover_color = kwargs.pop("hover_color")
- require_redraw = True
-
- if "text_color" in kwargs:
- self.text_color = kwargs.pop("text_color")
- require_redraw = True
-
- if "command" in kwargs:
- self.command = kwargs.pop("command")
-
- if "textvariable" in kwargs:
- self.textvariable = kwargs.pop("textvariable")
- if self.text_label is not None:
- self.text_label.configure(textvariable=self.textvariable)
-
- if "width" in kwargs:
- self.set_dimensions(width=kwargs.pop("width"))
-
- if "height" in kwargs:
- self.set_dimensions(height=kwargs.pop("height"))
-
- super().configure(require_redraw=require_redraw, **kwargs)
-
- def set_cursor(self):
- if Settings.cursor_manipulation_enabled:
- if self.state == tkinter.DISABLED:
- if sys.platform == "darwin" and self.command is not None and Settings.cursor_manipulation_enabled:
- self.configure(cursor="arrow")
- elif sys.platform.startswith("win") and self.command is not None and Settings.cursor_manipulation_enabled:
- self.configure(cursor="arrow")
-
- elif self.state == tkinter.NORMAL:
- if sys.platform == "darwin" and self.command is not None and Settings.cursor_manipulation_enabled:
- self.configure(cursor="pointinghand")
- elif sys.platform.startswith("win") and self.command is not None and Settings.cursor_manipulation_enabled:
- self.configure(cursor="hand2")
-
- def set_image(self, image):
- """ will be removed in next major """
- self.configure(image=image)
-
- def set_text(self, text):
- """ will be removed in next major """
- self.configure(text=text)
-
- def on_enter(self, event=None):
- if self.hover is True and self.state == tkinter.NORMAL:
- if self.hover_color is None:
- inner_parts_color = self.fg_color
- else:
- inner_parts_color = self.hover_color
-
- # set color of inner button parts to hover color
- self.canvas.itemconfig("inner_parts",
- outline=ThemeManager.single_color(inner_parts_color, self._appearance_mode),
- fill=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
-
- # set text_label bg color to button hover color
- if self.text_label is not None:
- self.text_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
-
- # set image_label bg color to button hover color
- if self.image_label is not None:
- self.image_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
-
- def on_leave(self, event=None):
- self.click_animation_running = False
-
- if self.hover is True:
- if self.fg_color is None:
- inner_parts_color = self.bg_color
- else:
- inner_parts_color = self.fg_color
-
- # set color of inner button parts
- self.canvas.itemconfig("inner_parts",
- outline=ThemeManager.single_color(inner_parts_color, self._appearance_mode),
- fill=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
-
- # set text_label bg color (label color)
- if self.text_label is not None:
- self.text_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
-
- # set image_label bg color (image bg color)
- if self.image_label is not None:
- self.image_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode))
-
- def click_animation(self):
- if self.click_animation_running:
- self.on_enter()
-
- def clicked(self, event=None):
- if self.command is not None:
- if self.state != tkinter.DISABLED:
-
- # click animation: change color with .on_leave() and back to normal after 100ms with click_animation()
- self.on_leave()
- self.click_animation_running = True
- self.after(100, self.click_animation)
-
- self.command()
diff --git a/customtkinter/widgets/ctk_checkbox.py b/customtkinter/widgets/ctk_checkbox.py
deleted file mode 100644
index 38931c0..0000000
--- a/customtkinter/widgets/ctk_checkbox.py
+++ /dev/null
@@ -1,322 +0,0 @@
-import tkinter
-import sys
-from typing import Union
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..settings import Settings
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkCheckBox(CTkBaseClass):
- """ tkinter custom checkbox with border, rounded corners and hover effect """
-
- def __init__(self, *args,
- bg_color=None,
- fg_color="default_theme",
- hover_color="default_theme",
- border_color="default_theme",
- border_width="default_theme",
- checkmark_color="default_theme",
- width=24,
- height=24,
- corner_radius="default_theme",
- text_font="default_theme",
- text_color="default_theme",
- text="CTkCheckBox",
- text_color_disabled="default_theme",
- hover=True,
- command=None,
- state=tkinter.NORMAL,
- onvalue=1,
- offvalue=0,
- variable=None,
- textvariable=None,
- **kwargs):
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
-
- # color
- self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
- self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
- self.border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
- self.checkmark_color = ThemeManager.theme["color"]["checkmark"] if checkmark_color == "default_theme" else checkmark_color
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["checkbox_corner_radius"] if corner_radius == "default_theme" else corner_radius
- self.border_width = ThemeManager.theme["shape"]["checkbox_border_width"] if border_width == "default_theme" else border_width
-
- # text
- self.text = text
- self.text_label: Union[tkinter.Label, None] = None
- self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
- self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
- self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
-
- # callback and hover functionality
- self.command = command
- self.state = state
- self.hover = hover
- self.check_state = False
-
- self.onvalue = onvalue
- self.offvalue = offvalue
- self.variable: tkinter.Variable = variable
- self.variable_callback_blocked = False
- self.textvariable: tkinter.Variable = textvariable
- self.variable_callback_name = None
-
- # configure grid system (1x3)
- self.grid_columnconfigure(0, weight=0)
- self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
- self.grid_columnconfigure(2, weight=1)
- self.grid_rowconfigure(0, weight=1)
-
- self.bg_canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, rowspan=1)
- self.draw_engine = DrawEngine(self.canvas)
-
- self.canvas.bind("", self.on_enter)
- self.canvas.bind("", self.on_leave)
- self.canvas.bind("", self.toggle)
-
- self.text_label = tkinter.Label(master=self,
- bd=0,
- text=self.text,
- justify=tkinter.LEFT,
- font=self.apply_font_scaling(self.text_font),
- textvariable=self.textvariable)
- self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
- self.text_label["anchor"] = "w"
-
- self.text_label.bind("", self.on_enter)
- self.text_label.bind("", self.on_leave)
- self.text_label.bind("", self.toggle)
-
- # register variable callback and set state according to variable
- if self.variable is not None and self.variable != "":
- self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
- self.check_state = True if variable.get() == self.onvalue else False
-
- self.draw() # initial draw
- self.set_cursor()
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
-
- self.canvas.delete("checkmark")
- self.bg_canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def destroy(self):
- if self.variable is not None:
- self.variable.trace_remove("write", self.variable_callback_name)
-
- super().destroy()
-
- def draw(self, no_color_updates=False):
- requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width))
-
- if self.check_state is True:
- self.draw_engine.draw_checkmark(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self._current_height * 0.58))
- else:
- self.canvas.delete("checkmark")
-
- self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- if self.check_state is True:
- self.canvas.itemconfig("inner_parts",
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts",
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- if "create_line" in self.canvas.gettags("checkmark"):
- self.canvas.itemconfig("checkmark", fill=ThemeManager.single_color(self.checkmark_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("checkmark", fill=ThemeManager.single_color(self.checkmark_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("inner_parts",
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts",
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
-
- if self.state == tkinter.DISABLED:
- self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)))
- else:
- self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
-
- self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- def configure(self, require_redraw=False, **kwargs):
- if "text" in kwargs:
- self.text = kwargs.pop("text")
- self.text_label.configure(text=self.text)
-
- if "text_font" in kwargs:
- self.text_font = kwargs.pop("text_font")
- if self.text_label is not None:
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
-
- if "state" in kwargs:
- self.state = kwargs.pop("state")
- self.set_cursor()
- require_redraw = True
-
- if "fg_color" in kwargs:
- self.fg_color = kwargs.pop("fg_color")
- require_redraw = True
-
- if "hover_color" in kwargs:
- self.hover_color = kwargs.pop("hover_color")
- require_redraw = True
-
- if "text_color" in kwargs:
- self.text_color = kwargs.pop("text_color")
- require_redraw = True
-
- if "border_color" in kwargs:
- self.border_color = kwargs.pop("border_color")
- require_redraw = True
-
- 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 "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
-
- super().configure(require_redraw=require_redraw, **kwargs)
-
- def set_cursor(self):
- if Settings.cursor_manipulation_enabled:
- if self.state == tkinter.DISABLED:
- if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="arrow")
- if self.text_label is not None:
- self.text_label.configure(cursor="arrow")
- elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="arrow")
- if self.text_label is not None:
- self.text_label.configure(cursor="arrow")
-
- elif self.state == tkinter.NORMAL:
- if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="pointinghand")
- if self.text_label is not None:
- self.text_label.configure(cursor="pointinghand")
- elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="hand2")
- if self.text_label is not None:
- self.text_label.configure(cursor="hand2")
-
- def on_enter(self, event=0):
- if self.hover is True and self.state == tkinter.NORMAL:
- if self.check_state is True:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.hover_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.hover_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.hover_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.hover_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.hover_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.hover_color, self._appearance_mode))
-
- def on_leave(self, event=0):
- if self.hover is True:
- if self.check_state is True:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
-
- def variable_callback(self, var_name, index, mode):
- if not self.variable_callback_blocked:
- if self.variable.get() == self.onvalue:
- self.select(from_variable_callback=True)
- elif self.variable.get() == self.offvalue:
- self.deselect(from_variable_callback=True)
-
- def toggle(self, event=0):
- if self.state == tkinter.NORMAL:
- if self.check_state is True:
- self.check_state = False
- self.draw()
- 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
-
- 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
-
- 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
-
- def get(self):
- return self.onvalue if self.check_state is True else self.offvalue
diff --git a/customtkinter/widgets/ctk_combobox.py b/customtkinter/widgets/ctk_combobox.py
deleted file mode 100644
index eda3a64..0000000
--- a/customtkinter/widgets/ctk_combobox.py
+++ /dev/null
@@ -1,297 +0,0 @@
-import tkinter
-import sys
-
-from .dropdown_menu import DropdownMenu
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..settings import Settings
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkComboBox(CTkBaseClass):
- def __init__(self, *args,
- bg_color=None,
- fg_color="default_theme",
- border_color="default_theme",
- button_color="default_theme",
- button_hover_color="default_theme",
- dropdown_color="default_theme",
- dropdown_hover_color="default_theme",
- dropdown_text_color="default_theme",
- variable=None,
- values=None,
- command=None,
- width=140,
- height=28,
- corner_radius="default_theme",
- border_width="default_theme",
- text_font="default_theme",
- dropdown_text_font="default_theme",
- text_color="default_theme",
- text_color_disabled="default_theme",
- hover=True,
- state=tkinter.NORMAL,
- **kwargs):
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
-
- # color variables
- self.fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
- self.border_color = ThemeManager.theme["color"]["combobox_border"] if border_color == "default_theme" else border_color
- self.button_color = ThemeManager.theme["color"]["combobox_border"] if button_color == "default_theme" else button_color
- self.button_hover_color = ThemeManager.theme["color"]["combobox_button_hover"] if button_hover_color == "default_theme" else button_hover_color
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
- self.border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width
-
- # text and font
- self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
- self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
- self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
-
- # callback and hover functionality
- self.command = command
- self.textvariable = variable
- self.state = state
- self.hover = hover
-
- if values is None:
- self.values = ["CTkComboBox"]
- else:
- self.values = values
-
- self.dropdown_menu = DropdownMenu(master=self,
- values=self.values,
- command=self.dropdown_callback,
- fg_color=dropdown_color,
- hover_color=dropdown_hover_color,
- text_color=dropdown_text_color,
- text_font=dropdown_text_font)
-
- # configure grid system (1x1)
- self.grid_rowconfigure(0, weight=1)
- self.grid_columnconfigure(0, weight=1)
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
- self.draw_engine = DrawEngine(self.canvas)
-
- self.entry = tkinter.Entry(master=self,
- state=self.state,
- width=1,
- bd=0,
- highlightthickness=0,
- font=self.apply_font_scaling(self.text_font))
- left_section_width = self._current_width - self._current_height
- self.entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew",
- padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(3)),
- max(self.apply_widget_scaling(self._current_width - left_section_width + 3), self.apply_widget_scaling(3))))
-
- # insert default value
- if len(self.values) > 0:
- self.entry.insert(0, self.values[0])
- else:
- self.entry.insert(0, "CTkComboBox")
-
- self.draw() # initial draw
-
- # event bindings
- self.canvas.tag_bind("right_parts", "", self.on_enter)
- self.canvas.tag_bind("dropdown_arrow", "", self.on_enter)
- self.canvas.tag_bind("right_parts", "", self.on_leave)
- self.canvas.tag_bind("dropdown_arrow", "", self.on_leave)
- self.canvas.tag_bind("right_parts", "", self.clicked)
- self.canvas.tag_bind("dropdown_arrow", "", self.clicked)
- self.bind('', self.update_dimensions_event)
-
- if self.textvariable is not None:
- self.entry.configure(textvariable=self.textvariable)
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- # change entry font size and grid padding
- left_section_width = self._current_width - self._current_height
- self.entry.configure(font=self.apply_font_scaling(self.text_font))
- self.entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew",
- padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(3)),
- max(self.apply_widget_scaling(self._current_width - left_section_width + 3), self.apply_widget_scaling(3))))
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def set_dimensions(self, width: int = None, height: int = None):
- super().set_dimensions(width, height)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def draw(self, no_color_updates=False):
- left_section_width = self._current_width - self._current_height
- requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width),
- self.apply_widget_scaling(left_section_width))
-
- requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self.apply_widget_scaling(self._current_width - (self._current_height / 2)),
- self.apply_widget_scaling(self._current_height / 2),
- self.apply_widget_scaling(self._current_height / 3))
-
- if no_color_updates is False or requires_recoloring or requires_recoloring_2:
-
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- self.canvas.itemconfig("inner_parts_left",
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts_left",
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
- self.canvas.itemconfig("inner_parts_right",
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts_right",
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
-
- self.entry.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
- disabledforeground=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode),
- disabledbackground=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- if self.state == tkinter.DISABLED:
- self.canvas.itemconfig("dropdown_arrow",
- fill=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode))
- else:
- self.canvas.itemconfig("dropdown_arrow",
- fill=ThemeManager.single_color(self.text_color, self._appearance_mode))
-
- def open_dropdown_menu(self):
- self.dropdown_menu.open(self.winfo_rootx(),
- self.winfo_rooty() + self.apply_widget_scaling(self._current_height + 0))
-
- def configure(self, require_redraw=False, **kwargs):
- if "state" in kwargs:
- self.state = kwargs.pop("state")
- self.entry.configure(state=self.state)
- require_redraw = True
-
- if "fg_color" in kwargs:
- self.fg_color = kwargs.pop("fg_color")
- require_redraw = True
-
- if "button_color" in kwargs:
- self.button_color = kwargs.pop("button_color")
- require_redraw = True
-
- if "button_hover_color" in kwargs:
- self.button_hover_color = kwargs.pop("button_hover_color")
- require_redraw = True
-
- if "text_color" in kwargs:
- self.text_color = kwargs.pop("text_color")
- require_redraw = True
-
- if "text_font" in kwargs:
- self.text_font = kwargs.pop("text_font")
- self.entry.configure(font=self.apply_font_scaling(self.text_font))
-
- if "command" in kwargs:
- self.command = kwargs.pop("command")
-
- if "variable" in kwargs:
- self.textvariable = kwargs.pop("variable")
- self.entry.configure(textvariable=self.textvariable)
-
- if "width" in kwargs:
- self.set_dimensions(width=kwargs.pop("width"))
-
- if "height" in kwargs:
- self.set_dimensions(height=kwargs.pop("height"))
-
- if "values" in kwargs:
- self.values = kwargs.pop("values")
- self.dropdown_menu.configure(values=self.values)
-
- if "dropdown_color" in kwargs:
- self.dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color"))
-
- if "dropdown_hover_color" in kwargs:
- self.dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
-
- if "dropdown_text_color" in kwargs:
- self.dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
-
- if "dropdown_text_font" in kwargs:
- self.dropdown_menu.configure(text_font=kwargs.pop("dropdown_text_font"))
-
- super().configure(require_redraw=require_redraw, **kwargs)
-
- def on_enter(self, event=0):
- if self.hover is True and self.state == tkinter.NORMAL and len(self.values) > 0:
- if sys.platform == "darwin" and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="pointinghand")
- elif sys.platform.startswith("win") and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="hand2")
-
- # set color of inner button parts to hover color
- self.canvas.itemconfig("inner_parts_right",
- outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts_right",
- outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
-
- def on_leave(self, event=0):
- if self.hover is True:
- if sys.platform == "darwin" and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="arrow")
- elif sys.platform.startswith("win") and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="arrow")
-
- # set color of inner button parts
- self.canvas.itemconfig("inner_parts_right",
- outline=ThemeManager.single_color(self.button_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.button_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts_right",
- outline=ThemeManager.single_color(self.button_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.button_color, self._appearance_mode))
-
- def dropdown_callback(self, value: str):
- if self.state == "readonly":
- self.entry.configure(state="normal")
- self.entry.delete(0, tkinter.END)
- self.entry.insert(0, value)
- self.entry.configure(state="readonly")
- else:
- self.entry.delete(0, tkinter.END)
- self.entry.insert(0, value)
-
- if self.command is not None:
- self.command(value)
-
- def set(self, value: str):
- if self.state == "readonly":
- self.entry.configure(state="normal")
- self.entry.delete(0, tkinter.END)
- self.entry.insert(0, value)
- self.entry.configure(state="readonly")
- else:
- self.entry.delete(0, tkinter.END)
- self.entry.insert(0, value)
-
- def get(self) -> str:
- return self.entry.get()
-
- def clicked(self, event=0):
- if self.state is not tkinter.DISABLED and len(self.values) > 0:
- self.open_dropdown_menu()
diff --git a/customtkinter/widgets/ctk_entry.py b/customtkinter/widgets/ctk_entry.py
deleted file mode 100644
index ef6171c..0000000
--- a/customtkinter/widgets/ctk_entry.py
+++ /dev/null
@@ -1,252 +0,0 @@
-import tkinter
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkEntry(CTkBaseClass):
- def __init__(self, *args,
- bg_color=None,
- fg_color="default_theme",
- text_color="default_theme",
- placeholder_text_color="default_theme",
- text_font="default_theme",
- placeholder_text=None,
- corner_radius="default_theme",
- border_width="default_theme",
- border_color="default_theme",
- width=140,
- height=28,
- state=tkinter.NORMAL,
- textvariable: tkinter.Variable = None,
- **kwargs):
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- if "master" in kwargs:
- super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs.pop("master"))
- else:
- super().__init__(*args, bg_color=bg_color, width=width, height=height)
-
- # configure grid system (1x1)
- self.grid_rowconfigure(0, weight=1)
- self.grid_columnconfigure(0, weight=1)
-
- # color
- self.fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
- self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
- self.placeholder_text_color = ThemeManager.theme["color"]["entry_placeholder_text"] if placeholder_text_color == "default_theme" else placeholder_text_color
- self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
- self.border_color = ThemeManager.theme["color"]["entry_border"] if border_color == "default_theme" else border_color
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
- self.border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width
-
- # placeholder text
- self.placeholder_text = placeholder_text
- self.placeholder_text_active = False
- self.pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back
-
- # textvariable
- self.textvariable = textvariable
-
- self.state = state
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._current_width),
- height=self.apply_widget_scaling(self._current_height))
- self.canvas.grid(column=0, row=0, sticky="nswe")
- self.draw_engine = DrawEngine(self.canvas)
-
- self.entry = tkinter.Entry(master=self,
- bd=0,
- width=1,
- highlightthickness=0,
- font=self.apply_font_scaling(self.text_font),
- state=self.state,
- textvariable=self.textvariable,
- **kwargs)
- self.entry.grid(column=0, row=0, sticky="nswe",
- padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6),
- pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width + 1)))
-
- super().bind('', self.update_dimensions_event)
- self.entry.bind('', self.entry_focus_out)
- self.entry.bind('', self.entry_focus_in)
-
- self.activate_placeholder()
- self.draw()
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling( *args, **kwargs)
-
- self.entry.configure(font=self.apply_font_scaling(self.text_font))
- self.entry.grid(column=0, row=0, sticky="we",
- padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6))
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def set_dimensions(self, width=None, height=None):
- super().set_dimensions(width, height)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def draw(self, no_color_updates=False):
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width))
-
- if requires_recoloring or no_color_updates is False:
- if ThemeManager.single_color(self.fg_color, self._appearance_mode) is not None:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- self.entry.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- disabledbackground=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- highlightcolor=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
- disabledforeground=ThemeManager.single_color(self.text_color, self._appearance_mode),
- insertbackground=ThemeManager.single_color(self.text_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- self.entry.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- disabledbackground=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- highlightcolor=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
- disabledforeground=ThemeManager.single_color(self.text_color, self._appearance_mode),
- insertbackground=ThemeManager.single_color(self.text_color, self._appearance_mode))
-
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
-
- if self.placeholder_text_active:
- self.entry.config(fg=ThemeManager.single_color(self.placeholder_text_color, self._appearance_mode))
-
- def bind(self, *args, **kwargs):
- self.entry.bind(*args, **kwargs)
-
- def configure(self, require_redraw=False, **kwargs):
- if "state" in kwargs:
- self.state = kwargs.pop("state")
- self.entry.configure(state=self.state)
-
- if "fg_color" in kwargs:
- self.fg_color = kwargs.pop("fg_color")
- require_redraw = True
-
- if "text_color" in kwargs:
- self.text_color = kwargs.pop("text_color")
- require_redraw = True
-
- if "border_color" in kwargs:
- self.border_color = kwargs.pop("border_color")
- require_redraw = True
-
- if "corner_radius" in kwargs:
- self.corner_radius = kwargs.pop("corner_radius")
-
- if self.corner_radius * 2 > self._current_height:
- self.corner_radius = self._current_height / 2
- elif self.corner_radius * 2 > self._current_width:
- self.corner_radius = self._current_width / 2
-
- self.entry.grid(column=0, row=0, sticky="we", padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6))
- require_redraw = True
-
- if "width" in kwargs:
- self.set_dimensions(width=kwargs.pop("width"))
-
- if "height" in kwargs:
- self.set_dimensions(height=kwargs.pop("height"))
-
- if "placeholder_text" in kwargs:
- self.placeholder_text = kwargs.pop("placeholder_text")
- if self.placeholder_text_active:
- self.entry.delete(0, tkinter.END)
- self.entry.insert(0, self.placeholder_text)
- else:
- self.activate_placeholder()
-
- if "placeholder_text_color" in kwargs:
- self.placeholder_text_color = kwargs.pop("placeholder_text_color")
- require_redraw = True
-
- if "textvariable" in kwargs:
- self.textvariable = kwargs.pop("textvariable")
- self.entry.configure(textvariable=self.textvariable)
-
- if "text_font" in kwargs:
- self.text_font = kwargs.pop("text_font")
- self.entry.configure(font=self.apply_font_scaling(self.text_font))
-
- if "show" in kwargs:
- if self.placeholder_text_active:
- self.pre_placeholder_arguments["show"] = kwargs.pop("show")
- else:
- self.entry.configure(show=kwargs.pop("show"))
-
- if "bg_color" in kwargs:
- super().configure(bg_color=kwargs.pop("bg_color"), require_redraw=require_redraw)
- else:
- super().configure(require_redraw=require_redraw)
-
- self.entry.configure(**kwargs) # pass remaining kwargs to entry
-
- def activate_placeholder(self):
- if self.entry.get() == "" and self.placeholder_text is not None and (self.textvariable is None or self.textvariable == ""):
- self.placeholder_text_active = True
-
- self.pre_placeholder_arguments = {"show": self.entry.cget("show")}
- self.entry.config(fg=ThemeManager.single_color(self.placeholder_text_color, self._appearance_mode), show="")
- self.entry.delete(0, tkinter.END)
- self.entry.insert(0, self.placeholder_text)
-
- def deactivate_placeholder(self):
- if self.placeholder_text_active:
- self.placeholder_text_active = False
-
- self.entry.config(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
- self.entry.delete(0, tkinter.END)
- for argument, value in self.pre_placeholder_arguments.items():
- self.entry[argument] = value
-
- def entry_focus_out(self, event=None):
- self.activate_placeholder()
-
- def entry_focus_in(self, event=None):
- self.deactivate_placeholder()
-
- def delete(self, *args, **kwargs):
- self.entry.delete(*args, **kwargs)
-
- if self.entry.get() == "":
- self.activate_placeholder()
-
- def insert(self, *args, **kwargs):
- self.deactivate_placeholder()
-
- return self.entry.insert(*args, **kwargs)
-
- def get(self):
- if self.placeholder_text_active:
- return ""
- else:
- return self.entry.get()
-
- def focus(self):
- self.entry.focus()
-
- def focus_force(self):
- self.entry.focus_force()
diff --git a/customtkinter/widgets/ctk_frame.py b/customtkinter/widgets/ctk_frame.py
deleted file mode 100644
index 2e5ced7..0000000
--- a/customtkinter/widgets/ctk_frame.py
+++ /dev/null
@@ -1,132 +0,0 @@
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkFrame(CTkBaseClass):
- def __init__(self, *args,
- bg_color=None,
- fg_color="default_theme",
- border_color="default_theme",
- border_width="default_theme",
- corner_radius="default_theme",
- width=200,
- height=200,
- overwrite_preferred_drawing_method: str = None,
- **kwargs):
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
-
- # color
- self.border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color
-
- # determine fg_color of frame
- if fg_color == "default_theme":
- if isinstance(self.master, CTkFrame):
- if self.master.fg_color == ThemeManager.theme["color"]["frame_low"]:
- self.fg_color = ThemeManager.theme["color"]["frame_high"]
- else:
- self.fg_color = ThemeManager.theme["color"]["frame_low"]
- else:
- self.fg_color = ThemeManager.theme["color"]["frame_low"]
- else:
- self.fg_color = fg_color
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius
- self.border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._current_width),
- height=self.apply_widget_scaling(self._current_height))
- self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- self.draw_engine = DrawEngine(self.canvas)
- self._overwrite_preferred_drawing_method = overwrite_preferred_drawing_method
-
- self.bind('', self.update_dimensions_event)
-
- self.draw()
-
- def winfo_children(self):
- """ winfo_children of CTkFrame without self.canvas widget,
- because it's not a child but part of the CTkFrame itself """
-
- child_widgets = super().winfo_children()
- try:
- child_widgets.remove(self.canvas)
- return child_widgets
- except ValueError:
- return child_widgets
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def set_dimensions(self, width=None, height=None):
- super().set_dimensions(width, height)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def draw(self, no_color_updates=False):
-
- requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width),
- overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method)
-
- if no_color_updates is False or requires_recoloring:
- if self.fg_color is None:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- self.canvas.tag_lower("inner_parts")
- self.canvas.tag_lower("border_parts")
-
- def configure(self, require_redraw=False, **kwargs):
- if "fg_color" in kwargs:
- self.fg_color = kwargs.pop("fg_color")
- require_redraw = True
-
- # check if CTk widgets are children of the frame and change their bg_color to new frame fg_color
- for child in self.winfo_children():
- if isinstance(child, CTkBaseClass):
- child.configure(bg_color=self.fg_color)
-
- if "border_color" in kwargs:
- self.border_color = kwargs.pop("border_color")
- 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
-
- if "width" in kwargs:
- self.set_dimensions(width=kwargs.pop("width"))
-
- if "height" in kwargs:
- self.set_dimensions(height=kwargs.pop("height"))
-
- super().configure(require_redraw=require_redraw, **kwargs)
diff --git a/customtkinter/widgets/ctk_label.py b/customtkinter/widgets/ctk_label.py
deleted file mode 100644
index cac1154..0000000
--- a/customtkinter/widgets/ctk_label.py
+++ /dev/null
@@ -1,159 +0,0 @@
-import sys
-import tkinter
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkLabel(CTkBaseClass):
- def __init__(self, *args,
- bg_color=None,
- fg_color="default_theme",
- text_color="default_theme",
- corner_radius="default_theme",
- width=140,
- height=28,
- text="CTkLabel",
- text_font="default_theme",
- anchor="center", # label anchor: center, n, e, s, w
- **kwargs):
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- if "master" in kwargs:
- super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs.pop("master"))
- else:
- super().__init__(*args, bg_color=bg_color, width=width, height=height)
-
- # color
- self.fg_color = ThemeManager.theme["color"]["label"] if fg_color == "default_theme" else fg_color
- if self.fg_color is None:
- self.fg_color = self.bg_color
- self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["label_corner_radius"] if corner_radius == "default_theme" else corner_radius
-
- # text
- self.anchor = anchor
- self.text = text
- self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
-
- # configure grid system (1x1)
- self.grid_rowconfigure(0, weight=1)
- self.grid_columnconfigure(0, weight=1)
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.canvas.grid(row=0, column=0, sticky="nswe")
- self.draw_engine = DrawEngine(self.canvas)
-
- self.text_label = tkinter.Label(master=self,
- highlightthickness=0,
- bd=0,
- anchor=self.anchor,
- text=self.text,
- font=self.apply_font_scaling(self.text_font),
- **kwargs)
- text_label_grid_sticky = self.anchor if self.anchor != "center" else ""
- self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius),
- sticky=text_label_grid_sticky)
-
- self.bind('', self.update_dimensions_event)
- self.draw()
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
- text_label_grid_sticky = self.anchor if self.anchor != "center" else ""
- self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius),
- sticky=text_label_grid_sticky)
-
- self.draw()
-
- def set_dimensions(self, width=None, height=None):
- super().set_dimensions(width, height)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def draw(self, no_color_updates=False):
- requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- 0)
-
- if no_color_updates is False or requires_recoloring:
- if ThemeManager.single_color(self.fg_color, self._appearance_mode) is not None:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
- bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
- bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- def config(self, **kwargs):
- sys.stderr.write("Warning: Use .configure() instead of .config()")
- self.configure(**kwargs)
-
- def configure(self, require_redraw=False, **kwargs):
- if "anchor" in kwargs:
- self.anchor = kwargs.pop("anchor")
- text_label_grid_sticky = self.anchor if self.anchor != "center" else ""
- self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius),
- sticky=text_label_grid_sticky)
-
- if "text" in kwargs:
- self.text = kwargs["text"]
- self.text_label.configure(text=self.text)
- del kwargs["text"]
-
- if "text_font" in kwargs:
- self.text_font = kwargs.pop("text_font")
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
-
- if "fg_color" in kwargs:
- self.fg_color = kwargs["fg_color"]
- require_redraw = True
- del kwargs["fg_color"]
-
- if "text_color" in kwargs:
- self.text_color = kwargs["text_color"]
- require_redraw = True
- del kwargs["text_color"]
-
- if "width" in kwargs:
- self.set_dimensions(width=kwargs["width"])
- del kwargs["width"]
-
- if "height" in kwargs:
- self.set_dimensions(height=kwargs["height"])
- del kwargs["height"]
-
- if "bg_color" in kwargs:
- super().configure(bg_color=kwargs.pop("bg_color"), require_redraw=require_redraw)
- else:
- super().configure(require_redraw=require_redraw)
-
- self.text_label.configure(**kwargs) # pass remaining kwargs to label
-
- def set_text(self, text):
- """ Will be removed in the next major release """
-
- self.text = text
- self.text_label.configure(text=self.text)
diff --git a/customtkinter/widgets/ctk_optionmenu.py b/customtkinter/widgets/ctk_optionmenu.py
deleted file mode 100644
index cc8601f..0000000
--- a/customtkinter/widgets/ctk_optionmenu.py
+++ /dev/null
@@ -1,311 +0,0 @@
-import tkinter
-import sys
-
-from .dropdown_menu import DropdownMenu
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..settings import Settings
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkOptionMenu(CTkBaseClass):
- def __init__(self, *args,
- bg_color=None,
- fg_color="default_theme",
- button_color="default_theme",
- button_hover_color="default_theme",
- text_color="default_theme",
- text_color_disabled="default_theme",
- dropdown_color="default_theme",
- dropdown_hover_color="default_theme",
- dropdown_text_color="default_theme",
- variable=None,
- values=None,
- command=None,
- width=140,
- height=28,
- corner_radius="default_theme",
- text_font="default_theme",
- dropdown_text_font="default_theme",
- hover=True,
- state=tkinter.NORMAL,
- dynamic_resizing=True,
- **kwargs):
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
-
- # color variables
- self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
- self.button_color = ThemeManager.theme["color"]["optionmenu_button"] if button_color == "default_theme" else button_color
- self.button_hover_color = ThemeManager.theme["color"]["optionmenu_button_hover"] if button_hover_color == "default_theme" else button_hover_color
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
-
- # text and font
- self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
- self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
- self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
- self.dropdown_text_font = dropdown_text_font
-
- # callback and hover functionality
- self.command = command
- self.variable = variable
- self.variable_callback_blocked = False
- self.variable_callback_name = None
- self.state = state
- self.hover = hover
- self.dynamic_resizing = dynamic_resizing
-
- if values is None:
- self.values = ["CTkOptionMenu"]
- else:
- self.values = values
-
- if len(self.values) > 0:
- self.current_value = self.values[0]
- else:
- self.current_value = "CTkOptionMenu"
-
- self.dropdown_menu = DropdownMenu(master=self,
- values=self.values,
- command=self.dropdown_callback,
- fg_color=dropdown_color,
- hover_color=dropdown_hover_color,
- text_color=dropdown_text_color,
- text_font=dropdown_text_font)
-
- # configure grid system (1x1)
- self.grid_rowconfigure(0, weight=1)
- self.grid_columnconfigure(0, weight=1)
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
- self.draw_engine = DrawEngine(self.canvas)
-
- left_section_width = self._current_width - self._current_height
- self.text_label = tkinter.Label(master=self,
- font=self.apply_font_scaling(self.text_font),
- anchor="w",
- text=self.current_value)
- self.text_label.grid(row=0, column=0, sticky="w",
- padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(3)),
- max(self.apply_widget_scaling(self._current_width - left_section_width + 3), self.apply_widget_scaling(3))))
-
- if not self.dynamic_resizing:
- self.grid_propagate(0)
-
- if Settings.cursor_manipulation_enabled:
- if sys.platform == "darwin":
- self.configure(cursor="pointinghand")
- elif sys.platform.startswith("win"):
- self.configure(cursor="hand2")
-
- # event bindings
- self.canvas.bind("", self.on_enter)
- self.canvas.bind("", self.on_leave)
- self.canvas.bind("", self.clicked)
- self.canvas.bind("", self.clicked)
-
- 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.bind('', self.update_dimensions_event)
-
- self.draw() # initial draw
-
- if self.variable is not None:
- self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
- self.current_value = self.variable.get()
- self.text_label.configure(text=self.current_value)
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- # change label text size and grid padding
- left_section_width = self._current_width - self._current_height
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
- self.text_label.grid(row=0, column=0, sticky="w",
- padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(3)),
- max(self.apply_widget_scaling(self._current_width - left_section_width + 3), self.apply_widget_scaling(3))))
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def set_dimensions(self, width: int = None, height: int = None):
- super().set_dimensions(width, height)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def draw(self, no_color_updates=False):
- left_section_width = self._current_width - self._current_height
- requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- 0,
- self.apply_widget_scaling(left_section_width))
-
- requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self.apply_widget_scaling(self._current_width - (self._current_height / 2)),
- self.apply_widget_scaling(self._current_height / 2),
- self.apply_widget_scaling(self._current_height / 3))
-
- if no_color_updates is False or requires_recoloring or requires_recoloring_2:
-
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- self.canvas.itemconfig("inner_parts_left",
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- self.canvas.itemconfig("inner_parts_right",
- outline=ThemeManager.single_color(self.button_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.button_color, self._appearance_mode))
-
- self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
-
- if self.state == tkinter.DISABLED:
- self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)))
- self.canvas.itemconfig("dropdown_arrow",
- fill=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode))
- else:
- self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
- self.canvas.itemconfig("dropdown_arrow",
- fill=ThemeManager.single_color(self.text_color, self._appearance_mode))
-
- self.text_label.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- self.canvas.update_idletasks()
-
- def open_dropdown_menu(self):
- self.dropdown_menu.open(self.winfo_rootx(),
- self.winfo_rooty() + self.apply_widget_scaling(self._current_height + 0))
-
- def configure(self, require_redraw=False, **kwargs):
- if "state" in kwargs:
- self.state = kwargs.pop("state")
- require_redraw = True
-
- if "fg_color" in kwargs:
- self.fg_color = kwargs.pop("fg_color")
- require_redraw = True
-
- if "button_color" in kwargs:
- self.button_color = kwargs.pop("button_color")
- require_redraw = True
-
- if "button_hover_color" in kwargs:
- self.button_hover_color = kwargs.pop("button_hover_color")
- require_redraw = True
-
- if "text_color" in kwargs:
- self.text_color = kwargs.pop("text_color")
- require_redraw = True
-
- if "text_font" in kwargs:
- self.text_font = kwargs.pop("text_font")
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
-
- if "command" in kwargs:
- self.command = kwargs.pop("command")
-
- if "variable" in kwargs:
- if self.variable is not None: # remove old callback
- self.variable.trace_remove("write", self.variable_callback_name)
-
- 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 "width" in kwargs:
- self.set_dimensions(width=kwargs.pop("width"))
-
- if "height" in kwargs:
- self.set_dimensions(height=kwargs.pop("height"))
-
- if "values" in kwargs:
- self.values = kwargs.pop("values")
- self.dropdown_menu.configure(values=self.values)
-
- if "dropdown_color" in kwargs:
- self.dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color"))
-
- if "dropdown_hover_color" in kwargs:
- self.dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
-
- if "dropdown_text_color" in kwargs:
- self.dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
-
- if "dropdown_text_font" in kwargs:
- self.dropdown_text_font = kwargs.pop("dropdown_text_font")
- self.dropdown_menu.configure(text_font=self.dropdown_text_font)
-
- if "dynamic_resizing" in kwargs:
- self.dynamic_resizing = kwargs.pop("dynamic_resizing")
- if not self.dynamic_resizing:
- self.grid_propagate(0)
- else:
- self.grid_propagate(1)
-
- super().configure(require_redraw=require_redraw, **kwargs)
-
- def on_enter(self, event=0):
- if self.hover is True and self.state == tkinter.NORMAL and len(self.values) > 0:
- # set color of inner button parts to hover color
- self.canvas.itemconfig("inner_parts_right",
- outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
-
- def on_leave(self, event=0):
- if self.hover is True:
- # set color of inner button parts
- self.canvas.itemconfig("inner_parts_right",
- outline=ThemeManager.single_color(self.button_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.button_color, self._appearance_mode))
-
- def variable_callback(self, var_name, index, mode):
- if not self.variable_callback_blocked:
- self.current_value = self.variable.get()
- self.text_label.configure(text=self.current_value)
-
- def dropdown_callback(self, value: str):
- self.current_value = value
- self.text_label.configure(text=self.current_value)
-
- if self.variable is not None:
- self.variable_callback_blocked = True
- self.variable.set(self.current_value)
- self.variable_callback_blocked = False
-
- if self.command is not None:
- self.command(self.current_value)
-
- def set(self, value: str):
- self.current_value = value
- self.text_label.configure(text=self.current_value)
-
- if self.variable is not None:
- self.variable_callback_blocked = True
- self.variable.set(self.current_value)
- self.variable_callback_blocked = False
-
- def get(self) -> str:
- return self.current_value
-
- def clicked(self, event=0):
- if self.state is not tkinter.DISABLED and len(self.values) > 0:
- self.open_dropdown_menu()
diff --git a/customtkinter/widgets/ctk_progressbar.py b/customtkinter/widgets/ctk_progressbar.py
deleted file mode 100644
index 1c9494f..0000000
--- a/customtkinter/widgets/ctk_progressbar.py
+++ /dev/null
@@ -1,257 +0,0 @@
-import tkinter
-import math
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkProgressBar(CTkBaseClass):
- """ tkinter custom progressbar, values from 0 to 1 """
-
- def __init__(self, *args,
- variable=None,
- bg_color=None,
- border_color="default_theme",
- fg_color="default_theme",
- progress_color="default_theme",
- corner_radius="default_theme",
- width=None,
- height=None,
- border_width="default_theme",
- orient="horizontal",
- mode="determinate",
- determinate_speed=1,
- indeterminate_speed=1,
- **kwargs):
-
- # set default dimensions according to orientation
- if width is None:
- if orient.lower() == "vertical":
- width = 8
- else:
- width = 200
- if height is None:
- if orient.lower() == "vertical":
- height = 200
- else:
- height = 8
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
-
- # color
- self.border_color = ThemeManager.theme["color"]["progressbar_border"] if border_color == "default_theme" else border_color
- self.fg_color = ThemeManager.theme["color"]["progressbar"] if fg_color == "default_theme" else fg_color
- self.progress_color = ThemeManager.theme["color"]["progressbar_progress"] if progress_color == "default_theme" else progress_color
-
- # control variable
- self.variable = variable
- self.variable_callback_blocked = False
- self.variable_callback_name = None
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["progressbar_corner_radius"] if corner_radius == "default_theme" else corner_radius
- self.border_width = ThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width
- self.determinate_value = 0.5 # range 0-1
- self.determinate_speed = determinate_speed # range 0-1
- self.indeterminate_value = 0 # range 0-inf
- self.indeterminate_width = 0.4 # range 0-1
- self.indeterminate_speed = indeterminate_speed # range 0-1 to travel in 50ms
- self.loop_running = False
- self.orient = orient
- self.mode = mode # "determinate" or "indeterminate"
-
- self.grid_rowconfigure(0, weight=1)
- self.grid_columnconfigure(0, weight=1)
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe")
- self.draw_engine = DrawEngine(self.canvas)
-
- # Each time an item is resized due to pack position mode, the binding Configure is called on the widget
- self.bind('', self.update_dimensions_event)
-
- self.draw() # initial draw
-
- if self.variable is not None:
- self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
- self.variable_callback_blocked = True
- self.set(self.variable.get(), from_variable_callback=True)
- self.variable_callback_blocked = False
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def set_dimensions(self, width=None, height=None):
- super().set_dimensions(width, height)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def destroy(self):
- if self.variable is not None:
- self.variable.trace_remove("write", self.variable_callback_name)
-
- super().destroy()
-
- def draw(self, no_color_updates=False):
- if self.orient.lower() == "horizontal":
- orientation = "w"
- elif self.orient.lower() == "vertical":
- orientation = "s"
- else:
- orientation = "w"
-
- if self.mode == "determinate":
- requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width),
- 0,
- self.determinate_value,
- orientation)
- else: # indeterminate mode
- progress_value = (math.sin(self.indeterminate_value * math.pi / 40) + 1) / 2
- progress_value_1 = min(1.0, progress_value + (self.indeterminate_width / 2))
- progress_value_2 = max(0.0, progress_value - (self.indeterminate_width / 2))
-
- requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width),
- progress_value_1,
- progress_value_2,
- orientation)
-
- if no_color_updates is False or requires_recoloring:
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- self.canvas.itemconfig("progress_parts",
- fill=ThemeManager.single_color(self.progress_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.progress_color, self._appearance_mode))
-
- def configure(self, require_redraw=False, **kwargs):
- if "fg_color" in kwargs:
- self.fg_color = kwargs["fg_color"]
- del kwargs["fg_color"]
- require_redraw = True
-
- if "border_color" in kwargs:
- self.border_color = kwargs["border_color"]
- del kwargs["border_color"]
- require_redraw = True
-
- if "progress_color" in kwargs:
- self.progress_color = kwargs["progress_color"]
- del kwargs["progress_color"]
- require_redraw = True
-
- if "border_width" in kwargs:
- self.border_width = kwargs["border_width"]
- del kwargs["border_width"]
- require_redraw = True
-
- if "variable" in kwargs:
- if self.variable is not None:
- self.variable.trace_remove("write", self.variable_callback_name)
-
- self.variable = kwargs["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
-
- del kwargs["variable"]
-
- if "mode" in kwargs:
- self.mode = kwargs.pop("mode")
- require_redraw = True
-
- if "determinate_speed" in kwargs:
- self.determinate_speed = kwargs.pop("determinate_speed")
-
- if "indeterminate_speed" in kwargs:
- self.indeterminate_speed = kwargs.pop("indeterminate_speed")
-
- if "width" in kwargs:
- self.set_dimensions(width=kwargs["width"])
- del kwargs["width"]
-
- if "height" in kwargs:
- self.set_dimensions(height=kwargs["height"])
- del kwargs["height"]
-
- super().configure(require_redraw=require_redraw, **kwargs)
-
- def variable_callback(self, var_name, index, mode):
- if not self.variable_callback_blocked:
- self.set(self.variable.get(), from_variable_callback=True)
-
- def set(self, value, from_variable_callback=False):
- """ set determinate value """
- self.determinate_value = value
-
- if self.determinate_value > 1:
- self.determinate_value = 1
- elif self.determinate_value < 0:
- self.determinate_value = 0
-
- self.draw(no_color_updates=True)
-
- if self.variable is not None and not from_variable_callback:
- self.variable_callback_blocked = True
- self.variable.set(round(self.determinate_value) if isinstance(self.variable, tkinter.IntVar) else self.determinate_value)
- self.variable_callback_blocked = False
-
- def get(self):
- """ get determinate value """
- return self.determinate_value
-
- def start(self):
- """ start indeterminate mode """
- if not self.loop_running:
- self.loop_running = True
- self.internal_loop()
-
- def stop(self):
- """ stop indeterminate mode """
- self.loop_running = False
-
- def internal_loop(self):
- if self.loop_running:
- if self.mode == "determinate":
- self.determinate_value += self.determinate_speed / 50
- if self.determinate_value > 1:
- self.determinate_value -= 1
- self.draw()
- self.after(20, self.internal_loop)
- else:
- self.indeterminate_value += self.indeterminate_speed
- self.draw()
- self.after(20, self.internal_loop)
-
- def step(self):
- if self.mode == "determinate":
- self.determinate_value += self.determinate_speed / 50
- if self.determinate_value > 1:
- self.determinate_value -= 1
- self.draw()
- else:
- self.indeterminate_value += self.indeterminate_speed
- self.draw()
diff --git a/customtkinter/widgets/ctk_radiobutton.py b/customtkinter/widgets/ctk_radiobutton.py
deleted file mode 100644
index 6082af1..0000000
--- a/customtkinter/widgets/ctk_radiobutton.py
+++ /dev/null
@@ -1,281 +0,0 @@
-import tkinter
-import sys
-from typing import Union
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..settings import Settings
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkRadioButton(CTkBaseClass):
- def __init__(self, *args,
- bg_color=None,
- fg_color="default_theme",
- hover_color="default_theme",
- border_color="default_theme",
- border_width_unchecked="default_theme",
- border_width_checked="default_theme",
- width=22,
- height=22,
- corner_radius="default_theme",
- text_font="default_theme",
- text_color="default_theme",
- text="CTkRadioButton",
- text_color_disabled="default_theme",
- hover=True,
- command=None,
- state=tkinter.NORMAL,
- value=0,
- variable=None,
- textvariable=None,
- **kwargs):
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
-
- # color
- self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
- self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
- self.border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["radiobutton_corner_radius"] if corner_radius == "default_theme" else corner_radius
- self.border_width_unchecked = ThemeManager.theme["shape"]["radiobutton_border_width_unchecked"] if border_width_unchecked == "default_theme" else border_width_unchecked
- self.border_width_checked = ThemeManager.theme["shape"]["radiobutton_border_width_checked"] if border_width_checked == "default_theme" else border_width_checked
- self.border_width = self.border_width_unchecked
-
- # text
- self.text = text
- self.text_label: Union[tkinter.Label, None] = None
- self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
- self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
- self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
-
- # callback and control variables
- self.command = command
- self.state = state
- self.hover = hover
- self.check_state = False
- self.value = value
- self.variable: tkinter.Variable = variable
- self.variable_callback_blocked = False
- self.textvariable = textvariable
- self.variable_callback_name = None
-
- # configure grid system (3x1)
- self.grid_columnconfigure(0, weight=0)
- self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
- self.grid_columnconfigure(2, weight=1)
-
- self.bg_canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._current_width),
- height=self.apply_widget_scaling(self._current_height))
- self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._current_width),
- height=self.apply_widget_scaling(self._current_height))
- self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1)
- self.draw_engine = DrawEngine(self.canvas)
-
- self.canvas.bind("", self.on_enter)
- self.canvas.bind("", self.on_leave)
- self.canvas.bind("", self.invoke)
-
- self.text_label = tkinter.Label(master=self,
- bd=0,
- text=self.text,
- justify=tkinter.LEFT,
- font=self.apply_font_scaling(self.text_font),
- textvariable=self.textvariable)
- self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
- self.text_label["anchor"] = "w"
-
- self.text_label.bind("", self.on_enter)
- self.text_label.bind("", self.on_leave)
- self.text_label.bind("", self.invoke)
-
- if self.variable is not None:
- self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
- self.check_state = True if self.variable.get() == self.value else False
-
- self.draw() # initial draw
- self.set_cursor()
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
-
- self.bg_canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def destroy(self):
- if self.variable is not None:
- self.variable.trace_remove("write", self.variable_callback_name)
-
- super().destroy()
-
- def draw(self, no_color_updates=False):
- requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width))
-
- self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- if self.check_state is False:
- self.canvas.itemconfig("border_parts",
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("border_parts",
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- self.canvas.itemconfig("inner_parts",
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- if self.state == tkinter.DISABLED:
- self.text_label.configure(fg=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode))
- else:
- self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
-
- self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- def configure(self, require_redraw=False, **kwargs):
- if "text" in kwargs:
- self.text = kwargs.pop("text")
- self.text_label.configure(text=self.text)
-
- if "text_font" in kwargs:
- self.text_font = kwargs.pop("text_font")
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
-
- if "state" in kwargs:
- self.state = kwargs.pop("state")
- self.set_cursor()
- require_redraw = True
-
- if "fg_color" in kwargs:
- self.fg_color = kwargs.pop("fg_color")
- require_redraw = True
-
- if "hover_color" in kwargs:
- self.hover_color = kwargs.pop("hover_color")
- require_redraw = True
-
- if "text_color" in kwargs:
- self.text_color = kwargs.pop("text_color")
- require_redraw = True
-
- if "border_color" in kwargs:
- self.border_color = kwargs.pop("border_color")
- require_redraw = True
-
- if "border_width" in kwargs:
- self.border_width = kwargs.pop("border_width")
- require_redraw = True
-
- 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 "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
-
- super().configure(require_redraw=require_redraw, **kwargs)
-
- def set_cursor(self):
- if Settings.cursor_manipulation_enabled:
- if self.state == tkinter.DISABLED:
- if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="arrow")
- if self.text_label is not None:
- self.text_label.configure(cursor="arrow")
- elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="arrow")
- if self.text_label is not None:
- self.text_label.configure(cursor="arrow")
-
- elif self.state == tkinter.NORMAL:
- if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="pointinghand")
- if self.text_label is not None:
- self.text_label.configure(cursor="pointinghand")
- elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="hand2")
- if self.text_label is not None:
- self.text_label.configure(cursor="hand2")
-
- def on_enter(self, event=0):
- if self.hover is True and self.state == tkinter.NORMAL:
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.hover_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.hover_color, self._appearance_mode))
-
- def on_leave(self, event=0):
- if self.hover is True:
- if self.check_state is True:
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
-
- def variable_callback(self, var_name, index, mode):
- if not self.variable_callback_blocked:
- if self.variable.get() == self.value:
- self.select(from_variable_callback=True)
- else:
- self.deselect(from_variable_callback=True)
-
- def invoke(self, event=0):
- if self.state == tkinter.NORMAL:
- if self.check_state is False:
- self.check_state = True
- self.select()
-
- if self.command is not None:
- self.command()
-
- def select(self, from_variable_callback=False):
- self.check_state = True
- self.border_width = self.border_width_checked
- self.draw()
-
- if self.variable is not None and not from_variable_callback:
- self.variable_callback_blocked = True
- self.variable.set(self.value)
- self.variable_callback_blocked = False
-
- def deselect(self, from_variable_callback=False):
- self.check_state = False
- self.border_width = self.border_width_unchecked
- self.draw()
-
- if self.variable is not None and not from_variable_callback:
- self.variable_callback_blocked = True
- self.variable.set("")
- self.variable_callback_blocked = False
diff --git a/customtkinter/widgets/ctk_scrollbar.py b/customtkinter/widgets/ctk_scrollbar.py
deleted file mode 100644
index 6d60fac..0000000
--- a/customtkinter/widgets/ctk_scrollbar.py
+++ /dev/null
@@ -1,225 +0,0 @@
-import sys
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkScrollbar(CTkBaseClass):
- def __init__(self, *args,
- bg_color=None,
- fg_color="default_theme",
- scrollbar_color="default_theme",
- scrollbar_hover_color="default_theme",
- border_spacing="default_theme",
- corner_radius="default_theme",
- width=None,
- height=None,
- minimum_pixel_length=20,
- orientation="vertical",
- command=None,
- hover=True,
- **kwargs):
-
- # set default dimensions according to orientation
- if width is None:
- if orientation.lower() == "vertical":
- width = 16
- else:
- width = 200
- if height is None:
- if orientation.lower() == "horizontal":
- height = 16
- else:
- height = 200
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
-
- # color
- self.fg_color = ThemeManager.theme["color"]["frame_high"] if fg_color == "default_theme" else fg_color
- self.scrollbar_color = ThemeManager.theme["color"]["scrollbar_button"] if scrollbar_color == "default_theme" else scrollbar_color
- self.scrollbar_hover_color = ThemeManager.theme["color"]["scrollbar_button_hover"] if scrollbar_hover_color == "default_theme" else scrollbar_hover_color
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["scrollbar_corner_radius"] if corner_radius == "default_theme" else corner_radius
- self.border_spacing = ThemeManager.theme["shape"]["scrollbar_border_spacing"] if border_spacing == "default_theme" else border_spacing
-
- self.hover = hover
- self.hover_state = False
- self.command = command
- self.orientation = orientation
- self.start_value: float = 0 # 0 to 1
- self.end_value: float = 1 # 0 to 1
- self.minimum_pixel_length = minimum_pixel_length
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._current_width),
- height=self.apply_widget_scaling(self._current_height))
- self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
- self.draw_engine = DrawEngine(self.canvas)
-
- self.canvas.bind("", self.on_enter)
- self.canvas.bind("", self.on_leave)
- self.canvas.tag_bind("border_parts", "", self.clicked)
- self.canvas.bind("", self.clicked)
- self.canvas.bind("", self.mouse_scroll_event)
- self.bind('', self.update_dimensions_event)
-
- self.draw()
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.draw(no_color_updates=True)
-
- def set_dimensions(self, width=None, height=None):
- super().set_dimensions(width, height)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw(no_color_updates=True)
-
- def get_scrollbar_values_for_minimum_pixel_size(self):
- # correct scrollbar float values if scrollbar is too small
- if self.orientation == "vertical":
- scrollbar_pixel_length = (self.end_value - self.start_value) * self._current_height
- if scrollbar_pixel_length < self.minimum_pixel_length and -scrollbar_pixel_length + self._current_height != 0:
- # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
- interval_extend_factor = (-scrollbar_pixel_length + self.minimum_pixel_length) / (-scrollbar_pixel_length + self._current_height)
- corrected_end_value = self.end_value + (1 - self.end_value) * interval_extend_factor
- corrected_start_value = self.start_value - self.start_value * interval_extend_factor
- return corrected_start_value, corrected_end_value
- else:
- return self.start_value, self.end_value
-
- else:
- scrollbar_pixel_length = (self.end_value - self.start_value) * self._current_width
- if scrollbar_pixel_length < self.minimum_pixel_length and -scrollbar_pixel_length + self._current_width != 0:
- # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
- interval_extend_factor = (-scrollbar_pixel_length + self.minimum_pixel_length) / (-scrollbar_pixel_length + self._current_width)
- corrected_end_value = self.end_value + (1 - self.end_value) * interval_extend_factor
- corrected_start_value = self.start_value - self.start_value * interval_extend_factor
- return corrected_start_value, corrected_end_value
- else:
- return self.start_value, self.end_value
-
- def draw(self, no_color_updates=False):
- corrected_start_value, corrected_end_value = self.get_scrollbar_values_for_minimum_pixel_size()
- requires_recoloring = self.draw_engine.draw_rounded_scrollbar(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_spacing),
- corrected_start_value,
- corrected_end_value,
- self.orientation)
-
- if no_color_updates is False or requires_recoloring:
- if self.hover_state is True:
- self.canvas.itemconfig("scrollbar_parts",
- fill=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("scrollbar_parts",
- fill=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode))
-
- if self.fg_color is None:
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- else:
- self.canvas.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- self.canvas.update_idletasks()
-
- def set(self, start_value: float, end_value: float):
- self.start_value = float(start_value)
- self.end_value = float(end_value)
- self.draw()
-
- def get(self):
- return self.start_value, self.end_value
-
- def configure(self, require_redraw=False, **kwargs):
- if "fg_color" in kwargs:
- self.fg_color = kwargs["fg_color"]
- require_redraw = True
- del kwargs["fg_color"]
-
- if "scrollbar_color" in kwargs:
- self.scrollbar_color = kwargs["scrollbar_color"]
- require_redraw = True
- del kwargs["scrollbar_color"]
-
- if "scrollbar_hover_color" in kwargs:
- self.scrollbar_hover_color = kwargs["scrollbar_hover_color"]
- require_redraw = True
- del kwargs["scrollbar_hover_color"]
-
- if "command" in kwargs:
- self.command = kwargs["command"]
- del kwargs["command"]
-
- if "corner_radius" in kwargs:
- self.corner_radius = kwargs["corner_radius"]
- require_redraw = True
- del kwargs["corner_radius"]
-
- if "border_spacing" in kwargs:
- self.border_spacing = kwargs["border_spacing"]
- require_redraw = True
- del kwargs["border_spacing"]
-
- if "width" in kwargs:
- self.set_dimensions(width=kwargs["width"])
- del kwargs["width"]
-
- if "height" in kwargs:
- self.set_dimensions(height=kwargs["height"])
- del kwargs["height"]
-
- super().configure(require_redraw=require_redraw, **kwargs)
-
- def on_enter(self, event=0):
- if self.hover is True:
- self.hover_state = True
- self.canvas.itemconfig("scrollbar_parts",
- outline=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode))
-
- def on_leave(self, event=0):
- self.hover_state = False
- self.canvas.itemconfig("scrollbar_parts",
- outline=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode),
- fill=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode))
-
- def clicked(self, event):
- if self.orientation == "vertical":
- value = ((event.y - self.border_spacing) / (self._current_height - 2 * self.border_spacing)) / self._widget_scaling
- else:
- value = ((event.x - self.border_spacing) / (self._current_width - 2 * self.border_spacing)) / self._widget_scaling
-
- current_scrollbar_length = self.end_value - self.start_value
- value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2)))
- self.start_value = value - (current_scrollbar_length / 2)
- self.end_value = value + (current_scrollbar_length / 2)
- self.draw()
-
- if self.command is not None:
- self.command('moveto', self.start_value)
-
- def mouse_scroll_event(self, event=None):
- if self.command is not None:
- if sys.platform.startswith("win"):
- self.command('scroll', -int(event.delta/40), 'units')
- else:
- self.command('scroll', -event.delta, 'units')
-
diff --git a/customtkinter/widgets/ctk_slider.py b/customtkinter/widgets/ctk_slider.py
deleted file mode 100644
index 5978f13..0000000
--- a/customtkinter/widgets/ctk_slider.py
+++ /dev/null
@@ -1,339 +0,0 @@
-import tkinter
-import sys
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..settings import Settings
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkSlider(CTkBaseClass):
- """ tkinter custom slider"""
-
- def __init__(self, *args,
- bg_color=None,
- border_color=None,
- fg_color="default_theme",
- progress_color="default_theme",
- button_color="default_theme",
- button_hover_color="default_theme",
- from_=0,
- to=1,
- number_of_steps=None,
- width=None,
- height=None,
- corner_radius="default_theme",
- button_corner_radius="default_theme",
- border_width="default_theme",
- button_length="default_theme",
- command=None,
- variable=None,
- orient="horizontal",
- state="normal",
- **kwargs):
-
- # set default dimensions according to orientation
- if width is None:
- if orient.lower() == "vertical":
- width = 16
- else:
- width = 200
- if height is None:
- if orient.lower() == "vertical":
- height = 200
- else:
- height = 16
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
-
- # color
- self.border_color = border_color
- self.fg_color = ThemeManager.theme["color"]["slider"] if fg_color == "default_theme" else fg_color
- self.progress_color = ThemeManager.theme["color"]["slider_progress"] if progress_color == "default_theme" else progress_color
- self.button_color = ThemeManager.theme["color"]["slider_button"] if button_color == "default_theme" else button_color
- self.button_hover_color = ThemeManager.theme["color"]["slider_button_hover"] if button_hover_color == "default_theme" else button_hover_color
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["slider_corner_radius"] if corner_radius == "default_theme" else corner_radius
- self.button_corner_radius = ThemeManager.theme["shape"]["slider_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius
- self.border_width = ThemeManager.theme["shape"]["slider_border_width"] if border_width == "default_theme" else border_width
- self.button_length = ThemeManager.theme["shape"]["slider_button_length"] if button_length == "default_theme" else button_length
- self.value = 0.5 # initial value of slider in percent
- self.orientation = orient
- self.hover_state = False
- self.from_ = from_
- self.to = to
- self.number_of_steps = number_of_steps
- self.output_value = self.from_ + (self.value * (self.to - self.from_))
-
- if self.corner_radius < self.button_corner_radius:
- self.corner_radius = self.button_corner_radius
-
- # callback and control variables
- self.command = command
- self.variable: tkinter.Variable = variable
- self.variable_callback_blocked = False
- self.variable_callback_name = None
- self.state = state
-
- self.grid_rowconfigure(0, weight=1)
- self.grid_columnconfigure(0, weight=1)
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe")
- self.draw_engine = DrawEngine(self.canvas)
-
- self.canvas.bind("", self.on_enter)
- self.canvas.bind("", self.on_leave)
- self.canvas.bind("", self.clicked)
- self.canvas.bind("", self.clicked)
-
- # Each time an item is resized due to pack position mode, the binding Configure is called on the widget
- self.bind('', self.update_dimensions_event)
-
- self.set_cursor()
- self.draw() # initial draw
-
- if self.variable is not None:
- self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
- self.variable_callback_blocked = True
- self.set(self.variable.get(), from_variable_callback=True)
- self.variable_callback_blocked = False
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def set_dimensions(self, width=None, height=None):
- super().set_dimensions(width, height)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def destroy(self):
- # remove variable_callback from variable callbacks if variable exists
- if self.variable is not None:
- self.variable.trace_remove("write", self.variable_callback_name)
-
- super().destroy()
-
- def set_cursor(self):
- if self.state == "normal" and Settings.cursor_manipulation_enabled:
- if sys.platform == "darwin":
- self.configure(cursor="pointinghand")
- elif sys.platform.startswith("win"):
- self.configure(cursor="hand2")
-
- elif self.state == "disabled" and Settings.cursor_manipulation_enabled:
- if sys.platform == "darwin":
- self.configure(cursor="arrow")
- elif sys.platform.startswith("win"):
- self.configure(cursor="arrow")
-
- def draw(self, no_color_updates=False):
- if self.orientation.lower() == "horizontal":
- orientation = "w"
- elif self.orientation.lower() == "vertical":
- orientation = "s"
- else:
- orientation = "w"
-
- requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width),
- self.apply_widget_scaling(self.button_length),
- self.apply_widget_scaling(self.button_corner_radius),
- self.value, orientation)
-
- if no_color_updates is False or requires_recoloring:
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- if self.border_color is None:
- self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
-
- self.canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- if self.progress_color is None:
- self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.progress_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.progress_color, self._appearance_mode))
-
- if self.hover_state is True:
- self.canvas.itemconfig("slider_parts",
- fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("slider_parts",
- fill=ThemeManager.single_color(self.button_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.button_color, self._appearance_mode))
-
- def clicked(self, event=None):
- if self.state == "normal":
- if self.orientation.lower() == "horizontal":
- self.value = (event.x / self._current_width) / self._widget_scaling
- else:
- self.value = 1 - (event.y / self._current_height) / self._widget_scaling
-
- if self.value > 1:
- self.value = 1
- if self.value < 0:
- self.value = 0
-
- self.output_value = self.round_to_step_size(self.from_ + (self.value * (self.to - self.from_)))
- self.value = (self.output_value - self.from_) / (self.to - self.from_)
-
- self.draw(no_color_updates=False)
-
- if self.variable is not None:
- self.variable_callback_blocked = True
- self.variable.set(round(self.output_value) if isinstance(self.variable, tkinter.IntVar) else self.output_value)
- self.variable_callback_blocked = False
-
- if self.command is not None:
- self.command(self.output_value)
-
- def on_enter(self, event=0):
- if self.state == "normal":
- self.hover_state = True
- self.canvas.itemconfig("slider_parts",
- fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
-
- def on_leave(self, event=0):
- self.hover_state = False
- self.canvas.itemconfig("slider_parts",
- fill=ThemeManager.single_color(self.button_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.button_color, self._appearance_mode))
-
- def round_to_step_size(self, value):
- if self.number_of_steps is not None:
- step_size = (self.to - self.from_) / self.number_of_steps
- value = self.to - (round((self.to - value) / step_size) * step_size)
- return value
- else:
- return value
-
- def get(self):
- return self.output_value
-
- def set(self, output_value, from_variable_callback=False):
- if self.from_ < self.to:
- if output_value > self.to:
- output_value = self.to
- elif output_value < self.from_:
- output_value = self.from_
- else:
- if output_value < self.to:
- output_value = self.to
- elif output_value > self.from_:
- output_value = self.from_
-
- self.output_value = self.round_to_step_size(output_value)
- self.value = (self.output_value - self.from_) / (self.to - self.from_)
-
- self.draw(no_color_updates=False)
-
- if self.variable is not None and not from_variable_callback:
- self.variable_callback_blocked = True
- self.variable.set(round(self.output_value) if isinstance(self.variable, tkinter.IntVar) else self.output_value)
- self.variable_callback_blocked = False
-
- def variable_callback(self, var_name, index, mode):
- if not self.variable_callback_blocked:
- self.set(self.variable.get(), from_variable_callback=True)
-
- def configure(self, require_redraw=False, **kwargs):
- if "state" in kwargs:
- self.state = kwargs["state"]
- self.set_cursor()
- require_redraw = True
- del kwargs["state"]
-
- if "fg_color" in kwargs:
- self.fg_color = kwargs["fg_color"]
- require_redraw = True
- del kwargs["fg_color"]
-
- if "progress_color" in kwargs:
- if kwargs["progress_color"] is None:
- self.progress_color = self.fg_color
- else:
- self.progress_color = kwargs["progress_color"]
- require_redraw = True
- del kwargs["progress_color"]
-
- if "button_color" in kwargs:
- self.button_color = kwargs["button_color"]
- require_redraw = True
- del kwargs["button_color"]
-
- if "button_hover_color" in kwargs:
- self.button_hover_color = kwargs["button_hover_color"]
- require_redraw = True
- del kwargs["button_hover_color"]
-
- if "border_color" in kwargs:
- self.border_color = kwargs["border_color"]
- require_redraw = True
- del kwargs["border_color"]
-
- if "border_width" in kwargs:
- self.border_width = kwargs["border_width"]
- require_redraw = True
- del kwargs["border_width"]
-
- if "from_" in kwargs:
- self.from_ = kwargs["from_"]
- del kwargs["from_"]
-
- if "to" in kwargs:
- self.to = kwargs["to"]
- del kwargs["to"]
-
- if "number_of_steps" in kwargs:
- self.number_of_steps = kwargs["number_of_steps"]
- del kwargs["number_of_steps"]
-
- if "command" in kwargs:
- self.command = kwargs["command"]
- del kwargs["command"]
-
- if "variable" in kwargs:
- if self.variable is not None:
- self.variable.trace_remove("write", self.variable_callback_name)
-
- self.variable = kwargs["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
-
- del kwargs["variable"]
-
- if "width" in kwargs:
- self.set_dimensions(width=kwargs["width"])
- del kwargs["width"]
-
- if "height" in kwargs:
- self.set_dimensions(height=kwargs["height"])
- del kwargs["height"]
-
- super().configure(require_redraw=require_redraw, **kwargs)
diff --git a/customtkinter/widgets/ctk_switch.py b/customtkinter/widgets/ctk_switch.py
deleted file mode 100644
index 1f58518..0000000
--- a/customtkinter/widgets/ctk_switch.py
+++ /dev/null
@@ -1,324 +0,0 @@
-import tkinter
-import sys
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..settings import Settings
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkSwitch(CTkBaseClass):
- def __init__(self, *args,
- text="CTkSwitch",
- text_font="default_theme",
- text_color="default_theme",
- text_color_disabled="default_theme",
- bg_color=None,
- border_color=None,
- fg_color="default_theme",
- progress_color="default_theme",
- button_color="default_theme",
- button_hover_color="default_theme",
- width=36,
- height=18,
- corner_radius="default_theme",
- # button_corner_radius="default_theme",
- border_width="default_theme",
- button_length="default_theme",
- command=None,
- onvalue=1,
- offvalue=0,
- variable=None,
- textvariable=None,
- state=tkinter.NORMAL,
- **kwargs):
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
-
- # color
- self.border_color = border_color
- self.fg_color = ThemeManager.theme["color"]["switch"] if fg_color == "default_theme" else fg_color
- self.progress_color = ThemeManager.theme["color"]["switch_progress"] if progress_color == "default_theme" else progress_color
- self.button_color = ThemeManager.theme["color"]["switch_button"] if button_color == "default_theme" else button_color
- self.button_hover_color = ThemeManager.theme["color"]["switch_button_hover"] if button_hover_color == "default_theme" else button_hover_color
- self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
- self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
-
- # text
- self.text = text
- self.text_label = None
- self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["switch_corner_radius"] if corner_radius == "default_theme" else corner_radius
- # self.button_corner_radius = ThemeManager.theme["shape"]["switch_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius
- self.border_width = ThemeManager.theme["shape"]["switch_border_width"] if border_width == "default_theme" else border_width
- self.button_length = ThemeManager.theme["shape"]["switch_button_length"] if button_length == "default_theme" else button_length
- self.hover_state = False
- self.check_state = False # True if switch is activated
- self.state = state
- self.onvalue = onvalue
- self.offvalue = offvalue
-
- # callback and control variables
- self.command = command
- self.variable: tkinter.Variable = variable
- self.variable_callback_blocked = False
- self.variable_callback_name = None
- self.textvariable = textvariable
-
- # configure grid system (3x1)
- self.grid_columnconfigure(0, weight=1)
- self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
- self.grid_columnconfigure(2, weight=0)
-
- self.bg_canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._current_width),
- height=self.apply_widget_scaling(self._current_height))
- self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._current_width),
- height=self.apply_widget_scaling(self._current_height))
- self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, sticky="nswe")
- self.draw_engine = DrawEngine(self.canvas)
-
- self.canvas.bind("", self.on_enter)
- self.canvas.bind("", self.on_leave)
- self.canvas.bind("", self.toggle)
-
- self.text_label = tkinter.Label(master=self,
- bd=0,
- text=self.text,
- justify=tkinter.LEFT,
- font=self.apply_font_scaling(self.text_font),
- textvariable=self.textvariable)
- self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
- self.text_label["anchor"] = "w"
-
- self.text_label.bind("", self.on_enter)
- self.text_label.bind("", self.on_leave)
- self.text_label.bind("", self.toggle)
-
- if self.variable is not None and self.variable != "":
- self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
- self.check_state = True if self.variable.get() == self.onvalue else False
-
- self.draw() # initial draw
- self.set_cursor()
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
-
- self.bg_canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def destroy(self):
- # remove variable_callback from variable callbacks if variable exists
- if self.variable is not None:
- self.variable.trace_remove("write", self.variable_callback_name)
-
- super().destroy()
-
- def set_cursor(self):
- if Settings.cursor_manipulation_enabled:
- if self.state == tkinter.DISABLED:
- if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="arrow")
- if self.text_label is not None:
- self.text_label.configure(cursor="arrow")
- elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="arrow")
- if self.text_label is not None:
- self.text_label.configure(cursor="arrow")
-
- elif self.state == tkinter.NORMAL:
- if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="pointinghand")
- if self.text_label is not None:
- self.text_label.configure(cursor="pointinghand")
- elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
- self.canvas.configure(cursor="hand2")
- if self.text_label is not None:
- self.text_label.configure(cursor="hand2")
-
- def draw(self, no_color_updates=False):
-
- if self.check_state is True:
- requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width),
- self.apply_widget_scaling(self.button_length),
- self.apply_widget_scaling(self.corner_radius),
- 1, "w")
- else:
- requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width),
- self.apply_widget_scaling(self.button_length),
- self.apply_widget_scaling(self.corner_radius),
- 0, "w")
-
- if no_color_updates is False or requires_recoloring:
- self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- if self.border_color is None:
- self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
-
- self.canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- if self.progress_color is None:
- self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.progress_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.progress_color, self._appearance_mode))
-
- self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.button_color, self._appearance_mode))
-
- if self.state == tkinter.DISABLED:
- self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)))
- else:
- self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
-
- self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- 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:
- self.variable_callback_blocked = True
- self.variable.set(self.onvalue if self.check_state is True else self.offvalue)
- self.variable_callback_blocked = False
-
- if self.command is not None:
- self.command()
-
- 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
-
- 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
-
- def get(self):
- return self.onvalue if self.check_state is True else self.offvalue
-
- def on_enter(self, event=0):
- self.hover_state = True
-
- if self.state is not tkinter.DISABLED:
- self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode))
-
- def on_leave(self, event=0):
- self.hover_state = False
- self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.button_color, self._appearance_mode))
-
- def variable_callback(self, var_name, index, mode):
- if not self.variable_callback_blocked:
- if self.variable.get() == self.onvalue:
- self.select(from_variable_callback=True)
- elif self.variable.get() == self.offvalue:
- self.deselect(from_variable_callback=True)
-
- def configure(self, require_redraw=False, **kwargs):
- if "text" in kwargs:
- self.text = kwargs.pop("text")
- self.text_label.configure(text=self.text)
-
- if "text_font" in kwargs:
- self.text_font = kwargs.pop("text_font")
- self.text_label.configure(font=self.apply_font_scaling(self.text_font))
-
- if "state" in kwargs:
- self.state = kwargs.pop("state")
- self.set_cursor()
- require_redraw = True
-
- if "fg_color" in kwargs:
- self.fg_color = kwargs.pop("fg_color")
- require_redraw = True
-
- if "progress_color" in kwargs:
- new_progress_color = kwargs.pop("progress_color")
- if new_progress_color is None:
- self.progress_color = self.fg_color
- else:
- self.progress_color = new_progress_color
- require_redraw = True
-
- if "button_color" in kwargs:
- self.button_color = kwargs.pop("button_color")
- require_redraw = True
-
- if "button_hover_color" in kwargs:
- self.button_hover_color = kwargs.pop("button_hover_color")
- require_redraw = True
-
- if "border_color" in kwargs:
- self.border_color = kwargs.pop("border_color")
- require_redraw = True
-
- if "border_width" in kwargs:
- self.border_width = kwargs.pop("border_width")
- require_redraw = True
-
- 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 "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
-
- super().configure(require_redraw=require_redraw, **kwargs)
diff --git a/customtkinter/widgets/ctk_textbox.py b/customtkinter/widgets/ctk_textbox.py
deleted file mode 100644
index bad0d35..0000000
--- a/customtkinter/widgets/ctk_textbox.py
+++ /dev/null
@@ -1,176 +0,0 @@
-import tkinter
-
-from .ctk_canvas import CTkCanvas
-from ..theme_manager import ThemeManager
-from ..draw_engine import DrawEngine
-from .widget_base_class import CTkBaseClass
-
-
-class CTkTextbox(CTkBaseClass):
- def __init__(self, *args,
- bg_color=None,
- fg_color="default_theme",
- border_color="default_theme",
- border_width="default_theme",
- corner_radius="default_theme",
- text_font="default_theme",
- text_color="default_theme",
- width=200,
- height=200,
- **kwargs):
-
- # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
- if "master" in kwargs:
- super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs.pop("master"))
- else:
- super().__init__(*args, bg_color=bg_color, width=width, height=height)
-
- # color
- self.fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
- self.border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color
- self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
-
- # shape
- self.corner_radius = ThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius
- self.border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width
-
- # text
- self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
-
- # configure 1x1 grid
- self.grid_rowconfigure(0, weight=1)
- self.grid_columnconfigure(0, weight=1)
-
- self.canvas = CTkCanvas(master=self,
- highlightthickness=0,
- width=self.apply_widget_scaling(self._current_width),
- height=self.apply_widget_scaling(self._current_height))
- self.canvas.grid(row=0, column=0, padx=0, pady=0, rowspan=1, columnspan=1, sticky="nsew")
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- self.draw_engine = DrawEngine(self.canvas)
-
- for arg in ["highlightthickness", "fg", "bg", "font", "width", "height"]:
- kwargs.pop(arg, None)
- self.textbox = tkinter.Text(self,
- fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
- width=0,
- height=0,
- font=self.text_font,
- highlightthickness=0,
- relief="flat",
- insertbackground=ThemeManager.single_color(("black", "white"), self._appearance_mode),
- bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- **kwargs)
- self.textbox.grid(row=0, column=0, padx=self.corner_radius, pady=self.corner_radius, rowspan=1, columnspan=1, sticky="nsew")
-
- self.bind('', self.update_dimensions_event)
- self.draw()
-
- def set_scaling(self, *args, **kwargs):
- super().set_scaling(*args, **kwargs)
-
- self.textbox.configure(font=self.apply_font_scaling(self.text_font))
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def set_dimensions(self, width=None, height=None):
- super().set_dimensions(width, height)
-
- self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
- self.draw()
-
- def draw(self, no_color_updates=False):
-
- requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width),
- self.apply_widget_scaling(self._current_height),
- self.apply_widget_scaling(self.corner_radius),
- self.apply_widget_scaling(self.border_width))
-
- if no_color_updates is False or requires_recoloring:
- if self.fg_color is None:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
- else:
- self.canvas.itemconfig("inner_parts",
- fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- self.canvas.itemconfig("border_parts",
- fill=ThemeManager.single_color(self.border_color, self._appearance_mode),
- outline=ThemeManager.single_color(self.border_color, self._appearance_mode))
- self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- self.textbox.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
- bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- insertbackground=ThemeManager.single_color(("black", "white"), self._appearance_mode))
-
- self.canvas.tag_lower("inner_parts")
- self.canvas.tag_lower("border_parts")
-
- def yview(self, *args):
- return self.textbox.yview(*args)
-
- def xview(self, *args):
- return self.textbox.xview(*args)
-
- def insert(self, *args, **kwargs):
- return self.textbox.insert(*args, **kwargs)
-
- def focus(self):
- return self.textbox.focus()
-
- def tag_add(self, *args, **kwargs):
- return self.textbox.tag_add(*args, **kwargs)
-
- def tag_config(self, *args, **kwargs):
- return self.textbox.tag_config(*args, **kwargs)
-
- def tag_configure(self, *args, **kwargs):
- return self.textbox.tag_configure(*args, **kwargs)
-
- def tag_remove(self, *args, **kwargs):
- return self.textbox.tag_remove(*args, **kwargs)
-
- def configure(self, require_redraw=False, **kwargs):
- if "fg_color" in kwargs:
- self.fg_color = kwargs.pop("fg_color")
- require_redraw = True
-
- # check if CTk widgets are children of the frame and change their bg_color to new frame fg_color
- for child in self.winfo_children():
- if isinstance(child, CTkBaseClass):
- child.configure(bg_color=self.fg_color)
-
- if "border_color" in kwargs:
- self.border_color = kwargs.pop("border_color")
- 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
-
- if "width" in kwargs:
- self.set_dimensions(width=kwargs.pop("width"))
-
- if "height" in kwargs:
- self.set_dimensions(height=kwargs.pop("height"))
-
- if "text_font" in kwargs:
- self.text_font = kwargs.pop("text_font")
- self.textbox.configure(font=self.apply_font_scaling(self.text_font))
-
- if "font" in kwargs:
- raise ValueError("No attribute named font. Use text_font instead of font for CTk widgets")
-
- if "bg_color" in kwargs:
- super().configure(bg_color=kwargs.pop("bg_color"), require_redraw=require_redraw)
- else:
- super().configure(require_redraw=require_redraw)
-
- self.textbox.configure(**kwargs)
diff --git a/customtkinter/widgets/dropdown_menu.py b/customtkinter/widgets/dropdown_menu.py
deleted file mode 100644
index a553c47..0000000
--- a/customtkinter/widgets/dropdown_menu.py
+++ /dev/null
@@ -1,170 +0,0 @@
-import tkinter
-import sys
-import copy
-import re
-from typing import Union
-
-from ..theme_manager import ThemeManager
-from ..appearance_mode_tracker import AppearanceModeTracker
-from ..scaling_tracker import ScalingTracker
-
-
-class DropdownMenu(tkinter.Menu):
- def __init__(self, *args,
- min_character_width=18,
- fg_color="default_theme",
- hover_color="default_theme",
- text_color="default_theme",
- text_font="default_theme",
- command=None,
- values=None,
- **kwargs):
- super().__init__(*args, **kwargs)
-
- ScalingTracker.add_widget(self.set_scaling, self)
- self._widget_scaling = ScalingTracker.get_widget_scaling(self)
- self._spacing_scaling = ScalingTracker.get_spacing_scaling(self)
-
- AppearanceModeTracker.add(self.set_appearance_mode, self)
- self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
-
- self.min_character_width = min_character_width
- self.fg_color = ThemeManager.theme["color"]["dropdown_color"] if fg_color == "default_theme" else fg_color
- self.hover_color = ThemeManager.theme["color"]["dropdown_hover"] if hover_color == "default_theme" else hover_color
- self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
- self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
-
- self.configure_menu_for_platforms()
-
- self.values = values
- self.command = command
-
- self.add_menu_commands()
-
- def configure_menu_for_platforms(self):
- """ apply platform specific appearance attributes """
-
- if sys.platform == "darwin":
- self.configure(tearoff=False,
- font=self.apply_font_scaling(self.text_font))
-
- elif sys.platform.startswith("win"):
- self.configure(tearoff=False,
- relief="flat",
- activebackground=ThemeManager.single_color(self.hover_color, self._appearance_mode),
- borderwidth=0,
- activeborderwidth=self.apply_widget_scaling(4),
- bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
- activeforeground=ThemeManager.single_color(self.text_color, self._appearance_mode),
- font=self.apply_font_scaling(self.text_font),
- cursor="hand2")
-
- else:
- self.configure(tearoff=False,
- relief="flat",
- activebackground=ThemeManager.single_color(self.hover_color, self._appearance_mode),
- borderwidth=0,
- activeborderwidth=0,
- bg=ThemeManager.single_color(self.fg_color, self._appearance_mode),
- fg=ThemeManager.single_color(self.text_color, self._appearance_mode),
- activeforeground=ThemeManager.single_color(self.text_color, self._appearance_mode),
- font=self.apply_font_scaling(self.text_font))
-
- def add_menu_commands(self):
- if sys.platform.startswith("linux"):
- for value in self.values:
- self.add_command(label=" " + value.ljust(self.min_character_width) + " ",
- command=lambda v=value: self.button_callback(v),
- compound="left")
- else:
- for value in self.values:
- self.add_command(label=value.ljust(self.min_character_width),
- command=lambda v=value: self.button_callback(v),
- compound="left")
-
- def open(self, x: Union[int, float], y: Union[int, float]):
- if sys.platform == "darwin":
- y += self.apply_widget_scaling(8)
- else:
- y += self.apply_widget_scaling(3)
-
- if sys.platform == "darwin" or sys.platform.startswith("win"):
- self.post(int(x), int(y))
- else: # Linux
- self.tk_popup(int(x), int(y))
-
- def button_callback(self, value):
- if self.command is not None:
- self.command(value)
-
- def configure(self, **kwargs):
- if "values" in kwargs:
- self.values = kwargs.pop("values")
- self.delete(0, "end") # delete all old commands
- self.add_menu_commands()
-
- if "fg_color" in kwargs:
- self.fg_color = kwargs.pop("fg_color")
- self.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode))
-
- if "hover_color" in kwargs:
- self.hover_color = kwargs.pop("hover_color")
- self.configure(activebackground=ThemeManager.single_color(self.hover_color, self._appearance_mode))
-
- if "text_color" in kwargs:
- self.text_color = kwargs.pop("text_color")
- self.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode))
-
- if "text_font" in kwargs:
- self.text_font = kwargs.pop("text_font")
- self.configure(font=self.apply_font_scaling(self.text_font))
-
- super().configure(**kwargs)
-
- def apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
- if isinstance(value, (int, float)):
- return value * self._widget_scaling
- else:
- return value
-
- def apply_font_scaling(self, font):
- if type(font) == tuple or type(font) == list:
- font_list = list(font)
- for i in range(len(font_list)):
- if (type(font_list[i]) == int or type(font_list[i]) == float) and font_list[i] < 0:
- font_list[i] = int(font_list[i] * self._widget_scaling)
- return tuple(font_list)
-
- elif type(font) == str:
- for negative_number in re.findall(r" -\d* ", font):
- font = font.replace(negative_number, f" {int(int(negative_number) * self._widget_scaling)} ")
- return font
-
- elif isinstance(font, tkinter.font.Font):
- new_font_object = copy.copy(font)
- if font.cget("size") < 0:
- new_font_object.config(size=int(font.cget("size") * self._widget_scaling))
- return new_font_object
-
- else:
- return font
-
- def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
- self._widget_scaling = new_widget_scaling
- self._spacing_scaling = new_spacing_scaling
-
- self.configure(font=self.apply_font_scaling(self.text_font))
-
- if sys.platform.startswith("win"):
- self.configure(activeborderwidth=self.apply_widget_scaling(4))
-
- def set_appearance_mode(self, mode_string):
- """ colors won't update on appearance mode change when dropdown is open, because it's not necessary """
-
- if mode_string.lower() == "dark":
- self._appearance_mode = 1
- elif mode_string.lower() == "light":
- self._appearance_mode = 0
-
- self.configure_menu_for_platforms()
diff --git a/customtkinter/widgets/widget_base_class.py b/customtkinter/widgets/widget_base_class.py
deleted file mode 100644
index 303a7d7..0000000
--- a/customtkinter/widgets/widget_base_class.py
+++ /dev/null
@@ -1,234 +0,0 @@
-import tkinter
-import tkinter.ttk as ttk
-import copy
-import re
-from typing import Callable, Union
-
-try:
- from typing import TypedDict
-except ImportError:
- from typing_extensions import TypedDict
-
-from ..windows.ctk_tk import CTk
-from ..windows.ctk_toplevel import CTkToplevel
-from ..appearance_mode_tracker import AppearanceModeTracker
-from ..scaling_tracker import ScalingTracker
-from ..theme_manager import ThemeManager
-
-
-class CTkBaseClass(tkinter.Frame):
- """ Base class of every CTk widget, handles the dimensions, bg_color,
- appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """
-
- def __init__(self,
- *args,
- bg_color: Union[str, tuple] = None,
- width: int,
- height: int,
- **kwargs):
-
- super().__init__(*args, width=width, height=height, **kwargs) # set desired size of underlying tkinter.Frame
-
- # dimensions
- self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget
- self._current_height = height # _current_width and _current_height are independent of the scale
- self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height
- self._desired_height = height
-
- # scaling
- ScalingTracker.add_widget(self.set_scaling, self) # add callback for automatic scaling changes
- self._widget_scaling = ScalingTracker.get_widget_scaling(self)
- self._spacing_scaling = ScalingTracker.get_spacing_scaling(self)
-
- super().configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
-
- # save latest geometry function and kwargs
- class GeometryCallDict(TypedDict):
- function: Callable
- kwargs: dict
-
- self._last_geometry_manager_call: Union[GeometryCallDict, None] = None
-
- # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
- AppearanceModeTracker.add(self.set_appearance_mode, self)
- self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
-
- # background color
- self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color
-
- super().configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
-
- # overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well
- if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame)) and not isinstance(self.master, (CTkBaseClass, CTk, CTkToplevel)):
- master_old_configure = self.master.config
-
- def new_configure(*args, **kwargs):
- if "bg" in kwargs:
- self.configure(bg_color=kwargs["bg"])
- elif "background" in kwargs:
- self.configure(bg_color=kwargs["background"])
-
- # args[0] is dict when attribute gets changed by widget[] syntax
- elif len(args) > 0 and type(args[0]) == dict:
- if "bg" in args[0]:
- self.configure(bg_color=args[0]["bg"])
- elif "background" in args[0]:
- self.configure(bg_color=args[0]["background"])
- master_old_configure(*args, **kwargs)
-
- self.master.config = new_configure
- self.master.configure = new_configure
-
- def destroy(self):
- AppearanceModeTracker.remove(self.set_appearance_mode)
- super().destroy()
-
- def place(self, **kwargs):
- self._last_geometry_manager_call = {"function": super().place, "kwargs": kwargs}
- super().place(**self.apply_argument_scaling(kwargs))
-
- def pack(self, **kwargs):
- self._last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs}
- super().pack(**self.apply_argument_scaling(kwargs))
-
- def grid(self, **kwargs):
- self._last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs}
- super().grid(**self.apply_argument_scaling(kwargs))
-
- def apply_argument_scaling(self, kwargs: dict) -> dict:
- scaled_kwargs = copy.copy(kwargs)
-
- if "pady" in scaled_kwargs:
- if isinstance(scaled_kwargs["pady"], (int, float, str)):
- scaled_kwargs["pady"] = self.apply_spacing_scaling(scaled_kwargs["pady"])
- elif isinstance(scaled_kwargs["pady"], tuple):
- scaled_kwargs["pady"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["pady"]])
- if "padx" in kwargs:
- if isinstance(scaled_kwargs["padx"], (int, float, str)):
- scaled_kwargs["padx"] = self.apply_spacing_scaling(scaled_kwargs["padx"])
- elif isinstance(scaled_kwargs["padx"], tuple):
- scaled_kwargs["padx"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["padx"]])
-
- if "x" in scaled_kwargs:
- scaled_kwargs["x"] = self.apply_spacing_scaling(scaled_kwargs["x"])
- if "y" in scaled_kwargs:
- scaled_kwargs["y"] = self.apply_spacing_scaling(scaled_kwargs["y"])
-
- return scaled_kwargs
-
- def configure(self, require_redraw=False, **kwargs):
- """ basic configure with bg_color support, to be overridden """
-
- if "bg_color" in kwargs:
- new_bg_color = kwargs.pop("bg_color")
- if new_bg_color is None:
- self.bg_color = self.detect_color_of_master()
- else:
- self.bg_color = new_bg_color
- require_redraw = True
-
- super().configure(**kwargs)
-
- if require_redraw:
- self.draw()
-
- def update_dimensions_event(self, event):
- # only redraw if dimensions changed (for performance), independent of scaling
- if round(self._current_width) != round(event.width / self._widget_scaling) or round(self._current_height) != round(event.height / self._widget_scaling):
- self._current_width = (event.width / self._widget_scaling) # adjust current size according to new size given by event
- self._current_height = (event.height / self._widget_scaling) # _current_width and _current_height are independent of the scale
-
- self.draw(no_color_updates=True) # faster drawing without color changes
-
- def detect_color_of_master(self, master_widget=None):
- """ detect color of self.master widget to set correct bg_color """
-
- if master_widget is None:
- master_widget = self.master
-
- if isinstance(master_widget, (CTkBaseClass, CTk, CTkToplevel)) and hasattr(master_widget, "fg_color"):
- if master_widget.fg_color is not None:
- return master_widget.fg_color
-
- # if fg_color of master is None, try to retrieve fg_color from master of master
- elif hasattr(master_widget.master, "master"):
- return self.detect_color_of_master(master_widget.master)
-
- elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget
- try:
- ttk_style = ttk.Style()
- return ttk_style.lookup(master_widget.winfo_class(), 'background')
- except Exception:
- return "#FFFFFF", "#000000"
-
- else: # master is normal tkinter widget
- try:
- return master_widget.cget("bg") # try to get bg color by .cget() method
- except Exception:
- return "#FFFFFF", "#000000"
-
- def set_appearance_mode(self, mode_string):
- if mode_string.lower() == "dark":
- self._appearance_mode = 1
- elif mode_string.lower() == "light":
- self._appearance_mode = 0
-
- self.draw()
-
- def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
- self._widget_scaling = new_widget_scaling
- self._spacing_scaling = new_spacing_scaling
-
- super().configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
-
- if self._last_geometry_manager_call is not None:
- self._last_geometry_manager_call["function"](**self.apply_argument_scaling(self._last_geometry_manager_call["kwargs"]))
-
- def set_dimensions(self, width=None, height=None):
- if width is not None:
- self._desired_width = width
- if height is not None:
- self._desired_height = height
-
- super().configure(width=self.apply_widget_scaling(self._desired_width),
- height=self.apply_widget_scaling(self._desired_height))
-
- def apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
- if isinstance(value, (int, float)):
- return value * self._widget_scaling
- else:
- return value
-
- def apply_spacing_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
- if isinstance(value, (int, float)):
- return value * self._spacing_scaling
- else:
- return value
-
- def apply_font_scaling(self, font):
- if type(font) == tuple or type(font) == list:
- font_list = list(font)
- for i in range(len(font_list)):
- if (type(font_list[i]) == int or type(font_list[i]) == float) and font_list[i] < 0:
- font_list[i] = int(font_list[i] * self._widget_scaling)
- return tuple(font_list)
-
- elif type(font) == str:
- for negative_number in re.findall(r" -\d* ", font):
- font = font.replace(negative_number, f" {int(int(negative_number) * self._widget_scaling)} ")
- return font
-
- elif isinstance(font, tkinter.font.Font):
- new_font_object = copy.copy(font)
- if font.cget("size") < 0:
- new_font_object.config(size=int(font.cget("size") * self._widget_scaling))
- return new_font_object
-
- else:
- return font
-
- def draw(self, no_color_updates: bool = False):
- """ abstract of draw method to be overridden """
- pass
diff --git a/customtkinter/windows/__init__.py b/customtkinter/windows/__init__.py
index e69de29..ca681b7 100644
--- a/customtkinter/windows/__init__.py
+++ b/customtkinter/windows/__init__.py
@@ -0,0 +1,3 @@
+from .ctk_tk import CTk
+from .ctk_toplevel import CTkToplevel
+from .ctk_input_dialog import CTkInputDialog
diff --git a/customtkinter/windows/ctk_input_dialog.py b/customtkinter/windows/ctk_input_dialog.py
index d0c204f..fc37527 100644
--- a/customtkinter/windows/ctk_input_dialog.py
+++ b/customtkinter/windows/ctk_input_dialog.py
@@ -1,119 +1,110 @@
-import tkinter
-import time
+from typing import Union, Tuple, Optional
-from ..widgets.ctk_label import CTkLabel
-from ..widgets.ctk_entry import CTkEntry
-from ..widgets.ctk_frame import CTkFrame
-from ..windows.ctk_toplevel import CTkToplevel
-from ..widgets.ctk_button import CTkButton
-from ..appearance_mode_tracker import AppearanceModeTracker
-from ..theme_manager import ThemeManager
+from .widgets import CTkLabel
+from .widgets import CTkEntry
+from .widgets import CTkButton
+from .widgets.theme import ThemeManager
+from .ctk_toplevel import CTkToplevel
-class CTkInputDialog:
+class CTkInputDialog(CTkToplevel):
+ """
+ Dialog with extra window, message, entry widget, cancel and ok button.
+ For detailed information check out the documentation.
+ """
+
def __init__(self,
- master=None,
- title="CTkDialog",
- text="CTkDialog",
- fg_color="default_theme",
- hover_color="default_theme",
- border_color="default_theme"):
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ text_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_text_color: Optional[Union[str, Tuple[str, str]]] = None,
+ entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ entry_border_color: Optional[Union[str, Tuple[str, str]]] = None,
+ entry_text_color: Optional[Union[str, Tuple[str, str]]] = None,
- self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
- self.master = master
+ title: str = "CTkDialog",
+ text: str = "CTkDialog"):
- self.user_input = None
- self.running = False
+ super().__init__(fg_color=fg_color)
- self.height = len(text.split("\n"))*20 + 150
+ 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._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)
+ self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color)
+ self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color)
+ self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color)
- self.text = text
- self.window_bg_color = ThemeManager.theme["color"]["window_bg_color"]
- self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
- self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
- self.border_color = ThemeManager.theme["color"]["button_hover"] if border_color == "default_theme" else border_color
+ self._user_input: Union[str, None] = None
+ self._running: bool = False
+ self._text = text
- self.top = CTkToplevel()
- self.top.geometry(f"{280}x{self.height}")
- self.top.minsize(280, self.height)
- self.top.maxsize(280, self.height)
- self.top.title(title)
- self.top.lift()
- self.top.focus_force()
- self.top.grab_set()
+ self.title(title)
+ self.lift() # lift window on top
+ self.attributes("-topmost", True) # stay on top
+ self.protocol("WM_DELETE_WINDOW", self._on_closing)
+ self.after(10, self._create_widgets) # create widgets with slight delay, to avoid white flickering of background
+ self.resizable(False, False)
+ self.grab_set() # make other windows not clickable
- self.top.protocol("WM_DELETE_WINDOW", self.on_closing)
+ def _create_widgets(self):
- self.top.after(10, self.create_widgets) # create widgets with slight delay, to avoid white flickering of background
+ self.grid_columnconfigure((0, 1), weight=1)
+ self.rowconfigure(0, weight=1)
- def create_widgets(self):
- self.label_frame = CTkFrame(master=self.top,
- corner_radius=0,
- fg_color=self.window_bg_color,
- width=300,
- height=self.height-100)
- self.label_frame.place(relx=0.5, rely=0, anchor=tkinter.N)
+ self._label = CTkLabel(master=self,
+ width=300,
+ wraplength=300,
+ fg_color="transparent",
+ text_color=self._text_color,
+ text=self._text,)
+ self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew")
- self.button_and_entry_frame = CTkFrame(master=self.top,
- corner_radius=0,
- fg_color=self.window_bg_color,
- width=300,
- height=100)
- self.button_and_entry_frame.place(relx=0.5, rely=1, anchor=tkinter.S)
+ self._entry = CTkEntry(master=self,
+ width=230,
+ fg_color=self._entry_fg_color,
+ border_color=self._entry_border_color,
+ text_color=self._entry_text_color)
+ self._entry.grid(row=1, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew")
- self.myLabel = CTkLabel(master=self.label_frame,
- text=self.text,
- width=300,
- fg_color=None,
- height=self.height-100)
- self.myLabel.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
+ self._ok_button = CTkButton(master=self,
+ width=100,
+ border_width=0,
+ fg_color=self._button_fg_color,
+ hover_color=self._button_hover_color,
+ text_color=self._button_text_color,
+ text='Ok',
+ command=self._ok_event)
+ self._ok_button.grid(row=2, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew")
- self.entry = CTkEntry(master=self.button_and_entry_frame,
- width=230)
- self.entry.place(relx=0.5, rely=0.15, anchor=tkinter.CENTER)
+ self._cancel_button = CTkButton(master=self,
+ width=100,
+ border_width=0,
+ fg_color=self._button_fg_color,
+ hover_color=self._button_hover_color,
+ text_color=self._button_text_color,
+ text='Cancel',
+ command=self._ok_event)
+ self._cancel_button.grid(row=2, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew")
- self.ok_button = CTkButton(master=self.button_and_entry_frame,
- text='Ok',
- width=100,
- command=self.ok_event,
- fg_color=self.fg_color,
- hover_color=self.hover_color,
- border_color=self.border_color)
- self.ok_button.place(relx=0.28, rely=0.65, anchor=tkinter.CENTER)
+ self.after(150, lambda: self._entry.focus()) # set focus to entry with slight delay, otherwise it won't work
+ self._entry.bind("", self._ok_event)
- self.cancel_button = CTkButton(master=self.button_and_entry_frame,
- text='Cancel',
- width=100,
- command=self.cancel_event,
- fg_color=self.fg_color,
- hover_color=self.hover_color,
- border_color=self.border_color)
- self.cancel_button.place(relx=0.72, rely=0.65, anchor=tkinter.CENTER)
+ def _ok_event(self, event=None):
+ self._user_input = self._entry.get()
+ self.grab_release()
+ self.destroy()
- self.entry.entry.focus_force()
- self.entry.bind("", self.ok_event)
+ def _on_closing(self):
+ self.grab_release()
+ self.destroy()
- def ok_event(self, event=None):
- self.user_input = self.entry.get()
- self.running = False
-
- def on_closing(self):
- self.running = False
-
- def cancel_event(self):
- self.running = False
+ def _cancel_event(self):
+ self.grab_release()
+ self.destroy()
def get_input(self):
- self.running = True
-
- while self.running:
- try:
- self.top.update()
- except Exception:
- return self.user_input
- finally:
- time.sleep(0.01)
-
- time.sleep(0.05)
- self.top.destroy()
- return self.user_input
+ self.master.wait_window(self)
+ return self._user_input
diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py
index 7c3b4b7..2371997 100644
--- a/customtkinter/windows/ctk_tk.py
+++ b/customtkinter/windows/ctk_tk.py
@@ -4,276 +4,230 @@ import sys
import os
import platform
import ctypes
-import re
-from typing import Union, Tuple
+from typing import Union, Tuple, Optional
-from ..appearance_mode_tracker import AppearanceModeTracker
-from ..theme_manager import ThemeManager
-from ..scaling_tracker import ScalingTracker
-from ..settings import Settings
+from .widgets.theme import ThemeManager
+from .widgets.scaling import CTkScalingBaseClass
+from .widgets.appearance_mode import CTkAppearanceModeBaseClass
+
+from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
-class CTk(tkinter.Tk):
- def __init__(self, *args,
- fg_color="default_theme",
+class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
+ """
+ Main app window with dark titlebar on Windows and macOS.
+ For detailed information check out the documentation.
+ """
+
+ _valid_tk_constructor_arguments: set = {"screenName", "baseName", "className", "useTk", "sync", "use"}
+
+ _valid_tk_configure_arguments: set = {'bd', 'borderwidth', 'class', 'menu', 'relief', 'screen',
+ 'use', 'container', 'cursor', 'height',
+ 'highlightthickness', 'padx', 'pady', 'takefocus', 'visual', 'width'}
+
+ _deactivate_macos_window_header_manipulation: bool = False
+ _deactivate_windows_window_header_manipulation: bool = False
+
+ def __init__(self,
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
**kwargs):
- ScalingTracker.activate_high_dpi_awareness() # make process DPI aware
- self.enable_macos_dark_title_bar()
+ self._enable_macos_dark_title_bar()
- super().__init__(*args, **kwargs)
+ # call init methods of super classes
+ tkinter.Tk.__init__(self, **pop_from_dict_by_set(kwargs, self._valid_tk_constructor_arguments))
+ CTkAppearanceModeBaseClass.__init__(self)
+ CTkScalingBaseClass.__init__(self, scaling_type="window")
+ check_kwargs_empty(kwargs, raise_error=True)
- # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
- AppearanceModeTracker.add(self.set_appearance_mode, self)
- self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
+ self._current_width = 600 # initial window size, independent of scaling
+ self._current_height = 500
+ self._min_width: int = 0
+ self._min_height: int = 0
+ self._max_width: int = 1_000_000
+ self._max_height: int = 1_000_000
+ self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
- # add set_scaling method to callback list of ScalingTracker for automatic scaling changes
- ScalingTracker.add_widget(self.set_scaling, self)
- self.window_scaling = ScalingTracker.get_window_scaling(self)
+ self._fg_color = ThemeManager.theme["CTk"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
- self.current_width = 600 # initial window size, always without scaling
- self.current_height = 500
- self.min_width: int = 0
- self.min_height: int = 0
- self.max_width: int = 1_000_000
- self.max_height: int = 1_000_000
- self.last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
+ # set bg of tkinter.Tk
+ super().configure(bg=self._apply_appearance_mode(self._fg_color))
- self.fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
+ # set title and initial geometry
+ self.title("CTk")
+ # self.geometry(f"{self._current_width}x{self._current_height}")
- if "bg" in kwargs:
- self.fg_color = kwargs["bg"]
- del kwargs["bg"]
- elif "background" in kwargs:
- self.fg_color = kwargs["background"]
- del kwargs["background"]
-
- super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
- super().title("CTk")
- self.geometry(f"{self.current_width}x{self.current_height}")
-
- self.state_before_windows_set_titlebar_color = None
- self.window_exists = False # indicates if the window is already shown through update() or mainloop() after init
- self.withdraw_called_before_window_exists = False # indicates if withdraw() was called before window is first shown through update() or mainloop()
- self.iconify_called_before_window_exists = False # indicates if iconify() was called before window is first shown through update() or mainloop()
+ self._state_before_windows_set_titlebar_color = None
+ self._window_exists = False # indicates if the window is already shown through update() or mainloop() after init
+ self._withdraw_called_before_window_exists = False # indicates if withdraw() was called before window is first shown through update() or mainloop()
+ self._iconify_called_before_window_exists = False # indicates if iconify() was called before window is first shown through update() or mainloop()
if sys.platform.startswith("win"):
- if self.appearance_mode == 1:
- self.windows_set_titlebar_color("dark")
- else:
- self.windows_set_titlebar_color("light")
+ self._windows_set_titlebar_color(self._get_appearance_mode())
- self.bind('', self.update_dimensions_event)
+ self.bind('', self._update_dimensions_event)
+ self.bind('', self._focus_in_event)
- self.block_update_dimensions_event = False
-
- def update_dimensions_event(self, event=None):
- if not self.block_update_dimensions_event:
- detected_width = self.winfo_width() # detect current window size
- detected_height = self.winfo_height()
-
- if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling):
- self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event
- self.current_height = round(detected_height / self.window_scaling) # _current_width and _current_height are independent of the scale
-
- def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
- self.window_scaling = new_window_scaling
-
- # block update_dimensions_event to prevent current_width and current_height to get updated
- self.block_update_dimensions_event = True
-
- # force new dimensions on window by using min, max, and geometry
- super().minsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
- super().maxsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
- super().geometry(f"{self.apply_window_scaling(self.current_width)}x"+f"{self.apply_window_scaling(self.current_height)}")
-
- # set new scaled min and max with 400ms delay (otherwise it won't work for some reason)
- self.after(400, self.set_scaled_min_max)
-
- # release the blocking of update_dimensions_event after a small amount of time (slight delay is necessary)
- def set_block_update_dimensions_event_false():
- self.block_update_dimensions_event = False
- self.after(100, lambda: set_block_update_dimensions_event_false())
-
- def set_scaled_min_max(self):
- if self.min_width is not None or self.min_height is not None:
- super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
- if self.max_width is not None or self.max_height is not None:
- super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
+ self._block_update_dimensions_event = False
def destroy(self):
- AppearanceModeTracker.remove(self.set_appearance_mode)
- ScalingTracker.remove_window(self.set_scaling, self)
- self.disable_macos_dark_title_bar()
- super().destroy()
+ self._disable_macos_dark_title_bar()
+
+ # call destroy methods of super classes
+ tkinter.Tk.destroy(self)
+ CTkAppearanceModeBaseClass.destroy(self)
+ CTkScalingBaseClass.destroy(self)
+
+ def _focus_in_event(self, event):
+ # sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
+ if sys.platform == "darwin":
+ self.lift()
+
+ def _update_dimensions_event(self, event=None):
+ if not self._block_update_dimensions_event:
+
+ detected_width = super().winfo_width() # detect current window size
+ detected_height = super().winfo_height()
+
+ # detected_width = event.width
+ # detected_height = event.height
+
+ if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height):
+ self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event
+ self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale
+
+ def _set_scaling(self, new_widget_scaling, new_window_scaling):
+ super()._set_scaling(new_widget_scaling, new_window_scaling)
+
+ # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work.
+ super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
+ super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
+
+ super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}")
+
+ # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window)
+ self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11)
+
+ def block_update_dimensions_event(self):
+ self._block_update_dimensions_event = False
+
+ def unblock_update_dimensions_event(self):
+ self._block_update_dimensions_event = False
+
+ def _set_scaled_min_max(self):
+ if self._min_width is not None or self._min_height is not None:
+ super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
+ if self._max_width is not None or self._max_height is not None:
+ super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def withdraw(self):
- if self.window_exists is False:
- self.withdraw_called_before_window_exists = True
+ if self._window_exists is False:
+ self._withdraw_called_before_window_exists = True
super().withdraw()
def iconify(self):
- if self.window_exists is False:
- self.iconify_called_before_window_exists = True
+ if self._window_exists is False:
+ self._iconify_called_before_window_exists = True
super().iconify()
def update(self):
- if self.window_exists is False:
- self.window_exists = True
+ if self._window_exists is False:
+ self._window_exists = True
if sys.platform.startswith("win"):
- if not self.withdraw_called_before_window_exists and not self.iconify_called_before_window_exists:
+ if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists:
# print("window dont exists -> deiconify in update")
self.deiconify()
super().update()
def mainloop(self, *args, **kwargs):
- if not self.window_exists:
- self.window_exists = True
+ if not self._window_exists:
+ self._window_exists = True
if sys.platform.startswith("win"):
- if not self.withdraw_called_before_window_exists and not self.iconify_called_before_window_exists:
+ if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists:
# print("window dont exists -> deiconify in mainloop")
self.deiconify()
super().mainloop(*args, **kwargs)
- def resizable(self, *args, **kwargs):
- super().resizable(*args, **kwargs)
- self.last_resizable_args = (args, kwargs)
+ def resizable(self, width: bool = None, height: bool = None):
+ current_resizable_values = super().resizable(width, height)
+ self._last_resizable_args = ([], {"width": width, "height": height})
if sys.platform.startswith("win"):
- if self.appearance_mode == 1:
- self.windows_set_titlebar_color("dark")
- else:
- self.windows_set_titlebar_color("light")
+ self._windows_set_titlebar_color(self._get_appearance_mode())
- def minsize(self, width=None, height=None):
- self.min_width = width
- self.min_height = height
- if self.current_width < width: self.current_width = width
- if self.current_height < height: self.current_height = height
- super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
+ return current_resizable_values
- def maxsize(self, width=None, height=None):
- self.max_width = width
- self.max_height = height
- if self.current_width > width: self.current_width = width
- if self.current_height > height: self.current_height = height
- super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
+ def minsize(self, width: int = None, height: int = None):
+ self._min_width = width
+ self._min_height = height
+ if self._current_width < width:
+ self._current_width = width
+ if self._current_height < height:
+ self._current_height = height
+ super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
+
+ def maxsize(self, width: int = None, height: int = None):
+ self._max_width = width
+ self._max_height = height
+ if self._current_width > width:
+ self._current_width = width
+ if self._current_height > height:
+ self._current_height = height
+ super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def geometry(self, geometry_string: str = None):
if geometry_string is not None:
- super().geometry(self.apply_geometry_scaling(geometry_string))
+ super().geometry(self._apply_geometry_scaling(geometry_string))
# update width and height attributes
- width, height, x, y = self.parse_geometry_string(geometry_string)
+ width, height, x, y = self._parse_geometry_string(geometry_string)
if width is not None and height is not None:
- self.current_width = max(self.min_width, min(width, self.max_width)) # bound value between min and max
- self.current_height = max(self.min_height, min(height, self.max_height))
+ self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max
+ self._current_height = max(self._min_height, min(height, self._max_height))
else:
- return self.reverse_geometry_scaling(super().geometry())
+ return self._reverse_geometry_scaling(super().geometry())
- @staticmethod
- def parse_geometry_string(geometry_string: str) -> tuple:
- # index: 1 2 3 4 5 6
- # regex group structure: ('x', '', '', '+-+-', '-', '-')
- result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
-
- width = int(result.group(2)) if result.group(2) is not None else None
- height = int(result.group(3)) if result.group(3) is not None else None
- x = int(result.group(5)) if result.group(5) is not None else None
- y = int(result.group(6)) if result.group(6) is not None else None
-
- return width, height, x, y
-
- def apply_geometry_scaling(self, geometry_string: str) -> str:
- width, height, x, y = self.parse_geometry_string(geometry_string)
-
- if x is None and y is None: # no and in geometry_string
- return f"{round(width * self.window_scaling)}x{round(height * self.window_scaling)}"
-
- elif width is None and height is None: # no and in geometry_string
- return f"+{x}+{y}"
-
- else:
- return f"{round(width * self.window_scaling)}x{round(height * self.window_scaling)}+{x}+{y}"
-
- def reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
- width, height, x, y = self.parse_geometry_string(scaled_geometry_string)
-
- if x is None and y is None: # no and in geometry_string
- return f"{round(width / self.window_scaling)}x{round(height / self.window_scaling)}"
-
- elif width is None and height is None: # no and in geometry_string
- return f"+{x}+{y}"
-
- else:
- return f"{round(width / self.window_scaling)}x{round(height / self.window_scaling)}+{x}+{y}"
-
- def apply_window_scaling(self, value):
- if isinstance(value, (int, float)):
- return int(value * self.window_scaling)
- else:
- return value
-
- def config(self, *args, **kwargs):
- self.configure(*args, **kwargs)
-
- def configure(self, *args, **kwargs):
- bg_changed = False
-
- if "bg" in kwargs:
- self.fg_color = kwargs["bg"]
- bg_changed = True
- kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
- elif "background" in kwargs:
- self.fg_color = kwargs["background"]
- bg_changed = True
- kwargs["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
- elif "fg_color" in kwargs:
- self.fg_color = kwargs["fg_color"]
- kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
- del kwargs["fg_color"]
- bg_changed = True
-
- elif len(args) > 0 and type(args[0]) == dict:
- if "bg" in args[0]:
- self.fg_color=args[0]["bg"]
- bg_changed = True
- args[0]["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
- elif "background" in args[0]:
- self.fg_color=args[0]["background"]
- bg_changed = True
- args[0]["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
-
- if bg_changed:
- from ..widgets.widget_base_class import CTkBaseClass
+ def configure(self, **kwargs):
+ 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))
for child in self.winfo_children():
- if isinstance(child, CTkBaseClass):
- child.configure(bg_color=self.fg_color)
+ try:
+ child.configure(bg_color=self._fg_color)
+ except Exception:
+ pass
- super().configure(*args, **kwargs)
+ super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_configure_arguments))
+ check_kwargs_empty(kwargs)
- @staticmethod
- def enable_macos_dark_title_bar():
- if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
+ def cget(self, attribute_name: str) -> any:
+ if attribute_name == "fg_color":
+ return self._fg_color
+ else:
+ return super().cget(attribute_name)
+
+ @classmethod
+ def _enable_macos_dark_title_bar(cls):
+ if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
# This command allows dark-mode for all programs
- @staticmethod
- def disable_macos_dark_title_bar():
- if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
+ @classmethod
+ def _disable_macos_dark_title_bar(cls):
+ if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
# This command reverts the dark-mode setting for all programs.
- def windows_set_titlebar_color(self, color_mode: str):
+ def _windows_set_titlebar_color(self, color_mode: str):
"""
Set the titlebar color of the window to light or dark theme on Microsoft Windows.
@@ -284,13 +238,13 @@ class CTk(tkinter.Tk):
https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
"""
- if sys.platform.startswith("win") and not Settings.deactivate_windows_window_header_manipulation:
+ if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation:
- if self.window_exists:
- self.state_before_windows_set_titlebar_color = self.state()
+ if self._window_exists:
+ self._state_before_windows_set_titlebar_color = self.state()
# print("window_exists -> state_before_windows_set_titlebar_color: ", self.state_before_windows_set_titlebar_color)
- if self.state_before_windows_set_titlebar_color != "iconic" or self.state_before_windows_set_titlebar_color != "withdrawn":
+ if self._state_before_windows_set_titlebar_color != "iconic" or self._state_before_windows_set_titlebar_color != "withdrawn":
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
else:
# print("window dont exists -> withdraw and update")
@@ -322,29 +276,23 @@ class CTk(tkinter.Tk):
except Exception as err:
print(err)
- if self.window_exists:
+ if self._window_exists:
# print("window_exists -> return to original state: ", self.state_before_windows_set_titlebar_color)
- if self.state_before_windows_set_titlebar_color == "normal":
+ if self._state_before_windows_set_titlebar_color == "normal":
self.deiconify()
- elif self.state_before_windows_set_titlebar_color == "iconic":
+ elif self._state_before_windows_set_titlebar_color == "iconic":
self.iconify()
- elif self.state_before_windows_set_titlebar_color == "zoomed":
+ elif self._state_before_windows_set_titlebar_color == "zoomed":
self.state("zoomed")
else:
- self.state(self.state_before_windows_set_titlebar_color) # other states
+ self.state(self._state_before_windows_set_titlebar_color) # other states
else:
pass # wait for update or mainloop to be called
- def set_appearance_mode(self, mode_string):
- if mode_string.lower() == "dark":
- self.appearance_mode = 1
- elif mode_string.lower() == "light":
- self.appearance_mode = 0
+ def _set_appearance_mode(self, mode_string: str):
+ super()._set_appearance_mode(mode_string)
if sys.platform.startswith("win"):
- if self.appearance_mode == 1:
- self.windows_set_titlebar_color("dark")
- else:
- self.windows_set_titlebar_color("light")
+ self._windows_set_titlebar_color(mode_string)
- super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
+ super().configure(bg=self._apply_appearance_mode(self._fg_color))
diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py
index e960560..4e43839 100644
--- a/customtkinter/windows/ctk_toplevel.py
+++ b/customtkinter/windows/ctk_toplevel.py
@@ -4,239 +4,200 @@ import sys
import os
import platform
import ctypes
-import re
-from typing import Union, Tuple
+from typing import Union, Tuple, Optional
-from ..appearance_mode_tracker import AppearanceModeTracker
-from ..theme_manager import ThemeManager
-from ..settings import Settings
-from ..scaling_tracker import ScalingTracker
+from .widgets.theme import ThemeManager
+from .widgets.scaling import CTkScalingBaseClass
+from .widgets.appearance_mode import CTkAppearanceModeBaseClass
+
+from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
-class CTkToplevel(tkinter.Toplevel):
+class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
+ """
+ Toplevel window with dark titlebar on Windows and macOS.
+ For detailed information check out the documentation.
+ """
+
+ _valid_tk_toplevel_arguments: set = {"bd", "borderwidth", "class", "container", "cursor", "height",
+ "highlightbackground", "highlightthickness", "menu", "relief",
+ "screen", "takefocus", "use", "visual", "width"}
+
+ _deactivate_macos_window_header_manipulation: bool = False
+ _deactivate_windows_window_header_manipulation: bool = False
+
def __init__(self, *args,
- fg_color="default_theme",
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
**kwargs):
- self.enable_macos_dark_title_bar()
- super().__init__(*args, **kwargs)
- self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
+ self._enable_macos_dark_title_bar()
- # add set_scaling method to callback list of ScalingTracker for automatic scaling changes
- ScalingTracker.add_widget(self.set_scaling, self)
- self.window_scaling = ScalingTracker.get_window_scaling(self)
+ # call init methods of super classes
+ super().__init__(*args, **pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
+ CTkAppearanceModeBaseClass.__init__(self)
+ CTkScalingBaseClass.__init__(self, scaling_type="window")
+ check_kwargs_empty(kwargs, raise_error=True)
- self.current_width = 200 # initial window size, always without scaling
- self.current_height = 200
- self.min_width: int = 0
- self.min_height: int = 0
- self.max_width: int = 1_000_000
- self.max_height: int = 1_000_000
- self.last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
+ self._current_width = 200 # initial window size, always without scaling
+ self._current_height = 200
+ self._min_width: int = 0
+ self._min_height: int = 0
+ self._max_width: int = 1_000_000
+ self._max_height: int = 1_000_000
+ self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
- self.fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
+ self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
- if "bg" in kwargs:
- self.fg_color = kwargs["bg"]
- del kwargs["bg"]
- elif "background" in kwargs:
- self.fg_color = kwargs["background"]
- del kwargs["background"]
+ # set bg color of tkinter.Toplevel
+ super().configure(bg=self._apply_appearance_mode(self._fg_color))
- # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
- AppearanceModeTracker.add(self.set_appearance_mode, self)
- super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
+ # set title of tkinter.Toplevel
super().title("CTkToplevel")
- self.state_before_windows_set_titlebar_color = None
- self.windows_set_titlebar_color_called = False # indicates if windows_set_titlebar_color was called, stays True until revert_withdraw_after_windows_set_titlebar_color is called
- self.withdraw_called_after_windows_set_titlebar_color = False # indicates if withdraw() was called after windows_set_titlebar_color
- self.iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color
+ self._state_before_windows_set_titlebar_color = None
+ self._windows_set_titlebar_color_called = False # indicates if windows_set_titlebar_color was called, stays True until revert_withdraw_after_windows_set_titlebar_color is called
+ self._withdraw_called_after_windows_set_titlebar_color = False # indicates if withdraw() was called after windows_set_titlebar_color
+ self._iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color
if sys.platform.startswith("win"):
- if self.appearance_mode == 1:
- self.windows_set_titlebar_color("dark")
- else:
- self.windows_set_titlebar_color("light")
+ self._windows_set_titlebar_color(self._get_appearance_mode())
- self.bind('', self.update_dimensions_event)
+ self.bind('', self._update_dimensions_event)
+ self.bind('', self._focus_in_event)
- def update_dimensions_event(self, event=None):
- detected_width = self.winfo_width() # detect current window size
- detected_height = self.winfo_height()
+ self._block_update_dimensions_event = False
- if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling):
- self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event
- self.current_height = round(detected_height / self.window_scaling) # _current_width and _current_height are independent of the scale
+ def destroy(self):
+ self._disable_macos_dark_title_bar()
- def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
- self.window_scaling = new_window_scaling
+ # call destroy methods of super classes
+ tkinter.Toplevel.destroy(self)
+ CTkAppearanceModeBaseClass.destroy(self)
+ CTkScalingBaseClass.destroy(self)
- # force new dimensions on window by using min, max, and geometry
- super().minsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
- super().maxsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
- super().geometry(
- f"{self.apply_window_scaling(self.current_width)}x" + f"{self.apply_window_scaling(self.current_height)}")
+ def _focus_in_event(self, event):
+ # sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
+ if sys.platform == "darwin":
+ self.lift()
- # set new scaled min and max with 400ms delay (otherwise it won't work for some reason)
- self.after(400, self.set_scaled_min_max)
+ def _update_dimensions_event(self, event=None):
+ if not self._block_update_dimensions_event:
+ detected_width = self.winfo_width() # detect current window size
+ detected_height = self.winfo_height()
- def set_scaled_min_max(self):
- if self.min_width is not None or self.min_height is not None:
- super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
- if self.max_width is not None or self.max_height is not None:
- super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
+ if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height):
+ self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event
+ self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale
+
+ def _set_scaling(self, new_widget_scaling, new_window_scaling):
+ super()._set_scaling(new_widget_scaling, new_window_scaling)
+
+ # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work.
+ super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
+ super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
+
+ super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}")
+
+ # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window)
+ self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11)
+
+ def block_update_dimensions_event(self):
+ self._block_update_dimensions_event = False
+
+ def unblock_update_dimensions_event(self):
+ self._block_update_dimensions_event = False
+
+ def _set_scaled_min_max(self):
+ if self._min_width is not None or self._min_height is not None:
+ super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
+ if self._max_width is not None or self._max_height is not None:
+ super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def geometry(self, geometry_string: str = None):
if geometry_string is not None:
- super().geometry(self.apply_geometry_scaling(geometry_string))
+ super().geometry(self._apply_geometry_scaling(geometry_string))
# update width and height attributes
- width, height, x, y = self.parse_geometry_string(geometry_string)
+ width, height, x, y = self._parse_geometry_string(geometry_string)
if width is not None and height is not None:
- self.current_width = max(self.min_width, min(width, self.max_width)) # bound value between min and max
- self.current_height = max(self.min_height, min(height, self.max_height))
+ self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max
+ self._current_height = max(self._min_height, min(height, self._max_height))
else:
- return self.reverse_geometry_scaling(super().geometry())
-
- @staticmethod
- def parse_geometry_string(geometry_string: str) -> tuple:
- # index: 1 2 3 4 5 6
- # regex group structure: ('x', '', '', '+-+-', '-', '-')
- result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
-
- width = int(result.group(2)) if result.group(2) is not None else None
- height = int(result.group(3)) if result.group(3) is not None else None
- x = int(result.group(5)) if result.group(5) is not None else None
- y = int(result.group(6)) if result.group(6) is not None else None
-
- return width, height, x, y
-
- def apply_geometry_scaling(self, geometry_string: str) -> str:
- width, height, x, y = self.parse_geometry_string(geometry_string)
-
- if x is None and y is None: # no and in geometry_string
- return f"{round(width * self.window_scaling)}x{round(height * self.window_scaling)}"
-
- elif width is None and height is None: # no and in geometry_string
- return f"+{x}+{y}"
-
- else:
- return f"{round(width * self.window_scaling)}x{round(height * self.window_scaling)}+{x}+{y}"
-
- def reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
- width, height, x, y = self.parse_geometry_string(scaled_geometry_string)
-
- if x is None and y is None: # no and in geometry_string
- return f"{round(width / self.window_scaling)}x{round(height / self.window_scaling)}"
-
- elif width is None and height is None: # no and in geometry_string
- return f"+{x}+{y}"
-
- else:
- return f"{round(width / self.window_scaling)}x{round(height / self.window_scaling)}+{x}+{y}"
-
- def apply_window_scaling(self, value):
- if isinstance(value, (int, float)):
- return int(value * self.window_scaling)
- else:
- return value
-
- def destroy(self):
- AppearanceModeTracker.remove(self.set_appearance_mode)
- ScalingTracker.remove_window(self.set_scaling, self)
- self.disable_macos_dark_title_bar()
- super().destroy()
+ return self._reverse_geometry_scaling(super().geometry())
def withdraw(self):
- if self.windows_set_titlebar_color_called:
- self.withdraw_called_after_windows_set_titlebar_color = True
+ if self._windows_set_titlebar_color_called:
+ self._withdraw_called_after_windows_set_titlebar_color = True
super().withdraw()
def iconify(self):
- if self.windows_set_titlebar_color_called:
- self.iconify_called_after_windows_set_titlebar_color = True
+ if self._windows_set_titlebar_color_called:
+ self._iconify_called_after_windows_set_titlebar_color = True
super().iconify()
- def resizable(self, *args, **kwargs):
- super().resizable(*args, **kwargs)
- self.last_resizable_args = (args, kwargs)
+ def resizable(self, width: bool = None, height: bool = None):
+ current_resizable_values = super().resizable(width, height)
+ self._last_resizable_args = ([], {"width": width, "height": height})
if sys.platform.startswith("win"):
- if self.appearance_mode == 1:
- self.after(10, lambda: self.windows_set_titlebar_color("dark"))
- else:
- self.after(10, lambda: self.windows_set_titlebar_color("light"))
+ self.after(10, lambda: self._windows_set_titlebar_color(self._get_appearance_mode()))
+
+ return current_resizable_values
def minsize(self, width=None, height=None):
- self.min_width = width
- self.min_height = height
- if self.current_width < width: self.current_width = width
- if self.current_height < height: self.current_height = height
- super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
+ self._min_width = width
+ self._min_height = height
+ if self._current_width < width:
+ self._current_width = width
+ if self._current_height < height:
+ self._current_height = height
+ super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
def maxsize(self, width=None, height=None):
- self.max_width = width
- self.max_height = height
- if self.current_width > width: self.current_width = width
- if self.current_height > height: self.current_height = height
- super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height))
+ self._max_width = width
+ self._max_height = height
+ if self._current_width > width:
+ self._current_width = width
+ if self._current_height > height:
+ self._current_height = height
+ super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
- def config(self, *args, **kwargs):
- self.configure(*args, **kwargs)
-
- def configure(self, *args, **kwargs):
- bg_changed = False
-
- if "bg" in kwargs:
- self.fg_color = kwargs["bg"]
- bg_changed = True
- kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
- elif "background" in kwargs:
- self.fg_color = kwargs["background"]
- bg_changed = True
- kwargs["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
- elif "fg_color" in kwargs:
- self.fg_color = kwargs["fg_color"]
- kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
- del kwargs["fg_color"]
- bg_changed = True
-
- elif len(args) > 0 and type(args[0]) == dict:
- if "bg" in args[0]:
- self.fg_color=args[0]["bg"]
- bg_changed = True
- args[0]["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
- elif "background" in args[0]:
- self.fg_color=args[0]["background"]
- bg_changed = True
- args[0]["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode)
-
- if bg_changed:
- from ..widgets.widget_base_class import CTkBaseClass
+ def configure(self, **kwargs):
+ 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))
for child in self.winfo_children():
- if isinstance(child, CTkBaseClass):
- child.configure(bg_color=self.fg_color)
+ try:
+ child.configure(bg_color=self._fg_color)
+ except Exception:
+ pass
- super().configure(*args, **kwargs)
+ super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
+ check_kwargs_empty(kwargs)
- @staticmethod
- def enable_macos_dark_title_bar():
- if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
+ def cget(self, attribute_name: str) -> any:
+ if attribute_name == "fg_color":
+ return self._fg_color
+ else:
+ return super().cget(attribute_name)
+
+ @classmethod
+ def _enable_macos_dark_title_bar(cls):
+ if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
- @staticmethod
- def disable_macos_dark_title_bar():
- if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS
+ @classmethod
+ def _disable_macos_dark_title_bar(cls):
+ if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
# This command reverts the dark-mode setting for all programs.
- def windows_set_titlebar_color(self, color_mode: str):
+ def _windows_set_titlebar_color(self, color_mode: str):
"""
Set the titlebar color of the window to light or dark theme on Microsoft Windows.
@@ -247,9 +208,9 @@ class CTkToplevel(tkinter.Toplevel):
https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
"""
- if sys.platform.startswith("win") and not Settings.deactivate_windows_window_header_manipulation:
+ if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation:
- self.state_before_windows_set_titlebar_color = self.state()
+ self._state_before_windows_set_titlebar_color = self.state()
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
super().update()
@@ -277,41 +238,35 @@ class CTkToplevel(tkinter.Toplevel):
except Exception as err:
print(err)
- self.windows_set_titlebar_color_called = True
- self.after(5, self.revert_withdraw_after_windows_set_titlebar_color)
+ self._windows_set_titlebar_color_called = True
+ self.after(5, self._revert_withdraw_after_windows_set_titlebar_color)
- def revert_withdraw_after_windows_set_titlebar_color(self):
+ def _revert_withdraw_after_windows_set_titlebar_color(self):
""" if in a short time (5ms) after """
- if self.windows_set_titlebar_color_called:
+ if self._windows_set_titlebar_color_called:
- if self.withdraw_called_after_windows_set_titlebar_color:
+ if self._withdraw_called_after_windows_set_titlebar_color:
pass # leave it withdrawed
- elif self.iconify_called_after_windows_set_titlebar_color:
+ elif self._iconify_called_after_windows_set_titlebar_color:
super().iconify()
else:
- if self.state_before_windows_set_titlebar_color == "normal":
+ if self._state_before_windows_set_titlebar_color == "normal":
self.deiconify()
- elif self.state_before_windows_set_titlebar_color == "iconic":
+ elif self._state_before_windows_set_titlebar_color == "iconic":
self.iconify()
- elif self.state_before_windows_set_titlebar_color == "zoomed":
+ elif self._state_before_windows_set_titlebar_color == "zoomed":
self.state("zoomed")
else:
- self.state(self.state_before_windows_set_titlebar_color) # other states
+ self.state(self._state_before_windows_set_titlebar_color) # other states
- self.windows_set_titlebar_color_called = False
- self.withdraw_called_after_windows_set_titlebar_color = False
- self.iconify_called_after_windows_set_titlebar_color = False
+ self._windows_set_titlebar_color_called = False
+ self._withdraw_called_after_windows_set_titlebar_color = False
+ self._iconify_called_after_windows_set_titlebar_color = False
- def set_appearance_mode(self, mode_string):
- if mode_string.lower() == "dark":
- self.appearance_mode = 1
- elif mode_string.lower() == "light":
- self.appearance_mode = 0
+ def _set_appearance_mode(self, mode_string):
+ super()._set_appearance_mode(mode_string)
if sys.platform.startswith("win"):
- if self.appearance_mode == 1:
- self.windows_set_titlebar_color("dark")
- else:
- self.windows_set_titlebar_color("light")
+ self._windows_set_titlebar_color(mode_string)
- super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
+ super().configure(bg=self._apply_appearance_mode(self._fg_color))
diff --git a/customtkinter/windows/widgets/__init__.py b/customtkinter/windows/widgets/__init__.py
new file mode 100644
index 0000000..2e21484
--- /dev/null
+++ b/customtkinter/windows/widgets/__init__.py
@@ -0,0 +1,15 @@
+from .ctk_button import CTkButton
+from .ctk_checkbox import CTkCheckBox
+from .ctk_combobox import CTkComboBox
+from .ctk_entry import CTkEntry
+from .ctk_frame import CTkFrame
+from .ctk_label import CTkLabel
+from .ctk_optionmenu import CTkOptionMenu
+from .ctk_progressbar import CTkProgressBar
+from .ctk_radiobutton import CTkRadioButton
+from .ctk_scrollbar import CTkScrollbar
+from .ctk_segmented_button import CTkSegmentedButton
+from .ctk_slider import CTkSlider
+from .ctk_switch import CTkSwitch
+from .ctk_tabview import CTkTabview
+from .ctk_textbox import CTkTextbox
diff --git a/customtkinter/windows/widgets/appearance_mode/__init__.py b/customtkinter/windows/widgets/appearance_mode/__init__.py
new file mode 100644
index 0000000..e979ca8
--- /dev/null
+++ b/customtkinter/windows/widgets/appearance_mode/__init__.py
@@ -0,0 +1,4 @@
+from .appearance_mode_base_class import CTkAppearanceModeBaseClass
+from .appearance_mode_tracker import AppearanceModeTracker
+
+AppearanceModeTracker.init_appearance_mode()
diff --git a/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py b/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py
new file mode 100644
index 0000000..0d19147
--- /dev/null
+++ b/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py
@@ -0,0 +1,61 @@
+from typing import Union, Tuple, List
+
+from .appearance_mode_tracker import AppearanceModeTracker
+
+
+class CTkAppearanceModeBaseClass:
+ """
+ Super-class that manages the appearance mode. Methods:
+
+ - destroy() must be called when sub-class is destroyed
+ - _set_appearance_mode() abstractmethod, gets called when appearance mode changes, must be overridden
+ - _apply_appearance_mode()
+
+ """
+ def __init__(self):
+ AppearanceModeTracker.add(self._set_appearance_mode, self)
+ self.__appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
+
+ def destroy(self):
+ AppearanceModeTracker.remove(self._set_appearance_mode)
+
+ def _set_appearance_mode(self, mode_string: str):
+ """ can be overridden but super method must be called at the beginning """
+ if mode_string.lower() == "dark":
+ self.__appearance_mode = 1
+ elif mode_string.lower() == "light":
+ self.__appearance_mode = 0
+
+ def _get_appearance_mode(self) -> str:
+ """ get appearance mode as a string, 'light' or 'dark' """
+ if self.__appearance_mode == 0:
+ return "light"
+ else:
+ return "dark"
+
+ def _apply_appearance_mode(self, color: Union[str, Tuple[str, str], List[str]]) -> str:
+ """
+ color can be either a single hex color string or a color name or it can be a
+ tuple color with (light_color, dark_color). The functions returns
+ always a single color string
+ """
+
+ if isinstance(color, (tuple, list)):
+ return color[self.__appearance_mode]
+ else:
+ return color
+
+ @staticmethod
+ def _check_color_type(color: any, transparency: bool = False):
+ if color is None:
+ raise ValueError(f"color is None, for transparency set color='transparent'")
+ elif isinstance(color, (tuple, list)) and (color[0] == "transparent" or color[1] == "transparent"):
+ raise ValueError(f"transparency is not allowed in tuple color {color}, use 'transparent'")
+ elif color == "transparent" and transparency is False:
+ raise ValueError(f"transparency is not allowed for this attribute")
+ elif isinstance(color, str):
+ return color
+ elif isinstance(color, (tuple, list)) and len(color) == 2 and isinstance(color[0], str) and isinstance(color[1], str):
+ return color
+ else:
+ raise ValueError(f"color {color} must be string ('transparent' or 'color-name' or 'hex-color') or tuple of two strings, not {type(color)}")
diff --git a/customtkinter/appearance_mode_tracker.py b/customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py
similarity index 98%
rename from customtkinter/appearance_mode_tracker.py
rename to customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py
index 210b8bf..4958b93 100644
--- a/customtkinter/appearance_mode_tracker.py
+++ b/customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py
@@ -21,7 +21,7 @@ class AppearanceModeTracker:
callback_list = []
app_list = []
update_loop_running = False
- update_loop_interval = 500 # milliseconds
+ update_loop_interval = 30 # milliseconds
appearance_mode_set_by = "system"
appearance_mode = 0 # Light (standard)
diff --git a/customtkinter/windows/widgets/core_rendering/__init__.py b/customtkinter/windows/widgets/core_rendering/__init__.py
new file mode 100644
index 0000000..ccadbc7
--- /dev/null
+++ b/customtkinter/windows/widgets/core_rendering/__init__.py
@@ -0,0 +1,12 @@
+import sys
+
+from .ctk_canvas import CTkCanvas
+from .draw_engine import DrawEngine
+
+CTkCanvas.init_font_character_mapping()
+
+# determine draw method based on current platform
+if sys.platform == "darwin":
+ DrawEngine.preferred_drawing_method = "polygon_shapes"
+else:
+ DrawEngine.preferred_drawing_method = "font_shapes"
diff --git a/customtkinter/widgets/ctk_canvas.py b/customtkinter/windows/widgets/core_rendering/ctk_canvas.py
similarity index 68%
rename from customtkinter/widgets/ctk_canvas.py
rename to customtkinter/windows/widgets/core_rendering/ctk_canvas.py
index c0754b1..f291e2c 100644
--- a/customtkinter/widgets/ctk_canvas.py
+++ b/customtkinter/windows/widgets/core_rendering/ctk_canvas.py
@@ -4,11 +4,32 @@ from typing import Union, Tuple
class CTkCanvas(tkinter.Canvas):
+ """
+ Canvas with additional functionality to draw antialiased circles on Windows/Linux.
+
+ Call .init_font_character_mapping() at program start to load the correct character
+ dictionary according to the operating system. Characters (circle sizes) are optimised
+ to look best for rendering CustomTkinter shapes on the different operating systems.
+
+ - .create_aa_circle() creates antialiased circle and returns int identifier.
+ - .coords() is modified to support the aa-circle shapes correctly like you would expect.
+ - .itemconfig() is also modified to support aa-cricle shapes.
+
+ The aa-circles are created by choosing a character from the custom created and loaded
+ font 'CustomTkinter_shapes_font'. It contains circle shapes with different sizes filling
+ either the whole character space or just pert of it (characters A to R). Circles with a smaller
+ radius need a smaller circle character to look correct when rendered on the canvas.
+
+ For an optimal result, the draw-engine creates two aa-circles on top of each other, while
+ one is rotated by 90 degrees. This helps to make the circle look more symetric, which is
+ not can be a problem when using only a single circle character.
+ """
+
radius_to_char_fine: dict = None # dict to map radius to font circle character
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.aa_circle_canvas_ids = set()
+ self._aa_circle_canvas_ids = set()
@classmethod
def init_font_character_mapping(cls):
@@ -43,7 +64,7 @@ class CTkCanvas(tkinter.Canvas):
else:
cls.radius_to_char_fine = radius_to_char_fine_windows_10
- def get_char_from_radius(self, radius: int) -> str:
+ def _get_char_from_radius(self, radius: int) -> str:
if radius >= 20:
return "A"
else:
@@ -52,10 +73,10 @@ class CTkCanvas(tkinter.Canvas):
def create_aa_circle(self, x_pos: int, y_pos: int, radius: int, angle: int = 0, fill: str = "white",
tags: Union[str, Tuple[str, ...]] = "", anchor: str = tkinter.CENTER) -> int:
# create a circle with a font element
- circle_1 = self.create_text(x_pos, y_pos, text=self.get_char_from_radius(radius), anchor=anchor, fill=fill,
+ circle_1 = self.create_text(x_pos, y_pos, text=self._get_char_from_radius(radius), anchor=anchor, fill=fill,
font=("CustomTkinter_shapes_font", -radius * 2), tags=tags, angle=angle)
self.addtag_withtag("ctk_aa_circle_font_element", circle_1)
- self.aa_circle_canvas_ids.add(circle_1)
+ self._aa_circle_canvas_ids.add(circle_1)
return circle_1
@@ -66,13 +87,13 @@ class CTkCanvas(tkinter.Canvas):
super().coords(coords_id, *args[:2])
if len(args) == 3:
- super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self.get_char_from_radius(args[2]))
+ super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self._get_char_from_radius(args[2]))
- elif type(tag_or_id) == int and tag_or_id in self.aa_circle_canvas_ids:
+ elif type(tag_or_id) == int and tag_or_id in self._aa_circle_canvas_ids:
super().coords(tag_or_id, *args[:2])
if len(args) == 3:
- super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self.get_char_from_radius(args[2]))
+ super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self._get_char_from_radius(args[2]))
else:
super().coords(tag_or_id, *args)
@@ -83,14 +104,14 @@ class CTkCanvas(tkinter.Canvas):
del kwargs_except_outline["outline"]
if type(tag_or_id) == int:
- if tag_or_id in self.aa_circle_canvas_ids:
+ if tag_or_id in self._aa_circle_canvas_ids:
super().itemconfigure(tag_or_id, *args, **kwargs_except_outline)
else:
super().itemconfigure(tag_or_id, *args, **kwargs)
else:
configure_ids = self.find_withtag(tag_or_id)
for configure_id in configure_ids:
- if configure_id in self.aa_circle_canvas_ids:
+ if configure_id in self._aa_circle_canvas_ids:
super().itemconfigure(configure_id, *args, **kwargs_except_outline)
else:
super().itemconfigure(configure_id, *args, **kwargs)
diff --git a/customtkinter/draw_engine.py b/customtkinter/windows/widgets/core_rendering/draw_engine.py
similarity index 95%
rename from customtkinter/draw_engine.py
rename to customtkinter/windows/widgets/core_rendering/draw_engine.py
index eafaeb2..5acea56 100644
--- a/customtkinter/draw_engine.py
+++ b/customtkinter/windows/widgets/core_rendering/draw_engine.py
@@ -5,7 +5,7 @@ import tkinter
from typing import Union, TYPE_CHECKING
if TYPE_CHECKING:
- from .widgets.ctk_canvas import CTkCanvas
+ from ..core_rendering import CTkCanvas
class DrawEngine:
@@ -30,6 +30,12 @@ class DrawEngine:
def __init__(self, canvas: CTkCanvas):
self._canvas = canvas
+ self._round_width_to_even_numbers: bool = True
+ self._round_height_to_even_numbers: bool = True
+
+ def set_round_to_even_numbers(self, round_width_to_even_numbers: bool = True, round_height_to_even_numbers: bool = True):
+ self._round_width_to_even_numbers: bool = round_width_to_even_numbers
+ self._round_height_to_even_numbers: bool = round_height_to_even_numbers
def __calc_optimal_corner_radius(self, user_corner_radius: Union[float, int]) -> Union[float, int]:
# optimize for drawing with polygon shapes
@@ -55,6 +61,38 @@ class DrawEngine:
else:
return user_corner_radius
+ def draw_background_corners(self, width: Union[float, int], height: Union[float, int], ):
+ if self._round_width_to_even_numbers:
+ width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only
+ if self._round_height_to_even_numbers:
+ height = math.floor(height / 2) * 2
+
+ requires_recoloring = False
+
+ if not self._canvas.find_withtag("background_corner_top_left"):
+ self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_top_left"), width=0)
+ requires_recoloring = True
+ if not self._canvas.find_withtag("background_corner_top_right"):
+ self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_top_right"), width=0)
+ requires_recoloring = True
+ if not self._canvas.find_withtag("background_corner_bottom_right"):
+ self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_bottom_right"), width=0)
+ requires_recoloring = True
+ if not self._canvas.find_withtag("background_corner_bottom_left"):
+ self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_bottom_left"), width=0)
+ requires_recoloring = True
+
+ mid_width, mid_height = round(width / 2), round(height / 2)
+ self._canvas.coords("background_corner_top_left", (0, 0, mid_width, mid_height))
+ self._canvas.coords("background_corner_top_right", (mid_width, 0, width, mid_height))
+ self._canvas.coords("background_corner_bottom_right", (mid_width, mid_height, width, height))
+ self._canvas.coords("background_corner_bottom_left", (0, mid_height, mid_width, height))
+
+ if requires_recoloring: # new parts were added -> manage z-order
+ self._canvas.tag_lower("background_parts")
+
+ return requires_recoloring
+
def draw_rounded_rect_with_border(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int],
border_width: Union[float, int], overwrite_preferred_drawing_method: str = None) -> bool:
""" Draws a rounded rectangle with a corner_radius and border_width on the canvas. The border elements have a 'border_parts' tag,
@@ -62,11 +100,13 @@ class DrawEngine:
returns bool if recoloring is necessary """
- width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only
- height = math.floor(height / 2) * 2
+ if self._round_width_to_even_numbers:
+ width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only
+ if self._round_height_to_even_numbers:
+ height = math.floor(height / 2) * 2
corner_radius = round(corner_radius)
- if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
+ if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too large
corner_radius = min(width / 2, height / 2)
border_width = round(border_width)
@@ -139,6 +179,7 @@ class DrawEngine:
if requires_recoloring: # new parts were added -> manage z-order
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
+ self._canvas.tag_lower("background_parts")
return requires_recoloring
@@ -277,6 +318,7 @@ class DrawEngine:
if requires_recoloring: # new parts were added -> manage z-order
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
+ self._canvas.tag_lower("background_parts")
return requires_recoloring
@@ -364,8 +406,10 @@ class DrawEngine:
returns bool if recoloring is necessary """
left_section_width = round(left_section_width)
- width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only
- height = math.floor(height / 2) * 2
+ if self._round_width_to_even_numbers:
+ width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only
+ if self._round_height_to_even_numbers:
+ height = math.floor(height / 2) * 2
corner_radius = round(corner_radius)
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
@@ -478,6 +522,7 @@ class DrawEngine:
if requires_recoloring: # new parts were added -> manage z-order
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
+ self._canvas.tag_lower("background_parts")
return requires_recoloring
@@ -641,6 +686,7 @@ class DrawEngine:
if requires_recoloring: # new parts were added -> manage z-order
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
+ self._canvas.tag_lower("background_parts")
return requires_recoloring
@@ -652,8 +698,10 @@ class DrawEngine:
returns bool if recoloring is necessary """
- width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only
- height = math.floor(height / 2) * 2
+ if self._round_width_to_even_numbers:
+ width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only
+ if self._round_height_to_even_numbers:
+ height = math.floor(height / 2) * 2
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
corner_radius = min(width / 2, height / 2)
@@ -824,8 +872,10 @@ class DrawEngine:
border_width: Union[float, int], button_length: Union[float, int], button_corner_radius: Union[float, int],
slider_value: float, orientation: str) -> bool:
- width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only
- height = math.floor(height / 2) * 2
+ if self._round_width_to_even_numbers:
+ width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only
+ if self._round_height_to_even_numbers:
+ height = math.floor(height / 2) * 2
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
corner_radius = min(width / 2, height / 2)
@@ -980,8 +1030,11 @@ class DrawEngine:
def draw_rounded_scrollbar(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int],
border_spacing: Union[float, int], start_value: float, end_value: float, orientation: str) -> bool:
- width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only
- height = math.floor(height / 2) * 2
+
+ if self._round_width_to_even_numbers:
+ width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only
+ if self._round_height_to_even_numbers:
+ height = math.floor(height / 2) * 2
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
corner_radius = min(width / 2, height / 2)
@@ -1176,6 +1229,7 @@ class DrawEngine:
self._canvas.tag_raise("dropdown_arrow")
requires_recoloring = True
+ self._canvas.itemconfigure("dropdown_arrow", font=("CustomTkinter_shapes_font", -size))
self._canvas.coords("dropdown_arrow", x_position, y_position)
return requires_recoloring
diff --git a/customtkinter/windows/widgets/core_widget_classes/__init__.py b/customtkinter/windows/widgets/core_widget_classes/__init__.py
new file mode 100644
index 0000000..75e2d84
--- /dev/null
+++ b/customtkinter/windows/widgets/core_widget_classes/__init__.py
@@ -0,0 +1,2 @@
+from .dropdown_menu import DropdownMenu
+from .ctk_base_class import CTkBaseClass
diff --git a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py
new file mode 100644
index 0000000..491a253
--- /dev/null
+++ b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py
@@ -0,0 +1,312 @@
+import sys
+import tkinter
+import tkinter.ttk as ttk
+from typing import Union, Callable, Tuple
+
+try:
+ from typing import TypedDict
+except ImportError:
+ from typing_extensions import TypedDict
+
+# removed due to circular import
+# from ...ctk_tk import CTk
+# from ...ctk_toplevel import CTkToplevel
+from .... import windows # import windows for isinstance checks
+
+from ..theme import ThemeManager
+from ..font import CTkFont
+from ..image import CTkImage
+from ..appearance_mode import CTkAppearanceModeBaseClass
+from ..scaling import CTkScalingBaseClass
+
+from ..utility import pop_from_dict_by_set, check_kwargs_empty
+
+
+class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
+ """ Base class of every CTk widget, handles the dimensions, bg_color,
+ appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """
+
+ # attributes that are passed to and managed by the tkinter frame only:
+ _valid_tk_frame_attributes: set = {"cursor"}
+
+ _cursor_manipulation_enabled: bool = True
+
+ def __init__(self,
+ master: any,
+ width: int = 0,
+ height: int = 0,
+
+ bg_color: Union[str, Tuple[str, str]] = "transparent",
+ **kwargs):
+
+ # call init methods of super classes
+ tkinter.Frame.__init__(self, master=master, width=width, height=height, **pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))
+ CTkAppearanceModeBaseClass.__init__(self)
+ CTkScalingBaseClass.__init__(self, scaling_type="widget")
+
+ # check if kwargs is empty, if not raise error for unsupported arguments
+ check_kwargs_empty(kwargs, raise_error=True)
+
+ # dimensions independent of scaling
+ self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget
+ self._current_height = height # _current_width and _current_height are independent of the scale
+ self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height
+ self._desired_height = height
+
+ # set width and height of tkinter.Frame
+ super().configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+
+ # save latest geometry function and kwargs
+ class GeometryCallDict(TypedDict):
+ function: Callable
+ kwargs: dict
+ self._last_geometry_manager_call: Union[GeometryCallDict, None] = None
+
+ # background color
+ self._bg_color: Union[str, Tuple[str, str]] = self._detect_color_of_master() if bg_color == "transparent" else self._check_color_type(bg_color, transparency=True)
+
+ # set bg color of tkinter.Frame
+ super().configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ # add configure callback to tkinter.Frame
+ super().bind('', self._update_dimensions_event)
+
+ # overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well
+ if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame, tkinter.LabelFrame, ttk.Frame, ttk.LabelFrame, ttk.Notebook)) and not isinstance(self.master, CTkBaseClass):
+ master_old_configure = self.master.config
+
+ def new_configure(*args, **kwargs):
+ if "bg" in kwargs:
+ self.configure(bg_color=kwargs["bg"])
+ elif "background" in kwargs:
+ self.configure(bg_color=kwargs["background"])
+
+ # args[0] is dict when attribute gets changed by widget[] syntax
+ elif len(args) > 0 and type(args[0]) == dict:
+ if "bg" in args[0]:
+ self.configure(bg_color=args[0]["bg"])
+ elif "background" in args[0]:
+ self.configure(bg_color=args[0]["background"])
+ master_old_configure(*args, **kwargs)
+
+ self.master.config = new_configure
+ self.master.configure = new_configure
+
+ def destroy(self):
+ """ Destroy this and all descendants widgets. """
+
+ # call destroy methods of super classes
+ tkinter.Frame.destroy(self)
+ CTkAppearanceModeBaseClass.destroy(self)
+ CTkScalingBaseClass.destroy(self)
+
+ def _draw(self, no_color_updates: bool = False):
+ """ can be overridden but super method must be called """
+ if no_color_updates is False:
+ # Configuring color of tkinter.Frame not necessary at the moment?
+ # Causes flickering on Windows and Linux for segmented button for some reason!
+ # super().configure(bg=self._apply_appearance_mode(self._bg_color))
+ pass
+
+ def config(self, *args, **kwargs):
+ raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.")
+
+ def configure(self, require_redraw=False, **kwargs):
+ """ basic configure with bg_color, width, height support, calls configure of tkinter.Frame, updates in the end """
+
+ if "width" in kwargs:
+ self._set_dimensions(width=kwargs.pop("width"))
+
+ if "height" in kwargs:
+ self._set_dimensions(height=kwargs.pop("height"))
+
+ if "bg_color" in kwargs:
+ new_bg_color = self._check_color_type(kwargs.pop("bg_color"), transparency=True)
+ if new_bg_color == "transparent":
+ self._bg_color = self._detect_color_of_master()
+ else:
+ self._bg_color = self._check_color_type(new_bg_color)
+ require_redraw = True
+
+ super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes)) # configure tkinter.Frame
+
+ # if there are still items in the kwargs dict, raise ValueError
+ check_kwargs_empty(kwargs, raise_error=True)
+
+ if require_redraw:
+ self._draw()
+
+ def cget(self, attribute_name: str):
+ """ basic cget with bg_color, width, height support, calls cget of tkinter.Frame """
+
+ if attribute_name == "bg_color":
+ return self._bg_color
+ elif attribute_name == "width":
+ return self._desired_width
+ elif attribute_name == "height":
+ return self._desired_height
+
+ elif attribute_name in self._valid_tk_frame_attributes:
+ return super().cget(attribute_name) # cget of tkinter.Frame
+ else:
+ raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.")
+
+ def _check_font_type(self, font: any):
+ """ check font type when passed to widget """
+ if isinstance(font, CTkFont):
+ return font
+
+ elif type(font) == tuple and len(font) == 1:
+ sys.stderr.write(f"{type(self).__name__} Warning: font {font} given without size, will be extended with default text size of current theme\n")
+ return font[0], ThemeManager.theme["text"]["size"]
+
+ elif type(font) == tuple and 2 <= len(font) <= 3:
+ return font
+
+ else:
+ raise ValueError(f"Wrong font type {type(font)}\n" +
+ f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 or 3 or an instance of CTkFont.\n" +
+ f"\nUsage example:\n" +
+ f"font=customtkinter.CTkFont(family='', size=)\n" +
+ f"font=('', )\n")
+
+ def _check_image_type(self, image: any):
+ """ check image type when passed to widget """
+ if image is None:
+ return image
+ elif isinstance(image, CTkImage):
+ return image
+ else:
+ sys.stderr.write(f"{type(self).__name__} Warning: Given image is not CTkImage but {type(image)}. " +
+ f"Image can not be scaled on HighDPI displays, use CTkImage instead.\n")
+ return image
+
+ def _update_dimensions_event(self, event):
+ # only redraw if dimensions changed (for performance), independent of scaling
+ if round(self._current_width) != round(self._reverse_widget_scaling(event.width)) or round(self._current_height) != round(self._reverse_widget_scaling(event.height)):
+ self._current_width = self._reverse_widget_scaling(event.width) # adjust current size according to new size given by event
+ self._current_height = self._reverse_widget_scaling(event.height) # _current_width and _current_height are independent of the scale
+
+ self._draw(no_color_updates=True) # faster drawing without color changes
+
+ def _detect_color_of_master(self, master_widget=None) -> Union[str, Tuple[str, str]]:
+ """ detect foreground color of master widget for bg_color and transparent color """
+
+ if master_widget is None:
+ master_widget = self.master
+
+ if isinstance(master_widget, (windows.widgets.core_widget_classes.CTkBaseClass, windows.CTk, windows.CTkToplevel)):
+ if master_widget.cget("fg_color") is not None and master_widget.cget("fg_color") != "transparent":
+ return master_widget.cget("fg_color")
+
+ # if fg_color of master is None, try to retrieve fg_color from master of master
+ elif hasattr(master_widget.master, "master"):
+ return self._detect_color_of_master(master_widget.master)
+
+ elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget
+ try:
+ ttk_style = ttk.Style()
+ return ttk_style.lookup(master_widget.winfo_class(), 'background')
+ except Exception:
+ return "#FFFFFF", "#000000"
+
+ else: # master is normal tkinter widget
+ try:
+ return master_widget.cget("bg") # try to get bg color by .cget() method
+ except Exception:
+ return "#FFFFFF", "#000000"
+
+ def _set_appearance_mode(self, mode_string):
+ super()._set_appearance_mode(mode_string)
+ self._draw()
+ super().update_idletasks()
+
+ def _set_scaling(self, new_widget_scaling, new_window_scaling):
+ super()._set_scaling(new_widget_scaling, new_window_scaling)
+
+ super().configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+
+ if self._last_geometry_manager_call is not None:
+ self._last_geometry_manager_call["function"](**self._apply_argument_scaling(self._last_geometry_manager_call["kwargs"]))
+
+ def _set_dimensions(self, width=None, height=None):
+ if width is not None:
+ self._desired_width = width
+ if height is not None:
+ self._desired_height = height
+
+ super().configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+
+ def place(self, **kwargs):
+ """
+ Place a widget in the parent widget. Use as options:
+ in=master - master relative to which the widget is placed
+ in_=master - see 'in' option description
+ x=amount - locate anchor of this widget at position x of master
+ y=amount - locate anchor of this widget at position y of master
+ relx=amount - locate anchor of this widget between 0.0 and 1.0 relative to width of master (1.0 is right edge)
+ rely=amount - locate anchor of this widget between 0.0 and 1.0 relative to height of master (1.0 is bottom edge)
+ anchor=NSEW (or subset) - position anchor according to given direction
+ width=amount - width of this widget in pixel
+ height=amount - height of this widget in pixel
+ relwidth=amount - width of this widget between 0.0 and 1.0 relative to width of master (1.0 is the same width as the master)
+ relheight=amount - height of this widget between 0.0 and 1.0 relative to height of master (1.0 is the same height as the master)
+ bordermode="inside" or "outside" - whether to take border width of master widget into account
+ """
+ self._last_geometry_manager_call = {"function": super().place, "kwargs": kwargs}
+ return super().place(**self._apply_argument_scaling(kwargs))
+
+ def place_forget(self):
+ """ Unmap this widget. """
+ self._last_geometry_manager_call = None
+ return super().place_forget()
+
+ def pack(self, **kwargs):
+ """
+ Pack a widget in the parent widget. Use as options:
+ after=widget - pack it after you have packed widget
+ anchor=NSEW (or subset) - position widget according to given direction
+ before=widget - pack it before you will pack widget
+ expand=bool - expand widget if parent size grows
+ fill=NONE or X or Y or BOTH - fill widget if widget grows
+ in=master - use master to contain this widget
+ in_=master - see 'in' option description
+ ipadx=amount - add internal padding in x direction
+ ipady=amount - add internal padding in y direction
+ padx=amount - add padding in x direction
+ pady=amount - add padding in y direction
+ side=TOP or BOTTOM or LEFT or RIGHT - where to add this widget.
+ """
+ self._last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs}
+ return super().pack(**self._apply_argument_scaling(kwargs))
+
+ def pack_forget(self):
+ """ Unmap this widget and do not use it for the packing order. """
+ self._last_geometry_manager_call = None
+ return super().pack_forget()
+
+ def grid(self, **kwargs):
+ """
+ Position a widget in the parent widget in a grid. Use as options:
+ column=number - use cell identified with given column (starting with 0)
+ columnspan=number - this widget will span several columns
+ in=master - use master to contain this widget
+ in_=master - see 'in' option description
+ ipadx=amount - add internal padding in x direction
+ ipady=amount - add internal padding in y direction
+ padx=amount - add padding in x direction
+ pady=amount - add padding in y direction
+ row=number - use cell identified with given row (starting with 0)
+ rowspan=number - this widget will span several rows
+ sticky=NSEW - if cell is larger on which sides will this widget stick to the cell boundary
+ """
+ self._last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs}
+ return super().grid(**self._apply_argument_scaling(kwargs))
+
+ def grid_forget(self):
+ """ Unmap this widget. """
+ self._last_geometry_manager_call = None
+ return super().grid_forget()
diff --git a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py
new file mode 100644
index 0000000..a6b8186
--- /dev/null
+++ b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py
@@ -0,0 +1,198 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, List, Optional
+
+from ..theme import ThemeManager
+from ..font import CTkFont
+from ..appearance_mode import CTkAppearanceModeBaseClass
+from ..scaling import CTkScalingBaseClass
+
+
+class DropdownMenu(tkinter.Menu, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
+ def __init__(self, *args,
+ min_character_width: int = 18,
+
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+ text_color: Optional[Union[str, Tuple[str, str]]] = None,
+
+ font: Optional[Union[tuple, CTkFont]] = None,
+ command: Union[Callable, None] = None,
+ values: Optional[List[str]] = None,
+ **kwargs):
+
+ # call init methods of super classes
+ tkinter.Menu.__init__(self, *args, **kwargs)
+ CTkAppearanceModeBaseClass.__init__(self)
+ CTkScalingBaseClass.__init__(self, scaling_type="widget")
+
+ self._min_character_width = min_character_width
+ self._fg_color = ThemeManager.theme["DropdownMenu"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+ self._hover_color = ThemeManager.theme["DropdownMenu"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
+ self._text_color = ThemeManager.theme["DropdownMenu"]["text_color"] if text_color is None else self._check_color_type(text_color)
+
+ # font
+ self._font = CTkFont() if font is None else self._check_font_type(font)
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+
+ self._configure_menu_for_platforms()
+
+ self._values = values
+ self._command = command
+
+ self._add_menu_commands()
+
+ def destroy(self):
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+
+ # call destroy methods of super classes
+ tkinter.Menu.destroy(self)
+ CTkAppearanceModeBaseClass.destroy(self)
+
+ def _update_font(self):
+ """ pass font to tkinter widgets with applied font scaling """
+ super().configure(font=self._apply_font_scaling(self._font))
+
+ def _configure_menu_for_platforms(self):
+ """ apply platform specific appearance attributes, configure all colors """
+
+ if sys.platform == "darwin":
+ super().configure(tearoff=False,
+ font=self._apply_font_scaling(self._font))
+
+ elif sys.platform.startswith("win"):
+ super().configure(tearoff=False,
+ relief="flat",
+ activebackground=self._apply_appearance_mode(self._hover_color),
+ borderwidth=self._apply_widget_scaling(4),
+ activeborderwidth=self._apply_widget_scaling(4),
+ bg=self._apply_appearance_mode(self._fg_color),
+ fg=self._apply_appearance_mode(self._text_color),
+ activeforeground=self._apply_appearance_mode(self._text_color),
+ font=self._apply_font_scaling(self._font),
+ cursor="hand2")
+
+ else:
+ super().configure(tearoff=False,
+ relief="flat",
+ activebackground=self._apply_appearance_mode(self._hover_color),
+ borderwidth=0,
+ activeborderwidth=0,
+ bg=self._apply_appearance_mode(self._fg_color),
+ fg=self._apply_appearance_mode(self._text_color),
+ activeforeground=self._apply_appearance_mode(self._text_color),
+ font=self._apply_font_scaling(self._font))
+
+ def _add_menu_commands(self):
+ """ delete existing menu labels and createe new labels with command according to values list """
+
+ self.delete(0, "end") # delete all old commands
+
+ if sys.platform.startswith("linux"):
+ for value in self._values:
+ self.add_command(label=" " + value.ljust(self._min_character_width) + " ",
+ command=lambda v=value: self._button_callback(v),
+ compound="left")
+ else:
+ for value in self._values:
+ self.add_command(label=value.ljust(self._min_character_width),
+ command=lambda v=value: self._button_callback(v),
+ compound="left")
+
+ def _button_callback(self, value):
+ if self._command is not None:
+ self._command(value)
+
+ def open(self, x: Union[int, float], y: Union[int, float]):
+
+ if sys.platform == "darwin":
+ y += self._apply_widget_scaling(8)
+ else:
+ y += self._apply_widget_scaling(3)
+
+ if sys.platform == "darwin" or sys.platform.startswith("win"):
+ self.post(int(x), int(y))
+ else: # Linux
+ self.tk_popup(int(x), int(y))
+
+ def configure(self, **kwargs):
+ 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))
+
+ if "hover_color" in kwargs:
+ self._hover_color = self._check_color_type(kwargs.pop("hover_color"))
+ super().configure(activebackground=self._apply_appearance_mode(self._hover_color))
+
+ if "text_color" in kwargs:
+ self._text_color = self._check_color_type(kwargs.pop("text_color"))
+ super().configure(fg=self._apply_appearance_mode(self._text_color))
+
+ 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 "command" in kwargs:
+ self._command = kwargs.pop("command")
+
+ if "values" in kwargs:
+ self._values = kwargs.pop("values")
+ self._add_menu_commands()
+
+ super().configure(**kwargs)
+
+ def cget(self, attribute_name: str) -> any:
+ if attribute_name == "min_character_width":
+ return self._min_character_width
+
+ elif attribute_name == "fg_color":
+ return self._fg_color
+ elif attribute_name == "hover_color":
+ return self._hover_color
+ elif attribute_name == "text_color":
+ return self._text_color
+
+ elif attribute_name == "font":
+ return self._font
+ elif attribute_name == "command":
+ return self._command
+ elif attribute_name == "values":
+ return self._values
+
+ else:
+ return super().cget(attribute_name)
+
+ @staticmethod
+ def _check_font_type(font: any):
+ if isinstance(font, CTkFont):
+ return font
+
+ elif type(font) == tuple and len(font) == 1:
+ sys.stderr.write(f"Warning: font {font} given without size, will be extended with default text size of current theme\n")
+ return font[0], ThemeManager.theme["text"]["size"]
+
+ elif type(font) == tuple and 2 <= len(font) <= 3:
+ return font
+
+ else:
+ raise ValueError(f"Wrong font type {type(font)} for font '{font}'\n" +
+ f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 or 3 or an instance of CTkFont.\n" +
+ f"\nUsage example:\n" +
+ f"font=customtkinter.CTkFont(family='', size=)\n" +
+ f"font=('', )\n")
+
+ def _set_scaling(self, new_widget_scaling, new_window_scaling):
+ super()._set_scaling(new_widget_scaling, new_window_scaling)
+ self._configure_menu_for_platforms()
+
+ def _set_appearance_mode(self, mode_string):
+ """ colors won't update on appearance mode change when dropdown is open, because it's not necessary """
+ super()._set_appearance_mode(mode_string)
+ self._configure_menu_for_platforms()
diff --git a/customtkinter/windows/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py
new file mode 100644
index 0000000..595e703
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_button.py
@@ -0,0 +1,554 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+from .font import CTkFont
+from .image import CTkImage
+
+
+class CTkButton(CTkBaseClass):
+ """
+ Button with rounded corners, border, hover effect, image support, click command and textvariable.
+ For detailed information check out the documentation.
+ """
+
+ _image_label_spacing: int = 6
+
+ def __init__(self,
+ master: any,
+ width: int = 140,
+ height: int = 28,
+ corner_radius: Optional[int] = None,
+ border_width: Optional[int] = None,
+ border_spacing: int = 2,
+
+ bg_color: Union[str, Tuple[str, str]] = "transparent",
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ hover_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,
+
+ background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None,
+ round_width_to_even_numbers: bool = True,
+ round_height_to_even_numbers: bool = True,
+
+ text: str = "CTkButton",
+ font: Optional[Union[tuple, CTkFont]] = None,
+ textvariable: Union[tkinter.Variable, None] = None,
+ image: Union[tkinter.PhotoImage, CTkImage, None] = None,
+ state: str = "normal",
+ hover: bool = True,
+ command: Union[Callable[[], None], None] = None,
+ compound: str = "left",
+ anchor: str = "center",
+ **kwargs):
+
+ # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # shape
+ self._corner_radius: int = ThemeManager.theme["CTkButton"]["corner_radius"] if corner_radius is None else corner_radius
+ self._corner_radius = min(self._corner_radius, round(self._current_height / 2))
+ self._border_width: int = ThemeManager.theme["CTkButton"]["border_width"] if border_width is None else border_width
+ self._border_spacing: int = border_spacing
+
+ # color
+ self._fg_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
+ self._hover_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
+ self._border_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["border_color"] if border_color is None else self._check_color_type(border_color)
+ self._text_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["text_color"] if text_color is None else self._check_color_type(text_color)
+ self._text_color_disabled: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
+
+ # rendering options
+ self._background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = background_corner_colors # rendering options for DrawEngine
+ self._round_width_to_even_numbers: bool = round_width_to_even_numbers # rendering options for DrawEngine
+ self._round_height_to_even_numbers: bool = round_height_to_even_numbers # rendering options for DrawEngine
+
+ # text, font
+ self._text = text
+ self._text_label: Union[tkinter.Label, None] = None
+ self._textvariable: tkinter.Variable = textvariable
+ self._font: Union[tuple, CTkFont] = CTkFont() if font is None else self._check_font_type(font)
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+
+ # image
+ self._image = self._check_image_type(image)
+ self._image_label: Union[tkinter.Label, None] = None
+ if isinstance(self._image, CTkImage):
+ self._image.add_configure_callback(self._update_image)
+
+ # other
+ self._state: str = state
+ self._hover: bool = hover
+ self._command: Callable = command
+ self._compound: str = compound
+ self._anchor: str = anchor
+ self._click_animation_running: bool = False
+
+ # canvas and draw engine
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew")
+ self._draw_engine = DrawEngine(self._canvas)
+ self._draw_engine.set_round_to_even_numbers(self._round_width_to_even_numbers, self._round_height_to_even_numbers) # rendering options
+
+ # canvas event bindings
+ self._canvas.bind("", self._on_enter)
+ self._canvas.bind("", self._on_leave)
+ self._canvas.bind("", self._clicked)
+ self._canvas.bind("", self._clicked)
+
+ # configure cursor and initial draw
+ self._set_cursor()
+ self._draw()
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self._create_grid()
+
+ if self._text_label is not None:
+ self._text_label.configure(font=self._apply_font_scaling(self._font))
+
+ self._update_image()
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw(no_color_updates=True)
+
+ def _set_appearance_mode(self, mode_string):
+ super()._set_appearance_mode(mode_string)
+ self._update_image()
+
+ def _set_dimensions(self, width: int = None, height: int = None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw()
+
+ def _update_font(self):
+ """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
+ if self._text_label is not None:
+ self._text_label.configure(font=self._apply_font_scaling(self._font))
+
+ # Workaround to force grid to be resized when text changes size.
+ # Otherwise grid will lag and only resizes if other mouse action occurs.
+ self._canvas.grid_forget()
+ self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew")
+
+ def _update_image(self):
+ if self._image_label is not None:
+ self._image_label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(),
+ self._get_appearance_mode()))
+
+ def destroy(self):
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+ super().destroy()
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ if self._background_corner_colors is not None:
+ self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height))
+ self._canvas.itemconfig("background_corner_top_left", fill=self._apply_appearance_mode(self._background_corner_colors[0]))
+ self._canvas.itemconfig("background_corner_top_right", fill=self._apply_appearance_mode(self._background_corner_colors[1]))
+ self._canvas.itemconfig("background_corner_bottom_right", fill=self._apply_appearance_mode(self._background_corner_colors[2]))
+ self._canvas.itemconfig("background_corner_bottom_left", fill=self._apply_appearance_mode(self._background_corner_colors[3]))
+ else:
+ self._canvas.delete("background_parts")
+
+ requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width))
+
+ if no_color_updates is False or requires_recoloring:
+
+ 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 button parts
+ if self._fg_color == "transparent":
+ self._canvas.itemconfig("inner_parts",
+ outline=self._apply_appearance_mode(self._bg_color),
+ fill=self._apply_appearance_mode(self._bg_color))
+ else:
+ self._canvas.itemconfig("inner_parts",
+ outline=self._apply_appearance_mode(self._fg_color),
+ fill=self._apply_appearance_mode(self._fg_color))
+
+ # create text label if text given
+ if self._text is not None and self._text != "":
+
+ if self._text_label is None:
+ self._text_label = tkinter.Label(master=self,
+ font=self._apply_font_scaling(self._font),
+ text=self._text,
+ padx=0,
+ pady=0,
+ borderwidth=1,
+ textvariable=self._textvariable)
+ self._create_grid()
+
+ 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)
+
+ if no_color_updates is False:
+ # set text_label fg color (text color)
+ self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
+
+ if self._state == tkinter.DISABLED:
+ self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
+ else:
+ self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
+
+ if self._apply_appearance_mode(self._fg_color) == "transparent":
+ self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
+ else:
+ self._text_label.configure(bg=self._apply_appearance_mode(self._fg_color))
+
+ else:
+ # delete text_label if no text given
+ if self._text_label is not None:
+ self._text_label.destroy()
+ self._text_label = None
+ self._create_grid()
+
+ # create image label if image given
+ if self._image is not None:
+
+ if self._image_label is None:
+ self._image_label = tkinter.Label(master=self)
+ self._update_image() # set image
+ self._create_grid()
+
+ self._image_label.bind("", self._on_enter)
+ self._image_label.bind("", self._on_leave)
+ self._image_label.bind("", self._clicked)
+ self._image_label.bind("", self._clicked)
+
+ if no_color_updates is False:
+ # set image_label bg color (background color of label)
+ if self._apply_appearance_mode(self._fg_color) == "transparent":
+ self._image_label.configure(bg=self._apply_appearance_mode(self._bg_color))
+ else:
+ self._image_label.configure(bg=self._apply_appearance_mode(self._fg_color))
+
+ else:
+ # delete text_label if no text given
+ if self._image_label is not None:
+ self._image_label.destroy()
+ self._image_label = None
+ self._create_grid()
+
+ def _create_grid(self):
+ """ configure grid system (5x5) """
+
+ # Outer rows and columns have weight of 1000 to overpower the rows and columns of the label and image with weight 1.
+ # Rows and columns of image and label need weight of 1 to collapse in case of missing space on the button,
+ # so image and label need sticky option to stick together in the center, and therefore outer rows and columns
+ # need weight of 100 in case of other anchor than center.
+ n_padding_weight, s_padding_weight, e_padding_weight, w_padding_weight = 1000, 1000, 1000, 1000
+ if self._anchor != "center":
+ if "n" in self._anchor:
+ n_padding_weight, s_padding_weight = 0, 1000
+ if "s" in self._anchor:
+ n_padding_weight, s_padding_weight = 1000, 0
+ if "e" in self._anchor:
+ e_padding_weight, w_padding_weight = 1000, 0
+ if "w" in self._anchor:
+ e_padding_weight, w_padding_weight = 0, 1000
+
+ scaled_minsize_rows = self._apply_widget_scaling(max(self._border_width + 1, self._border_spacing))
+ scaled_minsize_columns = self._apply_widget_scaling(max(self._corner_radius, self._border_width + 1, self._border_spacing))
+
+ self.grid_rowconfigure(0, weight=n_padding_weight, minsize=scaled_minsize_rows)
+ self.grid_rowconfigure(4, weight=s_padding_weight, minsize=scaled_minsize_rows)
+ self.grid_columnconfigure(0, weight=e_padding_weight, minsize=scaled_minsize_columns)
+ self.grid_columnconfigure(4, weight=w_padding_weight, minsize=scaled_minsize_columns)
+
+ if self._compound in ("right", "left"):
+ self.grid_rowconfigure(2, weight=1)
+ 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_rowconfigure((1, 3), weight=0)
+ self.grid_columnconfigure((1, 3), weight=1)
+ else:
+ self.grid_columnconfigure(2, weight=1)
+ 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_columnconfigure((1, 3), weight=0)
+ self.grid_rowconfigure((1, 3), weight=1)
+
+ if self._compound == "right":
+ if self._image_label is not None:
+ self._image_label.grid(row=2, column=3, sticky="w")
+ if self._text_label is not None:
+ self._text_label.grid(row=2, column=1, sticky="e")
+ elif self._compound == "left":
+ if self._image_label is not None:
+ self._image_label.grid(row=2, column=1, sticky="e")
+ if self._text_label is not None:
+ self._text_label.grid(row=2, column=3, sticky="w")
+ elif self._compound == "top":
+ if self._image_label is not None:
+ self._image_label.grid(row=1, column=2, sticky="s")
+ if self._text_label is not None:
+ self._text_label.grid(row=3, column=2, sticky="n")
+ elif self._compound == "bottom":
+ if self._image_label is not None:
+ self._image_label.grid(row=3, column=2, sticky="n")
+ if self._text_label is not None:
+ self._text_label.grid(row=1, column=2, sticky="s")
+
+ 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 "border_spacing" in kwargs:
+ self._border_spacing = kwargs.pop("border_spacing")
+ 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 "hover_color" in 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
+
+ if "text_color_disabled" in kwargs:
+ self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
+ require_redraw = True
+
+ if "background_corner_colors" in kwargs:
+ self._background_corner_colors = kwargs.pop("background_corner_colors")
+ require_redraw = True
+
+ if "text" in kwargs:
+ self._text = kwargs.pop("text")
+ if self._text_label is None:
+ require_redraw = True # text_label will be created in .draw()
+ else:
+ self._text_label.configure(text=self._text)
+
+ 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")
+ if self._text_label is not None:
+ self._text_label.configure(textvariable=self._textvariable)
+
+ if "image" in kwargs:
+ if isinstance(self._image, CTkImage):
+ self._image.remove_configure_callback(self._update_image)
+ self._image = self._check_image_type(kwargs.pop("image"))
+ if isinstance(self._image, CTkImage):
+ self._image.add_configure_callback(self._update_image)
+ require_redraw = 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 "compound" in kwargs:
+ self._compound = kwargs.pop("compound")
+ require_redraw = True
+
+ if "anchor" in kwargs:
+ self._anchor = kwargs.pop("anchor")
+ require_redraw = True
+
+ super().configure(require_redraw=require_redraw, **kwargs)
+
+ def cget(self, attribute_name: str) -> any:
+ if attribute_name == "corner_radius":
+ return self._corner_radius
+ elif attribute_name == "border_width":
+ return self._border_width
+ elif attribute_name == "border_spacing":
+ return self._border_spacing
+
+ elif attribute_name == "fg_color":
+ return self._fg_color
+ elif attribute_name == "hover_color":
+ return self._hover_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":
+ return self._text_color_disabled
+ elif attribute_name == "background_corner_colors":
+ return self._background_corner_colors
+
+ elif attribute_name == "text":
+ return self._text
+ elif attribute_name == "font":
+ return self._font
+ elif attribute_name == "textvariable":
+ return self._textvariable
+ elif attribute_name == "image":
+ return self._image
+ elif attribute_name == "state":
+ return self._state
+ elif attribute_name == "hover":
+ return self._hover
+ elif attribute_name == "command":
+ return self._command
+ elif attribute_name == "compound":
+ return self._compound
+ elif attribute_name == "anchor":
+ return self._anchor
+ else:
+ return super().cget(attribute_name)
+
+ def _set_cursor(self):
+ if self._cursor_manipulation_enabled:
+ if self._state == tkinter.DISABLED:
+ if sys.platform == "darwin" and self._command is not None:
+ self.configure(cursor="arrow")
+ elif sys.platform.startswith("win") and self._command is not None:
+ self.configure(cursor="arrow")
+
+ elif self._state == tkinter.NORMAL:
+ if sys.platform == "darwin" and self._command is not None:
+ self.configure(cursor="pointinghand")
+ elif sys.platform.startswith("win") and self._command is not None:
+ self.configure(cursor="hand2")
+
+ def _on_enter(self, event=None):
+ if self._hover is True and self._state == "normal":
+ if self._hover_color is None:
+ inner_parts_color = self._fg_color
+ else:
+ inner_parts_color = self._hover_color
+
+ # set color of inner button parts to hover color
+ self._canvas.itemconfig("inner_parts",
+ outline=self._apply_appearance_mode(inner_parts_color),
+ fill=self._apply_appearance_mode(inner_parts_color))
+
+ # set text_label bg color to button hover color
+ if self._text_label is not None:
+ self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
+
+ # set image_label bg color to button hover color
+ if self._image_label is not None:
+ self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
+
+ def _on_leave(self, event=None):
+ self._click_animation_running = False
+
+ if self._fg_color == "transparent":
+ inner_parts_color = self._bg_color
+ else:
+ inner_parts_color = self._fg_color
+
+ # set color of inner button parts
+ self._canvas.itemconfig("inner_parts",
+ outline=self._apply_appearance_mode(inner_parts_color),
+ fill=self._apply_appearance_mode(inner_parts_color))
+
+ # set text_label bg color (label color)
+ if self._text_label is not None:
+ self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
+
+ # set image_label bg color (image bg color)
+ if self._image_label is not None:
+ self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
+
+ def _click_animation(self):
+ if self._click_animation_running:
+ self._on_enter()
+
+ def _clicked(self, event=None):
+ if self._state != tkinter.DISABLED:
+
+ # click animation: change color with .on_leave() and back to normal after 100ms with click_animation()
+ self._on_leave()
+ self._click_animation_running = True
+ self.after(100, self._click_animation)
+
+ if self._command is not None:
+ self._command()
+
+ def invoke(self):
+ """ calls command function if button is not disabled """
+ if self._state != tkinter.DISABLED:
+ if self._command is not None:
+ return self._command()
+
+ def bind(self, sequence: str = None, command: Callable = None, add: str = None) -> str:
+ """ called on the tkinter.Label and tkinter.Canvas """
+ canvas_bind_return = self._canvas.bind(sequence, command, add)
+ label_bind_return = self._text_label.bind(sequence, command, add)
+ return canvas_bind_return + " + " + label_bind_return
+
+ def unbind(self, sequence: str, funcid: str = None):
+ """ called on the tkinter.Label and tkinter.Canvas """
+ canvas_bind_return, label_bind_return = funcid.split(" + ")
+ self._canvas.unbind(sequence, canvas_bind_return)
+ self._text_label.unbind(sequence, label_bind_return)
+
+ def focus(self):
+ return self._text_label.focus()
+
+ def focus_set(self):
+ return self._text_label.focus_set()
+
+ def focus_force(self):
+ return self._text_label.focus_force()
diff --git a/customtkinter/windows/widgets/ctk_checkbox.py b/customtkinter/windows/widgets/ctk_checkbox.py
new file mode 100644
index 0000000..fb8a7e2
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_checkbox.py
@@ -0,0 +1,448 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+from .font import CTkFont
+
+
+class CTkCheckBox(CTkBaseClass):
+ """
+ Checkbox with rounded corners, border, variable support and hover effect.
+ For detailed information check out the documentation.
+ """
+
+ def __init__(self,
+ master: any,
+ width: int = 100,
+ height: int = 24,
+ checkbox_width: int = 24,
+ checkbox_height: int = 24,
+ 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,
+ hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+ border_color: Optional[Union[str, Tuple[str, str]]] = None,
+ checkmark_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,
+
+ text: str = "CTkCheckBox",
+ font: Optional[Union[tuple, CTkFont]] = None,
+ textvariable: Union[tkinter.Variable, None] = None,
+ state: str = tkinter.NORMAL,
+ hover: bool = True,
+ command: Union[Callable[[], None], None] = None,
+ onvalue: Union[int, str] = 1,
+ offvalue: Union[int, str] = 0,
+ variable: Union[tkinter.Variable, None] = None,
+ **kwargs):
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # dimensions
+ self._checkbox_width = checkbox_width
+ self._checkbox_height = checkbox_height
+
+ # color
+ self._fg_color = ThemeManager.theme["CTkCheckbox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+ self._hover_color = ThemeManager.theme["CTkCheckbox"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
+ self._border_color = ThemeManager.theme["CTkCheckbox"]["border_color"] if border_color is None else self._check_color_type(border_color)
+ self._checkmark_color = ThemeManager.theme["CTkCheckbox"]["checkmark_color"] if checkmark_color is None else self._check_color_type(checkmark_color)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkCheckbox"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_width = ThemeManager.theme["CTkCheckbox"]["border_width"] if border_width is None else border_width
+
+ # text
+ self._text = text
+ self._text_label: Union[tkinter.Label, None] = None
+ self._text_color = ThemeManager.theme["CTkCheckbox"]["text_color"] if text_color is None else self._check_color_type(text_color)
+ self._text_color_disabled = ThemeManager.theme["CTkCheckbox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
+
+ # font
+ self._font = CTkFont() if font is None else self._check_font_type(font)
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+
+ # callback and hover functionality
+ self._command = command
+ self._state = state
+ self._hover = hover
+ self._check_state = False
+
+ self._onvalue = onvalue
+ self._offvalue = offvalue
+ self._variable: tkinter.Variable = variable
+ self._variable_callback_blocked = False
+ self._textvariable: tkinter.Variable = textvariable
+ self._variable_callback_name = None
+
+ # configure grid system (1x3)
+ self.grid_columnconfigure(0, weight=0)
+ self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
+ self.grid_columnconfigure(2, weight=1)
+ self.grid_rowconfigure(0, weight=1)
+
+ self._bg_canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._checkbox_width),
+ height=self._apply_widget_scaling(self._checkbox_height))
+ self._canvas.grid(row=0, column=0, sticky="e")
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._canvas.bind("", self._on_enter)
+ self._canvas.bind("", self._on_leave)
+ self._canvas.bind("", self.toggle)
+
+ self._text_label = tkinter.Label(master=self,
+ bd=0,
+ padx=0,
+ pady=0,
+ text=self._text,
+ justify=tkinter.LEFT,
+ font=self._apply_font_scaling(self._font),
+ textvariable=self._textvariable)
+ self._text_label.grid(row=0, column=2, sticky="w")
+ self._text_label["anchor"] = "w"
+
+ self._text_label.bind("", self._on_enter)
+ self._text_label.bind("", self._on_leave)
+ self._text_label.bind("", self.toggle)
+
+ # register variable callback and set state according to variable
+ if self._variable is not None and self._variable != "":
+ self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
+ self._check_state = True if self._variable.get() == self._onvalue else False
+
+ self._draw() # initial draw
+ self._set_cursor()
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
+ self._text_label.configure(font=self._apply_font_scaling(self._font))
+
+ self._canvas.delete("checkmark")
+ self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width),
+ height=self._apply_widget_scaling(self._checkbox_height))
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width: int = None, height: int = None):
+ super()._set_dimensions(width, height)
+
+ self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+
+ def _update_font(self):
+ """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
+ if self._text_label is not None:
+ self._text_label.configure(font=self._apply_font_scaling(self._font))
+
+ # Workaround to force grid to be resized when text changes size.
+ # Otherwise grid will lag and only resizes if other mouse action occurs.
+ self._bg_canvas.grid_forget()
+ self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
+
+ def destroy(self):
+ if self._variable is not None:
+ self._variable.trace_remove("write", self._variable_callback_name)
+
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+
+ super().destroy()
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ requires_recoloring_1 = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._checkbox_width),
+ self._apply_widget_scaling(self._checkbox_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width))
+
+ if self._check_state is True:
+ requires_recoloring_2 = self._draw_engine.draw_checkmark(self._apply_widget_scaling(self._checkbox_width),
+ self._apply_widget_scaling(self._checkbox_height),
+ self._apply_widget_scaling(self._checkbox_height * 0.58))
+ else:
+ requires_recoloring_2 = False
+ self._canvas.delete("checkmark")
+
+ if no_color_updates is False or requires_recoloring_1 or requires_recoloring_2:
+ self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ if self._check_state is True:
+ self._canvas.itemconfig("inner_parts",
+ outline=self._apply_appearance_mode(self._fg_color),
+ fill=self._apply_appearance_mode(self._fg_color))
+ self._canvas.itemconfig("border_parts",
+ outline=self._apply_appearance_mode(self._fg_color),
+ fill=self._apply_appearance_mode(self._fg_color))
+
+ if "create_line" in self._canvas.gettags("checkmark"):
+ self._canvas.itemconfig("checkmark", fill=self._apply_appearance_mode(self._checkmark_color))
+ else:
+ self._canvas.itemconfig("checkmark", fill=self._apply_appearance_mode(self._checkmark_color))
+ else:
+ self._canvas.itemconfig("inner_parts",
+ outline=self._apply_appearance_mode(self._bg_color),
+ fill=self._apply_appearance_mode(self._bg_color))
+ self._canvas.itemconfig("border_parts",
+ outline=self._apply_appearance_mode(self._border_color),
+ fill=self._apply_appearance_mode(self._border_color))
+
+ if self._state == tkinter.DISABLED:
+ self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
+ else:
+ self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
+
+ 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 "checkbox_width" in kwargs:
+ self._checkbox_width = kwargs.pop("checkbox_width")
+ self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width))
+ require_redraw = True
+
+ if "checkbox_height" in kwargs:
+ self._checkbox_height = kwargs.pop("checkbox_height")
+ 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 "state" in kwargs:
+ self._state = kwargs.pop("state")
+ self._set_cursor()
+ require_redraw = True
+
+ if "fg_color" in kwargs:
+ self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
+ require_redraw = True
+
+ if "hover_color" in kwargs:
+ self._hover_color = self._check_color_type(kwargs.pop("hover_color"))
+ require_redraw = True
+
+ if "text_color" in kwargs:
+ self._text_color = self._check_color_type(kwargs.pop("text_color"))
+ 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 "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 "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
+
+ 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":
+ return self._checkbox_width
+ elif attribute_name == "checkbox_height":
+ return self._checkbox_height
+
+ elif attribute_name == "fg_color":
+ return self._fg_color
+ elif attribute_name == "hover_color":
+ return self._hover_color
+ elif attribute_name == "border_color":
+ return self._border_color
+ elif attribute_name == "checkmark_color":
+ return self._checkmark_color
+ elif attribute_name == "text_color":
+ return self._text_color
+ elif attribute_name == "text_color_disabled":
+ return self._text_color_disabled
+
+ elif attribute_name == "text":
+ return self._text
+ elif attribute_name == "font":
+ return self._font
+ elif attribute_name == "textvariable":
+ return self._textvariable
+ elif attribute_name == "state":
+ return self._state
+ elif attribute_name == "hover":
+ return self._hover
+ 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)
+
+ def _set_cursor(self):
+ if self._cursor_manipulation_enabled:
+ if self._state == tkinter.DISABLED:
+ if sys.platform == "darwin":
+ self._canvas.configure(cursor="arrow")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="arrow")
+ elif sys.platform.startswith("win"):
+ self._canvas.configure(cursor="arrow")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="arrow")
+
+ elif self._state == tkinter.NORMAL:
+ if sys.platform == "darwin":
+ self._canvas.configure(cursor="pointinghand")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="pointinghand")
+ elif sys.platform.startswith("win"):
+ self._canvas.configure(cursor="hand2")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="hand2")
+
+ def _on_enter(self, event=0):
+ if self._hover is True and self._state == tkinter.NORMAL:
+ if self._check_state is True:
+ self._canvas.itemconfig("inner_parts",
+ fill=self._apply_appearance_mode(self._hover_color),
+ outline=self._apply_appearance_mode(self._hover_color))
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._hover_color),
+ outline=self._apply_appearance_mode(self._hover_color))
+ else:
+ self._canvas.itemconfig("inner_parts",
+ fill=self._apply_appearance_mode(self._hover_color),
+ outline=self._apply_appearance_mode(self._hover_color))
+
+ def _on_leave(self, event=0):
+ if self._check_state is True:
+ self._canvas.itemconfig("inner_parts",
+ fill=self._apply_appearance_mode(self._fg_color),
+ outline=self._apply_appearance_mode(self._fg_color))
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._fg_color),
+ outline=self._apply_appearance_mode(self._fg_color))
+ else:
+ self._canvas.itemconfig("inner_parts",
+ fill=self._apply_appearance_mode(self._bg_color),
+ outline=self._apply_appearance_mode(self._bg_color))
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._border_color),
+ outline=self._apply_appearance_mode(self._border_color))
+
+ def _variable_callback(self, var_name, index, mode):
+ if not self._variable_callback_blocked:
+ if self._variable.get() == self._onvalue:
+ self.select(from_variable_callback=True)
+ elif self._variable.get() == self._offvalue:
+ self.deselect(from_variable_callback=True)
+
+ def toggle(self, event=0):
+ if self._state == tkinter.NORMAL:
+ if self._check_state is True:
+ self._check_state = False
+ self._draw()
+ 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
+
+ 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
+
+ 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
+
+ def get(self) -> Union[int, str]:
+ return self._onvalue if self._check_state is True else self._offvalue
+
+ def bind(self, sequence=None, command=None, add=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.bind(sequence, command, add)
+
+ def unbind(self, sequence, funcid=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.unbind(sequence, funcid)
+
+ def focus(self):
+ return self._text_label.focus()
+
+ def focus_set(self):
+ return self._text_label.focus_set()
+
+ def focus_force(self):
+ return self._text_label.focus_force()
diff --git a/customtkinter/windows/widgets/ctk_combobox.py b/customtkinter/windows/widgets/ctk_combobox.py
new file mode 100644
index 0000000..070919a
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_combobox.py
@@ -0,0 +1,413 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, List, Optional
+
+from .core_widget_classes import DropdownMenu
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+from .font import CTkFont
+
+
+class CTkComboBox(CTkBaseClass):
+ """
+ Combobox with dropdown menu, rounded corners, border, variable support.
+ For detailed information check out the documentation.
+ """
+
+ def __init__(self,
+ master: any,
+ width: int = 140,
+ 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,
+ button_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+ dropdown_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ dropdown_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+ dropdown_text_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,
+
+ font: Optional[Union[tuple, CTkFont]] = None,
+ dropdown_font: Optional[Union[tuple, CTkFont]] = None,
+ values: Optional[List[str]] = None,
+ state: str = tkinter.NORMAL,
+ hover: bool = True,
+ variable: Union[tkinter.Variable, None] = None,
+ command: Union[Callable[[str], None], None] = None,
+ justify: str = "left",
+ **kwargs):
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkComboBox"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_width = ThemeManager.theme["CTkComboBox"]["border_width"] if border_width is None else border_width
+
+ # color
+ self._fg_color = ThemeManager.theme["CTkComboBox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+ self._border_color = ThemeManager.theme["CTkComboBox"]["border_color"] if border_color is None else self._check_color_type(border_color)
+ self._button_color = ThemeManager.theme["CTkComboBox"]["button_color"] if button_color is None else self._check_color_type(button_color)
+ self._button_hover_color = ThemeManager.theme["CTkComboBox"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
+ self._text_color = ThemeManager.theme["CTkComboBox"]["text_color"] if text_color is None else self._check_color_type(text_color)
+ self._text_color_disabled = ThemeManager.theme["CTkComboBox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
+
+ # font
+ self._font = CTkFont() if font is None else self._check_font_type(font)
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+
+ # callback and hover functionality
+ self._command = command
+ self._variable = variable
+ self._state = state
+ self._hover = hover
+
+ if values is None:
+ self._values = ["CTkComboBox"]
+ else:
+ self._values = values
+
+ self._dropdown_menu = DropdownMenu(master=self,
+ values=self._values,
+ command=self._dropdown_callback,
+ fg_color=dropdown_fg_color,
+ hover_color=dropdown_hover_color,
+ text_color=dropdown_text_color,
+ font=dropdown_font)
+
+ # configure grid system (1x1)
+ self.grid_rowconfigure(0, weight=1)
+ self.grid_columnconfigure(0, weight=1)
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self.draw_engine = DrawEngine(self._canvas)
+
+ self._entry = tkinter.Entry(master=self,
+ state=self._state,
+ width=1,
+ bd=0,
+ justify=justify,
+ highlightthickness=0,
+ font=self._apply_font_scaling(self._font))
+
+ self._create_grid()
+
+ # insert default value
+ if len(self._values) > 0:
+ self._entry.insert(0, self._values[0])
+ else:
+ self._entry.insert(0, "CTkComboBox")
+
+ self._draw() # initial draw
+
+ # event bindings
+ self._canvas.tag_bind("right_parts", "", self._on_enter)
+ self._canvas.tag_bind("dropdown_arrow", "", self._on_enter)
+ self._canvas.tag_bind("right_parts", "", self._on_leave)
+ self._canvas.tag_bind("dropdown_arrow", "", self._on_leave)
+ self._canvas.tag_bind("right_parts", "", self._clicked)
+ self._canvas.tag_bind("dropdown_arrow", "", self._clicked)
+
+ if self._variable is not None:
+ self._entry.configure(textvariable=self._variable)
+
+ def _create_grid(self):
+ self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
+
+ left_section_width = self._current_width - self._current_height
+ self._entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew",
+ padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)),
+ max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))),
+ pady=self._apply_widget_scaling(self._border_width))
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ # change entry font size and grid padding
+ self._entry.configure(font=self._apply_font_scaling(self._font))
+ self._create_grid()
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width: int = None, height: int = None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw()
+
+ def _update_font(self):
+ """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
+ self._entry.configure(font=self._apply_font_scaling(self._font))
+
+ # Workaround to force grid to be resized when text changes size.
+ # Otherwise grid will lag and only resizes if other mouse action occurs.
+ self._canvas.grid_forget()
+ self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
+
+ def destroy(self):
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+
+ super().destroy()
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ left_section_width = self._current_width - self._current_height
+ requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width),
+ self._apply_widget_scaling(left_section_width))
+
+ requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self._apply_widget_scaling(self._current_width - (self._current_height / 2)),
+ self._apply_widget_scaling(self._current_height / 2),
+ self._apply_widget_scaling(self._current_height / 3))
+
+ if no_color_updates is False or requires_recoloring or requires_recoloring_2:
+
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ self._canvas.itemconfig("inner_parts_left",
+ outline=self._apply_appearance_mode(self._fg_color),
+ fill=self._apply_appearance_mode(self._fg_color))
+ self._canvas.itemconfig("border_parts_left",
+ outline=self._apply_appearance_mode(self._border_color),
+ fill=self._apply_appearance_mode(self._border_color))
+ self._canvas.itemconfig("inner_parts_right",
+ outline=self._apply_appearance_mode(self._button_color),
+ fill=self._apply_appearance_mode(self._button_color))
+ self._canvas.itemconfig("border_parts_right",
+ outline=self._apply_appearance_mode(self._button_color),
+ fill=self._apply_appearance_mode(self._button_color))
+
+ self._entry.configure(bg=self._apply_appearance_mode(self._fg_color),
+ fg=self._apply_appearance_mode(self._text_color),
+ disabledbackground=self._apply_appearance_mode(self._fg_color),
+ disabledforeground=self._apply_appearance_mode(self._text_color_disabled),
+ highlightcolor=self._apply_appearance_mode(self._fg_color),
+ insertbackground=self._apply_appearance_mode(self._text_color))
+
+ if self._state == tkinter.DISABLED:
+ self._canvas.itemconfig("dropdown_arrow",
+ fill=self._apply_appearance_mode(self._text_color_disabled))
+ else:
+ self._canvas.itemconfig("dropdown_arrow",
+ fill=self._apply_appearance_mode(self._text_color))
+
+ def _open_dropdown_menu(self):
+ self._dropdown_menu.open(self.winfo_rootx(),
+ self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0))
+
+ def configure(self, require_redraw=False, **kwargs):
+ if "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")
+ self._create_grid()
+ require_redraw = True
+
+ 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 "button_color" in kwargs:
+ self._button_color = self._check_color_type(kwargs.pop("button_color"))
+ require_redraw = True
+
+ if "button_hover_color" in kwargs:
+ self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
+ require_redraw = True
+
+ if "dropdown_fg_color" in kwargs:
+ self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_fg_color"))
+
+ if "dropdown_hover_color" in kwargs:
+ self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
+
+ if "dropdown_text_color" in kwargs:
+ self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
+
+ if "text_color" in kwargs:
+ self._text_color = self._check_color_type(kwargs.pop("text_color"))
+ require_redraw = True
+
+ if "text_color_disabled" in kwargs:
+ self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
+ 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()
+
+ if "dropdown_font" in kwargs:
+ self._dropdown_menu.configure(font=kwargs.pop("dropdown_font"))
+
+ if "values" in kwargs:
+ self._values = kwargs.pop("values")
+ self._dropdown_menu.configure(values=self._values)
+
+ if "state" in kwargs:
+ self._state = kwargs.pop("state")
+ self._entry.configure(state=self._state)
+ require_redraw = True
+
+ if "hover" in kwargs:
+ self._hover = kwargs.pop("hover")
+
+ if "variable" in kwargs:
+ self._variable = kwargs.pop("variable")
+ self._entry.configure(textvariable=self._variable)
+
+ if "command" in kwargs:
+ self._command = kwargs.pop("command")
+
+ if "justify" in kwargs:
+ self._entry.configure(justify=kwargs.pop("justify"))
+
+ 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 == "fg_color":
+ return self._fg_color
+ elif attribute_name == "border_color":
+ return self._border_color
+ elif attribute_name == "button_color":
+ return self._button_color
+ elif attribute_name == "button_hover_color":
+ return self._button_hover_color
+ elif attribute_name == "dropdown_fg_color":
+ return self._dropdown_menu.cget("fg_color")
+ elif attribute_name == "dropdown_hover_color":
+ return self._dropdown_menu.cget("hover_color")
+ elif attribute_name == "dropdown_text_color":
+ return self._dropdown_menu.cget("text_color")
+ elif attribute_name == "text_color":
+ return self._text_color
+ elif attribute_name == "text_color_disabled":
+ return self._text_color_disabled
+
+ elif attribute_name == "font":
+ return self._font
+ elif attribute_name == "dropdown_font":
+ return self._dropdown_menu.cget("font")
+ elif attribute_name == "values":
+ return self._values
+ elif attribute_name == "state":
+ return self._state
+ elif attribute_name == "hover":
+ return self._hover
+ elif attribute_name == "variable":
+ return self._variable
+ elif attribute_name == "command":
+ return self._command
+ elif attribute_name == "justify":
+ return self._entry.cget("justify")
+ else:
+ return super().cget(attribute_name)
+
+ def _on_enter(self, event=0):
+ if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0:
+ if sys.platform == "darwin" and len(self._values) > 0 and self._cursor_manipulation_enabled:
+ self._canvas.configure(cursor="pointinghand")
+ elif sys.platform.startswith("win") and len(self._values) > 0 and self._cursor_manipulation_enabled:
+ self._canvas.configure(cursor="hand2")
+
+ # set color of inner button parts to hover color
+ self._canvas.itemconfig("inner_parts_right",
+ outline=self._apply_appearance_mode(self._button_hover_color),
+ fill=self._apply_appearance_mode(self._button_hover_color))
+ self._canvas.itemconfig("border_parts_right",
+ outline=self._apply_appearance_mode(self._button_hover_color),
+ fill=self._apply_appearance_mode(self._button_hover_color))
+
+ def _on_leave(self, event=0):
+ if sys.platform == "darwin" and len(self._values) > 0 and self._cursor_manipulation_enabled:
+ self._canvas.configure(cursor="arrow")
+ elif sys.platform.startswith("win") and len(self._values) > 0 and self._cursor_manipulation_enabled:
+ self._canvas.configure(cursor="arrow")
+
+ # set color of inner button parts
+ self._canvas.itemconfig("inner_parts_right",
+ outline=self._apply_appearance_mode(self._button_color),
+ fill=self._apply_appearance_mode(self._button_color))
+ self._canvas.itemconfig("border_parts_right",
+ outline=self._apply_appearance_mode(self._button_color),
+ fill=self._apply_appearance_mode(self._button_color))
+
+ def _dropdown_callback(self, value: str):
+ if self._state == "readonly":
+ self._entry.configure(state="normal")
+ self._entry.delete(0, tkinter.END)
+ self._entry.insert(0, value)
+ self._entry.configure(state="readonly")
+ else:
+ self._entry.delete(0, tkinter.END)
+ self._entry.insert(0, value)
+
+ if self._command is not None:
+ self._command(value)
+
+ def set(self, value: str):
+ if self._state == "readonly":
+ self._entry.configure(state="normal")
+ self._entry.delete(0, tkinter.END)
+ self._entry.insert(0, value)
+ self._entry.configure(state="readonly")
+ else:
+ self._entry.delete(0, tkinter.END)
+ self._entry.insert(0, value)
+
+ def get(self) -> str:
+ return self._entry.get()
+
+ def _clicked(self, event=0):
+ if self._state is not tkinter.DISABLED and len(self._values) > 0:
+ self._open_dropdown_menu()
+
+ def bind(self, sequence=None, command=None, add=None):
+ """ called on the tkinter.Entry """
+ return self._entry.bind(sequence, command, add)
+
+ def unbind(self, sequence, funcid=None):
+ """ called on the tkinter.Entry """
+ return self._entry.unbind(sequence, funcid)
+
+ def focus(self):
+ return self._entry.focus()
+
+ def focus_set(self):
+ return self._entry.focus_set()
+
+ def focus_force(self):
+ return self._entry.focus_force()
diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py
new file mode 100644
index 0000000..ac1edb6
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_entry.py
@@ -0,0 +1,372 @@
+import tkinter
+from typing import Union, Tuple, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+from .font import CTkFont
+from .utility import pop_from_dict_by_set, check_kwargs_empty
+
+
+class CTkEntry(CTkBaseClass):
+ """
+ Entry with rounded corners, border, textvariable support, focus and placeholder.
+ For detailed information check out the documentation.
+ """
+
+ _minimum_x_padding = 6 # minimum padding between tkinter entry and frame border
+
+ # attributes that are passed to and managed by the tkinter entry only:
+ _valid_tk_entry_attributes = {"exportselection", "insertborderwidth", "insertofftime",
+ "insertontime", "insertwidth", "justify", "selectborderwidth",
+ "show", "takefocus", "validate", "validatecommand", "xscrollcommand"}
+
+ def __init__(self,
+ master: any,
+ width: int = 140,
+ 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,
+ placeholder_text_color: Optional[Union[str, Tuple[str, str]]] = None,
+
+ textvariable: Union[tkinter.Variable, None] = None,
+ placeholder_text: Union[str, None] = None,
+ font: Optional[Union[tuple, CTkFont]] = None,
+ state: str = tkinter.NORMAL,
+ **kwargs):
+
+ # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height)
+
+ # configure grid system (1x1)
+ self.grid_rowconfigure(0, weight=1)
+ self.grid_columnconfigure(0, weight=1)
+
+ # color
+ self._fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
+ self._text_color = ThemeManager.theme["CTkEntry"]["text_color"] if text_color is None else self._check_color_type(text_color)
+ self._placeholder_text_color = ThemeManager.theme["CTkEntry"]["placeholder_text_color"] if placeholder_text_color is None else self._check_color_type(placeholder_text_color)
+ self._border_color = ThemeManager.theme["CTkEntry"]["border_color"] if border_color is None else self._check_color_type(border_color)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkEntry"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_width = ThemeManager.theme["CTkEntry"]["border_width"] if border_width is None else border_width
+
+ # text and state
+ self._is_focused: bool = True
+ self._placeholder_text = placeholder_text
+ self._placeholder_text_active = False
+ self._pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back
+ self._textvariable = textvariable
+ self._state = state
+ self._textvariable_callback_name: str = ""
+
+ # font
+ self._font = CTkFont() if font is None else self._check_font_type(font)
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+
+ if not (self._textvariable is None or self._textvariable == ""):
+ self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback)
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._current_width),
+ height=self._apply_widget_scaling(self._current_height))
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._entry = tkinter.Entry(master=self,
+ bd=0,
+ width=1,
+ highlightthickness=0,
+ font=self._apply_font_scaling(self._font),
+ state=self._state,
+ textvariable=self._textvariable,
+ **pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes))
+
+ self._create_grid()
+
+ check_kwargs_empty(kwargs, raise_error=True)
+
+ self._entry.bind('', self._entry_focus_out)
+ self._entry.bind('', self._entry_focus_in)
+
+ self._activate_placeholder()
+ self._draw()
+
+ def _create_grid(self):
+ self._canvas.grid(column=0, row=0, sticky="nswe")
+
+ if self._corner_radius >= self._minimum_x_padding:
+ self._entry.grid(column=0, row=0, sticky="nswe",
+ padx=min(self._apply_widget_scaling(self._corner_radius), round(self._apply_widget_scaling(self._current_height/2))),
+ pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1)))
+ else:
+ self._entry.grid(column=0, row=0, sticky="nswe",
+ padx=self._apply_widget_scaling(self._minimum_x_padding),
+ pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1)))
+
+ def _textvariable_callback(self, var_name, index, mode):
+ if self._textvariable.get() == "":
+ self._activate_placeholder()
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self._entry.configure(font=self._apply_font_scaling(self._font))
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height))
+ self._create_grid()
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width=None, height=None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw()
+
+ def _update_font(self):
+ """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
+ self._entry.configure(font=self._apply_font_scaling(self._font))
+
+ # Workaround to force grid to be resized when text changes size.
+ # Otherwise grid will lag and only resizes if other mouse action occurs.
+ self._canvas.grid_forget()
+ self._canvas.grid(column=0, row=0, sticky="nswe")
+
+ def destroy(self):
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+
+ super().destroy()
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width))
+
+ if requires_recoloring or no_color_updates is False:
+ if self._apply_appearance_mode(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))
+ self._entry.configure(bg=self._apply_appearance_mode(self._bg_color),
+ disabledbackground=self._apply_appearance_mode(self._bg_color),
+ highlightcolor=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))
+ self._entry.configure(bg=self._apply_appearance_mode(self._fg_color),
+ disabledbackground=self._apply_appearance_mode(self._fg_color),
+ highlightcolor=self._apply_appearance_mode(self._fg_color))
+
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._border_color),
+ outline=self._apply_appearance_mode(self._border_color))
+
+ if self._placeholder_text_active:
+ self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color),
+ disabledforeground=self._apply_appearance_mode(self._placeholder_text_color),
+ insertbackground=self._apply_appearance_mode(self._placeholder_text_color))
+ else:
+ self._entry.config(fg=self._apply_appearance_mode(self._text_color),
+ disabledforeground=self._apply_appearance_mode(self._text_color),
+ 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"))
+ require_redraw = True
+
+ if "text_color" in kwargs:
+ self._text_color = self._check_color_type(kwargs.pop("text_color"))
+ require_redraw = True
+
+ if "placeholder_text_color" in kwargs:
+ self._placeholder_text_color = self._check_color_type(kwargs.pop("placeholder_text_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()
+ require_redraw = True
+
+ if "corner_radius" in kwargs:
+ self._corner_radius = kwargs.pop("corner_radius")
+ self._create_grid()
+ require_redraw = True
+
+ if "placeholder_text" in kwargs:
+ self._placeholder_text = kwargs.pop("placeholder_text")
+ if self._placeholder_text_active:
+ self._entry.delete(0, tkinter.END)
+ self._entry.insert(0, self._placeholder_text)
+ 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 "show" in kwargs:
+ if self._placeholder_text_active:
+ self._pre_placeholder_arguments["show"] = kwargs.pop("show") # remember show argument for when placeholder gets deactivated
+ else:
+ self._entry.configure(show=kwargs.pop("show"))
+
+ self._entry.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes)) # configure Tkinter.Entry
+ super().configure(require_redraw=require_redraw, **kwargs) # configure CTkBaseClass
+
+ 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 == "placeholder_text_color":
+ return self._placeholder_text_color
+
+ elif attribute_name == "textvariable":
+ return self._textvariable
+ elif attribute_name == "placeholder_text":
+ return self._placeholder_text
+ elif attribute_name == "font":
+ return self._font
+ elif attribute_name == "state":
+ return self._state
+
+ elif attribute_name in self._valid_tk_entry_attributes:
+ return self._entry.cget(attribute_name) # cget of tkinter.Entry
+ else:
+ return super().cget(attribute_name) # cget of CTkBaseClass
+
+ def bind(self, sequence=None, command=None, add=None):
+ """ called on the tkinter.Entry """
+ return self._entry.bind(sequence, command, add)
+
+ def unbind(self, sequence, funcid=None):
+ """ called on the tkinter.Entry """
+ return self._entry.unbind(sequence, funcid)
+
+ def _activate_placeholder(self):
+ if self._entry.get() == "" and self._placeholder_text is not None and (self._textvariable is None or self._textvariable == ""):
+ self._placeholder_text_active = True
+
+ self._pre_placeholder_arguments = {"show": self._entry.cget("show")}
+ self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color),
+ disabledforeground=self._apply_appearance_mode(self._placeholder_text_color),
+ show="")
+ self._entry.delete(0, tkinter.END)
+ self._entry.insert(0, self._placeholder_text)
+
+ def _deactivate_placeholder(self):
+ if self._placeholder_text_active:
+ self._placeholder_text_active = False
+
+ self._entry.config(fg=self._apply_appearance_mode(self._text_color),
+ disabledforeground=self._apply_appearance_mode(self._text_color),)
+ self._entry.delete(0, tkinter.END)
+ for argument, value in self._pre_placeholder_arguments.items():
+ self._entry[argument] = value
+
+ def _entry_focus_out(self, event=None):
+ self._activate_placeholder()
+ self._is_focused = False
+
+ def _entry_focus_in(self, event=None):
+ self._deactivate_placeholder()
+ self._is_focused = True
+
+ def delete(self, first_index, last_index=None):
+ self._entry.delete(first_index, last_index)
+
+ if not self._is_focused and self._entry.get() == "":
+ self._activate_placeholder()
+
+ def insert(self, index, string):
+ self._deactivate_placeholder()
+
+ return self._entry.insert(index, string)
+
+ def get(self):
+ if self._placeholder_text_active:
+ return ""
+ else:
+ return self._entry.get()
+
+ def focus(self):
+ return self._entry.focus()
+
+ def focus_set(self):
+ return self._entry.focus_set()
+
+ def focus_force(self):
+ return self._entry.focus_force()
+
+ def index(self, index):
+ return self._entry.index(index)
+
+ def icursor(self, index):
+ return self._entry.icursor(index)
+
+ def select_adjust(self, index):
+ return self._entry.select_adjust(index)
+
+ def select_from(self, index):
+ return self._entry.icursor(index)
+
+ def select_clear(self):
+ return self._entry.select_clear()
+
+ def select_present(self):
+ return self._entry.select_present()
+
+ def select_range(self, start_index, end_index):
+ return self._entry.select_range(start_index, end_index)
+
+ def select_to(self, index):
+ return self._entry.select_to(index)
+
+ def xview(self, index):
+ return self._entry.xview(index)
+
+ def xview_moveto(self, f):
+ return self._entry.xview_moveto(f)
+
+ def xview_scroll(self, number, what):
+ return self._entry.xview_scroll(number, what)
diff --git a/customtkinter/windows/widgets/ctk_frame.py b/customtkinter/windows/widgets/ctk_frame.py
new file mode 100644
index 0000000..baa9fc3
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_frame.py
@@ -0,0 +1,191 @@
+from typing import Union, Tuple, List, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+
+
+class CTkFrame(CTkBaseClass):
+ """
+ Frame with rounded corners and border.
+ Default foreground colors are set according to theme.
+ To make the frame transparent set fg_color=None.
+ For detailed information check out the documentation.
+ """
+
+ def __init__(self,
+ master: any,
+ width: int = 200,
+ height: int = 200,
+ corner_radius: Optional[Union[int, str]] = None,
+ border_width: Optional[Union[int, str]] = 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,
+ background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None,
+
+ overwrite_preferred_drawing_method: Union[str, None] = None,
+ **kwargs):
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # color
+ self._border_color = ThemeManager.theme["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color)
+
+ # determine fg_color of frame
+ if fg_color is None:
+ if isinstance(self.master, CTkFrame):
+ if self.master._fg_color == ThemeManager.theme["CTkFrame"]["fg_color"]:
+ self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"]
+ else:
+ self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
+ else:
+ self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
+ else:
+ self._fg_color = self._check_color_type(fg_color, transparency=True)
+
+ self._background_corner_colors = background_corner_colors # rendering options for DrawEngine
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkFrame"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_width = ThemeManager.theme["CTkFrame"]["border_width"] if border_width is None else border_width
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._current_width),
+ height=self._apply_widget_scaling(self._current_height))
+ self._canvas.place(x=0, y=0, relwidth=1, relheight=1)
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+ self._draw_engine = DrawEngine(self._canvas)
+ self._overwrite_preferred_drawing_method = overwrite_preferred_drawing_method
+
+ self._draw(no_color_updates=True)
+
+ def winfo_children(self) -> List[any]:
+ """
+ winfo_children of CTkFrame without self.canvas widget,
+ because it's not a child but part of the CTkFrame itself
+ """
+
+ child_widgets = super().winfo_children()
+ try:
+ child_widgets.remove(self._canvas)
+ return child_widgets
+ except ValueError:
+ return child_widgets
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw()
+
+ def _set_dimensions(self, width=None, height=None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw()
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ if not self._canvas.winfo_exists():
+ return
+
+ if self._background_corner_colors is not None:
+ self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height))
+ self._canvas.itemconfig("background_corner_top_left", fill=self._apply_appearance_mode(self._background_corner_colors[0]))
+ self._canvas.itemconfig("background_corner_top_right", fill=self._apply_appearance_mode(self._background_corner_colors[1]))
+ self._canvas.itemconfig("background_corner_bottom_right", fill=self._apply_appearance_mode(self._background_corner_colors[2]))
+ self._canvas.itemconfig("background_corner_bottom_left", fill=self._apply_appearance_mode(self._background_corner_colors[3]))
+ else:
+ self._canvas.delete("background_parts")
+
+ requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width),
+ overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method)
+
+ if no_color_updates is False or requires_recoloring:
+ if self._fg_color == "transparent":
+ self._canvas.itemconfig("inner_parts",
+ fill=self._apply_appearance_mode(self._bg_color),
+ outline=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))
+
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._border_color),
+ outline=self._apply_appearance_mode(self._border_color))
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ # self._canvas.tag_lower("inner_parts") # maybe unnecessary, I don't know ???
+ # self._canvas.tag_lower("border_parts")
+
+ def configure(self, require_redraw=False, **kwargs):
+ if "fg_color" in kwargs:
+ self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
+ require_redraw = True
+
+ # check if CTk widgets are children of the frame and change their bg_color to new frame fg_color
+ for child in self.winfo_children():
+ if isinstance(child, CTkBaseClass):
+ child.configure(bg_color=self._fg_color)
+
+ if "bg_color" in kwargs:
+ # pass bg_color change to children if fg_color is "transparent"
+ if self._fg_color == "transparent":
+ for child in self.winfo_children():
+ if isinstance(child, CTkBaseClass):
+ child.configure(bg_color=self._fg_color)
+
+ if "border_color" in kwargs:
+ self._border_color = self._check_color_type(kwargs.pop("border_color"))
+ require_redraw = True
+
+ if "background_corner_colors" in 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:
+ 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 == "background_corner_colors":
+ return self._background_corner_colors
+
+ else:
+ return super().cget(attribute_name)
+
+ def bind(self, sequence=None, command=None, add=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.bind(sequence, command, add)
+
+ def unbind(self, sequence, funcid=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.unbind(sequence, funcid)
diff --git a/customtkinter/windows/widgets/ctk_label.py b/customtkinter/windows/widgets/ctk_label.py
new file mode 100644
index 0000000..293f923
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_label.py
@@ -0,0 +1,269 @@
+import tkinter
+from typing import Union, Tuple, Callable, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+from .font import CTkFont
+from .image import CTkImage
+from .utility import pop_from_dict_by_set, check_kwargs_empty
+
+
+class CTkLabel(CTkBaseClass):
+ """
+ Label with rounded corners. Default is fg_color=None (transparent fg_color).
+ For detailed information check out the documentation.
+ """
+
+ # attributes that are passed to and managed by the tkinter entry only:
+ _valid_tk_label_attributes = {"cursor", "justify", "padx", "pady",
+ "textvariable", "state", "takefocus", "underline"}
+
+ def __init__(self,
+ master: any,
+ width: int = 0,
+ height: int = 28,
+ corner_radius: Optional[int] = None,
+
+ bg_color: Union[str, Tuple[str, str]] = "transparent",
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ text_color: Optional[Union[str, Tuple[str, str]]] = None,
+
+ text: str = "CTkLabel",
+ font: Optional[Union[tuple, CTkFont]] = None,
+ image: Union[tkinter.PhotoImage, CTkImage, None] = None,
+ compound: str = "center",
+ anchor: str = "center", # label anchor: center, n, e, s, w
+ wraplength: int = 0,
+ **kwargs):
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height)
+
+ # color
+ self._fg_color = ThemeManager.theme["CTkLabel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
+ self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(text_color)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkLabel"]["corner_radius"] if corner_radius is None else corner_radius
+
+ # text
+ self._anchor = anchor
+ self._text = text
+ self._wraplength = wraplength
+
+ # image
+ self._image = self._check_image_type(image)
+ self._compound = compound
+ if isinstance(self._image, CTkImage):
+ self._image.add_configure_callback(self._update_image)
+
+ # font
+ self._font = CTkFont() if font is None else self._check_font_type(font)
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+
+ # configure grid system (1x1)
+ self.grid_rowconfigure(0, weight=1)
+ self.grid_columnconfigure(0, weight=1)
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._canvas.grid(row=0, column=0, sticky="nswe")
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._label = tkinter.Label(master=self,
+ highlightthickness=0,
+ padx=0,
+ pady=0,
+ borderwidth=0,
+ anchor=self._anchor,
+ compound=self._compound,
+ wraplength=self._apply_widget_scaling(self._wraplength),
+ text=self._text,
+ font=self._apply_font_scaling(self._font))
+ self._label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes))
+
+ check_kwargs_empty(kwargs, raise_error=True)
+
+ self._create_grid()
+ self._update_image()
+ self._draw()
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height))
+ self._label.configure(font=self._apply_font_scaling(self._font))
+ self._label.configure(wraplength=self._apply_widget_scaling(self._wraplength))
+
+ self._create_grid()
+ self._update_image()
+ self._draw(no_color_updates=True)
+
+ def _set_appearance_mode(self, mode_string):
+ super()._set_appearance_mode(mode_string)
+ self._update_image()
+
+ def _set_dimensions(self, width=None, height=None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._create_grid()
+ self._draw()
+
+ def _update_font(self):
+ """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
+ self._label.configure(font=self._apply_font_scaling(self._font))
+
+ # Workaround to force grid to be resized when text changes size.
+ # Otherwise grid will lag and only resizes if other mouse action occurs.
+ self._canvas.grid_forget()
+ self._canvas.grid(row=0, column=0, sticky="nswe")
+
+ def _update_image(self):
+ if isinstance(self._image, CTkImage):
+ self._label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(),
+ self._get_appearance_mode()))
+ elif self._image is not None:
+ self._label.configure(image=self._image)
+
+ def destroy(self):
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+ super().destroy()
+
+ def _create_grid(self):
+ """ configure grid system (1x1) """
+
+ text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
+ self._label.grid(row=0, column=0, sticky=text_label_grid_sticky,
+ padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height / 2))))
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ 0)
+
+ if no_color_updates is False or requires_recoloring:
+ if self._apply_appearance_mode(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))
+
+ self._label.configure(fg=self._apply_appearance_mode(self._text_color),
+ 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))
+
+ self._label.configure(fg=self._apply_appearance_mode(self._text_color),
+ 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 "fg_color" in kwargs:
+ self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
+ require_redraw = True
+
+ if "text_color" in kwargs:
+ self._text_color = self._check_color_type(kwargs.pop("text_color"))
+ require_redraw = True
+
+ if "text" in kwargs:
+ self._text = kwargs.pop("text")
+ self._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 "image" in kwargs:
+ if isinstance(self._image, CTkImage):
+ self._image.remove_configure_callback(self._update_image)
+ 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 "compound" in kwargs:
+ self._compound = kwargs.pop("compound")
+ self._label.configure(compound=self._compound)
+
+ if "anchor" in kwargs:
+ self._anchor = kwargs.pop("anchor")
+ self._label.configure(anchor=self._anchor)
+ self._create_grid()
+
+ if "wraplength" in kwargs:
+ self._wraplength = kwargs.pop("wraplength")
+ self._label.configure(wraplength=self._apply_widget_scaling(self._wraplength))
+
+ self._label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes)) # configure tkinter.Label
+ super().configure(require_redraw=require_redraw, **kwargs) # configure CTkBaseClass
+
+ def cget(self, attribute_name: str) -> any:
+ if attribute_name == "corner_radius":
+ return self._corner_radius
+
+ elif attribute_name == "fg_color":
+ return self._fg_color
+ elif attribute_name == "text_color":
+ return self._text_color
+
+ elif attribute_name == "text":
+ return self._text
+ elif attribute_name == "font":
+ return self._font
+ elif attribute_name == "image":
+ return self._image
+ elif attribute_name == "compound":
+ return self._compound
+ elif attribute_name == "anchor":
+ return self._anchor
+ elif attribute_name == "wraplength":
+ return self._wraplength
+
+ elif attribute_name in self._valid_tk_label_attributes:
+ return self._label.cget(attribute_name) # cget of tkinter.Label
+ else:
+ return super().cget(attribute_name) # cget of CTkBaseClass
+
+ def bind(self, sequence: str = None, command: Callable = None, add: str = None) -> str:
+ """ called on the tkinter.Label and tkinter.Canvas """
+ canvas_bind_return = self._canvas.bind(sequence, command, add)
+ label_bind_return = self._label.bind(sequence, command, add)
+ return canvas_bind_return + " + " + label_bind_return
+
+ def unbind(self, sequence: str, funcid: str = None):
+ """ called on the tkinter.Label and tkinter.Canvas """
+ canvas_bind_return, label_bind_return = funcid.split(" + ")
+ self._canvas.unbind(sequence, canvas_bind_return)
+ self._label.unbind(sequence, label_bind_return)
+
+ def focus(self):
+ return self._label.focus()
+
+ def focus_set(self):
+ return self._label.focus_set()
+
+ def focus_force(self):
+ return self._label.focus_force()
diff --git a/customtkinter/windows/widgets/ctk_optionmenu.py b/customtkinter/windows/widgets/ctk_optionmenu.py
new file mode 100644
index 0000000..e6229fe
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_optionmenu.py
@@ -0,0 +1,415 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+from .core_widget_classes import DropdownMenu
+from .font import CTkFont
+
+
+class CTkOptionMenu(CTkBaseClass):
+ """
+ Optionmenu with rounded corners, dropdown menu, variable support, command.
+ For detailed information check out the documentation.
+ """
+
+ def __init__(self,
+ master: any,
+ width: int = 140,
+ height: int = 28,
+ corner_radius: Optional[Union[int]] = None,
+
+ bg_color: Union[str, Tuple[str, str]] = "transparent",
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_hover_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,
+ dropdown_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ dropdown_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+ dropdown_text_color: Optional[Union[str, Tuple[str, str]]] = None,
+
+ font: Optional[Union[tuple, CTkFont]] = None,
+ dropdown_font: Optional[Union[tuple, CTkFont]] = None,
+ values: Optional[list] = None,
+ variable: Union[tkinter.Variable, None] = None,
+ state: str = tkinter.NORMAL,
+ hover: bool = True,
+ command: Union[Callable[[str], None], None] = None,
+ dynamic_resizing: bool = True,
+ anchor: str = "w",
+ **kwargs):
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # color variables
+ self._fg_color = ThemeManager.theme["CTkOptionMenu"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+ self._button_color = ThemeManager.theme["CTkOptionMenu"]["button_color"] if button_color is None else self._check_color_type(button_color)
+ self._button_hover_color = ThemeManager.theme["CTkOptionMenu"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkOptionMenu"]["corner_radius"] if corner_radius is None else corner_radius
+
+ # text and font
+ self._text_color = ThemeManager.theme["CTkOptionMenu"]["text_color"] if text_color is None else self._check_color_type(text_color)
+ self._text_color_disabled = ThemeManager.theme["CTkOptionMenu"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
+
+ # font
+ self._font = CTkFont() if font is None else self._check_font_type(font)
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+
+ # callback and hover functionality
+ self._command = command
+ self._variable = variable
+ self._variable_callback_blocked: bool = False
+ self._variable_callback_name: Union[str, None] = None
+ self._state = state
+ self._hover = hover
+ self._dynamic_resizing = dynamic_resizing
+
+ if values is None:
+ self._values = ["CTkOptionMenu"]
+ else:
+ self._values = values
+
+ if len(self._values) > 0:
+ self._current_value = self._values[0]
+ else:
+ self._current_value = "CTkOptionMenu"
+
+ self._dropdown_menu = DropdownMenu(master=self,
+ values=self._values,
+ command=self._dropdown_callback,
+ fg_color=dropdown_fg_color,
+ hover_color=dropdown_hover_color,
+ text_color=dropdown_text_color,
+ font=dropdown_font)
+
+ # configure grid system (1x1)
+ self.grid_rowconfigure(0, weight=1)
+ self.grid_columnconfigure(0, weight=1)
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._text_label = tkinter.Label(master=self,
+ font=self._apply_font_scaling(self._font),
+ anchor=anchor,
+ padx=0,
+ pady=0,
+ borderwidth=1,
+ text=self._current_value)
+ self._create_grid()
+
+ if not self._dynamic_resizing:
+ self.grid_propagate(0)
+
+ if self._cursor_manipulation_enabled:
+ if sys.platform == "darwin":
+ self.configure(cursor="pointinghand")
+ elif sys.platform.startswith("win"):
+ self.configure(cursor="hand2")
+
+ # event bindings
+ self._canvas.bind("", self._on_enter)
+ self._canvas.bind("", self._on_leave)
+ self._canvas.bind("", self._clicked)
+ self._canvas.bind("", self._clicked)
+
+ 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._draw() # initial draw
+
+ if self._variable is not None:
+ self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
+ self._current_value = self._variable.get()
+ self._text_label.configure(text=self._current_value)
+
+ def _create_grid(self):
+ self._canvas.grid(row=0, column=0, sticky="nsew")
+
+ left_section_width = self._current_width - self._current_height
+ self._text_label.grid(row=0, column=0, sticky="ew",
+ padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)),
+ max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))))
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ # change label font size and grid padding
+ self._text_label.configure(font=self._apply_font_scaling(self._font))
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._create_grid()
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width: int = None, height: int = None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw()
+
+ def _update_font(self):
+ """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
+ self._text_label.configure(font=self._apply_font_scaling(self._font))
+
+ # Workaround to force grid to be resized when text changes size.
+ # Otherwise grid will lag and only resizes if other mouse action occurs.
+ self._canvas.grid_forget()
+ self._canvas.grid(row=0, column=0, sticky="nsew")
+
+ def destroy(self):
+ if self._variable is not None: # remove old callback
+ self._variable.trace_remove("write", self._variable_callback_name)
+
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+
+ super().destroy()
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ left_section_width = self._current_width - self._current_height
+ requires_recoloring = self._draw_engine.draw_rounded_rect_with_border_vertical_split(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ 0,
+ self._apply_widget_scaling(left_section_width))
+
+ requires_recoloring_2 = self._draw_engine.draw_dropdown_arrow(self._apply_widget_scaling(self._current_width - (self._current_height / 2)),
+ self._apply_widget_scaling(self._current_height / 2),
+ self._apply_widget_scaling(self._current_height / 3))
+
+ if no_color_updates is False or requires_recoloring or requires_recoloring_2:
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ self._canvas.itemconfig("inner_parts_left",
+ outline=self._apply_appearance_mode(self._fg_color),
+ fill=self._apply_appearance_mode(self._fg_color))
+ self._canvas.itemconfig("inner_parts_right",
+ outline=self._apply_appearance_mode(self._button_color),
+ fill=self._apply_appearance_mode(self._button_color))
+
+ self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
+
+ if self._state == tkinter.DISABLED:
+ self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
+ self._canvas.itemconfig("dropdown_arrow",
+ fill=self._apply_appearance_mode(self._text_color_disabled))
+ else:
+ self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
+ self._canvas.itemconfig("dropdown_arrow",
+ fill=self._apply_appearance_mode(self._text_color))
+
+ self._text_label.configure(bg=self._apply_appearance_mode(self._fg_color))
+
+ self._canvas.update_idletasks()
+
+ 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 "fg_color" in kwargs:
+ self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
+ require_redraw = True
+
+ if "button_color" in kwargs:
+ self._button_color = self._check_color_type(kwargs.pop("button_color"))
+ require_redraw = True
+
+ if "button_hover_color" in kwargs:
+ self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
+ require_redraw = True
+
+ if "text_color" in kwargs:
+ self._text_color = self._check_color_type(kwargs.pop("text_color"))
+ require_redraw = True
+
+ if "dropdown_color" in kwargs:
+ self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color"))
+
+ if "dropdown_hover_color" in kwargs:
+ self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
+
+ if "dropdown_text_color" in kwargs:
+ self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
+
+ 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 "command" in kwargs:
+ self._command = kwargs.pop("command")
+
+ if "variable" in kwargs:
+ if self._variable is not None: # remove old callback
+ self._variable.trace_remove("write", self._variable_callback_name)
+
+ 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 "values" in kwargs:
+ self._values = kwargs.pop("values")
+ self._dropdown_menu.configure(values=self._values)
+
+ if "dropdown_font" in kwargs:
+ self._dropdown_menu.configure(font=kwargs.pop("dropdown_font"))
+
+ if "hover" in kwargs:
+ self._hover = kwargs.pop("hover")
+
+ if "state" in kwargs:
+ self._state = kwargs.pop("state")
+ require_redraw = True
+
+ if "dynamic_resizing" in kwargs:
+ self._dynamic_resizing = kwargs.pop("dynamic_resizing")
+ if not self._dynamic_resizing:
+ self.grid_propagate(0)
+ else:
+ self.grid_propagate(1)
+
+ if "anchor" in kwargs:
+ self._text_label.configure(anchor=kwargs.pop("anchor"))
+
+ 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 == "fg_color":
+ return self._fg_color
+ elif attribute_name == "button_color":
+ return self._button_color
+ elif attribute_name == "button_hover_color":
+ return self._button_hover_color
+ elif attribute_name == "text_color":
+ return self._text_color
+ elif attribute_name == "text_color_disabled":
+ return self._text_color_disabled
+ elif attribute_name == "dropdown_fg_color":
+ return self._dropdown_menu.cget("fg_color")
+ elif attribute_name == "dropdown_hover_color":
+ return self._dropdown_menu.cget("hover_color")
+ elif attribute_name == "dropdown_text_color":
+ return self._dropdown_menu.cget("text_color")
+
+ elif attribute_name == "font":
+ return self._font
+ elif attribute_name == "dropdown_font":
+ return self._dropdown_menu.cget("font")
+ elif attribute_name == "values":
+ return self._values
+ elif attribute_name == "variable":
+ return self._variable
+ elif attribute_name == "state":
+ return self._state
+ elif attribute_name == "hover":
+ return self._hover
+ elif attribute_name == "command":
+ return self._command
+ elif attribute_name == "dynamic_resizing":
+ return self._dynamic_resizing
+ elif attribute_name == "anchor":
+ return self._text_label.cget("anchor")
+
+ else:
+ return super().cget(attribute_name)
+
+ def _open_dropdown_menu(self):
+ self._dropdown_menu.open(self.winfo_rootx(),
+ self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0))
+
+ def _on_enter(self, event=0):
+ if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0:
+ # set color of inner button parts to hover color
+ self._canvas.itemconfig("inner_parts_right",
+ outline=self._apply_appearance_mode(self._button_hover_color),
+ fill=self._apply_appearance_mode(self._button_hover_color))
+
+ def _on_leave(self, event=0):
+ # set color of inner button parts
+ self._canvas.itemconfig("inner_parts_right",
+ outline=self._apply_appearance_mode(self._button_color),
+ fill=self._apply_appearance_mode(self._button_color))
+
+ def _variable_callback(self, var_name, index, mode):
+ if not self._variable_callback_blocked:
+ self._current_value = self._variable.get()
+ self._text_label.configure(text=self._current_value)
+
+ def _dropdown_callback(self, value: str):
+ self._current_value = value
+ self._text_label.configure(text=self._current_value)
+
+ if self._variable is not None:
+ self._variable_callback_blocked = True
+ self._variable.set(self._current_value)
+ self._variable_callback_blocked = False
+
+ if self._command is not None:
+ self._command(self._current_value)
+
+ def set(self, value: str):
+ self._current_value = value
+ self._text_label.configure(text=self._current_value)
+
+ if self._variable is not None:
+ self._variable_callback_blocked = True
+ self._variable.set(self._current_value)
+ self._variable_callback_blocked = False
+
+ def get(self) -> str:
+ return self._current_value
+
+ def _clicked(self, event=0):
+ if self._state is not tkinter.DISABLED and len(self._values) > 0:
+ self._open_dropdown_menu()
+
+ def bind(self, sequence: str = None, command: Callable = None, add: str = None) -> str:
+ """ called on the tkinter.Label and tkinter.Canvas """
+ canvas_bind_return = self._canvas.bind(sequence, command, add)
+ label_bind_return = self._text_label.bind(sequence, command, add)
+ return canvas_bind_return + " + " + label_bind_return
+
+ def unbind(self, sequence: str, funcid: str = None):
+ """ called on the tkinter.Label and tkinter.Canvas """
+ canvas_bind_return, label_bind_return = funcid.split(" + ")
+ self._canvas.unbind(sequence, canvas_bind_return)
+ self._text_label.unbind(sequence, label_bind_return)
+
+ def focus(self):
+ return self._text_label.focus()
+
+ def focus_set(self):
+ return self._text_label.focus_set()
+
+ def focus_force(self):
+ return self._text_label.focus_force()
diff --git a/customtkinter/windows/widgets/ctk_progressbar.py b/customtkinter/windows/widgets/ctk_progressbar.py
new file mode 100644
index 0000000..2859ff4
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_progressbar.py
@@ -0,0 +1,299 @@
+import tkinter
+import math
+from typing import Union, Tuple, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+
+
+class CTkProgressBar(CTkBaseClass):
+ """
+ Progressbar with rounded corners, border, variable support,
+ indeterminate mode, vertical orientation.
+ For detailed information check out the documentation.
+ """
+
+ def __init__(self,
+ master: any,
+ width: Optional[int] = None,
+ height: Optional[int] = None,
+ 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,
+ progress_color: Optional[Union[str, Tuple[str, str]]] = None,
+
+ variable: Union[tkinter.Variable, None] = None,
+ orientation: str = "horizontal",
+ mode: str = "determinate",
+ determinate_speed: float = 1,
+ indeterminate_speed: float = 1,
+ **kwargs):
+
+ # set default dimensions according to orientation
+ if width is None:
+ if orientation.lower() == "vertical":
+ width = 8
+ else:
+ width = 200
+ if height is None:
+ if orientation.lower() == "vertical":
+ height = 200
+ else:
+ height = 8
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # color
+ self._border_color = ThemeManager.theme["CTkProgressBar"]["border_color"] if border_color is None else self._check_color_type(border_color)
+ self._fg_color = ThemeManager.theme["CTkProgressBar"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+ self._progress_color = ThemeManager.theme["CTkProgressBar"]["progress_color"] if progress_color is None else self._check_color_type(progress_color)
+
+ # control variable
+ self._variable = variable
+ self._variable_callback_blocked = False
+ self._variable_callback_name = None
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkProgressBar"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_width = ThemeManager.theme["CTkProgressBar"]["border_width"] if border_width is None else border_width
+ self._determinate_value: float = 0.5 # range 0-1
+ self._determinate_speed = determinate_speed # range 0-1
+ self._indeterminate_value: float = 0 # range 0-inf
+ self._indeterminate_width: float = 0.4 # range 0-1
+ self._indeterminate_speed = indeterminate_speed # range 0-1 to travel in 50ms
+ self._loop_running: bool = False
+ self._orientation = orientation
+ self._mode = mode # "determinate" or "indeterminate"
+
+ self.grid_rowconfigure(0, weight=1)
+ self.grid_columnconfigure(0, weight=1)
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe")
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._draw() # initial draw
+
+ if self._variable is not None:
+ self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
+ self._variable_callback_blocked = True
+ self.set(self._variable.get(), from_variable_callback=True)
+ self._variable_callback_blocked = False
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width=None, height=None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw()
+
+ def destroy(self):
+ if self._variable is not None:
+ self._variable.trace_remove("write", self._variable_callback_name)
+
+ super().destroy()
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ if self._orientation.lower() == "horizontal":
+ orientation = "w"
+ elif self._orientation.lower() == "vertical":
+ orientation = "s"
+ else:
+ orientation = "w"
+
+ if self._mode == "determinate":
+ requires_recoloring = self._draw_engine.draw_rounded_progress_bar_with_border(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width),
+ 0,
+ self._determinate_value,
+ orientation)
+ else: # indeterminate mode
+ progress_value = (math.sin(self._indeterminate_value * math.pi / 40) + 1) / 2
+ progress_value_1 = min(1.0, progress_value + (self._indeterminate_width / 2))
+ progress_value_2 = max(0.0, progress_value - (self._indeterminate_width / 2))
+
+ requires_recoloring = self._draw_engine.draw_rounded_progress_bar_with_border(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width),
+ progress_value_1,
+ progress_value_2,
+ orientation)
+
+ if no_color_updates is False or requires_recoloring:
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._border_color),
+ outline=self._apply_appearance_mode(self._border_color))
+ self._canvas.itemconfig("inner_parts",
+ fill=self._apply_appearance_mode(self._fg_color),
+ outline=self._apply_appearance_mode(self._fg_color))
+ self._canvas.itemconfig("progress_parts",
+ fill=self._apply_appearance_mode(self._progress_color),
+ outline=self._apply_appearance_mode(self._progress_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 "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 "progress_color" in kwargs:
+ self._progress_color = self._check_color_type(kwargs.pop("progress_color"))
+ require_redraw = True
+
+ 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")
+ require_redraw = True
+
+ if "determinate_speed" in kwargs:
+ self._determinate_speed = kwargs.pop("determinate_speed")
+
+ if "indeterminate_speed" in kwargs:
+ self._indeterminate_speed = kwargs.pop("indeterminate_speed")
+
+ 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 == "fg_color":
+ return self._fg_color
+ elif attribute_name == "border_color":
+ return self._border_color
+ elif attribute_name == "progress_color":
+ return self._progress_color
+
+ elif attribute_name == "variable":
+ return self._variable
+ elif attribute_name == "orientation":
+ return self._orientation
+ elif attribute_name == "mode":
+ return self._mode
+ elif attribute_name == "determinate_speed":
+ return self._determinate_speed
+ elif attribute_name == "indeterminate_speed":
+ return self._indeterminate_speed
+
+ else:
+ return super().cget(attribute_name)
+
+ def _variable_callback(self, var_name, index, mode):
+ if not self._variable_callback_blocked:
+ self.set(self._variable.get(), from_variable_callback=True)
+
+ def set(self, value, from_variable_callback=False):
+ """ set determinate value """
+ self._determinate_value = value
+
+ if self._determinate_value > 1:
+ self._determinate_value = 1
+ elif self._determinate_value < 0:
+ self._determinate_value = 0
+
+ self._draw(no_color_updates=True)
+
+ if self._variable is not None and not from_variable_callback:
+ self._variable_callback_blocked = True
+ self._variable.set(round(self._determinate_value) if isinstance(self._variable, tkinter.IntVar) else self._determinate_value)
+ self._variable_callback_blocked = False
+
+ def get(self) -> float:
+ """ get determinate value """
+ return self._determinate_value
+
+ def start(self):
+ """ start indeterminate mode """
+ if not self._loop_running:
+ self._loop_running = True
+ self._internal_loop()
+
+ def stop(self):
+ """ stop indeterminate mode """
+ self._loop_running = False
+
+ def _internal_loop(self):
+ if self._loop_running:
+ if self._mode == "determinate":
+ self._determinate_value += self._determinate_speed / 50
+ if self._determinate_value > 1:
+ self._determinate_value -= 1
+ self._draw()
+ self.after(20, self._internal_loop)
+ else:
+ self._indeterminate_value += self._indeterminate_speed
+ self._draw()
+ self.after(20, self._internal_loop)
+
+ def step(self):
+ if self._mode == "determinate":
+ self._determinate_value += self._determinate_speed / 50
+ if self._determinate_value > 1:
+ self._determinate_value -= 1
+ self._draw()
+ else:
+ self._indeterminate_value += self._indeterminate_speed
+ self._draw()
+
+ def bind(self, sequence=None, command=None, add=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.bind(sequence, command, add)
+
+ def unbind(self, sequence, funcid=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.unbind(sequence, funcid)
+
+ def focus(self):
+ return self._canvas.focus()
+
+ def focus_set(self):
+ return self._canvas.focus_set()
+
+ def focus_force(self):
+ return self._canvas.focus_force()
diff --git a/customtkinter/windows/widgets/ctk_radiobutton.py b/customtkinter/windows/widgets/ctk_radiobutton.py
new file mode 100644
index 0000000..9ba3a29
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_radiobutton.py
@@ -0,0 +1,416 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+from .font import CTkFont
+
+
+class CTkRadioButton(CTkBaseClass):
+ """
+ Radiobutton with rounded corners, border, label, variable support, command.
+ For detailed information check out the documentation.
+ """
+
+ def __init__(self,
+ master: any,
+ width: int = 100,
+ height: int = 22,
+ radiobutton_width: int = 22,
+ radiobutton_height: int = 22,
+ corner_radius: Optional[int] = None,
+ border_width_unchecked: Optional[int] = None,
+ border_width_checked: Optional[int] = None,
+
+ bg_color: Union[str, Tuple[str, str]] = "transparent",
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ hover_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,
+
+ text: str = "CTkRadioButton",
+ font: Optional[Union[tuple, CTkFont]] = None,
+ textvariable: Union[tkinter.Variable, None] = None,
+ variable: Union[tkinter.Variable, None] = None,
+ value: Union[int, str] = 0,
+ state: str = tkinter.NORMAL,
+ hover: bool = True,
+ command: Union[Callable, None] = None,
+ **kwargs):
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # dimensions
+ self._radiobutton_width = radiobutton_width
+ self._radiobutton_height = radiobutton_height
+
+ # color
+ self._fg_color = ThemeManager.theme["CTkRadiobutton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+ self._hover_color = ThemeManager.theme["CTkRadiobutton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
+ self._border_color = ThemeManager.theme["CTkRadiobutton"]["border_color"] if border_color is None else self._check_color_type(border_color)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkRadiobutton"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_width_unchecked = ThemeManager.theme["CTkRadiobutton"]["border_width_unchecked"] if border_width_unchecked is None else border_width_unchecked
+ self._border_width_checked = ThemeManager.theme["CTkRadiobutton"]["border_width_checked"] if border_width_checked is None else border_width_checked
+
+ # text
+ self._text = text
+ self._text_label: Union[tkinter.Label, None] = None
+ self._text_color = ThemeManager.theme["CTkRadiobutton"]["text_color"] if text_color is None else self._check_color_type(text_color)
+ self._text_color_disabled = ThemeManager.theme["CTkRadiobutton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
+
+ # font
+ self._font = CTkFont() if font is None else self._check_font_type(font)
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+
+ # callback and control variables
+ self._command = command
+ self._state = state
+ self._hover = hover
+ self._check_state: bool = False
+ self._value = value
+ self._variable: tkinter.Variable = variable
+ self._variable_callback_blocked: bool = False
+ self._textvariable = textvariable
+ self._variable_callback_name: Union[str, None] = None
+
+ # configure grid system (3x1)
+ self.grid_columnconfigure(0, weight=0)
+ self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
+ self.grid_columnconfigure(2, weight=1)
+
+ self._bg_canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._current_width),
+ height=self._apply_widget_scaling(self._current_height))
+ self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._radiobutton_width),
+ height=self._apply_widget_scaling(self._radiobutton_height))
+ self._canvas.grid(row=0, column=0)
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._canvas.bind("", self._on_enter)
+ self._canvas.bind("", self._on_leave)
+ self._canvas.bind("", self.invoke)
+
+ self._text_label = tkinter.Label(master=self,
+ bd=0,
+ padx=0,
+ pady=0,
+ text=self._text,
+ justify=tkinter.LEFT,
+ font=self._apply_font_scaling(self._font),
+ textvariable=self._textvariable)
+ self._text_label.grid(row=0, column=2, sticky="w")
+ self._text_label["anchor"] = "w"
+
+ self._text_label.bind("", self._on_enter)
+ self._text_label.bind("", self._on_leave)
+ self._text_label.bind("", self.invoke)
+
+ if self._variable is not None:
+ self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
+ self._check_state = True if self._variable.get() == self._value else False
+
+ self._draw() # initial draw
+ self._set_cursor()
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
+ self._text_label.configure(font=self._apply_font_scaling(self._font))
+
+ self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._canvas.configure(width=self._apply_widget_scaling(self._radiobutton_width),
+ height=self._apply_widget_scaling(self._radiobutton_height))
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width: int = None, height: int = None):
+ super()._set_dimensions(width, height)
+
+ self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+
+ def _update_font(self):
+ """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
+ self._text_label.configure(font=self._apply_font_scaling(self._font))
+
+ # Workaround to force grid to be resized when text changes size.
+ # Otherwise grid will lag and only resizes if other mouse action occurs.
+ self._bg_canvas.grid_forget()
+ self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
+
+ def destroy(self):
+ if self._variable is not None:
+ self._variable.trace_remove("write", self._variable_callback_name)
+
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+
+ super().destroy()
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ if self._check_state is True:
+ requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._radiobutton_width),
+ self._apply_widget_scaling(self._radiobutton_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width_checked))
+ else:
+ requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._radiobutton_width),
+ self._apply_widget_scaling(self._radiobutton_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width_unchecked))
+
+ if no_color_updates is False or requires_recoloring:
+ self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ if self._check_state is False:
+ self._canvas.itemconfig("border_parts",
+ outline=self._apply_appearance_mode(self._border_color),
+ fill=self._apply_appearance_mode(self._border_color))
+ else:
+ self._canvas.itemconfig("border_parts",
+ outline=self._apply_appearance_mode(self._fg_color),
+ fill=self._apply_appearance_mode(self._fg_color))
+
+ self._canvas.itemconfig("inner_parts",
+ outline=self._apply_appearance_mode(self._bg_color),
+ fill=self._apply_appearance_mode(self._bg_color))
+
+ if self._state == tkinter.DISABLED:
+ self._text_label.configure(fg=self._apply_appearance_mode(self._text_color_disabled))
+ else:
+ self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
+
+ 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
+
+ if "radiobutton_width" in kwargs:
+ self._radiobutton_width = kwargs.pop("radiobutton_width")
+ self._canvas.configure(width=self._apply_widget_scaling(self._radiobutton_width))
+ require_redraw = True
+
+ if "radiobutton_height" in kwargs:
+ self._radiobutton_height = kwargs.pop("radiobutton_height")
+ 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)
+
+ self._update_font()
+
+ if "state" in kwargs:
+ self._state = kwargs.pop("state")
+ self._set_cursor()
+ require_redraw = True
+
+ if "fg_color" in kwargs:
+ self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
+ require_redraw = True
+
+ if "hover_color" in kwargs:
+ self._hover_color = self._check_color_type(kwargs.pop("hover_color"))
+ require_redraw = True
+
+ if "text_color" in kwargs:
+ self._text_color = self._check_color_type(kwargs.pop("text_color"))
+ require_redraw = True
+
+ if "text_color_disabled" in 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 "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 "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
+
+ 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_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
+ elif attribute_name == "hover_color":
+ return self._hover_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":
+ return self._text_color_disabled
+
+ elif attribute_name == "text":
+ return self._text
+ elif attribute_name == "font":
+ return self._font
+ elif attribute_name == "textvariable":
+ return self._textvariable
+ elif attribute_name == "variable":
+ return self._variable
+ elif attribute_name == "value":
+ return self._value
+ elif attribute_name == "state":
+ return self._state
+ elif attribute_name == "hover":
+ return self._hover
+ elif attribute_name == "command":
+ return self._command
+
+ else:
+ return super().cget(attribute_name)
+
+ def _set_cursor(self):
+ if self._cursor_manipulation_enabled:
+ if self._state == tkinter.DISABLED:
+ if sys.platform == "darwin":
+ self._canvas.configure(cursor="arrow")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="arrow")
+ elif sys.platform.startswith("win"):
+ self._canvas.configure(cursor="arrow")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="arrow")
+
+ elif self._state == tkinter.NORMAL:
+ if sys.platform == "darwin":
+ self._canvas.configure(cursor="pointinghand")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="pointinghand")
+ elif sys.platform.startswith("win"):
+ self._canvas.configure(cursor="hand2")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="hand2")
+
+ def _on_enter(self, event=0):
+ if self._hover is True and self._state == tkinter.NORMAL:
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._hover_color),
+ outline=self._apply_appearance_mode(self._hover_color))
+
+ def _on_leave(self, event=0):
+ if self._check_state is True:
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._fg_color),
+ outline=self._apply_appearance_mode(self._fg_color))
+ else:
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._border_color),
+ outline=self._apply_appearance_mode(self._border_color))
+
+ def _variable_callback(self, var_name, index, mode):
+ if not self._variable_callback_blocked:
+ if self._variable.get() == self._value:
+ self.select(from_variable_callback=True)
+ else:
+ self.deselect(from_variable_callback=True)
+
+ def invoke(self, event=0):
+ if self._state == tkinter.NORMAL:
+ if self._check_state is False:
+ self._check_state = True
+ 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
+
+ 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
+
+ def bind(self, sequence=None, command=None, add=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.bind(sequence, command, add)
+
+ def unbind(self, sequence, funcid=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.unbind(sequence, funcid)
+
+ def focus(self):
+ return self._text_label.focus()
+
+ def focus_set(self):
+ return self._text_label.focus_set()
+
+ def focus_force(self):
+ return self._text_label.focus_force()
diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py
new file mode 100644
index 0000000..99134ff
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_scrollbar.py
@@ -0,0 +1,267 @@
+import sys
+from typing import Union, Tuple, Callable, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+
+
+class CTkScrollbar(CTkBaseClass):
+ """
+ Scrollbar with rounded corners, configurable spacing.
+ Connect to scrollable widget by passing .set() method and set command attribute.
+ For detailed information check out the documentation.
+ """
+
+ def __init__(self,
+ master: any,
+ width: Optional[Union[int, str]] = None,
+ height: Optional[Union[int, str]] = None,
+ corner_radius: Optional[int] = None,
+ border_spacing: Optional[int] = None,
+ minimum_pixel_length: int = 20,
+
+ bg_color: Union[str, Tuple[str, str]] = "transparent",
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+
+ hover: bool = True,
+ command: Union[Callable, None] = None,
+ orientation: str = "vertical",
+ **kwargs):
+
+ # set default dimensions according to orientation
+ if width is None:
+ if orientation.lower() == "vertical":
+ width = 16
+ else:
+ width = 200
+ if height is None:
+ if orientation.lower() == "horizontal":
+ height = 16
+ else:
+ height = 200
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # color
+ self._fg_color = ThemeManager.theme["CTkScrollbar"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
+ self._button_color = ThemeManager.theme["CTkScrollbar"]["scrollbar_color"] if button_color is None else self._check_color_type(button_color)
+ self._button_hover_color = ThemeManager.theme["CTkScrollbar"]["scrollbar_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkScrollbar"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_spacing = ThemeManager.theme["CTkScrollbar"]["border_spacing"] if border_spacing is None else border_spacing
+
+ self._hover = hover
+ self._hover_state: bool = False
+ self._command = command
+ self._orientation = orientation
+ self._start_value: float = 0 # 0 to 1
+ self._end_value: float = 1 # 0 to 1
+ self._minimum_pixel_length = minimum_pixel_length
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._current_width),
+ height=self._apply_widget_scaling(self._current_height))
+ self._canvas.place(x=0, y=0, relwidth=1, relheight=1)
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._canvas.bind("", self._on_enter)
+ self._canvas.bind("", self._on_leave)
+ self._canvas.tag_bind("border_parts", "", self._clicked)
+ self._canvas.bind("", self._clicked)
+ self._canvas.bind("", self._mouse_scroll_event)
+
+ self._draw()
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width=None, height=None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw(no_color_updates=True)
+
+ def _get_scrollbar_values_for_minimum_pixel_size(self):
+ # correct scrollbar float values if scrollbar is too small
+ if self._orientation == "vertical":
+ scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_height
+ if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_height != 0:
+ # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
+ interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_height)
+ corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor
+ corrected_start_value = self._start_value - self._start_value * interval_extend_factor
+ return corrected_start_value, corrected_end_value
+ else:
+ return self._start_value, self._end_value
+
+ else:
+ scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_width
+ if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_width != 0:
+ # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
+ interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_width)
+ corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor
+ corrected_start_value = self._start_value - self._start_value * interval_extend_factor
+ return corrected_start_value, corrected_end_value
+ else:
+ return self._start_value, self._end_value
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ corrected_start_value, corrected_end_value = self._get_scrollbar_values_for_minimum_pixel_size()
+ requires_recoloring = self._draw_engine.draw_rounded_scrollbar(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_spacing),
+ corrected_start_value,
+ corrected_end_value,
+ self._orientation)
+
+ if no_color_updates is False or requires_recoloring:
+ if self._hover_state is True:
+ self._canvas.itemconfig("scrollbar_parts",
+ fill=self._apply_appearance_mode(self._button_hover_color),
+ outline=self._apply_appearance_mode(self._button_hover_color))
+ else:
+ self._canvas.itemconfig("scrollbar_parts",
+ fill=self._apply_appearance_mode(self._button_color),
+ outline=self._apply_appearance_mode(self._button_color))
+
+ if self._fg_color == "transparent":
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._bg_color),
+ outline=self._apply_appearance_mode(self._bg_color))
+ else:
+ self._canvas.configure(bg=self._apply_appearance_mode(self._fg_color))
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._fg_color),
+ outline=self._apply_appearance_mode(self._fg_color))
+
+ self._canvas.update_idletasks()
+
+ def configure(self, require_redraw=False, **kwargs):
+ if "fg_color" in kwargs:
+ self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
+ require_redraw = True
+
+ if "button_color" in kwargs:
+ self._button_color = self._check_color_type(kwargs.pop("button_color"))
+ require_redraw = True
+
+ if "button_hover_color" in kwargs:
+ self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
+ require_redraw = True
+
+ if "hover" in kwargs:
+ self._hover = kwargs.pop("hover")
+
+ 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:
+ if attribute_name == "corner_radius":
+ return self._corner_radius
+ elif attribute_name == "border_spacing":
+ return self._border_spacing
+ elif attribute_name == "minimum_pixel_length":
+ return self._minimum_pixel_length
+
+ elif attribute_name == "fg_color":
+ return self._fg_color
+ elif attribute_name == "scrollbar_color":
+ return self._button_color
+ elif attribute_name == "scrollbar_hover_color":
+ return self._button_hover_color
+
+ elif attribute_name == "hover":
+ return self._hover
+ elif attribute_name == "command":
+ return self._command
+ elif attribute_name == "orientation":
+ return self._orientation
+
+ else:
+ return super().cget(attribute_name)
+
+ def _on_enter(self, event=0):
+ if self._hover is True:
+ self._hover_state = True
+ self._canvas.itemconfig("scrollbar_parts",
+ outline=self._apply_appearance_mode(self._button_hover_color),
+ fill=self._apply_appearance_mode(self._button_hover_color))
+
+ def _on_leave(self, event=0):
+ self._hover_state = False
+ self._canvas.itemconfig("scrollbar_parts",
+ outline=self._apply_appearance_mode(self._button_color),
+ fill=self._apply_appearance_mode(self._button_color))
+
+ def _clicked(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)))
+
+ current_scrollbar_length = self._end_value - self._start_value
+ value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2)))
+ self._start_value = value - (current_scrollbar_length / 2)
+ self._end_value = value + (current_scrollbar_length / 2)
+ self._draw()
+
+ if self._command is not None:
+ self._command('moveto', self._start_value)
+
+ def _mouse_scroll_event(self, event=None):
+ if self._command is not None:
+ if sys.platform.startswith("win"):
+ self._command('scroll', -int(event.delta/40), 'units')
+ else:
+ self._command('scroll', -event.delta, 'units')
+
+ def set(self, start_value: float, end_value: float):
+ self._start_value = float(start_value)
+ self._end_value = float(end_value)
+ self._draw()
+
+ def get(self):
+ return self._start_value, self._end_value
+
+ def bind(self, sequence=None, command=None, add=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.bind(sequence, command, add)
+
+ def unbind(self, sequence, funcid=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.unbind(sequence, funcid)
+
+ def focus(self):
+ return self._canvas.focus()
+
+ def focus_set(self):
+ return self._canvas.focus_set()
+
+ def focus_force(self):
+ return self._canvas.focus_force()
diff --git a/customtkinter/windows/widgets/ctk_segmented_button.py b/customtkinter/windows/widgets/ctk_segmented_button.py
new file mode 100644
index 0000000..05b57c9
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_segmented_button.py
@@ -0,0 +1,410 @@
+import tkinter
+from typing import Union, Tuple, List, Dict, Callable, Optional, Literal
+
+from .theme import ThemeManager
+from .font import CTkFont
+from .ctk_button import CTkButton
+from .ctk_frame import CTkFrame
+
+
+class CTkSegmentedButton(CTkFrame):
+ """
+ Segmented button with corner radius, border width, variable support.
+ For detailed information check out the documentation.
+ """
+
+ def __init__(self,
+ master: any,
+ width: int = 140,
+ height: int = 28,
+ corner_radius: Optional[int] = None,
+ border_width: int = 3,
+
+ bg_color: Union[str, Tuple[str, str]] = "transparent",
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ selected_color: Optional[Union[str, Tuple[str, str]]] = None,
+ selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+ unselected_color: Optional[Union[str, Tuple[str, str]]] = None,
+ unselected_hover_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,
+ background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None,
+
+ font: Optional[Union[tuple, CTkFont]] = None,
+ values: Optional[list] = None,
+ variable: Union[tkinter.Variable, None] = None,
+ dynamic_resizing: bool = True,
+ command: Union[Callable[[str], None], None] = None,
+ state: str = "normal",
+ **kwargs):
+
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ self._sb_fg_color = ThemeManager.theme["CTkSegmentedButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+
+ self._sb_selected_color = ThemeManager.theme["CTkSegmentedButton"]["selected_color"] if selected_color is None else self._check_color_type(selected_color)
+ self._sb_selected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["selected_hover_color"] if selected_hover_color is None else self._check_color_type(selected_hover_color)
+
+ self._sb_unselected_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_color"] if unselected_color is None else self._check_color_type(unselected_color)
+ self._sb_unselected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_hover_color"] if unselected_hover_color is None else self._check_color_type(unselected_hover_color)
+
+ self._sb_text_color = ThemeManager.theme["CTkSegmentedButton"]["text_color"] if text_color is None else self._check_color_type(text_color)
+ self._sb_text_color_disabled = ThemeManager.theme["CTkSegmentedButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
+
+ self._sb_corner_radius = ThemeManager.theme["CTkSegmentedButton"]["corner_radius"] if corner_radius is None else corner_radius
+ self._sb_border_width = ThemeManager.theme["CTkSegmentedButton"]["border_width"] if border_width is None else border_width
+
+ self._background_corner_colors = background_corner_colors # rendering options for DrawEngine
+
+ self._command: Callable[[str], None] = command
+ self._font = CTkFont() if font is None else font
+ self._state = state
+
+ self._buttons_dict: Dict[str, CTkButton] = {} # mapped from value to button object
+ if values is None:
+ self._value_list: List[str] = ["CTkSegmentedButton"]
+ else:
+ self._value_list: List[str] = values # Values ordered like buttons rendered on widget
+
+ self._dynamic_resizing = dynamic_resizing
+ if not self._dynamic_resizing:
+ self.grid_propagate(False)
+
+ self._check_unique_values(self._value_list)
+ self._current_value: str = ""
+ if len(self._value_list) > 0:
+ self._create_buttons_from_values()
+ self._create_button_grid()
+
+ self._variable = variable
+ self._variable_callback_blocked: bool = False
+ self._variable_callback_name: Union[str, None] = None
+
+ if self._variable is not None:
+ self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
+ self.set(self._variable.get(), from_variable_callback=True)
+
+ super().configure(corner_radius=self._sb_corner_radius, fg_color="transparent")
+
+ def destroy(self):
+ if self._variable is not None: # remove old callback
+ self._variable.trace_remove("write", self._variable_callback_name)
+
+ super().destroy()
+
+ def _set_dimensions(self, width: int = None, height: int = None):
+ super()._set_dimensions(width, height)
+
+ for button in self._buttons_dict.values():
+ button.configure(height=height)
+
+ def _variable_callback(self, var_name, index, mode):
+ if not self._variable_callback_blocked:
+ self.set(self._variable.get(), from_variable_callback=True)
+
+ def _get_index_by_value(self, value: str):
+ for index, value_from_list in enumerate(self._value_list):
+ if value_from_list == value:
+ return index
+
+ raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
+
+ def _configure_button_corners_for_index(self, index: int):
+ if index == 0 and len(self._value_list) == 1:
+ if self._background_corner_colors is None:
+ self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
+ else:
+ self._buttons_dict[self._value_list[index]].configure(background_corner_colors=self._background_corner_colors)
+
+ 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))
+ 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))
+ 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))
+
+ def _unselect_button_by_value(self, value: str):
+ if value in self._buttons_dict:
+ self._buttons_dict[value].configure(fg_color=self._sb_unselected_color,
+ hover_color=self._sb_unselected_hover_color)
+
+ def _select_button_by_value(self, value: str):
+ if self._current_value is not None and self._current_value != "":
+ self._unselect_button_by_value(self._current_value)
+
+ self._current_value = value
+
+ self._buttons_dict[value].configure(fg_color=self._sb_selected_color,
+ hover_color=self._sb_selected_hover_color)
+
+ def _create_button(self, index: int, value: str) -> CTkButton:
+ new_button = CTkButton(self,
+ width=0,
+ height=self._current_height,
+ corner_radius=self._sb_corner_radius,
+ border_width=self._sb_border_width,
+ fg_color=self._sb_unselected_color,
+ border_color=self._sb_fg_color,
+ hover_color=self._sb_unselected_hover_color,
+ text_color=self._sb_text_color,
+ text_color_disabled=self._sb_text_color_disabled,
+ text=value,
+ font=self._font,
+ state=self._state,
+ command=lambda v=value: self.set(v, from_button_callback=True),
+ background_corner_colors=None,
+ round_width_to_even_numbers=False,
+ round_height_to_even_numbers=False) # DrawEngine rendering option (so that theres no gap between buttons)
+
+ return new_button
+
+ @staticmethod
+ def _check_unique_values(values: List[str]):
+ """ raises exception if values are not unique """
+ if len(values) != len(set(values)):
+ raise ValueError("CTkSegmentedButton values are not unique")
+
+ def _create_button_grid(self):
+ # remove minsize from every grid cell in the first row
+ number_of_columns, _ = self.grid_size()
+ for n in range(number_of_columns):
+ self.grid_columnconfigure(n, weight=1, minsize=0)
+ self.grid_rowconfigure(0, weight=1)
+
+ for index, value in enumerate(self._value_list):
+ self.grid_columnconfigure(index, weight=1, minsize=self._current_height)
+ self._buttons_dict[value].grid(row=0, column=index, sticky="ew")
+
+ def _create_buttons_from_values(self):
+ assert len(self._buttons_dict) == 0
+ assert len(self._value_list) > 0
+
+ for index, value in enumerate(self._value_list):
+ self._buttons_dict[value] = self._create_button(index, value)
+ self._configure_button_corners_for_index(index)
+
+ def configure(self, **kwargs):
+ if "bg_color" in kwargs:
+ super().configure(bg_color=kwargs.pop("bg_color"))
+
+ if len(self._buttons_dict) > 0:
+ self._configure_button_corners_for_index(0)
+ if len(self._buttons_dict) > 1:
+ max_index = len(self._buttons_dict) - 1
+ self._configure_button_corners_for_index(max_index)
+
+ if "fg_color" in kwargs:
+ self._sb_fg_color = self._check_color_type(kwargs.pop("fg_color"))
+ for index, button in enumerate(self._buttons_dict.values()):
+ button.configure(border_color=self._sb_fg_color)
+ self._configure_button_corners_for_index(index)
+
+ if "selected_color" in kwargs:
+ self._sb_selected_color = self._check_color_type(kwargs.pop("selected_color"))
+ if self._current_value in self._buttons_dict:
+ self._buttons_dict[self._current_value].configure(fg_color=self._sb_selected_color)
+
+ if "selected_hover_color" in kwargs:
+ self._sb_selected_hover_color = self._check_color_type(kwargs.pop("selected_hover_color"))
+ if self._current_value in self._buttons_dict:
+ self._buttons_dict[self._current_value].configure(hover_color=self._sb_selected_hover_color)
+
+ if "unselected_color" in kwargs:
+ self._sb_unselected_color = self._check_color_type(kwargs.pop("unselected_color"))
+ for value, button in self._buttons_dict.items():
+ if value != self._current_value:
+ button.configure(fg_color=self._sb_unselected_color)
+
+ if "unselected_hover_color" in kwargs:
+ self._sb_unselected_hover_color = self._check_color_type(kwargs.pop("unselected_hover_color"))
+ for value, button in self._buttons_dict.items():
+ if value != self._current_value:
+ button.configure(hover_color=self._sb_unselected_hover_color)
+
+ if "text_color" in kwargs:
+ self._sb_text_color = self._check_color_type(kwargs.pop("text_color"))
+ for button in self._buttons_dict.values():
+ button.configure(text_color=self._sb_text_color)
+
+ if "text_color_disabled" in kwargs:
+ self._sb_text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
+ for button in self._buttons_dict.values():
+ button.configure(text_color_disabled=self._sb_text_color_disabled)
+
+ if "background_corner_colors" in kwargs:
+ self._background_corner_colors = kwargs.pop("background_corner_colors")
+ for i in range(len(self._buttons_dict)):
+ self._configure_button_corners_for_index(i)
+
+ if "font" in kwargs:
+ self._font = kwargs.pop("font")
+ for button in self._buttons_dict.values():
+ button.configure(font=self._font)
+
+ if "values" in kwargs:
+ for button in self._buttons_dict.values():
+ button.destroy()
+ self._buttons_dict.clear()
+ self._value_list = kwargs.pop("values")
+
+ self._check_unique_values(self._value_list)
+
+ if len(self._value_list) > 0:
+ self._create_buttons_from_values()
+ self._create_button_grid()
+
+ if self._current_value in self._value_list:
+ self._select_button_by_value(self._current_value)
+
+ if "variable" in kwargs:
+ if self._variable is not None: # remove old callback
+ self._variable.trace_remove("write", self._variable_callback_name)
+
+ self._variable = kwargs.pop("variable")
+
+ if self._variable is not None and self._variable != "":
+ self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
+ self.set(self._variable.get(), from_variable_callback=True)
+ else:
+ self._variable = None
+
+ if "dynamic_resizing" in kwargs:
+ self._dynamic_resizing = kwargs.pop("dynamic_resizing")
+ if not self._dynamic_resizing:
+ self.grid_propagate(False)
+ else:
+ self.grid_propagate(True)
+
+ if "command" in kwargs:
+ self._command = kwargs.pop("command")
+
+ if "state" in kwargs:
+ self._state = kwargs.pop("state")
+ for button in self._buttons_dict.values():
+ button.configure(state=self._state)
+
+ super().configure(**kwargs)
+
+ def cget(self, attribute_name: str) -> any:
+ if attribute_name == "corner_radius":
+ return self._sb_corner_radius
+ elif attribute_name == "border_width":
+ return self._sb_border_width
+
+ elif attribute_name == "fg_color":
+ return self._sb_fg_color
+ elif attribute_name == "selected_color":
+ return self._sb_selected_color
+ elif attribute_name == "selected_hover_color":
+ return self._sb_selected_hover_color
+ elif attribute_name == "unselected_color":
+ return self._sb_unselected_color
+ elif attribute_name == "unselected_hover_color":
+ return self._sb_unselected_hover_color
+ elif attribute_name == "text_color":
+ return self._sb_text_color
+ elif attribute_name == "text_color_disabled":
+ return self._sb_text_color_disabled
+
+ elif attribute_name == "font":
+ return self._font
+ elif attribute_name == "values":
+ return self._value_list
+ elif attribute_name == "variable":
+ return self._variable
+ elif attribute_name == "dynamic_resizing":
+ return self._dynamic_resizing
+ elif attribute_name == "command":
+ return self._command
+
+ else:
+ 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:
+ return
+ elif value in self._buttons_dict:
+ self._select_button_by_value(value)
+
+ if self._variable is not None and not from_variable_callback:
+ self._variable_callback_blocked = True
+ self._variable.set(value)
+ self._variable_callback_blocked = False
+ else:
+ if self._current_value in self._buttons_dict:
+ self._unselect_button_by_value(self._current_value)
+ self._current_value = value
+
+ if self._variable is not None and not from_variable_callback:
+ self._variable_callback_blocked = True
+ self._variable.set(value)
+ self._variable_callback_blocked = False
+
+ if from_button_callback:
+ if self._command is not None:
+ self._command(self._current_value)
+
+ def get(self) -> str:
+ return self._current_value
+
+ def insert(self, index: int, value: str):
+ if value not in self._buttons_dict:
+ if value != "":
+ self._value_list.insert(index, value)
+ self._buttons_dict[value] = self._create_button(index, value)
+
+ self._configure_button_corners_for_index(index)
+ if index > 0:
+ self._configure_button_corners_for_index(index - 1)
+ if index < len(self._buttons_dict) - 1:
+ self._configure_button_corners_for_index(index + 1)
+
+ self._create_button_grid()
+
+ if value == self._current_value:
+ self._select_button_by_value(self._current_value)
+ else:
+ raise ValueError(f"CTkSegmentedButton can not insert value ''")
+ else:
+ raise ValueError(f"CTkSegmentedButton can not insert value '{value}', already part of the values")
+
+ def move(self, new_index: int, value: str):
+ if 0 <= new_index < len(self._value_list):
+ if value in self._buttons_dict:
+ self.delete(value)
+ self.insert(new_index, value)
+ else:
+ raise ValueError(f"CTkSegmentedButton has no value named '{value}'")
+ else:
+ raise ValueError(f"CTkSegmentedButton new_index {new_index} not in range of value list with len {len(self._value_list)}")
+
+ def delete(self, value: str):
+ if value in self._buttons_dict:
+ self._buttons_dict[value].destroy()
+ self._buttons_dict.pop(value)
+ index_to_remove = self._get_index_by_value(value)
+ self._value_list.pop(index_to_remove)
+
+ # removed index was outer right element
+ if index_to_remove == len(self._buttons_dict) and len(self._buttons_dict) > 0:
+ self._configure_button_corners_for_index(index_to_remove - 1)
+
+ # removed index was outer left element
+ if index_to_remove == 0 and len(self._buttons_dict) > 0:
+ self._configure_button_corners_for_index(0)
+
+ #if index_to_remove <= len(self._buttons_dict) - 1:
+ # self._configure_button_corners_for_index(index_to_remove)
+
+ self._create_button_grid()
+ else:
+ raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
+
diff --git a/customtkinter/windows/widgets/ctk_slider.py b/customtkinter/windows/widgets/ctk_slider.py
new file mode 100644
index 0000000..a88e956
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_slider.py
@@ -0,0 +1,384 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+
+
+class CTkSlider(CTkBaseClass):
+ """
+ Slider with rounded corners, border, number of steps, variable support, vertical orientation.
+ For detailed information check out the documentation.
+ """
+
+ def __init__(self,
+ master: any,
+ width: Optional[int] = None,
+ height: Optional[int] = None,
+ corner_radius: Optional[int] = None,
+ button_corner_radius: Optional[int] = None,
+ border_width: Optional[int] = None,
+ button_length: Optional[int] = None,
+
+ bg_color: Union[str, Tuple[str, str]] = "transparent",
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ border_color: Union[str, Tuple[str, str]] = "transparent",
+ progress_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+
+ from_: int = 0,
+ to: int = 1,
+ state: str = "normal",
+ number_of_steps: Union[int, None] = None,
+ hover: bool = True,
+ command: Union[Callable[[float], None], None] = None,
+ variable: Union[tkinter.Variable, None] = None,
+ orientation: str = "horizontal",
+ **kwargs):
+
+ # set default dimensions according to orientation
+ if width is None:
+ if orientation.lower() == "vertical":
+ width = 16
+ else:
+ width = 200
+ if height is None:
+ if orientation.lower() == "vertical":
+ height = 200
+ else:
+ height = 16
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # color
+ self._border_color = self._check_color_type(border_color, transparency=True)
+ self._fg_color = ThemeManager.theme["CTkSlider"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+ self._progress_color = ThemeManager.theme["CTkSlider"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True)
+ self._button_color = ThemeManager.theme["CTkSlider"]["button_color"] if button_color is None else self._check_color_type(button_color)
+ self._button_hover_color = ThemeManager.theme["CTkSlider"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkSlider"]["corner_radius"] if corner_radius is None else corner_radius
+ self._button_corner_radius = ThemeManager.theme["CTkSlider"]["button_corner_radius"] if button_corner_radius is None else button_corner_radius
+ self._border_width = ThemeManager.theme["CTkSlider"]["border_width"] if border_width is None else border_width
+ self._button_length = ThemeManager.theme["CTkSlider"]["button_length"] if button_length is None else button_length
+ self._value: float = 0.5 # initial value of slider in percent
+ self._orientation = orientation
+ self._hover_state: bool = False
+ self._hover = hover
+ self._from_ = from_
+ self._to = to
+ self._number_of_steps = number_of_steps
+ self._output_value = self._from_ + (self._value * (self._to - self._from_))
+
+ if self._corner_radius < self._button_corner_radius:
+ self._corner_radius = self._button_corner_radius
+
+ # callback and control variables
+ self._command = command
+ self._variable: tkinter.Variable = variable
+ self._variable_callback_blocked: bool = False
+ self._variable_callback_name: Union[bool, None] = None
+ self._state = state
+
+ self.grid_rowconfigure(0, weight=1)
+ self.grid_columnconfigure(0, weight=1)
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe")
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._canvas.bind("", self._on_enter)
+ self._canvas.bind("", self._on_leave)
+ self._canvas.bind("", self._clicked)
+ self._canvas.bind("", self._clicked)
+
+ self._set_cursor()
+ self._draw() # initial draw
+
+ if self._variable is not None:
+ self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
+ self._variable_callback_blocked = True
+ self.set(self._variable.get(), from_variable_callback=True)
+ self._variable_callback_blocked = False
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width=None, height=None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw()
+
+ def destroy(self):
+ # remove variable_callback from variable callbacks if variable exists
+ if self._variable is not None:
+ self._variable.trace_remove("write", self._variable_callback_name)
+
+ super().destroy()
+
+ def _set_cursor(self):
+ if self._state == "normal" and self._cursor_manipulation_enabled:
+ if sys.platform == "darwin":
+ self.configure(cursor="pointinghand")
+ elif sys.platform.startswith("win"):
+ self.configure(cursor="hand2")
+
+ elif self._state == "disabled" and self._cursor_manipulation_enabled:
+ if sys.platform == "darwin":
+ self.configure(cursor="arrow")
+ elif sys.platform.startswith("win"):
+ self.configure(cursor="arrow")
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ if self._orientation.lower() == "horizontal":
+ orientation = "w"
+ elif self._orientation.lower() == "vertical":
+ orientation = "s"
+ else:
+ orientation = "w"
+
+ requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width),
+ self._apply_widget_scaling(self._button_length),
+ self._apply_widget_scaling(self._button_corner_radius),
+ self._value, orientation)
+
+ if no_color_updates is False or requires_recoloring:
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ if self._border_color == "transparent":
+ self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._bg_color),
+ outline=self._apply_appearance_mode(self._bg_color))
+ else:
+ self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._border_color),
+ outline=self._apply_appearance_mode(self._border_color))
+
+ self._canvas.itemconfig("inner_parts", fill=self._apply_appearance_mode(self._fg_color),
+ outline=self._apply_appearance_mode(self._fg_color))
+
+ if self._progress_color == "transparent":
+ self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._fg_color),
+ outline=self._apply_appearance_mode(self._fg_color))
+ else:
+ self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._progress_color),
+ outline=self._apply_appearance_mode(self._progress_color))
+
+ if self._hover_state is True:
+ self._canvas.itemconfig("slider_parts",
+ fill=self._apply_appearance_mode(self._button_hover_color),
+ outline=self._apply_appearance_mode(self._button_hover_color))
+ else:
+ self._canvas.itemconfig("slider_parts",
+ fill=self._apply_appearance_mode(self._button_color),
+ outline=self._apply_appearance_mode(self._button_color))
+
+ def configure(self, require_redraw=False, **kwargs):
+ if "state" in kwargs:
+ self._state = kwargs.pop("state")
+ self._set_cursor()
+ require_redraw = True
+
+ if "fg_color" in kwargs:
+ self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
+ require_redraw = True
+
+ if "progress_color" in kwargs:
+ self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True)
+ require_redraw = True
+
+ if "button_color" in kwargs:
+ self._button_color = self._check_color_type(kwargs.pop("button_color"))
+ require_redraw = True
+
+ if "button_hover_color" in kwargs:
+ self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
+ require_redraw = True
+
+ if "border_color" in kwargs:
+ self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True)
+ require_redraw = True
+
+ if "border_width" in kwargs:
+ self._border_width = kwargs.pop("border_width")
+ require_redraw = True
+
+ if "from_" in kwargs:
+ self._from_ = kwargs.pop("from_")
+
+ if "to" in kwargs:
+ self._to = kwargs.pop("to")
+
+ if "number_of_steps" in kwargs:
+ self._number_of_steps = kwargs.pop("number_of_steps")
+
+ if "hover" in kwargs:
+ self._hover = kwargs.pop("hover")
+
+ if "command" in kwargs:
+ self._command = kwargs.pop("command")
+
+ 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
+
+ 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 == "button_corner_radius":
+ return self._button_corner_radius
+ elif attribute_name == "border_width":
+ return self._border_width
+ elif attribute_name == "button_length":
+ return self._button_length
+
+ elif attribute_name == "fg_color":
+ return self._fg_color
+ elif attribute_name == "border_color":
+ return self._border_color
+ elif attribute_name == "progress_color":
+ return self._progress_color
+ elif attribute_name == "button_color":
+ return self._button_color
+ elif attribute_name == "button_hover_color":
+ return self._button_hover_color
+
+ elif attribute_name == "from_":
+ return self._from_
+ elif attribute_name == "to":
+ return self._to
+ elif attribute_name == "state":
+ return self._state
+ elif attribute_name == "number_of_steps":
+ return self._number_of_steps
+ elif attribute_name == "hover":
+ return self._hover
+ elif attribute_name == "command":
+ return self._command
+ elif attribute_name == "variable":
+ return self._variable
+ elif attribute_name == "orientation":
+ return self._orientation
+
+ else:
+ return super().cget(attribute_name)
+
+ 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)
+ else:
+ self._value = 1 - self._reverse_widget_scaling(event.y / self._current_height)
+
+ if self._value > 1:
+ self._value = 1
+ if self._value < 0:
+ self._value = 0
+
+ self._output_value = self._round_to_step_size(self._from_ + (self._value * (self._to - self._from_)))
+ self._value = (self._output_value - self._from_) / (self._to - self._from_)
+
+ self._draw(no_color_updates=False)
+
+ if self._variable is not None:
+ self._variable_callback_blocked = True
+ self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
+ self._variable_callback_blocked = False
+
+ if self._command is not None:
+ self._command(self._output_value)
+
+ def _on_enter(self, event=0):
+ if self._hover is True and self._state == "normal":
+ self._hover_state = True
+ self._canvas.itemconfig("slider_parts",
+ fill=self._apply_appearance_mode(self._button_hover_color),
+ outline=self._apply_appearance_mode(self._button_hover_color))
+
+ def _on_leave(self, event=0):
+ self._hover_state = False
+ self._canvas.itemconfig("slider_parts",
+ fill=self._apply_appearance_mode(self._button_color),
+ outline=self._apply_appearance_mode(self._button_color))
+
+ def _round_to_step_size(self, value) -> float:
+ if self._number_of_steps is not None:
+ step_size = (self._to - self._from_) / self._number_of_steps
+ value = self._to - (round((self._to - value) / step_size) * step_size)
+ return value
+ else:
+ return value
+
+ def get(self) -> float:
+ return self._output_value
+
+ def set(self, output_value, from_variable_callback=False):
+ if self._from_ < self._to:
+ if output_value > self._to:
+ output_value = self._to
+ elif output_value < self._from_:
+ output_value = self._from_
+ else:
+ if output_value < self._to:
+ output_value = self._to
+ elif output_value > self._from_:
+ output_value = self._from_
+
+ self._output_value = self._round_to_step_size(output_value)
+ self._value = (self._output_value - self._from_) / (self._to - self._from_)
+
+ self._draw(no_color_updates=False)
+
+ if self._variable is not None and not from_variable_callback:
+ self._variable_callback_blocked = True
+ self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
+ self._variable_callback_blocked = False
+
+ def _variable_callback(self, var_name, index, mode):
+ if not self._variable_callback_blocked:
+ self.set(self._variable.get(), from_variable_callback=True)
+
+ def bind(self, sequence=None, command=None, add=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.bind(sequence, command, add)
+
+ def unbind(self, sequence, funcid=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.unbind(sequence, funcid)
+
+ def focus(self):
+ return self._canvas.focus()
+
+ def focus_set(self):
+ return self._canvas.focus_set()
+
+ def focus_force(self):
+ return self._canvas.focus_force()
diff --git a/customtkinter/windows/widgets/ctk_switch.py b/customtkinter/windows/widgets/ctk_switch.py
new file mode 100644
index 0000000..a986a65
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_switch.py
@@ -0,0 +1,455 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional
+
+from .core_rendering import CTkCanvas
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+from .font import CTkFont
+
+
+class CTkSwitch(CTkBaseClass):
+ """
+ Switch with rounded corners, border, label, command, variable support.
+ For detailed information check out the documentation.
+ """
+
+ def __init__(self,
+ master: any,
+ width: int = 100,
+ height: int = 24,
+ switch_width: int = 36,
+ switch_height: int = 18,
+ corner_radius: Optional[int] = None,
+ border_width: Optional[int] = None,
+ button_length: Optional[int] = None,
+
+ bg_color: Union[str, Tuple[str, str]] = "transparent",
+ fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ border_color: Union[str, Tuple[str, str]] = "transparent",
+ progress_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_color: Optional[Union[str, Tuple[str, str]]] = None,
+ button_hover_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,
+
+ text: str = "CTkSwitch",
+ font: Optional[Union[tuple, CTkFont]] = None,
+ textvariable: Union[tkinter.Variable, None] = None,
+ onvalue: Union[int, str] = 1,
+ offvalue: Union[int, str] = 0,
+ variable: Union[tkinter.Variable, None] = None,
+ hover: bool = True,
+ command: Union[Callable, None] = None,
+ state: str = tkinter.NORMAL,
+ **kwargs):
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # dimensions
+ self._switch_width = switch_width
+ self._switch_height = switch_height
+
+ # color
+ self._border_color = self._check_color_type(border_color, transparency=True)
+ self._fg_color = ThemeManager.theme["CTkSwitch"]["fg_Color"] if fg_color is None else self._check_color_type(fg_color)
+ self._progress_color = ThemeManager.theme["CTkSwitch"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True)
+ self._button_color = ThemeManager.theme["CTkSwitch"]["button_color"] if button_color is None else self._check_color_type(button_color)
+ self._button_hover_color = ThemeManager.theme["CTkSwitch"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
+ self._text_color = ThemeManager.theme["CTkSwitch"]["text_color"] if text_color is None else self._check_color_type(text_color)
+ self._text_color_disabled = ThemeManager.theme["CTkSwitch"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
+
+ # text
+ self._text = text
+ self._text_label = None
+
+ # font
+ self._font = CTkFont() if font is None else self._check_font_type(font)
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkSwitch"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_width = ThemeManager.theme["CTkSwitch"]["border_width"] if border_width is None else border_width
+ self._button_length = ThemeManager.theme["CTkSwitch"]["button_length"] if button_length is None else button_length
+ self._hover_state: bool = False
+ self._check_state: bool = False # True if switch is activated
+ self._hover = hover
+ self._state = state
+ self._onvalue = onvalue
+ self._offvalue = offvalue
+
+ # callback and control variables
+ self._command = command
+ self._variable = variable
+ self._variable_callback_blocked = False
+ self._variable_callback_name = None
+ self._textvariable = textvariable
+
+ # configure grid system (3x1)
+ self.grid_columnconfigure(0, weight=0)
+ self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
+ self.grid_columnconfigure(2, weight=1)
+
+ self._bg_canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._current_width),
+ height=self._apply_widget_scaling(self._current_height))
+ self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._switch_width),
+ height=self._apply_widget_scaling(self._switch_height))
+ self._canvas.grid(row=0, column=0, sticky="")
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._canvas.bind("", self._on_enter)
+ self._canvas.bind("", self._on_leave)
+ self._canvas.bind("", self.toggle)
+
+ self._text_label = tkinter.Label(master=self,
+ bd=0,
+ padx=0,
+ pady=0,
+ text=self._text,
+ justify=tkinter.LEFT,
+ font=self._apply_font_scaling(self._font),
+ textvariable=self._textvariable)
+ self._text_label.grid(row=0, column=2, sticky="w")
+ self._text_label["anchor"] = "w"
+
+ self._text_label.bind("", self._on_enter)
+ self._text_label.bind("", self._on_leave)
+ self._text_label.bind("", self.toggle)
+
+ if self._variable is not None and self._variable != "":
+ self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
+ self.c_heck_state = True if self._variable.get() == self._onvalue else False
+
+ self._draw() # initial draw
+ self._set_cursor()
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
+ self._text_label.configure(font=self._apply_font_scaling(self._font))
+
+ self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._canvas.configure(width=self._apply_widget_scaling(self._switch_width),
+ height=self._apply_widget_scaling(self._switch_height))
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width: int = None, height: int = None):
+ super()._set_dimensions(width, height)
+
+ self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+
+ def _update_font(self):
+ """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
+ self._text_label.configure(font=self._apply_font_scaling(self._font))
+
+ # Workaround to force grid to be resized when text changes size.
+ # Otherwise grid will lag and only resizes if other mouse action occurs.
+ self._bg_canvas.grid_forget()
+ self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
+
+ def destroy(self):
+ # remove variable_callback from variable callbacks if variable exists
+ if self._variable is not None:
+ self._variable.trace_remove("write", self._variable_callback_name)
+
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+
+ super().destroy()
+
+ def _set_cursor(self):
+ if self._cursor_manipulation_enabled:
+ if self._state == tkinter.DISABLED:
+ if sys.platform == "darwin":
+ self._canvas.configure(cursor="arrow")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="arrow")
+ elif sys.platform.startswith("win"):
+ self._canvas.configure(cursor="arrow")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="arrow")
+
+ elif self._state == tkinter.NORMAL:
+ if sys.platform == "darwin":
+ self._canvas.configure(cursor="pointinghand")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="pointinghand")
+ elif sys.platform.startswith("win"):
+ self._canvas.configure(cursor="hand2")
+ if self._text_label is not None:
+ self._text_label.configure(cursor="hand2")
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ if self._check_state is True:
+ requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._switch_width),
+ self._apply_widget_scaling(self._switch_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width),
+ self._apply_widget_scaling(self._button_length),
+ self._apply_widget_scaling(self._corner_radius),
+ 1, "w")
+ else:
+ requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._switch_width),
+ self._apply_widget_scaling(self._switch_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width),
+ self._apply_widget_scaling(self._button_length),
+ self._apply_widget_scaling(self._corner_radius),
+ 0, "w")
+
+ if no_color_updates is False or requires_recoloring:
+ self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ if self._border_color == "transparent":
+ self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._bg_color),
+ outline=self._apply_appearance_mode(self._bg_color))
+ else:
+ self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._border_color),
+ outline=self._apply_appearance_mode(self._border_color))
+
+ self._canvas.itemconfig("inner_parts", fill=self._apply_appearance_mode(self._fg_color),
+ outline=self._apply_appearance_mode(self._fg_color))
+
+ if self._progress_color == "transparent":
+ self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._fg_color),
+ outline=self._apply_appearance_mode(self._fg_color))
+ else:
+ self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._progress_color),
+ outline=self._apply_appearance_mode(self._progress_color))
+
+ self._canvas.itemconfig("slider_parts", fill=self._apply_appearance_mode(self._button_color),
+ outline=self._apply_appearance_mode(self._button_color))
+
+ if self._state == tkinter.DISABLED:
+ self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
+ else:
+ self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
+
+ 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
+
+ if "switch_width" in kwargs:
+ self._switch_width = kwargs.pop("switch_width")
+ self._canvas.configure(width=self._apply_widget_scaling(self._switch_width))
+ require_redraw = True
+
+ if "switch_height" in kwargs:
+ self._switch_height = kwargs.pop("switch_height")
+ 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)
+
+ self._update_font()
+
+ if "state" in kwargs:
+ self._state = kwargs.pop("state")
+ self._set_cursor()
+ require_redraw = True
+
+ if "fg_color" in kwargs:
+ self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
+ require_redraw = True
+
+ if "progress_color" in kwargs:
+ self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True)
+ require_redraw = True
+
+ if "button_color" in kwargs:
+ self._button_color = self._check_color_type(kwargs.pop("button_color"))
+ require_redraw = True
+
+ if "button_hover_color" in kwargs:
+ self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
+ require_redraw = True
+
+ if "border_color" in kwargs:
+ self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True)
+ 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 "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
+
+ 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 == "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
+ elif attribute_name == "border_color":
+ return self._border_color
+ elif attribute_name == "progress_color":
+ return self._progress_color
+ elif attribute_name == "button_color":
+ return self._button_color
+ elif attribute_name == "button_hover_color":
+ return self._button_hover_color
+ elif attribute_name == "text_color":
+ return self._text_color
+ elif attribute_name == "text_color_disabled":
+ return self._text_color_disabled
+
+ elif attribute_name == "text":
+ return self._text
+ elif attribute_name == "font":
+ return self._font
+ elif attribute_name == "textvariable":
+ return self._textvariable
+ elif attribute_name == "onvalue":
+ return self._onvalue
+ elif attribute_name == "offvalue":
+ return self._offvalue
+ elif attribute_name == "variable":
+ return self._variable
+ elif attribute_name == "hover":
+ return self._hover
+ elif attribute_name == "command":
+ return self._command
+ elif attribute_name == "state":
+ return self._state
+
+ else:
+ return super().cget(attribute_name)
+
+ 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:
+ self._variable_callback_blocked = True
+ self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
+ self._variable_callback_blocked = False
+
+ if self._command is not None:
+ self._command()
+
+ 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
+
+ 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
+
+ def get(self) -> Union[int, str]:
+ return self._onvalue if self._check_state is True else self._offvalue
+
+ def _on_enter(self, event=0):
+ if self._hover is True and self._state == "normal":
+ self._hover_state = True
+ self._canvas.itemconfig("slider_parts",
+ fill=self._apply_appearance_mode(self._button_hover_color),
+ outline=self._apply_appearance_mode(self._button_hover_color))
+
+ def _on_leave(self, event=0):
+ self._hover_state = False
+ self._canvas.itemconfig("slider_parts",
+ fill=self._apply_appearance_mode(self._button_color),
+ outline=self._apply_appearance_mode(self._button_color))
+
+ def _variable_callback(self, var_name, index, mode):
+ if not self._variable_callback_blocked:
+ if self._variable.get() == self._onvalue:
+ self.select(from_variable_callback=True)
+ elif self._variable.get() == self._offvalue:
+ self.deselect(from_variable_callback=True)
+
+ def bind(self, sequence=None, command=None, add=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.bind(sequence, command, add)
+
+ def unbind(self, sequence, funcid=None):
+ """ called on the tkinter.Canvas """
+ return self._canvas.unbind(sequence, funcid)
+
+ def focus(self):
+ return self._text_label.focus()
+
+ def focus_set(self):
+ return self._text_label.focus_set()
+
+ def focus_force(self):
+ return self._text_label.focus_force()
diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py
new file mode 100644
index 0000000..75bde4b
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_tabview.py
@@ -0,0 +1,370 @@
+import tkinter
+from typing import Union, Tuple, Dict, List, Callable, Optional
+
+from .theme import ThemeManager
+from .ctk_frame import CTkFrame
+from .core_rendering import CTkCanvas
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+from .ctk_segmented_button import CTkSegmentedButton
+
+
+class CTkTabview(CTkBaseClass):
+ """
+ Tabview...
+ For detailed information check out the documentation.
+ """
+
+ _top_spacing: int = 10 # px on top of the buttons
+ _top_button_overhang: int = 8 # px
+ _button_height: int = 26
+ _segmented_button_border_width: int = 3
+
+ def __init__(self,
+ master: any,
+ width: int = 300,
+ height: int = 250,
+ 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,
+
+ segmented_button_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+ segmented_button_selected_color: Optional[Union[str, Tuple[str, str]]] = None,
+ 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,
+
+ text_color: Optional[Union[str, Tuple[str, str]]] = None,
+ text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
+
+ command: Union[Callable, None] = None,
+ state: str = "normal",
+ **kwargs):
+
+ # transfer some functionality to CTkFrame
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
+
+ # color
+ self._border_color = ThemeManager.theme["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color)
+
+ # determine fg_color of frame
+ if fg_color is None:
+ if isinstance(self.master, (CTkFrame, CTkTabview)):
+ if self.master.cget("fg_color") == ThemeManager.theme["CTkFrame"]["fg_color"]:
+ self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"]
+ else:
+ self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
+ else:
+ self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
+ else:
+ self._fg_color = self._check_color_type(fg_color, transparency=True)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkFrame"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_width = ThemeManager.theme["CTkFrame"]["border_width"] if border_width is None else border_width
+
+ self._canvas = CTkCanvas(master=self,
+ bg=self._apply_appearance_mode(self._bg_color),
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height - self._top_spacing - self._top_button_overhang))
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._segmented_button = CTkSegmentedButton(self,
+ values=[],
+ height=self._button_height,
+ fg_color=segmented_button_fg_color,
+ selected_color=segmented_button_selected_color,
+ selected_hover_color=segmented_button_selected_hover_color,
+ unselected_color=segmented_button_unselected_color,
+ unselected_hover_color=segmented_button_unselected_hover_color,
+ text_color=text_color,
+ text_color_disabled=text_color_disabled,
+ corner_radius=corner_radius,
+ border_width=self._segmented_button_border_width,
+ command=self._segmented_button_callback,
+ state=state)
+ self._configure_segmented_button_background_corners()
+ self._configure_grid()
+ self._set_grid_canvas()
+
+ self._tab_dict: Dict[str, CTkFrame] = {}
+ self._name_list: List[str] = [] # list of unique tab names in order of tabs
+ self._current_name: str = ""
+ self._command = command
+
+ self._draw()
+
+ def _segmented_button_callback(self, selected_name):
+ self._current_name = selected_name
+ self._grid_forget_all_tabs()
+ self._set_grid_tab_by_name(self._current_name)
+
+ if self._command is not None:
+ self._command()
+
+ def winfo_children(self) -> List[any]:
+ """
+ winfo_children of CTkTabview without canvas and segmented button widgets,
+ because it's not a child but part of the CTkTabview itself
+ """
+
+ child_widgets = super().winfo_children()
+ try:
+ child_widgets.remove(self._canvas)
+ child_widgets.remove(self._segmented_button)
+ return child_widgets
+ except ValueError:
+ return child_widgets
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height - self._top_spacing - self._top_button_overhang))
+ self._configure_grid()
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width=None, height=None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height - self._top_spacing - self._top_button_overhang))
+ self._draw()
+
+ def _configure_segmented_button_background_corners(self):
+ """ needs to be called for changes in fg_color, bg_color """
+
+ if self._fg_color is not None:
+ self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._fg_color, self._fg_color))
+ else:
+ self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
+
+ def _configure_tab_background_corners_by_name(self, name: str):
+ """ needs to be called for changes in fg_color, bg_color, border_width """
+
+ self._tab_dict[name].configure(background_corner_colors=None)
+
+ def _configure_grid(self):
+ """ create 3 x 4 grid system """
+
+ self.grid_rowconfigure(0, weight=0, minsize=self._apply_widget_scaling(self._top_spacing))
+ self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._top_button_overhang))
+ self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._top_button_overhang))
+ self.grid_rowconfigure(3, weight=1)
+
+ self.grid_columnconfigure(0, weight=1)
+
+ def _set_grid_canvas(self):
+ self._canvas.grid(row=2, rowspan=2, column=0, columnspan=1, sticky="nsew")
+
+ def _set_grid_segmented_button(self):
+ """ needs to be called for changes in corner_radius """
+ self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="ns")
+
+ def _set_grid_tab_by_name(self, name: str):
+ """ needs to be called for changes in corner_radius, border_width """
+ self._tab_dict[name].grid(row=3, column=0, sticky="nsew",
+ padx=self._apply_widget_scaling(max(self._corner_radius, self._border_width)),
+ pady=self._apply_widget_scaling(max(self._corner_radius, self._border_width)))
+
+ def _grid_forget_all_tabs(self):
+ for frame in self._tab_dict.values():
+ frame.grid_forget()
+
+ def _create_tab(self) -> CTkFrame:
+ new_tab = CTkFrame(self,
+ height=0,
+ width=0,
+ fg_color=self._fg_color,
+ border_width=0,
+ corner_radius=self._corner_radius)
+ return new_tab
+
+ def _draw(self, no_color_updates: bool = False):
+ super()._draw(no_color_updates)
+
+ if not self._canvas.winfo_exists():
+ return
+
+ 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._top_spacing - self._top_button_overhang),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width))
+
+ if no_color_updates is False or requires_recoloring:
+ if self._fg_color == "transparent":
+ self._canvas.itemconfig("inner_parts",
+ fill=self._apply_appearance_mode(self._bg_color),
+ outline=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))
+
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._border_color),
+ outline=self._apply_appearance_mode(self._border_color))
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+ tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._bg_color)) # configure bg color of tkinter.Frame, cuase canvas does not fill frame
+
+ 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
+ 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 "command" in kwargs:
+ self._command = kwargs.pop("command")
+ if "state" in kwargs:
+ self._segmented_button.configure(state=kwargs.pop("state"))
+
+ super().configure(require_redraw=require_redraw, **kwargs)
+
+ def cget(self, attribute_name: str):
+ 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 == "segmented_button_fg_color":
+ return self._segmented_button.cget(attribute_name)
+ elif attribute_name == "segmented_button_selected_color":
+ return self._segmented_button.cget(attribute_name)
+ elif attribute_name == "segmented_button_selected_hover_color":
+ return self._segmented_button.cget(attribute_name)
+ elif attribute_name == "segmented_button_unselected_color":
+ 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 == "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 == "command":
+ return self._command
+ elif attribute_name == "state":
+ return self._segmented_button.cget(attribute_name)
+
+ else:
+ return super().cget(attribute_name)
+
+ def tab(self, name: str) -> CTkFrame:
+ """ returns reference to the tab with given name """
+
+ if name in self._tab_dict:
+ return self._tab_dict[name]
+ else:
+ raise ValueError(f"CTkTabview has no tab named '{name}'")
+
+ def insert(self, index: int, name: str) -> CTkFrame:
+ """ creates new tab with given name at position index """
+
+ if name not in self._tab_dict:
+ # if no tab exists, set grid for segmented button
+ if len(self._tab_dict) == 0:
+ self._set_grid_segmented_button()
+
+ self._name_list.insert(index, name)
+ self._tab_dict[name] = self._create_tab()
+ self._segmented_button.insert(index, name)
+ self._configure_tab_background_corners_by_name(name)
+
+ # if created tab is only tab select this tab
+ if len(self._tab_dict) == 1:
+ self._current_name = name
+ self._segmented_button.set(self._current_name)
+ self._grid_forget_all_tabs()
+ self._set_grid_tab_by_name(self._current_name)
+
+ return self._tab_dict[name]
+ else:
+ raise ValueError(f"CTkTabview already has tab named '{name}'")
+
+ def add(self, name: str) -> CTkFrame:
+ """ appends new tab with given name """
+ return self.insert(len(self._tab_dict), name)
+
+ def move(self, new_index: int, name: str):
+ if 0 <= new_index < len(self._name_list):
+ if name in self._tab_dict:
+ self._segmented_button.move(new_index, name)
+ else:
+ raise ValueError(f"CTkTabview has no name '{name}'")
+ else:
+ raise ValueError(f"CTkTabview new_index {new_index} not in range of name list with len {len(self._name_list)}")
+
+ def delete(self, name: str):
+ """ delete tab by name """
+
+ if name in self._tab_dict:
+ self._name_list.remove(name)
+ self._tab_dict[name].grid_forget()
+ self._tab_dict.pop(name)
+ self._segmented_button.delete(name)
+
+ # set current_name to '' and remove segmented button if no tab is left
+ if len(self._name_list) == 0:
+ self._current_name = ""
+ self._segmented_button.grid_forget()
+
+ # if only one tab left, select this tab
+ elif len(self._name_list) == 1:
+ self._current_name = self._name_list[0]
+ self._segmented_button.set(self._current_name)
+ self._grid_forget_all_tabs()
+ self._set_grid_tab_by_name(self._current_name)
+
+ # more tabs are left
+ else:
+ # if current_name is deleted tab, select first tab at position 0
+ if self._current_name == name:
+ self.set(self._name_list[0])
+ else:
+ raise ValueError(f"CTkTabview has no tab named '{name}'")
+
+ def set(self, name: str):
+ """ select tab by name """
+
+ if name in self._tab_dict:
+ self._current_name = name
+ self._segmented_button.set(name)
+ self._grid_forget_all_tabs()
+ self._set_grid_tab_by_name(name)
+ 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
diff --git a/customtkinter/windows/widgets/ctk_textbox.py b/customtkinter/windows/widgets/ctk_textbox.py
new file mode 100644
index 0000000..2ba985a
--- /dev/null
+++ b/customtkinter/windows/widgets/ctk_textbox.py
@@ -0,0 +1,502 @@
+import tkinter
+from typing import Union, Tuple, Optional
+
+from .core_rendering import CTkCanvas
+from .ctk_scrollbar import CTkScrollbar
+from .theme import ThemeManager
+from .core_rendering import DrawEngine
+from .core_widget_classes import CTkBaseClass
+from .font import CTkFont
+from .utility import pop_from_dict_by_set, check_kwargs_empty
+
+
+class CTkTextbox(CTkBaseClass):
+ """
+ Textbox with x and y scrollbars, rounded corners, and all text features of tkinter.Text widget.
+ Scrollbars only appear when they are needed. Text is wrapped on line end by default,
+ set wrap='none' to disable automatic line wrapping.
+ For detailed information check out the documentation.
+
+ Detailed methods and parameters of the underlaying tkinter.Text widget can be found here:
+ https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/text.html
+ (most of them are implemented here too)
+ """
+
+ _scrollbar_update_time = 200 # interval in ms, to check if scrollbars are needed
+
+ # attributes that are passed to and managed by the tkinter textbox only:
+ _valid_tk_text_attributes = {"autoseparators", "cursor", "exportselection",
+ "insertborderwidth", "insertofftime", "insertontime", "insertwidth",
+ "maxundo", "padx", "pady", "selectborderwidth", "spacing1",
+ "spacing2", "spacing3", "state", "tabs", "takefocus", "undo", "wrap",
+ "xscrollcommand", "yscrollcommand"}
+
+ def __init__(self,
+ master: any,
+ width: int = 200,
+ height: int = 200,
+ corner_radius: Optional[int] = None,
+ border_width: Optional[int] = None,
+ border_spacing: int = 3,
+
+ 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, str]] = None,
+ scrollbar_button_color: Optional[Union[str, Tuple[str, str]]] = None,
+ scrollbar_button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+
+ font: Optional[Union[tuple, CTkFont]] = None,
+ activate_scrollbars: bool = True,
+ **kwargs):
+
+ # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
+ super().__init__(master=master, bg_color=bg_color, width=width, height=height)
+
+ # color
+ self._fg_color = ThemeManager.theme["CTkTextbox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
+ self._border_color = ThemeManager.theme["CTkTextbox"]["border_color"] if border_color is None else self._check_color_type(border_color)
+ self._text_color = ThemeManager.theme["CTkTextbox"]["text_color"] if text_color is None else self._check_color_type(text_color)
+ self._scrollbar_button_color = ThemeManager.theme["CTkTextbox"]["scrollbar_button_color"] if scrollbar_button_color is None else self._check_color_type(scrollbar_button_color)
+ self._scrollbar_button_hover_color = ThemeManager.theme["CTkTextbox"]["scrollbar_button_hover_color"] if scrollbar_button_hover_color is None else self._check_color_type(scrollbar_button_hover_color)
+
+ # shape
+ self._corner_radius = ThemeManager.theme["CTkTextbox"]["corner_radius"] if corner_radius is None else corner_radius
+ self._border_width = ThemeManager.theme["CTkTextbox"]["border_width"] if border_width is None else border_width
+ self._border_spacing = border_spacing
+
+ # font
+ self._font = CTkFont() if font is None else self._check_font_type(font)
+ if isinstance(self._font, CTkFont):
+ self._font.add_size_configure_callback(self._update_font)
+
+ self._canvas = CTkCanvas(master=self,
+ highlightthickness=0,
+ width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+ self._draw_engine = DrawEngine(self._canvas)
+
+ self._textbox = tkinter.Text(self,
+ fg=self._apply_appearance_mode(self._text_color),
+ width=0,
+ height=0,
+ font=self._apply_font_scaling(self._font),
+ highlightthickness=0,
+ relief="flat",
+ insertbackground=self._apply_appearance_mode(self._text_color),
+ bg=self._apply_appearance_mode(self._fg_color),
+ **pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes))
+
+ check_kwargs_empty(kwargs, raise_error=True)
+
+ # scrollbars
+ self._scrollbars_activated = activate_scrollbars
+ self._hide_x_scrollbar = True
+ self._hide_y_scrollbar = True
+
+ self._y_scrollbar = CTkScrollbar(self,
+ width=8,
+ height=0,
+ border_spacing=0,
+ fg_color=self._fg_color,
+ button_color=self._scrollbar_button_color,
+ button_hover_color=self._scrollbar_button_hover_color,
+ orientation="vertical",
+ command=self._textbox.yview)
+ self._textbox.configure(yscrollcommand=self._y_scrollbar.set)
+
+ self._x_scrollbar = CTkScrollbar(self,
+ height=8,
+ width=0,
+ border_spacing=0,
+ fg_color=self._fg_color,
+ button_color=self._scrollbar_button_color,
+ button_hover_color=self._scrollbar_button_hover_color,
+ orientation="horizontal",
+ command=self._textbox.xview)
+ self._textbox.configure(xscrollcommand=self._x_scrollbar.set)
+
+ self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
+
+ self.after(50, self._check_if_scrollbars_needed)
+ self._draw()
+
+ def _create_grid_for_text_and_scrollbars(self, re_grid_textbox=False, re_grid_x_scrollbar=False, re_grid_y_scrollbar=False):
+
+ # configure 2x2 grid
+ self.grid_rowconfigure(0, weight=1)
+ self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)))
+ self.grid_columnconfigure(0, weight=1)
+ self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)))
+
+ if re_grid_textbox:
+ self._textbox.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew",
+ padx=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0),
+ pady=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0))
+
+ if re_grid_x_scrollbar:
+ if not self._hide_x_scrollbar and self._scrollbars_activated:
+ self._x_scrollbar.grid(row=1, column=0, rowspan=1, columnspan=1, sticky="ewn",
+ pady=(3, self._border_spacing + self._border_width),
+ padx=(max(self._corner_radius, self._border_width + self._border_spacing), 0)) # scrollbar grid method without scaling
+ else:
+ self._x_scrollbar.grid_forget()
+
+ if re_grid_y_scrollbar:
+ if not self._hide_y_scrollbar and self._scrollbars_activated:
+ self._y_scrollbar.grid(row=0, column=1, rowspan=1, columnspan=1, sticky="nsw",
+ padx=(3, self._border_spacing + self._border_width),
+ pady=(max(self._corner_radius, self._border_width + self._border_spacing), 0)) # scrollbar grid method without scaling
+ else:
+ self._y_scrollbar.grid_forget()
+
+ def _check_if_scrollbars_needed(self, event=None, continue_loop: bool = True):
+ """ Method hides or places the scrollbars if they are needed on key release event of tkinter.text widget """
+
+ if self._scrollbars_activated:
+ if self._textbox.xview() != (0.0, 1.0) and not self._x_scrollbar.winfo_ismapped(): # x scrollbar needed
+ self._hide_x_scrollbar = False
+ self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True)
+ elif self._textbox.xview() == (0.0, 1.0) and self._x_scrollbar.winfo_ismapped(): # x scrollbar not needed
+ self._hide_x_scrollbar = True
+ self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True)
+
+ if self._textbox.yview() != (0.0, 1.0) and not self._y_scrollbar.winfo_ismapped(): # y scrollbar needed
+ self._hide_y_scrollbar = False
+ self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
+ elif self._textbox.yview() == (0.0, 1.0) and self._y_scrollbar.winfo_ismapped(): # y scrollbar not needed
+ self._hide_y_scrollbar = True
+ self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
+ else:
+ self._hide_x_scrollbar = False
+ self._hide_x_scrollbar = False
+ self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
+
+ if self._textbox.winfo_exists() and continue_loop is True:
+ self.after(self._scrollbar_update_time, lambda: self._check_if_scrollbars_needed(continue_loop=True))
+
+ def _set_scaling(self, *args, **kwargs):
+ super()._set_scaling(*args, **kwargs)
+
+ self._textbox.configure(font=self._apply_font_scaling(self._font))
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
+ self._draw(no_color_updates=True)
+
+ def _set_dimensions(self, width=None, height=None):
+ super()._set_dimensions(width, height)
+
+ self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+ height=self._apply_widget_scaling(self._desired_height))
+ self._draw()
+
+ def _update_font(self):
+ """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
+ self._textbox.configure(font=self._apply_font_scaling(self._font))
+
+ # Workaround to force grid to be resized when text changes size.
+ # Otherwise grid will lag and only resizes if other mouse action occurs.
+ self._canvas.grid_forget()
+ self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
+
+ def destroy(self):
+ if isinstance(self._font, CTkFont):
+ self._font.remove_size_configure_callback(self._update_font)
+
+ super().destroy()
+
+ def _draw(self, no_color_updates=False):
+ super()._draw(no_color_updates)
+
+ if not self._canvas.winfo_exists():
+ return
+
+ requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
+ self._apply_widget_scaling(self._current_height),
+ self._apply_widget_scaling(self._corner_radius),
+ self._apply_widget_scaling(self._border_width))
+
+ if no_color_updates is False or requires_recoloring:
+ if self._fg_color == "transparent":
+ self._canvas.itemconfig("inner_parts",
+ fill=self._apply_appearance_mode(self._bg_color),
+ outline=self._apply_appearance_mode(self._bg_color))
+ self._textbox.configure(fg=self._apply_appearance_mode(self._text_color),
+ bg=self._apply_appearance_mode(self._bg_color),
+ insertbackground=self._apply_appearance_mode(self._text_color))
+ self._x_scrollbar.configure(fg_color=self._bg_color, scrollbar_color=self._scrollbar_button_color,
+ scrollbar_hover_color=self._scrollbar_button_hover_color)
+ self._y_scrollbar.configure(fg_color=self._bg_color, scrollbar_color=self._scrollbar_button_color,
+ scrollbar_hover_color=self._scrollbar_button_hover_color)
+ else:
+ self._canvas.itemconfig("inner_parts",
+ fill=self._apply_appearance_mode(self._fg_color),
+ outline=self._apply_appearance_mode(self._fg_color))
+ self._textbox.configure(fg=self._apply_appearance_mode(self._text_color),
+ bg=self._apply_appearance_mode(self._fg_color),
+ insertbackground=self._apply_appearance_mode(self._text_color))
+ self._x_scrollbar.configure(fg_color=self._fg_color, button_color=self._scrollbar_button_color,
+ button_hover_color=self._scrollbar_button_hover_color)
+ self._y_scrollbar.configure(fg_color=self._fg_color, button_color=self._scrollbar_button_color,
+ button_hover_color=self._scrollbar_button_hover_color)
+
+ self._canvas.itemconfig("border_parts",
+ fill=self._apply_appearance_mode(self._border_color),
+ outline=self._apply_appearance_mode(self._border_color))
+ self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+ self._canvas.tag_lower("inner_parts")
+ self._canvas.tag_lower("border_parts")
+
+ def configure(self, require_redraw=False, **kwargs):
+ if "fg_color" in kwargs:
+ self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
+ require_redraw = True
+
+ # check if CTk widgets are children of the frame and change their _bg_color to new frame fg_color
+ for child in self.winfo_children():
+ if isinstance(child, CTkBaseClass) and hasattr(child, "_fg_color"):
+ child.configure(bg_color=self._fg_color)
+
+ 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
+
+ if "scrollbar_button_color" in kwargs:
+ self._scrollbar_button_color = self._check_color_type(kwargs.pop("scrollbar_button_color"))
+ self._x_scrollbar.configure(button_color=self._scrollbar_button_color)
+ self._y_scrollbar.configure(button_color=self._scrollbar_button_color)
+
+ if "scrollbar_button_hover_color" in kwargs:
+ self._scrollbar_button_hover_color = self._check_color_type(kwargs.pop("scrollbar_button_hover_color"))
+ 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))
+ 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 == "border_spacing":
+ return self._border_spacing
+
+ 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 == "font":
+ return self._font
+
+ else:
+ return super().cget(attribute_name)
+
+ def bind(self, sequence=None, command=None, add=None):
+ """ called on the tkinter.Text """
+
+ # if sequence is , allow only to add the binding to keep the _textbox_modified_event() being called
+ if sequence == "":
+ return self._textbox.bind(sequence, command, add="+")
+ else:
+ return self._textbox.bind(sequence, command, add)
+
+ def unbind(self, sequence, funcid=None):
+ """ called on the tkinter.Text """
+ return self._textbox.unbind(sequence, funcid)
+
+ def focus(self):
+ return self._textbox.focus()
+
+ def focus_set(self):
+ return self._textbox.focus_set()
+
+ def focus_force(self):
+ return self._textbox.focus_force()
+
+ def insert(self, index, text, tags=None):
+ self._check_if_scrollbars_needed()
+ return self._textbox.insert(index, text, tags)
+
+ def get(self, index1, index2=None):
+ return self._textbox.get(index1, index2)
+
+ def bbox(self, index):
+ return self._textbox.bbox(index)
+
+ def compare(self, index, op, index2):
+ return self._textbox.compare(index, op, index2)
+
+ def delete(self, index1, index2=None):
+ return self._textbox.delete(index1, index2)
+
+ def dlineinfo(self, index):
+ return self._textbox.dlineinfo(index)
+
+ def edit_modified(self, arg=None):
+ return self._textbox.edit_modified(arg)
+
+ def edit_redo(self):
+ self._check_if_scrollbars_needed()
+ return self._textbox.edit_redo()
+
+ def edit_reset(self):
+ return self._textbox.edit_reset()
+
+ def edit_separator(self):
+ return self._textbox.edit_separator()
+
+ def edit_undo(self):
+ self._check_if_scrollbars_needed()
+ return self._textbox.edit_undo()
+
+ def image_create(self, index, **kwargs):
+ raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")
+
+ def image_cget(self, index, option):
+ raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")
+
+ def image_configure(self, index):
+ raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")
+
+ def image_names(self):
+ raise AttributeError("embedding images is forbidden, because would be incompatible with scaling")
+
+ def index(self, i):
+ return self._textbox.index(i)
+
+ def mark_gravity(self, mark, gravity=None):
+ return self._textbox.mark_gravity(mark, gravity)
+
+ def mark_names(self):
+ return self._textbox.mark_names()
+
+ def mark_next(self, index):
+ return self._textbox.mark_next(index)
+
+ def mark_previous(self, index):
+ return self._textbox.mark_previous(index)
+
+ def mark_set(self, mark, index):
+ return self._textbox.mark_set(mark, index)
+
+ def mark_unset(self, mark):
+ return self._textbox.mark_unset(mark)
+
+ def scan_dragto(self, x, y):
+ return self._textbox.scan_dragto(x, y)
+
+ def scan_mark(self, x, y):
+ return self._textbox.scan_mark(x, y)
+
+ def search(self, pattern, index, *args, **kwargs):
+ return self._textbox.search(pattern, index, *args, **kwargs)
+
+ def see(self, index):
+ return self._textbox.see(index)
+
+ def tag_add(self, tagName, index1, index2=None):
+ return self._textbox.tag_add(tagName, index1, index2)
+
+ def tag_bind(self, tagName, sequence, func, add=None):
+ return self._textbox.tag_bind(tagName, sequence, func, add)
+
+ def tag_cget(self, tagName, option):
+ return self._textbox.tag_cget(tagName, option)
+
+ def tag_config(self, tagName, **kwargs):
+ if "font" in kwargs:
+ raise AttributeError("'font' option forbidden, because would be incompatible with scaling")
+ return self._textbox.tag_config(tagName, **kwargs)
+
+ def tag_delete(self, *tagName):
+ return self._textbox.tag_delete(*tagName)
+
+ def tag_lower(self, tagName, belowThis=None):
+ return self._textbox.tag_lower(tagName, belowThis)
+
+ def tag_names(self, index=None):
+ return self._textbox.tag_names(index)
+
+ def tag_nextrange(self, tagName, index1, index2=None):
+ return self._textbox.tag_nextrange(tagName, index1, index2)
+
+ def tag_prevrange(self, tagName, index1, index2=None):
+ return self._textbox.tag_prevrange(tagName, index1, index2)
+
+ def tag_raise(self, tagName, aboveThis=None):
+ return self._textbox.tag_raise(tagName, aboveThis)
+
+ def tag_ranges(self, tagName):
+ return self._textbox.tag_ranges(tagName)
+
+ def tag_remove(self, tagName, index1, index2=None):
+ return self._textbox.tag_remove(tagName, index1, index2)
+
+ def tag_unbind(self, tagName, sequence, funcid=None):
+ return self._textbox.tag_unbind(tagName, sequence, funcid)
+
+ def window_cget(self, index, option):
+ raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")
+
+ def window_configure(self, index, option):
+ raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")
+
+ def window_create(self, index, **kwargs):
+ raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")
+
+ def window_names(self):
+ raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)")
+
+ def xview(self, *args):
+ return self._textbox.xview(*args)
+
+ def xview_moveto(self, fraction):
+ return self._textbox.xview_moveto(fraction)
+
+ def xview_scroll(self, n, what):
+ return self._textbox.xview_scroll(n, what)
+
+ def yview(self, *args):
+ return self._textbox.yview(*args)
+
+ def yview_moveto(self, fraction):
+ return self._textbox.yview_moveto(fraction)
+
+ def yview_scroll(self, n, what):
+ return self._textbox.yview_scroll(n, what)
diff --git a/customtkinter/windows/widgets/font/__init__.py b/customtkinter/windows/widgets/font/__init__.py
new file mode 100644
index 0000000..64a49f1
--- /dev/null
+++ b/customtkinter/windows/widgets/font/__init__.py
@@ -0,0 +1,24 @@
+import os
+import sys
+
+from .ctk_font import CTkFont
+from .font_manager import FontManager
+
+# import DrawEngine to set preferred_drawing_method if loading shapes font fails
+from ..core_rendering import DrawEngine
+
+FontManager.init_font_manager()
+
+# load Roboto fonts (used on Windows/Linux)
+customtkinter_directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
+FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf"))
+FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "Roboto", "Roboto-Medium.ttf"))
+
+# load font necessary for rendering the widgets (used on Windows/Linux)
+if FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "CustomTkinter_shapes_font.otf")) is False:
+ # change draw method if font loading failed
+ if DrawEngine.preferred_drawing_method == "font_shapes":
+ sys.stderr.write("customtkinter.windows.widgets.font warning: " +
+ "Preferred drawing method 'font_shapes' can not be used because the font file could not be loaded.\n" +
+ "Using 'circle_shapes' instead. The rendering quality will be bad!\n")
+ DrawEngine.preferred_drawing_method = "circle_shapes"
diff --git a/customtkinter/windows/widgets/font/ctk_font.py b/customtkinter/windows/widgets/font/ctk_font.py
new file mode 100644
index 0000000..69fb3f7
--- /dev/null
+++ b/customtkinter/windows/widgets/font/ctk_font.py
@@ -0,0 +1,88 @@
+from tkinter.font import Font
+import copy
+from typing import List, Callable, Tuple, Optional, Literal
+
+from ..theme import ThemeManager
+
+
+class CTkFont(Font):
+ """
+ Font object with size in pixel, independent of scaling.
+ To get scaled tuple representation use create_scaled_tuple() method.
+
+ family The font family name as a string.
+ size The font height as an integer in pixel.
+ weight 'bold' for boldface, 'normal' for regular weight.
+ slant 'italic' for italic, 'roman' for unslanted.
+ underline 1 for underlined text, 0 for normal.
+ overstrike 1 for overstruck text, 0 for normal.
+
+ Tkinter Font: https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/fonts.html
+ """
+
+ def __init__(self,
+ family: Optional[str] = None,
+ size: Optional[int] = None,
+ weight: Literal["normal", "bold"] = None,
+ slant: Literal["italic", "roman"] = "roman",
+ underline: bool = False,
+ overstrike: bool = False):
+
+ self._size_configure_callback_list: List[Callable] = []
+
+ self._size = ThemeManager.theme["CTkFont"]["size"] if size is None else size
+
+ super().__init__(family=ThemeManager.theme["CTkFont"]["family"] if family is None else family,
+ size=-abs(self._size),
+ weight=ThemeManager.theme["CTkFont"]["weight"] if weight is None else weight,
+ slant=slant,
+ underline=underline,
+ overstrike=overstrike)
+
+ self._family = super().cget("family")
+ self._tuple_style_string = f"{super().cget('weight')} {slant} {'underline' if underline else ''} {'overstrike' if overstrike else ''}"
+
+ def add_size_configure_callback(self, callback: Callable):
+ """ add function, that gets called when font got configured """
+ self._size_configure_callback_list.append(callback)
+
+ def remove_size_configure_callback(self, callback: Callable):
+ """ remove function, that gets called when font got configured """
+ self._size_configure_callback_list.remove(callback)
+
+ def create_scaled_tuple(self, font_scaling: float) -> Tuple[str, int, str]:
+
+ """ return scaled tuple representation of font in the form (family: str, size: int, style: str)"""
+ return self._family, round(-abs(self._size) * font_scaling), self._tuple_style_string
+
+ 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")
+
+ super().configure(**kwargs)
+
+ # update style string for create_scaled_tuple() method
+ self._tuple_style_string = f"{super().cget('weight')} {super().cget('slant')} {'underline' if super().cget('underline') else ''} {'overstrike' if super().cget('overstrike') else ''}"
+
+ # call all functions registered with add_size_configure_callback()
+ for callback in self._size_configure_callback_list:
+ callback()
+
+ 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)
+
+ def copy(self) -> "CTkFont":
+ return copy.deepcopy(self)
diff --git a/customtkinter/font_manager.py b/customtkinter/windows/widgets/font/font_manager.py
similarity index 98%
rename from customtkinter/font_manager.py
rename to customtkinter/windows/widgets/font/font_manager.py
index 91dfd04..b3ef369 100644
--- a/customtkinter/font_manager.py
+++ b/customtkinter/windows/widgets/font/font_manager.py
@@ -44,7 +44,7 @@ class FontManager:
flags = (FR_PRIVATE if private else 0) | (FR_NOT_ENUM if not enumerable else 0)
num_fonts_added = add_font_resource_ex(byref(path_buffer), flags, 0)
- return bool(num_fonts_added)
+ return bool(min(num_fonts_added, 1))
@classmethod
def load_font(cls, font_path: str) -> bool:
diff --git a/customtkinter/windows/widgets/image/__init__.py b/customtkinter/windows/widgets/image/__init__.py
new file mode 100644
index 0000000..b712c89
--- /dev/null
+++ b/customtkinter/windows/widgets/image/__init__.py
@@ -0,0 +1 @@
+from .ctk_image import CTkImage
diff --git a/customtkinter/windows/widgets/image/ctk_image.py b/customtkinter/windows/widgets/image/ctk_image.py
new file mode 100644
index 0000000..e197b56
--- /dev/null
+++ b/customtkinter/windows/widgets/image/ctk_image.py
@@ -0,0 +1,122 @@
+from typing import Tuple, Dict, Callable, List
+try:
+ from PIL import Image, ImageTk
+except ImportError:
+ pass
+
+
+class CTkImage:
+ """
+ Class to store one or two PIl.Image.Image objects and display size independent of scaling:
+
+ light_image: PIL.Image.Image for light mode
+ dark_image: PIL.Image.Image for dark mode
+ size: tuple (, ) with display size for both images
+
+ One of the two images can be None and will be replaced by the other image.
+ """
+
+ _checked_PIL_import = False
+
+ def __init__(self,
+ light_image: Image.Image = None,
+ dark_image: Image.Image = None,
+ size: Tuple[int, int] = (20, 20)):
+
+ if not self._checked_PIL_import:
+ self._check_pil_import()
+
+ self._light_image = light_image
+ self._dark_image = dark_image
+ self._check_images()
+ self._size = size
+
+ self._configure_callback_list: List[Callable] = []
+ self._scaled_light_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {}
+ self._scaled_dark_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {}
+
+ @classmethod
+ def _check_pil_import(cls):
+ try:
+ _, _ = Image, ImageTk
+ except NameError:
+ raise ImportError("PIL.Image and PIL.ImageTk couldn't be imported")
+
+ def add_configure_callback(self, callback: Callable):
+ """ add function, that gets called when image got configured """
+ self._configure_callback_list.append(callback)
+
+ def remove_configure_callback(self, callback: Callable):
+ """ remove function, that gets called when image got configured """
+ self._configure_callback_list.remove(callback)
+
+ def configure(self, **kwargs):
+ if "light_image" in 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")
+
+ # call all functions registered with add_configure_callback()
+ for callback in self._configure_callback_list:
+ callback()
+
+ def cget(self, attribute_name: str) -> any:
+ if attribute_name == "light_image":
+ return self._light_image
+ if attribute_name == "dark_image":
+ return self._dark_image
+ if attribute_name == "size":
+ return self._size
+
+ def _check_images(self):
+ # check types
+ if self._light_image is not None and not isinstance(self._light_image, Image.Image):
+ raise ValueError(f"CTkImage: light_image must be instance if PIL.Image.Image, not {type(self._light_image)}")
+ if self._dark_image is not None and not isinstance(self._dark_image, Image.Image):
+ raise ValueError(f"CTkImage: dark_image must be instance if PIL.Image.Image, not {type(self._dark_image)}")
+
+ # check values
+ if self._light_image is None and self._dark_image is None:
+ raise ValueError("CTkImage: No image given, light_image is None and dark_image is None.")
+
+ # check sizes
+ if self._light_image is not None and self._dark_image is not None and self._light_image.size != self._dark_image.size:
+ raise ValueError(f"CTkImage: light_image size {self._light_image.size} must be the same as dark_image size {self._dark_image.size}.")
+
+ def _get_scaled_size(self, widget_scaling: float) -> Tuple[int, int]:
+ return round(self._size[0] * widget_scaling), round(self._size[1] * widget_scaling)
+
+ def _get_scaled_light_photo_image(self, scaled_size: Tuple[int, int]) -> ImageTk.PhotoImage:
+ if scaled_size in self._scaled_light_photo_images:
+ return self._scaled_light_photo_images[scaled_size]
+ else:
+ self._scaled_light_photo_images[scaled_size] = ImageTk.PhotoImage(self._light_image.resize(scaled_size))
+ return self._scaled_light_photo_images[scaled_size]
+
+ def _get_scaled_dark_photo_image(self, scaled_size: Tuple[int, int]) -> ImageTk.PhotoImage:
+ if scaled_size in self._scaled_dark_photo_images:
+ return self._scaled_dark_photo_images[scaled_size]
+ else:
+ self._scaled_dark_photo_images[scaled_size] = ImageTk.PhotoImage(self._dark_image.resize(scaled_size))
+ return self._scaled_dark_photo_images[scaled_size]
+
+ def create_scaled_photo_image(self, widget_scaling: float, appearance_mode: str) -> ImageTk.PhotoImage:
+ scaled_size = self._get_scaled_size(widget_scaling)
+
+ if appearance_mode == "light" and self._light_image is not None:
+ return self._get_scaled_light_photo_image(scaled_size)
+ elif appearance_mode == "light" and self._light_image is None:
+ return self._get_scaled_dark_photo_image(scaled_size)
+
+ elif appearance_mode == "dark" and self._dark_image is not None:
+ return self._get_scaled_dark_photo_image(scaled_size)
+ elif appearance_mode == "dark" and self._dark_image is None:
+ return self._get_scaled_light_photo_image(scaled_size)
+
+
diff --git a/customtkinter/windows/widgets/scaling/__init__.py b/customtkinter/windows/widgets/scaling/__init__.py
new file mode 100644
index 0000000..8fc0db8
--- /dev/null
+++ b/customtkinter/windows/widgets/scaling/__init__.py
@@ -0,0 +1,7 @@
+import sys
+
+from .scaling_base_class import CTkScalingBaseClass
+from .scaling_tracker import ScalingTracker
+
+if sys.platform.startswith("win") and sys.getwindowsversion().build < 9000: # No automatic scaling on Windows < 8.1
+ ScalingTracker.deactivate_automatic_dpi_awareness = True
diff --git a/customtkinter/windows/widgets/scaling/scaling_base_class.py b/customtkinter/windows/widgets/scaling/scaling_base_class.py
new file mode 100644
index 0000000..74838bc
--- /dev/null
+++ b/customtkinter/windows/widgets/scaling/scaling_base_class.py
@@ -0,0 +1,159 @@
+from typing import Union, Tuple
+import copy
+import re
+try:
+ from typing import Literal
+except ImportError:
+ from typing_extensions import Literal
+
+from .scaling_tracker import ScalingTracker
+from ..font import CTkFont
+
+
+class CTkScalingBaseClass:
+ """
+ Super-class that manages the scaling values and callbacks.
+ Works for widgets and windows, type must be set in init method with
+ scaling_type attribute. Methods:
+
+ - _set_scaling() abstractmethod, gets called when scaling changes, must be overridden
+ - destroy() must be called when sub-class is destroyed
+ - _apply_widget_scaling()
+ - _reverse_widget_scaling()
+ - _apply_window_scaling()
+ - _reverse_window_scaling()
+ - _apply_font_scaling()
+ - _apply_argument_scaling()
+ - _apply_geometry_scaling()
+ - _reverse_geometry_scaling()
+ - _parse_geometry_string()
+
+ """
+ def __init__(self, scaling_type: Literal["widget", "window"] = "widget"):
+ self.__scaling_type = scaling_type
+
+ if self.__scaling_type == "widget":
+ ScalingTracker.add_widget(self._set_scaling, self) # add callback for automatic scaling changes
+ self.__widget_scaling = ScalingTracker.get_widget_scaling(self)
+ elif self.__scaling_type == "window":
+ ScalingTracker.activate_high_dpi_awareness() # make process DPI aware
+ ScalingTracker.add_window(self._set_scaling, self) # add callback for automatic scaling changes
+ self.__window_scaling = ScalingTracker.get_window_scaling(self)
+
+ def destroy(self):
+ if self.__scaling_type == "widget":
+ ScalingTracker.remove_widget(self._set_scaling, self)
+ elif self.__scaling_type == "window":
+ ScalingTracker.remove_window(self._set_scaling, self)
+
+ def _set_scaling(self, new_widget_scaling, new_window_scaling):
+ """ can be overridden, but super method must be called at the beginning """
+ self.__widget_scaling = new_widget_scaling
+ self.__window_scaling = new_window_scaling
+
+ def _get_widget_scaling(self) -> float:
+ return self.__widget_scaling
+
+ def _get_window_scaling(self) -> float:
+ return self.__window_scaling
+
+ def _apply_widget_scaling(self, value: Union[int, float]) -> Union[float]:
+ assert self.__scaling_type == "widget"
+ return value * self.__widget_scaling
+
+ def _reverse_widget_scaling(self, value: Union[int, float]) -> Union[float]:
+ assert self.__scaling_type == "widget"
+ return value / self.__widget_scaling
+
+ def _apply_window_scaling(self, value: Union[int, float]) -> int:
+ assert self.__scaling_type == "window"
+ return int(value * self.__window_scaling)
+
+ def _reverse_window_scaling(self, scaled_value: Union[int, float]) -> int:
+ assert self.__scaling_type == "window"
+ return int(scaled_value / self.__window_scaling)
+
+ def _apply_font_scaling(self, font: Union[Tuple, CTkFont]) -> tuple:
+ """ Takes CTkFont object and returns tuple font with scaled size, has to be called again for every change of font object """
+ assert self.__scaling_type == "widget"
+
+ if type(font) == tuple:
+ if len(font) == 1:
+ return font
+ elif len(font) == 2:
+ return font[0], -abs(round(font[1] * self.__widget_scaling))
+ elif len(font) == 3:
+ return font[0], -abs(round(font[1] * self.__widget_scaling)), font[2]
+ else:
+ raise ValueError(f"Can not scale font {font}. font needs to be tuple of len 1, 2 or 3")
+
+ elif isinstance(font, CTkFont):
+ return font.create_scaled_tuple(self.__widget_scaling)
+ else:
+ raise ValueError(f"Can not scale font '{font}' of type {type(font)}. font needs to be tuple or instance of CTkFont")
+
+ def _apply_argument_scaling(self, kwargs: dict) -> dict:
+ assert self.__scaling_type == "widget"
+
+ scaled_kwargs = copy.copy(kwargs)
+
+ # scale padding values
+ if "pady" in scaled_kwargs:
+ if isinstance(scaled_kwargs["pady"], (int, float)):
+ scaled_kwargs["pady"] = self._apply_widget_scaling(scaled_kwargs["pady"])
+ elif isinstance(scaled_kwargs["pady"], tuple):
+ scaled_kwargs["pady"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["pady"]])
+ if "padx" in kwargs:
+ if isinstance(scaled_kwargs["padx"], (int, float)):
+ scaled_kwargs["padx"] = self._apply_widget_scaling(scaled_kwargs["padx"])
+ elif isinstance(scaled_kwargs["padx"], tuple):
+ scaled_kwargs["padx"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["padx"]])
+
+ # scaled x, y values for place geometry manager
+ if "x" in scaled_kwargs:
+ scaled_kwargs["x"] = self._apply_widget_scaling(scaled_kwargs["x"])
+ if "y" in scaled_kwargs:
+ scaled_kwargs["y"] = self._apply_widget_scaling(scaled_kwargs["y"])
+
+ return scaled_kwargs
+
+ @staticmethod
+ def _parse_geometry_string(geometry_string: str) -> tuple:
+ # index: 1 2 3 4 5 6
+ # regex group structure: ('x', '', '', '+-+-', '-', '-')
+ result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
+
+ width = int(result.group(2)) if result.group(2) is not None else None
+ height = int(result.group(3)) if result.group(3) is not None else None
+ x = int(result.group(5)) if result.group(5) is not None else None
+ y = int(result.group(6)) if result.group(6) is not None else None
+
+ return width, height, x, y
+
+ def _apply_geometry_scaling(self, geometry_string: str) -> str:
+ assert self.__scaling_type == "window"
+
+ width, height, x, y = self._parse_geometry_string(geometry_string)
+
+ if x is None and y is None: # no and in geometry_string
+ return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}"
+
+ elif width is None and height is None: # no and in geometry_string
+ return f"+{x}+{y}"
+
+ else:
+ return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}+{x}+{y}"
+
+ def _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
+ assert self.__scaling_type == "window"
+
+ width, height, x, y = self._parse_geometry_string(scaled_geometry_string)
+
+ if x is None and y is None: # no and in geometry_string
+ return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}"
+
+ elif width is None and height is None: # no and in geometry_string
+ return f"+{x}+{y}"
+
+ else:
+ return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}+{x}+{y}"
diff --git a/customtkinter/scaling_tracker.py b/customtkinter/windows/widgets/scaling/scaling_tracker.py
similarity index 73%
rename from customtkinter/scaling_tracker.py
rename to customtkinter/windows/widgets/scaling/scaling_tracker.py
index 2e9983a..d3627c2 100644
--- a/customtkinter/scaling_tracker.py
+++ b/customtkinter/windows/widgets/scaling/scaling_tracker.py
@@ -11,21 +11,16 @@ class ScalingTracker:
widget_scaling = 1 # user values which multiply to detected window scaling factor
window_scaling = 1
- spacing_scaling = 1
update_loop_running = False
- update_loop_interval = 150 # milliseconds
+ update_loop_interval = 100 # ms
+ loop_pause_after_new_scaling = 1500 # ms
@classmethod
def get_widget_scaling(cls, widget) -> float:
window_root = cls.get_window_root_of_widget(widget)
return cls.window_dpi_scaling_dict[window_root] * cls.widget_scaling
- @classmethod
- def get_spacing_scaling(cls, widget) -> float:
- window_root = cls.get_window_root_of_widget(widget)
- return cls.window_dpi_scaling_dict[window_root] * cls.spacing_scaling
-
@classmethod
def get_window_scaling(cls, window) -> float:
window_root = cls.get_window_root_of_widget(window)
@@ -36,11 +31,6 @@ class ScalingTracker:
cls.widget_scaling = max(widget_scaling_factor, 0.4)
cls.update_scaling_callbacks_all()
- @classmethod
- def set_spacing_scaling(cls, spacing_scaling_factor: float):
- cls.spacing_scaling = max(spacing_scaling_factor, 0.4)
- cls.update_scaling_callbacks_all()
-
@classmethod
def set_window_scaling(cls, window_scaling_factor: float):
cls.window_scaling = max(window_scaling_factor, 0.4)
@@ -62,11 +52,9 @@ class ScalingTracker:
for set_scaling_callback in callback_list:
if not cls.deactivate_automatic_dpi_awareness:
set_scaling_callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling,
- cls.window_dpi_scaling_dict[window] * cls.spacing_scaling,
cls.window_dpi_scaling_dict[window] * cls.window_scaling)
else:
set_scaling_callback(cls.widget_scaling,
- cls.spacing_scaling,
cls.window_scaling)
@classmethod
@@ -74,11 +62,9 @@ class ScalingTracker:
for set_scaling_callback in cls.window_widgets_dict[window]:
if not cls.deactivate_automatic_dpi_awareness:
set_scaling_callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling,
- cls.window_dpi_scaling_dict[window] * cls.spacing_scaling,
cls.window_dpi_scaling_dict[window] * cls.window_scaling)
else:
set_scaling_callback(cls.widget_scaling,
- cls.spacing_scaling,
cls.window_scaling)
@classmethod
@@ -132,9 +118,32 @@ class ScalingTracker:
pass # high DPI scaling works automatically on macOS
elif sys.platform.startswith("win"):
- from ctypes import windll
- windll.shcore.SetProcessDpiAwareness(2)
- # Microsoft Docs: https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness
+ import ctypes
+
+ # Values for SetProcessDpiAwareness and SetProcessDpiAwarenessContext:
+ # internal enum PROCESS_DPI_AWARENESS
+ # {
+ # Process_DPI_Unaware = 0,
+ # Process_System_DPI_Aware = 1,
+ # Process_Per_Monitor_DPI_Aware = 2
+ # }
+ #
+ # internal enum DPI_AWARENESS_CONTEXT
+ # {
+ # DPI_AWARENESS_CONTEXT_UNAWARE = 16,
+ # DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = 17,
+ # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = 18,
+ # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = 34
+ # }
+
+ # ctypes.windll.user32.SetProcessDpiAwarenessContext(34) # Non client area scaling at runtime (titlebar)
+ # does not work with resizable(False, False), window starts growing on monitor with different scaling (weird tkinter bug...)
+ # ctypes.windll.user32.EnableNonClientDpiScaling(hwnd) does not work for some reason (tested on Windows 11)
+
+ # It's too bad, that these Windows API methods don't work properly with tkinter. But I tested days with multiple monitor setups,
+ # and I don't think there is anything left to do. So this is the best option at the moment:
+
+ ctypes.windll.shcore.SetProcessDpiAwareness(2) # Titlebar does not scale at runtime
else:
pass # DPI awareness on Linux not implemented
@@ -162,18 +171,34 @@ class ScalingTracker:
@classmethod
def check_dpi_scaling(cls):
+ new_scaling_detected = False
+
# check for every window if scaling value changed
for window in cls.window_widgets_dict:
- if window.winfo_exists():
+ if window.winfo_exists() and not window.state() == "iconic":
current_dpi_scaling_value = cls.get_window_dpi_scaling(window)
if current_dpi_scaling_value != cls.window_dpi_scaling_dict[window]:
cls.window_dpi_scaling_dict[window] = current_dpi_scaling_value
+
+ if sys.platform.startswith("win"):
+ window.attributes("-alpha", 0.15)
+
+ window.block_update_dimensions_event()
cls.update_scaling_callbacks_for_window(window)
+ window.unblock_update_dimensions_event()
+
+ if sys.platform.startswith("win"):
+ window.attributes("-alpha", 1)
+
+ new_scaling_detected = True
# find an existing tkinter object for the next call of .after()
for app in cls.window_widgets_dict.keys():
try:
- app.after(cls.update_loop_interval, cls.check_dpi_scaling)
+ if new_scaling_detected:
+ app.after(cls.loop_pause_after_new_scaling, cls.check_dpi_scaling)
+ else:
+ app.after(cls.update_loop_interval, cls.check_dpi_scaling)
return
except Exception:
continue
diff --git a/customtkinter/windows/widgets/theme/__init__.py b/customtkinter/windows/widgets/theme/__init__.py
new file mode 100644
index 0000000..bd7395a
--- /dev/null
+++ b/customtkinter/windows/widgets/theme/__init__.py
@@ -0,0 +1,9 @@
+from .theme_manager import ThemeManager
+
+# load default blue theme
+try:
+ ThemeManager.load_theme("blue")
+except FileNotFoundError as err:
+ raise FileNotFoundError(f"{err}\n\nThe .json theme file for CustomTkinter could not be found.\n" +
+ f"If packaging with pyinstaller was used, have a look at the wiki:\n" +
+ f"https://github.com/TomSchimansky/CustomTkinter/wiki/Packaging#windows-pyinstaller-auto-py-to-exe")
diff --git a/customtkinter/windows/widgets/theme/theme_manager.py b/customtkinter/windows/widgets/theme/theme_manager.py
new file mode 100644
index 0000000..e04b679
--- /dev/null
+++ b/customtkinter/windows/widgets/theme/theme_manager.py
@@ -0,0 +1,47 @@
+import sys
+import os
+import json
+from typing import List, Union
+
+
+class ThemeManager:
+
+ theme: dict = {} # contains all the theme data
+ _built_in_themes: List[str] = ["blue", "green", "dark-blue", "sweetkind"]
+ _currently_loaded_theme: Union[str, None] = None
+
+ @classmethod
+ def load_theme(cls, theme_name_or_path: str):
+ script_directory = os.path.dirname(os.path.abspath(__file__))
+
+ if theme_name_or_path in cls._built_in_themes:
+ with open(os.path.join(script_directory, "../../../assets", "themes", f"{theme_name_or_path}.json"), "r") as f:
+ cls.theme = json.load(f)
+ else:
+ with open(theme_name_or_path, "r") as f:
+ cls.theme = json.load(f)
+
+ # store theme path for saving
+ cls._currently_loaded_theme = theme_name_or_path
+
+ # filter theme values for platform
+ for key in cls.theme.keys():
+ # check if values for key differ on platforms
+ if "macOS" in cls.theme[key].keys():
+ if sys.platform == "darwin":
+ cls.theme[key] = cls.theme[key]["macOS"]
+ elif sys.platform.startswith("win"):
+ cls.theme[key] = cls.theme[key]["Windows"]
+ else:
+ cls.theme[key] = cls.theme[key]["Linux"]
+
+ @classmethod
+ def save_theme(cls):
+ if cls._currently_loaded_theme is not None:
+ if cls._currently_loaded_theme not in cls._built_in_themes:
+ with open(cls._currently_loaded_theme, "r") as f:
+ json.dump(cls.theme, f, indent=2)
+ else:
+ raise ValueError(f"cannot modify builtin theme '{cls._currently_loaded_theme}'")
+ else:
+ raise ValueError(f"cannot save theme, no theme is loaded")
diff --git a/customtkinter/windows/widgets/utility/__init__.py b/customtkinter/windows/widgets/utility/__init__.py
new file mode 100644
index 0000000..c4b6fe8
--- /dev/null
+++ b/customtkinter/windows/widgets/utility/__init__.py
@@ -0,0 +1 @@
+from .utility_functions import pop_from_dict_by_set, check_kwargs_empty
diff --git a/customtkinter/windows/widgets/utility/utility_functions.py b/customtkinter/windows/widgets/utility/utility_functions.py
new file mode 100644
index 0000000..a9968bb
--- /dev/null
+++ b/customtkinter/windows/widgets/utility/utility_functions.py
@@ -0,0 +1,22 @@
+
+def pop_from_dict_by_set(dictionary: dict, valid_keys: set) -> dict:
+ """ remove and create new dict with key value pairs of dictionary, where key is in valid_keys """
+ new_dictionary = {}
+
+ for key in list(dictionary.keys()):
+ if key in valid_keys:
+ new_dictionary[key] = dictionary.pop(key)
+
+ return new_dictionary
+
+
+def check_kwargs_empty(kwargs_dict, raise_error=False) -> bool:
+ """ returns True if kwargs are empty, False otherwise, raises error if not empty """
+
+ if len(kwargs_dict) > 0:
+ if raise_error:
+ raise ValueError(f"{list(kwargs_dict.keys())} are not supported arguments. Look at the documentation for supported arguments.")
+ else:
+ return True
+ else:
+ return False
diff --git a/documentation_images/CustomTkinter_logo_dark.png b/documentation_images/CustomTkinter_logo_dark.png
new file mode 100644
index 0000000..8bd2744
Binary files /dev/null and b/documentation_images/CustomTkinter_logo_dark.png differ
diff --git a/documentation_images/CustomTkinter_logo_light.png b/documentation_images/CustomTkinter_logo_light.png
new file mode 100644
index 0000000..5ec1cfa
Binary files /dev/null and b/documentation_images/CustomTkinter_logo_light.png differ
diff --git a/documentation_images/Ubuntu_dark.png b/documentation_images/Ubuntu_dark.png
deleted file mode 100644
index d92be28..0000000
Binary files a/documentation_images/Ubuntu_dark.png and /dev/null differ
diff --git a/documentation_images/Windows_button_images_dark.png b/documentation_images/Windows_button_images_dark.png
deleted file mode 100644
index 795c24a..0000000
Binary files a/documentation_images/Windows_button_images_dark.png and /dev/null differ
diff --git a/documentation_images/Windows_button_images_light.png b/documentation_images/Windows_button_images_light.png
deleted file mode 100644
index d44e33b..0000000
Binary files a/documentation_images/Windows_button_images_light.png and /dev/null differ
diff --git a/documentation_images/Windows_dark.png b/documentation_images/Windows_dark.png
deleted file mode 100644
index 9d284ad..0000000
Binary files a/documentation_images/Windows_dark.png and /dev/null differ
diff --git a/documentation_images/Windows_manual_mode_change.gif b/documentation_images/Windows_manual_mode_change.gif
deleted file mode 100644
index 86aa5ab..0000000
Binary files a/documentation_images/Windows_manual_mode_change.gif and /dev/null differ
diff --git a/documentation_images/Windows_scaling.png b/documentation_images/Windows_scaling.png
deleted file mode 100755
index cf2737b..0000000
Binary files a/documentation_images/Windows_scaling.png and /dev/null differ
diff --git a/documentation_images/Windows_simple_example_dark.png b/documentation_images/Windows_simple_example_dark.png
deleted file mode 100644
index 913374c..0000000
Binary files a/documentation_images/Windows_simple_example_dark.png and /dev/null differ
diff --git a/documentation_images/Windows_system_mode_change.gif b/documentation_images/Windows_system_mode_change.gif
deleted file mode 100644
index 9a355bb..0000000
Binary files a/documentation_images/Windows_system_mode_change.gif and /dev/null differ
diff --git a/documentation_images/Winodws_light.png b/documentation_images/Winodws_light.png
deleted file mode 100644
index fbeeab5..0000000
Binary files a/documentation_images/Winodws_light.png and /dev/null differ
diff --git a/documentation_images/complex_example_dark_Windows.png b/documentation_images/complex_example_dark_Windows.png
new file mode 100644
index 0000000..c15703f
Binary files /dev/null and b/documentation_images/complex_example_dark_Windows.png differ
diff --git a/documentation_images/complex_example_light_macOS.png b/documentation_images/complex_example_light_macOS.png
new file mode 100644
index 0000000..80a7c0c
Binary files /dev/null and b/documentation_images/complex_example_light_macOS.png differ
diff --git a/documentation_images/image_example_dark_Windows.png b/documentation_images/image_example_dark_Windows.png
new file mode 100644
index 0000000..46aaa83
Binary files /dev/null and b/documentation_images/image_example_dark_Windows.png differ
diff --git a/documentation_images/macOS_button_dark.png b/documentation_images/macOS_button_dark.png
deleted file mode 100644
index feb1d1c..0000000
Binary files a/documentation_images/macOS_button_dark.png and /dev/null differ
diff --git a/documentation_images/macOS_button_images.png b/documentation_images/macOS_button_images.png
deleted file mode 100644
index e187ca8..0000000
Binary files a/documentation_images/macOS_button_images.png and /dev/null differ
diff --git a/documentation_images/macOS_dark.png b/documentation_images/macOS_dark.png
deleted file mode 100644
index 767f849..0000000
Binary files a/documentation_images/macOS_dark.png and /dev/null differ
diff --git a/documentation_images/macOS_light.png b/documentation_images/macOS_light.png
deleted file mode 100644
index 1948a24..0000000
Binary files a/documentation_images/macOS_light.png and /dev/null differ
diff --git a/documentation_images/macOS_system_mode_change.gif b/documentation_images/macOS_system_mode_change.gif
deleted file mode 100644
index 416838a..0000000
Binary files a/documentation_images/macOS_system_mode_change.gif and /dev/null differ
diff --git a/documentation_images/single_button_macOS.png b/documentation_images/single_button_macOS.png
new file mode 100644
index 0000000..cbb2eb4
Binary files /dev/null and b/documentation_images/single_button_macOS.png differ
diff --git a/documentation_images/tkinter_customtkinter_comparison.jpg b/documentation_images/tkinter_customtkinter_comparison.jpg
deleted file mode 100644
index 53ba517..0000000
Binary files a/documentation_images/tkinter_customtkinter_comparison.jpg and /dev/null differ
diff --git a/documentation_images/tkintermapview_example.gif b/documentation_images/tkintermapview_example.gif
deleted file mode 100644
index d177319..0000000
Binary files a/documentation_images/tkintermapview_example.gif and /dev/null differ
diff --git a/examples/complex_example.py b/examples/complex_example.py
index 199741a..978a706 100644
--- a/examples/complex_example.py
+++ b/examples/complex_example.py
@@ -7,184 +7,148 @@ customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "gre
class App(customtkinter.CTk):
-
- WIDTH = 780
- HEIGHT = 520
-
def __init__(self):
super().__init__()
+ # configure window
self.title("CustomTkinter complex_example.py")
- self.geometry(f"{App.WIDTH}x{App.HEIGHT}")
- self.protocol("WM_DELETE_WINDOW", self.on_closing) # call .on_closing() when app gets closed
+ self.geometry(f"{1100}x{580}")
- # ============ create two frames ============
-
- # configure grid layout (2x1)
+ # configure grid layout (4x4)
self.grid_columnconfigure(1, weight=1)
- self.grid_rowconfigure(0, weight=1)
+ self.grid_columnconfigure((2, 3), weight=0)
+ self.grid_rowconfigure((0, 1, 2), weight=1)
- self.frame_left = customtkinter.CTkFrame(master=self,
- width=180,
- corner_radius=0)
- self.frame_left.grid(row=0, column=0, sticky="nswe")
+ # create sidebar frame with widgets
+ self.sidebar_frame = customtkinter.CTkFrame(self, width=140, corner_radius=0)
+ self.sidebar_frame.grid(row=0, column=0, rowspan=4, sticky="nsew")
+ self.sidebar_frame.grid_rowconfigure(4, weight=1)
+ self.logo_label = customtkinter.CTkLabel(self.sidebar_frame, text="CustomTkinter", font=customtkinter.CTkFont(size=20, weight="bold"))
+ self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
+ self.sidebar_button_1 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_event)
+ self.sidebar_button_1.grid(row=1, column=0, padx=20, pady=10)
+ self.sidebar_button_2 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_event)
+ self.sidebar_button_2.grid(row=2, column=0, padx=20, pady=10)
+ self.sidebar_button_3 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_event)
+ self.sidebar_button_3.grid(row=3, column=0, padx=20, pady=10)
+ self.appearance_mode_label = customtkinter.CTkLabel(self.sidebar_frame, text="Appearance Mode:", anchor="w")
+ self.appearance_mode_label.grid(row=5, column=0, padx=20, pady=(10, 0))
+ self.appearance_mode_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["Light", "Dark", "System"],
+ command=self.change_appearance_mode_event)
+ self.appearance_mode_optionemenu.grid(row=6, column=0, padx=20, pady=(10, 10))
+ self.scaling_label = customtkinter.CTkLabel(self.sidebar_frame, text="UI Scaling:", anchor="w")
+ self.scaling_label.grid(row=7, column=0, padx=20, pady=(10, 0))
+ self.scaling_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["80%", "90%", "100%", "110%", "120%"],
+ command=self.change_scaling_event)
+ self.scaling_optionemenu.grid(row=8, column=0, padx=20, pady=(10, 20))
- self.frame_right = customtkinter.CTkFrame(master=self)
- self.frame_right.grid(row=0, column=1, sticky="nswe", padx=20, pady=20)
+ # create main entry and button
+ self.entry = customtkinter.CTkEntry(self, placeholder_text="CTkEntry")
+ self.entry.grid(row=3, column=1, columnspan=2, padx=(20, 0), pady=(20, 20), sticky="nsew")
- # ============ frame_left ============
+ self.main_button_1 = customtkinter.CTkButton(master=self, fg_color="transparent", border_width=2, text_color=("gray10", "#DCE4EE"))
+ self.main_button_1.grid(row=3, column=3, padx=(20, 20), pady=(20, 20), sticky="nsew")
- # configure grid layout (1x11)
- self.frame_left.grid_rowconfigure(0, minsize=10) # empty row with minsize as spacing
- self.frame_left.grid_rowconfigure(5, weight=1) # empty row as spacing
- self.frame_left.grid_rowconfigure(8, minsize=20) # empty row with minsize as spacing
- self.frame_left.grid_rowconfigure(11, minsize=10) # empty row with minsize as spacing
+ # create textbox
+ self.textbox = customtkinter.CTkTextbox(self, width=250)
+ self.textbox.grid(row=0, column=1, padx=(20, 0), pady=(20, 0), sticky="nsew")
- self.label_1 = customtkinter.CTkLabel(master=self.frame_left,
- text="CustomTkinter",
- text_font=("Roboto Medium", -16)) # font name and size in px
- self.label_1.grid(row=1, column=0, pady=10, padx=10)
+ # create tabview
+ self.tabview = customtkinter.CTkTabview(self, width=250)
+ self.tabview.grid(row=0, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
+ self.tabview.add("CTkTabview")
+ self.tabview.add("Tab 2")
+ self.tabview.add("Tab 3")
+ self.tabview.tab("CTkTabview").grid_columnconfigure(0, weight=1) # configure grid of individual tabs
+ self.tabview.tab("Tab 2").grid_columnconfigure(0, weight=1)
- self.button_1 = customtkinter.CTkButton(master=self.frame_left,
- text="CTkButton",
- command=self.button_event)
- self.button_1.grid(row=2, column=0, pady=10, padx=20)
-
- self.button_2 = customtkinter.CTkButton(master=self.frame_left,
- text="CTkButton",
- command=self.button_event)
- self.button_2.grid(row=3, column=0, pady=10, padx=20)
-
- self.button_3 = customtkinter.CTkButton(master=self.frame_left,
- text="CTkButton",
- command=self.button_event)
- self.button_3.grid(row=4, column=0, pady=10, padx=20)
-
- self.label_mode = customtkinter.CTkLabel(master=self.frame_left, text="Appearance Mode:")
- self.label_mode.grid(row=9, column=0, pady=0, padx=20, sticky="w")
-
- self.optionmenu_1 = customtkinter.CTkOptionMenu(master=self.frame_left,
- values=["Light", "Dark", "System"],
- command=self.change_appearance_mode)
- self.optionmenu_1.grid(row=10, column=0, pady=10, padx=20, sticky="w")
-
- # ============ frame_right ============
-
- # configure grid layout (3x7)
- self.frame_right.rowconfigure((0, 1, 2, 3), weight=1)
- self.frame_right.rowconfigure(7, weight=10)
- self.frame_right.columnconfigure((0, 1), weight=1)
- self.frame_right.columnconfigure(2, weight=0)
-
- self.frame_info = customtkinter.CTkFrame(master=self.frame_right)
- self.frame_info.grid(row=0, column=0, columnspan=2, rowspan=4, pady=20, padx=20, sticky="nsew")
-
- # ============ frame_info ============
-
- # configure grid layout (1x1)
- self.frame_info.rowconfigure(0, weight=1)
- self.frame_info.columnconfigure(0, weight=1)
-
- self.label_info_1 = customtkinter.CTkLabel(master=self.frame_info,
- text="CTkLabel: Lorem ipsum dolor sit,\n" +
- "amet consetetur sadipscing elitr,\n" +
- "sed diam nonumy eirmod tempor" ,
- height=100,
- corner_radius=6, # <- custom corner radius
- fg_color=("white", "gray38"), # <- custom tuple-color
- justify=tkinter.LEFT)
- self.label_info_1.grid(column=0, row=0, sticky="nwe", padx=15, pady=15)
-
- self.progressbar = customtkinter.CTkProgressBar(master=self.frame_info)
- self.progressbar.grid(row=1, column=0, sticky="ew", padx=15, pady=15)
-
- # ============ frame_right ============
+ self.optionmenu_1 = customtkinter.CTkOptionMenu(self.tabview.tab("CTkTabview"), dynamic_resizing=False,
+ values=["Value 1", "Value 2", "Value Long Long Long"])
+ self.optionmenu_1.grid(row=0, column=0, padx=20, pady=(20, 10))
+ self.combobox_1 = customtkinter.CTkComboBox(self.tabview.tab("CTkTabview"),
+ values=["Value 1", "Value 2", "Value Long....."])
+ self.combobox_1.grid(row=1, column=0, padx=20, pady=(10, 10))
+ self.string_input_button = customtkinter.CTkButton(self.tabview.tab("CTkTabview"), text="Open CTkInputDialog",
+ command=self.open_input_dialog_event)
+ self.string_input_button.grid(row=2, column=0, padx=20, pady=(10, 10))
+ self.label_tab_2 = customtkinter.CTkLabel(self.tabview.tab("Tab 2"), text="CTkLabel on Tab 2")
+ self.label_tab_2.grid(row=0, column=0, padx=20, pady=20)
+ # create radiobutton frame
+ self.radiobutton_frame = customtkinter.CTkFrame(self)
+ self.radiobutton_frame.grid(row=0, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew")
self.radio_var = tkinter.IntVar(value=0)
-
- self.label_radio_group = customtkinter.CTkLabel(master=self.frame_right,
- text="CTkRadioButton Group:")
- self.label_radio_group.grid(row=0, column=2, columnspan=1, pady=20, padx=10, sticky="")
-
- self.radio_button_1 = customtkinter.CTkRadioButton(master=self.frame_right,
- variable=self.radio_var,
- value=0)
+ self.label_radio_group = customtkinter.CTkLabel(master=self.radiobutton_frame, text="CTkRadioButton Group:")
+ self.label_radio_group.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="")
+ self.radio_button_1 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=0)
self.radio_button_1.grid(row=1, column=2, pady=10, padx=20, sticky="n")
-
- self.radio_button_2 = customtkinter.CTkRadioButton(master=self.frame_right,
- variable=self.radio_var,
- value=1)
+ self.radio_button_2 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=1)
self.radio_button_2.grid(row=2, column=2, pady=10, padx=20, sticky="n")
-
- self.radio_button_3 = customtkinter.CTkRadioButton(master=self.frame_right,
- variable=self.radio_var,
- value=2)
+ self.radio_button_3 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=2)
self.radio_button_3.grid(row=3, column=2, pady=10, padx=20, sticky="n")
- self.slider_1 = customtkinter.CTkSlider(master=self.frame_right,
- from_=0,
- to=1,
- number_of_steps=3,
- command=self.progressbar.set)
- self.slider_1.grid(row=4, column=0, columnspan=2, pady=10, padx=20, sticky="we")
+ # create checkbox and switch frame
+ self.checkbox_slider_frame = customtkinter.CTkFrame(self)
+ self.checkbox_slider_frame.grid(row=1, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew")
+ self.checkbox_1 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame)
+ self.checkbox_1.grid(row=1, column=0, pady=(20, 10), padx=20, sticky="n")
+ self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame)
+ self.checkbox_2.grid(row=2, column=0, pady=10, padx=20, sticky="n")
+ self.switch_1 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame, command=lambda: print("switch 1 toggle"))
+ self.switch_1.grid(row=3, column=0, pady=10, padx=20, sticky="n")
+ self.switch_2 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame)
+ self.switch_2.grid(row=4, column=0, pady=(10, 20), padx=20, sticky="n")
- self.slider_2 = customtkinter.CTkSlider(master=self.frame_right,
- command=self.progressbar.set)
- self.slider_2.grid(row=5, column=0, columnspan=2, pady=10, padx=20, sticky="we")
-
- self.switch_1 = customtkinter.CTkSwitch(master=self.frame_right,
- text="CTkSwitch")
- self.switch_1.grid(row=4, column=2, columnspan=1, pady=10, padx=20, sticky="we")
-
- self.switch_2 = customtkinter.CTkSwitch(master=self.frame_right,
- text="CTkSwitch")
- self.switch_2.grid(row=5, column=2, columnspan=1, pady=10, padx=20, sticky="we")
-
- self.combobox_1 = customtkinter.CTkComboBox(master=self.frame_right,
- values=["Value 1", "Value 2"])
- self.combobox_1.grid(row=6, column=2, columnspan=1, pady=10, padx=20, sticky="we")
-
- self.check_box_1 = customtkinter.CTkCheckBox(master=self.frame_right,
- text="CTkCheckBox")
- self.check_box_1.grid(row=6, column=0, pady=10, padx=20, sticky="w")
-
- self.check_box_2 = customtkinter.CTkCheckBox(master=self.frame_right,
- text="CTkCheckBox")
- self.check_box_2.grid(row=6, column=1, pady=10, padx=20, sticky="w")
-
- self.entry = customtkinter.CTkEntry(master=self.frame_right,
- width=120,
- placeholder_text="CTkEntry")
- self.entry.grid(row=8, column=0, columnspan=2, pady=20, padx=20, sticky="we")
-
- self.button_5 = customtkinter.CTkButton(master=self.frame_right,
- text="CTkButton",
- border_width=2, # <- custom border_width
- fg_color=None, # <- no fg_color
- command=self.button_event)
- self.button_5.grid(row=8, column=2, columnspan=1, pady=20, padx=20, sticky="we")
+ # create slider and progressbar frame
+ self.slider_progressbar_frame = customtkinter.CTkFrame(self, fg_color="transparent")
+ self.slider_progressbar_frame.grid(row=1, column=1, columnspan=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
+ self.slider_progressbar_frame.grid_columnconfigure(0, weight=1)
+ self.slider_progressbar_frame.grid_rowconfigure(4, weight=1)
+ self.seg_button_1 = customtkinter.CTkSegmentedButton(self.slider_progressbar_frame)
+ self.seg_button_1.grid(row=0, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
+ self.progressbar_1 = customtkinter.CTkProgressBar(self.slider_progressbar_frame)
+ self.progressbar_1.grid(row=1, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
+ self.progressbar_2 = customtkinter.CTkProgressBar(self.slider_progressbar_frame)
+ self.progressbar_2.grid(row=2, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
+ self.slider_1 = customtkinter.CTkSlider(self.slider_progressbar_frame, from_=0, to=1, number_of_steps=4)
+ self.slider_1.grid(row=3, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
+ self.slider_2 = customtkinter.CTkSlider(self.slider_progressbar_frame, orientation="vertical")
+ self.slider_2.grid(row=0, column=1, rowspan=5, padx=(10, 10), pady=(10, 10), sticky="ns")
+ self.progressbar_3 = customtkinter.CTkProgressBar(self.slider_progressbar_frame, orientation="vertical")
+ self.progressbar_3.grid(row=0, column=2, rowspan=5, padx=(10, 20), pady=(10, 10), sticky="ns")
# set default values
- self.optionmenu_1.set("Dark")
- self.button_3.configure(state="disabled", text="Disabled CTkButton")
- self.combobox_1.set("CTkCombobox")
- self.radio_button_1.select()
- self.slider_1.set(0.2)
- self.slider_2.set(0.7)
- self.progressbar.set(0.5)
- self.switch_2.select()
- self.radio_button_3.configure(state=tkinter.DISABLED)
- self.check_box_1.configure(state=tkinter.DISABLED, text="CheckBox disabled")
- self.check_box_2.select()
+ self.sidebar_button_3.configure(state="disabled", text="Disabled CTkButton")
+ self.checkbox_2.configure(state="disabled")
+ self.switch_2.configure(state="disabled")
+ self.checkbox_1.select()
+ self.switch_1.select()
+ self.radio_button_3.configure(state="disabled")
+ self.appearance_mode_optionemenu.set("Dark")
+ self.scaling_optionemenu.set("100%")
+ self.optionmenu_1.set("CTkOptionmenu")
+ self.combobox_1.set("CTkComboBox")
+ self.slider_1.configure(command=self.progressbar_2.set)
+ self.slider_2.configure(command=self.progressbar_3.set)
+ self.progressbar_1.configure(mode="indeterminnate")
+ self.progressbar_1.start()
+ 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.seg_button_1.configure(values=["CTkSegmentedButton", "Value 2", "Value 3"])
+ self.seg_button_1.set("Value 2")
- def button_event(self):
- print("Button pressed")
+ def open_input_dialog_event(self):
+ dialog = customtkinter.CTkInputDialog(text="Type in a number:", title="CTkInputDialog")
+ print("CTkInputDialog:", dialog.get_input())
- def change_appearance_mode(self, new_appearance_mode):
+ def change_appearance_mode_event(self, new_appearance_mode: str):
customtkinter.set_appearance_mode(new_appearance_mode)
- def on_closing(self, event=0):
- self.destroy()
+ def change_scaling_event(self, new_scaling: str):
+ new_scaling_float = int(new_scaling.replace("%", "")) / 100
+ customtkinter.set_widget_scaling(new_scaling_float)
+
+ def sidebar_button_event(self):
+ print("sidebar_button click")
if __name__ == "__main__":
diff --git a/examples/example_background_image.py b/examples/example_background_image.py
index 5771843..6a95841 100644
--- a/examples/example_background_image.py
+++ b/examples/example_background_image.py
@@ -1,69 +1,61 @@
-import tkinter
-import tkinter.messagebox
import customtkinter
-from PIL import Image, ImageTk
+from PIL import Image
import os
-customtkinter.set_appearance_mode("Dark") # Modes: "System" (standard), "Dark", "Light"
-customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
-
-PATH = os.path.dirname(os.path.realpath(__file__))
+customtkinter.set_appearance_mode("dark")
class App(customtkinter.CTk):
-
- APP_NAME = "CustomTkinter example_background_image.py"
- WIDTH = 900
- HEIGHT = 600
+ width = 900
+ height = 600
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.title(App.APP_NAME)
- self.geometry(f"{App.WIDTH}x{App.HEIGHT}")
- self.minsize(App.WIDTH, App.HEIGHT)
- self.maxsize(App.WIDTH, App.HEIGHT)
+ self.title("CustomTkinter example_background_image.py")
+ self.geometry(f"{self.width}x{self.height}")
self.resizable(False, False)
- self.protocol("WM_DELETE_WINDOW", self.on_closing)
+ # load and create background image
+ current_path = os.path.dirname(os.path.realpath(__file__))
+ self.bg_image = customtkinter.CTkImage(Image.open(current_path + "/test_images/bg_gradient.jpg"),
+ size=(self.width, self.height))
+ self.bg_image_label = customtkinter.CTkLabel(self, image=self.bg_image)
+ self.bg_image_label.grid(row=0, column=0)
- # load image with PIL and convert to PhotoImage
- image = Image.open(PATH + "/test_images/bg_gradient.jpg").resize((self.WIDTH, self.HEIGHT))
- self.bg_image = ImageTk.PhotoImage(image)
+ # create login frame
+ self.login_frame = customtkinter.CTkFrame(self, corner_radius=0)
+ self.login_frame.grid(row=0, column=0, sticky="ns")
+ self.login_label = customtkinter.CTkLabel(self.login_frame, text="CustomTkinter\nLogin Page",
+ font=customtkinter.CTkFont(size=20, weight="bold"))
+ self.login_label.grid(row=0, column=0, padx=30, pady=(150, 15))
+ self.username_entry = customtkinter.CTkEntry(self.login_frame, width=200, placeholder_text="username")
+ self.username_entry.grid(row=1, column=0, padx=30, pady=(15, 15))
+ self.password_entry = customtkinter.CTkEntry(self.login_frame, width=200, show="*", placeholder_text="password")
+ self.password_entry.grid(row=2, column=0, padx=30, pady=(0, 15))
+ self.login_button = customtkinter.CTkButton(self.login_frame, text="Login", command=self.login_event, width=200)
+ self.login_button.grid(row=3, column=0, padx=30, pady=(15, 15))
- self.image_label = tkinter.Label(master=self, image=self.bg_image)
- self.image_label.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
+ # create main frame
+ self.main_frame = customtkinter.CTkFrame(self, corner_radius=0)
+ self.main_frame.grid_columnconfigure(0, weight=1)
+ self.main_label = customtkinter.CTkLabel(self.main_frame, text="CustomTkinter\nMain Page",
+ font=customtkinter.CTkFont(size=20, weight="bold"))
+ self.main_label.grid(row=0, column=0, padx=30, pady=(30, 15))
+ self.back_button = customtkinter.CTkButton(self.main_frame, text="Back", command=self.back_event, width=200)
+ self.back_button.grid(row=1, column=0, padx=30, pady=(15, 15))
- self.frame = customtkinter.CTkFrame(master=self,
- width=300,
- height=App.HEIGHT,
- corner_radius=0)
- self.frame.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
+ def login_event(self):
+ print("Login pressed - username:", self.username_entry.get(), "password:", self.password_entry.get())
- self.label_1 = customtkinter.CTkLabel(master=self.frame, width=200, height=60,
- fg_color=("gray70", "gray25"), text="CustomTkinter\ninterface example")
- self.label_1.place(relx=0.5, rely=0.3, anchor=tkinter.CENTER)
+ self.login_frame.grid_forget() # remove login frame
+ self.main_frame.grid(row=0, column=0, sticky="nsew", padx=100) # show main frame
- self.entry_1 = customtkinter.CTkEntry(master=self.frame, corner_radius=6, width=200, placeholder_text="username")
- self.entry_1.place(relx=0.5, rely=0.52, anchor=tkinter.CENTER)
-
- self.entry_2 = customtkinter.CTkEntry(master=self.frame, corner_radius=6, width=200, show="*", placeholder_text="password")
- self.entry_2.place(relx=0.5, rely=0.6, anchor=tkinter.CENTER)
-
- self.button_2 = customtkinter.CTkButton(master=self.frame, text="Login",
- corner_radius=6, command=self.button_event, width=200)
- self.button_2.place(relx=0.5, rely=0.7, anchor=tkinter.CENTER)
-
- def button_event(self):
- print("Login pressed - username:", self.entry_1.get(), "password:", self.entry_2.get())
-
- def on_closing(self, event=0):
- self.destroy()
-
- def start(self):
- self.mainloop()
+ def back_event(self):
+ self.main_frame.grid_forget() # remove main frame
+ self.login_frame.grid(row=0, column=0, sticky="ns") # show login frame
if __name__ == "__main__":
app = App()
- app.start()
+ app.mainloop()
diff --git a/examples/example_button_images.py b/examples/example_button_images.py
deleted file mode 100644
index 0e65f09..0000000
--- a/examples/example_button_images.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import customtkinter
-from PIL import Image, ImageTk
-import os
-
-PATH = os.path.dirname(os.path.realpath(__file__))
-
-customtkinter.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light"
-customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
-
-
-class App(customtkinter.CTk):
- def __init__(self):
- super().__init__()
- self.geometry("450x260")
- self.title("CustomTkinter example_button_images.py")
-
- self.grid_rowconfigure(0, weight=1)
- self.grid_columnconfigure(0, weight=1, minsize=200)
-
- self.frame_1 = customtkinter.CTkFrame(master=self, width=250, height=240, corner_radius=15)
- self.frame_1.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
- self.frame_1.grid_columnconfigure(0, weight=1)
- self.frame_1.grid_columnconfigure(1, weight=1)
-
- self.settings_image = self.load_image("/test_images/settings.png", 20)
- self.bell_image = self.load_image("/test_images/bell.png", 20)
- self.add_folder_image = self.load_image("/test_images/add-folder.png", 20)
- self.add_list_image = self.load_image("/test_images/add-folder.png", 20)
- self.add_user_image = self.load_image("/test_images/add-user.png", 20)
- self.chat_image = self.load_image("/test_images/chat.png", 20)
- self.home_image = self.load_image("/test_images/home.png", 20)
-
- self.button_1 = customtkinter.CTkButton(master=self.frame_1, image=self.add_folder_image, text="Add Folder", height=32,
- compound="right", command=self.button_function)
- self.button_1.grid(row=1, column=0, columnspan=2, padx=20, pady=(20, 10), sticky="ew")
-
- self.button_2 = customtkinter.CTkButton(master=self.frame_1, image=self.add_list_image, text="Add Item", height=32,
- compound="right", fg_color="#D35B58", hover_color="#C77C78",
- command=self.button_function)
- self.button_2.grid(row=2, column=0, columnspan=2, padx=20, pady=10, sticky="ew")
-
- self.button_3 = customtkinter.CTkButton(master=self.frame_1, image=self.chat_image, text="", width=40, height=40,
- corner_radius=10, fg_color="gray40", hover_color="gray25",
- command=self.button_function)
- self.button_3.grid(row=3, column=0, columnspan=1, padx=20, pady=10, sticky="w")
-
- self.button_4 = customtkinter.CTkButton(master=self.frame_1, image=self.home_image, text="", width=40, height=40,
- corner_radius=10, fg_color="gray40", hover_color="gray25",
- command=self.button_function)
- self.button_4.grid(row=3, column=1, columnspan=1, padx=20, pady=10, sticky="e")
-
- self.button_5 = customtkinter.CTkButton(master=self, image=self.add_user_image, text="Add User", width=130, height=60, border_width=2,
- corner_radius=10, compound="bottom", border_color="#D35B58", fg_color=("gray84", "gray25"),
- hover_color="#C77C78", command=self.button_function)
- self.button_5.grid(row=0, column=1, padx=20, pady=20)
-
- def load_image(self, path, image_size):
- """ load rectangular image with path relative to PATH """
- return ImageTk.PhotoImage(Image.open(PATH + path).resize((image_size, image_size)))
-
- def button_function(self):
- print("button pressed")
-
-
-if __name__ == "__main__":
- app = App()
- app.mainloop()
diff --git a/examples/image_example.py b/examples/image_example.py
new file mode 100644
index 0000000..c4064b8
--- /dev/null
+++ b/examples/image_example.py
@@ -0,0 +1,118 @@
+import customtkinter
+import os
+from PIL import Image
+
+
+class App(customtkinter.CTk):
+ def __init__(self):
+ super().__init__()
+
+ self.title("image_example.py")
+ self.geometry("700x450")
+
+ # set grid layout 1x2
+ self.grid_rowconfigure(0, weight=1)
+ self.grid_columnconfigure(1, weight=1)
+
+ # load images with light and dark mode image
+ image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_images")
+ self.logo_image = customtkinter.CTkImage(Image.open(os.path.join(image_path, "CustomTkinter_logo_single.png")), size=(26, 26))
+ self.large_test_image = customtkinter.CTkImage(Image.open(os.path.join(image_path, "large_test_image.png")), size=(500, 150))
+ self.image_icon_image = customtkinter.CTkImage(Image.open(os.path.join(image_path, "image_icon_light.png")), size=(20, 20))
+ self.home_image = customtkinter.CTkImage(light_image=Image.open(os.path.join(image_path, "home_dark.png")),
+ dark_image=Image.open(os.path.join(image_path, "home_light.png")), size=(20, 20))
+ self.chat_image = customtkinter.CTkImage(light_image=Image.open(os.path.join(image_path, "chat_dark.png")),
+ dark_image=Image.open(os.path.join(image_path, "chat_light.png")), size=(20, 20))
+ self.add_user_image = customtkinter.CTkImage(light_image=Image.open(os.path.join(image_path, "add_user_dark.png")),
+ dark_image=Image.open(os.path.join(image_path, "add_user_light.png")), size=(20, 20))
+
+ # create navigation frame
+ self.navigation_frame = customtkinter.CTkFrame(self, corner_radius=0)
+ self.navigation_frame.grid(row=0, column=0, sticky="nsew")
+ self.navigation_frame.grid_rowconfigure(4, weight=1)
+
+ self.navigation_frame_label = customtkinter.CTkLabel(self.navigation_frame, text=" Image Example", image=self.logo_image,
+ compound="left", font=customtkinter.CTkFont(size=15, weight="bold"))
+ self.navigation_frame_label.grid(row=0, column=0, padx=20, pady=20)
+
+ self.home_button = customtkinter.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Home",
+ fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"),
+ image=self.home_image, anchor="w", command=self.home_button_event)
+ self.home_button.grid(row=1, column=0, sticky="ew")
+
+ self.frame_2_button = customtkinter.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Frame 2",
+ fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"),
+ image=self.chat_image, anchor="w", command=self.frame_2_button_event)
+ self.frame_2_button.grid(row=2, column=0, sticky="ew")
+
+ self.frame_3_button = customtkinter.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Frame 3",
+ fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"),
+ image=self.add_user_image, anchor="w", command=self.frame_3_button_event)
+ self.frame_3_button.grid(row=3, column=0, sticky="ew")
+
+ self.appearance_mode_menu = customtkinter.CTkOptionMenu(self.navigation_frame, values=["Light", "Dark", "System"],
+ command=self.change_appearance_mode_event)
+ self.appearance_mode_menu.grid(row=6, column=0, padx=20, pady=20, sticky="s")
+
+ # create home frame
+ self.home_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent")
+ self.home_frame.grid_columnconfigure(0, weight=1)
+
+ self.home_frame_large_image_label = customtkinter.CTkLabel(self.home_frame, text="", image=self.large_test_image)
+ self.home_frame_large_image_label.grid(row=0, column=0, padx=20, pady=10)
+
+ self.home_frame_button_1 = customtkinter.CTkButton(self.home_frame, text="", image=self.image_icon_image)
+ self.home_frame_button_1.grid(row=1, column=0, padx=20, pady=10)
+ self.home_frame_button_2 = customtkinter.CTkButton(self.home_frame, text="CTkButton", image=self.image_icon_image, compound="right")
+ self.home_frame_button_2.grid(row=2, column=0, padx=20, pady=10)
+ self.home_frame_button_3 = customtkinter.CTkButton(self.home_frame, text="CTkButton", image=self.image_icon_image, compound="top")
+ self.home_frame_button_3.grid(row=3, column=0, padx=20, pady=10)
+ self.home_frame_button_4 = customtkinter.CTkButton(self.home_frame, text="CTkButton", image=self.image_icon_image, compound="bottom", anchor="w")
+ self.home_frame_button_4.grid(row=4, column=0, padx=20, pady=10)
+
+ # create second frame
+ self.second_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent")
+
+ # create third frame
+ self.third_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent")
+
+ # select default frame
+ self.select_frame_by_name("home")
+
+ def select_frame_by_name(self, name):
+ # set button color for selected button
+ self.home_button.configure(fg_color=("gray75", "gray25") if name == "home" else "transparent")
+ self.frame_2_button.configure(fg_color=("gray75", "gray25") if name == "frame_2" else "transparent")
+ self.frame_3_button.configure(fg_color=("gray75", "gray25") if name == "frame_3" else "transparent")
+
+ # show selected frame
+ if name == "home":
+ self.home_frame.grid(row=0, column=1, sticky="nsew")
+ else:
+ self.home_frame.grid_forget()
+ if name == "frame_2":
+ self.second_frame.grid(row=0, column=1, sticky="nsew")
+ else:
+ self.second_frame.grid_forget()
+ if name == "frame_3":
+ self.third_frame.grid(row=0, column=1, sticky="nsew")
+ else:
+ self.third_frame.grid_forget()
+
+ def home_button_event(self):
+ self.select_frame_by_name("home")
+
+ def frame_2_button_event(self):
+ self.select_frame_by_name("frame_2")
+
+ def frame_3_button_event(self):
+ self.select_frame_by_name("frame_3")
+
+ def change_appearance_mode_event(self, new_appearance_mode):
+ customtkinter.set_appearance_mode(new_appearance_mode)
+
+
+if __name__ == "__main__":
+ app = App()
+ app.mainloop()
+
diff --git a/examples/simple_example.py b/examples/simple_example.py
index 370b4f4..26d900f 100644
--- a/examples/simple_example.py
+++ b/examples/simple_example.py
@@ -5,7 +5,7 @@ customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark",
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
app = customtkinter.CTk()
-app.geometry("400x580")
+app.geometry("400x780")
app.title("CustomTkinter simple_example.py")
@@ -21,41 +21,53 @@ frame_1 = customtkinter.CTkFrame(master=app)
frame_1.pack(pady=20, padx=60, fill="both", expand=True)
label_1 = customtkinter.CTkLabel(master=frame_1, justify=tkinter.LEFT)
-label_1.pack(pady=12, padx=10)
+label_1.pack(pady=10, padx=10)
progressbar_1 = customtkinter.CTkProgressBar(master=frame_1)
-progressbar_1.pack(pady=12, padx=10)
+progressbar_1.pack(pady=10, padx=10)
button_1 = customtkinter.CTkButton(master=frame_1, command=button_callback)
-button_1.pack(pady=12, padx=10)
+button_1.pack(pady=10, padx=10)
slider_1 = customtkinter.CTkSlider(master=frame_1, command=slider_callback, from_=0, to=1)
-slider_1.pack(pady=12, padx=10)
+slider_1.pack(pady=10, padx=10)
slider_1.set(0.5)
entry_1 = customtkinter.CTkEntry(master=frame_1, placeholder_text="CTkEntry")
-entry_1.pack(pady=12, padx=10)
+entry_1.pack(pady=10, padx=10)
optionmenu_1 = customtkinter.CTkOptionMenu(frame_1, values=["Option 1", "Option 2", "Option 42 long long long..."])
-optionmenu_1.pack(pady=12, padx=10)
+optionmenu_1.pack(pady=10, padx=10)
optionmenu_1.set("CTkOptionMenu")
combobox_1 = customtkinter.CTkComboBox(frame_1, values=["Option 1", "Option 2", "Option 42 long long long..."])
-combobox_1.pack(pady=12, padx=10)
+combobox_1.pack(pady=10, padx=10)
optionmenu_1.set("CTkComboBox")
checkbox_1 = customtkinter.CTkCheckBox(master=frame_1)
-checkbox_1.pack(pady=12, padx=10)
+checkbox_1.pack(pady=10, padx=10)
radiobutton_var = tkinter.IntVar(value=1)
radiobutton_1 = customtkinter.CTkRadioButton(master=frame_1, variable=radiobutton_var, value=1)
-radiobutton_1.pack(pady=12, padx=10)
+radiobutton_1.pack(pady=10, padx=10)
radiobutton_2 = customtkinter.CTkRadioButton(master=frame_1, variable=radiobutton_var, value=2)
-radiobutton_2.pack(pady=12, padx=10)
+radiobutton_2.pack(pady=10, padx=10)
switch_1 = customtkinter.CTkSwitch(master=frame_1)
-switch_1.pack(pady=12, padx=10)
+switch_1.pack(pady=10, padx=10)
+
+text_1 = customtkinter.CTkTextbox(master=frame_1, width=200, height=70)
+text_1.pack(pady=10, padx=10)
+text_1.insert("0.0", "CTkTextbox\n\n\n\n")
+
+segmented_button_1 = customtkinter.CTkSegmentedButton(master=frame_1, values=["CTkSegmentedButton", "Value 2"])
+segmented_button_1.pack(pady=10, padx=10)
+
+tabview_1 = customtkinter.CTkTabview(master=frame_1, width=200, height=70)
+tabview_1.pack(pady=10, padx=10)
+tabview_1.add("CTkTabview")
+tabview_1.add("Tab 2")
app.mainloop()
diff --git a/examples/test_images/CustomTkinter_logo_single.png b/examples/test_images/CustomTkinter_logo_single.png
new file mode 100644
index 0000000..7c22940
Binary files /dev/null and b/examples/test_images/CustomTkinter_logo_single.png differ
diff --git a/examples/test_images/add-list.png b/examples/test_images/add-list.png
deleted file mode 100644
index 193532f..0000000
Binary files a/examples/test_images/add-list.png and /dev/null differ
diff --git a/examples/test_images/add_user_dark.png b/examples/test_images/add_user_dark.png
new file mode 100644
index 0000000..937d770
Binary files /dev/null and b/examples/test_images/add_user_dark.png differ
diff --git a/examples/test_images/add-user.png b/examples/test_images/add_user_light.png
similarity index 100%
rename from examples/test_images/add-user.png
rename to examples/test_images/add_user_light.png
diff --git a/examples/test_images/bell.png b/examples/test_images/bell.png
deleted file mode 100644
index df3838f..0000000
Binary files a/examples/test_images/bell.png and /dev/null differ
diff --git a/examples/test_images/chat_dark.png b/examples/test_images/chat_dark.png
new file mode 100644
index 0000000..8ddbc57
Binary files /dev/null and b/examples/test_images/chat_dark.png differ
diff --git a/examples/test_images/chat.png b/examples/test_images/chat_light.png
similarity index 100%
rename from examples/test_images/chat.png
rename to examples/test_images/chat_light.png
diff --git a/examples/test_images/home_dark.png b/examples/test_images/home_dark.png
new file mode 100644
index 0000000..ff9f200
Binary files /dev/null and b/examples/test_images/home_dark.png differ
diff --git a/examples/test_images/home.png b/examples/test_images/home_light.png
similarity index 100%
rename from examples/test_images/home.png
rename to examples/test_images/home_light.png
diff --git a/examples/test_images/image_icon_light.png b/examples/test_images/image_icon_light.png
new file mode 100644
index 0000000..3f32d0f
Binary files /dev/null and b/examples/test_images/image_icon_light.png differ
diff --git a/examples/test_images/large_test_image.png b/examples/test_images/large_test_image.png
new file mode 100644
index 0000000..cfe5df1
Binary files /dev/null and b/examples/test_images/large_test_image.png differ
diff --git a/examples/test_images/settings.png b/examples/test_images/settings.png
deleted file mode 100644
index 36e5d79..0000000
Binary files a/examples/test_images/settings.png and /dev/null differ
diff --git a/requirements.txt b/requirements.txt
index e2ec11a..9c2eaa9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
-darkdetect~=0.3.1
\ No newline at end of file
+darkdetect~=0.3.1
+typing-extensions~=4.4.0
diff --git a/setup.cfg b/setup.cfg
index ec181cc..52f85aa 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,7 +2,7 @@
name = customtkinter
version = 4.6.3
description = Create modern looking GUIs with Python
-long_description = CustomTkinter UI-Library\n\n[](https://github.com/TomSchimansky/CustomTkinter/blob/master/documentation_images/Windows_dark.png)\n\nMore Information: https://github.com/TomSchimansky/CustomTkinter
+long_description = file: Readme.md
long_description_content_type = text/markdown
url = https://github.com/TomSchimansky/CustomTkinter
author = Tom Schimansky
@@ -17,8 +17,16 @@ classifiers =
python_requires = >=3.7
packages =
customtkinter
- customtkinter.widgets
+ customtkinter.utility
customtkinter.windows
+ customtkinter.windows.widgets
+ customtkinter.windows.widgets.appearance_mode
+ customtkinter.windows.widgets.core_rendering
+ customtkinter.windows.widgets.core_widget_classes
+ customtkinter.windows.widgets.font
+ customtkinter.windows.widgets.image
+ customtkinter.windows.widgets.scaling
+ customtkinter.windows.widgets.theme
install_requires =
darkdetect
typing_extensions; python_version<="3.7"
diff --git a/test/manual_integration_tests/complex_example_new.py b/test/manual_integration_tests/complex_example_new.py
deleted file mode 100644
index 1c9d869..0000000
--- a/test/manual_integration_tests/complex_example_new.py
+++ /dev/null
@@ -1,149 +0,0 @@
-import tkinter
-import tkinter.messagebox
-import customtkinter
-
-customtkinter.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light"
-customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
-
-
-class App(customtkinter.CTk):
-
- def __init__(self):
- super().__init__()
-
- self.title("CustomTkinter complex_example.py")
- self.geometry(f"{920}x{500}")
- self.protocol("WM_DELETE_WINDOW", self.on_closing) # call .on_closing() when app gets closed
-
- # configure grid layout (4x4)
- self.grid_columnconfigure(1, weight=1)
- self.grid_columnconfigure((2, 3), weight=0, minsize=200)
- self.grid_rowconfigure((0, 1, 2), weight=1)
-
- # create sidebar frame with widgets
- self.sidebar_frame = customtkinter.CTkFrame(self, width=140)
- self.sidebar_frame.grid(row=0, column=0, rowspan=4, sticky="nsew")
- self.sidebar_frame.grid_rowconfigure(4, weight=1)
- self.logo_label = customtkinter.CTkLabel(self.sidebar_frame, text="CustomTkinter", text_font=("Roboto", -16))
- self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
- self.sidebar_button_1 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_callback)
- self.sidebar_button_1.grid(row=1, column=0, padx=20, pady=10)
- self.sidebar_button_2 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_callback)
- self.sidebar_button_2.grid(row=2, column=0, padx=20, pady=10)
- self.sidebar_button_3 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_callback)
- self.sidebar_button_3.grid(row=3, column=0, padx=20, pady=10)
- self.appearance_mode_label = customtkinter.CTkLabel(self.sidebar_frame, text="Appearance Mode:", anchor="w")
- self.appearance_mode_label.grid(row=5, column=0, padx=20, pady=(10, 0))
- self.appearance_mode_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["Light", "Dark", "System"],
- command=self.change_appearance_mode)
- self.appearance_mode_optionemenu.grid(row=6, column=0, padx=20, pady=(10, 10))
- self.scaling_label = customtkinter.CTkLabel(self.sidebar_frame, text="UI Scaling:", anchor="w")
- self.scaling_label.grid(row=7, column=0, padx=20, pady=(10, 0))
- self.scaling_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["80%", "90%", "100%", "110%", "120%"],
- command=self.change_scaling)
- self.scaling_optionemenu.grid(row=8, column=0, padx=20, pady=(10, 20))
-
- # create main entry and button
- self.entry = customtkinter.CTkEntry(self, placeholder_text="CTkEntry")
- self.entry.grid(row=3, column=1, columnspan=2, padx=(20, 10), pady=(10, 20), sticky="nsew")
-
- self.main_button_1 = customtkinter.CTkButton(self, fg_color=None, border_width=2)
- self.main_button_1.grid(row=3, column=3, padx=(10, 20), pady=(10, 20), sticky="nsew")
-
- self.textbox = customtkinter.CTkTextbox(self)
- self.textbox.grid(row=0, column=1, padx=(20, 10), pady=(20, 10), sticky="nsew")
-
- # create radiobutton frame
- self.radiobutton_frame = customtkinter.CTkFrame(self)
- self.radiobutton_frame.grid(row=0, column=3, padx=(10, 20), pady=(20, 10), sticky="nsew")
- self.radio_var = tkinter.IntVar(value=0)
- self.label_radio_group = customtkinter.CTkLabel(master=self.radiobutton_frame, text="CTkRadioButton Group:")
- self.label_radio_group.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="")
- self.radio_button_1 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=0)
- self.radio_button_1.grid(row=1, column=2, pady=10, padx=20, sticky="n")
- self.radio_button_2 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=1)
- self.radio_button_2.grid(row=2, column=2, pady=10, padx=20, sticky="n")
- self.radio_button_3 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=2)
- self.radio_button_3.grid(row=3, column=2, pady=10, padx=20, sticky="n")
-
- # create optionemnu and combobox frame
- self.optionemnu_combobox_frame = customtkinter.CTkFrame(self)
- self.optionemnu_combobox_frame.grid(row=0, column=2, padx=(10, 10), pady=(20, 10), sticky="nsew")
- self.optionmenu_1 = customtkinter.CTkOptionMenu(self.optionemnu_combobox_frame,
- dynamic_resizing=False,
- values=["Value 1", "Value 2", "Value Long Long Long"])
- self.optionmenu_1.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="ew")
- self.combobox_1 = customtkinter.CTkComboBox(self.optionemnu_combobox_frame,
- values=["Value 1", "Value 2", "Value Long....."])
- self.combobox_1.grid(row=1, column=0, padx=20, pady=(10, 10), sticky="ew")
- self.string_input_button = customtkinter.CTkButton(self.optionemnu_combobox_frame, text="Open CTkInputDialog",
- command=self.open_input_dialog)
- self.string_input_button.grid(row=2, column=0, padx=20, pady=(10, 10), sticky="ew")
-
- # create checkbox and switch frame
- self.checkbox_slider_frame = customtkinter.CTkFrame(self)
- self.checkbox_slider_frame.grid(row=1, column=3, padx=(10, 20), pady=(10, 10), sticky="nsew")
- self.checkbox_1 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame)
- self.checkbox_1.grid(row=1, column=0, pady=(20, 10), padx=20, sticky="n")
- self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame)
- self.checkbox_2.grid(row=2, column=0, pady=10, padx=20, sticky="n")
- self.switch_1 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame, command=lambda: print("switch 1 toggle"))
- self.switch_1.grid(row=3, column=0, pady=10, padx=20, sticky="n")
- self.switch_2 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame)
- self.switch_2.grid(row=4, column=0, pady=(10, 20), padx=20, sticky="n")
-
- # create slider and progressbar frame
- self.slider_progressbar_frame = customtkinter.CTkFrame(self, fg_color=None)
- self.slider_progressbar_frame.grid(row=1, column=1, columnspan=2, padx=(20, 10), pady=(10, 10), sticky="nsew")
- self.slider_progressbar_frame.grid_columnconfigure(0, weight=1)
- self.slider_progressbar_frame.grid_rowconfigure(3, weight=1)
- self.progressbar_1 = customtkinter.CTkProgressBar(self.slider_progressbar_frame)
- self.progressbar_1.grid(row=0, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
- self.progressbar_2 = customtkinter.CTkProgressBar(self.slider_progressbar_frame)
- self.progressbar_2.grid(row=1, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
- self.slider_1 = customtkinter.CTkSlider(self.slider_progressbar_frame, from_=0, to=1, number_of_steps=4)
- self.slider_1.grid(row=2, column=0, padx=(20, 10), pady=(10, 10), sticky="ew")
- self.slider_2 = customtkinter.CTkSlider(self.slider_progressbar_frame, orient="vertical")
- self.slider_2.grid(row=0, column=1, rowspan=4, padx=(10, 10), pady=(10, 10), sticky="ns")
- self.progressbar_3 = customtkinter.CTkProgressBar(self.slider_progressbar_frame, orient="vertical")
- self.progressbar_3.grid(row=0, column=2, rowspan=4, padx=(10, 20), pady=(10, 10), sticky="ns")
-
- # set default values
- self.sidebar_button_3.configure(state="disabled", text="Disabled CTkButton")
- self.checkbox_2.configure(state="disabled")
- self.switch_2.configure(state="disabled")
- self.checkbox_1.select()
- self.switch_1.select()
- self.radio_button_3.configure(state="disabled")
- self.appearance_mode_optionemenu.set("Dark")
- self.scaling_optionemenu.set("100%")
- self.optionmenu_1.set("CTkOptionmenu")
- self.combobox_1.set("CTkComboBox")
- self.textbox.insert("1.0", "CTkTextbox\n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.")
- self.slider_1.configure(command=self.progressbar_2.set)
- self.slider_2.configure(command=self.progressbar_3.set)
- self.progressbar_1.configure(mode="indeterminnate")
- self.progressbar_1.start()
-
- def open_input_dialog(self):
- dialog = customtkinter.CTkInputDialog(master=None, text="Type in a number:", title="CTkInputDialog")
- print("CTkInputDialog:", dialog.get_input())
-
- def change_appearance_mode(self, new_appearance_mode: str):
- customtkinter.set_appearance_mode(new_appearance_mode)
-
- def change_scaling(self, new_scaling: str):
- new_scaling_float = int(new_scaling.replace("%", "")) / 100
- customtkinter.set_spacing_scaling(new_scaling_float)
- customtkinter.set_widget_scaling(new_scaling_float)
-
- def sidebar_button_callback(self):
- print("sidebar_button click")
-
- def on_closing(self, event=0):
- self.destroy()
-
-
-if __name__ == "__main__":
- app = App()
- app.mainloop()
diff --git a/examples/simple_example_standard_tkinter.py b/test/manual_integration_tests/simple_example_standard_tkinter.py
similarity index 99%
rename from examples/simple_example_standard_tkinter.py
rename to test/manual_integration_tests/simple_example_standard_tkinter.py
index a5078e1..c39ef3e 100644
--- a/examples/simple_example_standard_tkinter.py
+++ b/test/manual_integration_tests/simple_example_standard_tkinter.py
@@ -5,7 +5,6 @@ app = tkinter.Tk()
app.geometry("400x350")
app.title("simple_example_standard_tkinter.py")
-
def button_function():
print("button pressed")
diff --git a/test/manual_integration_tests/test_all_widgets_with_colors.py b/test/manual_integration_tests/test_all_widgets_with_colors.py
index 1bc58b6..2f35a27 100644
--- a/test/manual_integration_tests/test_all_widgets_with_colors.py
+++ b/test/manual_integration_tests/test_all_widgets_with_colors.py
@@ -2,7 +2,6 @@ import tkinter
import customtkinter
customtkinter.set_appearance_mode("System") # Other: "Dark", "Light"
-customtkinter.set_default_color_theme("green") # Themes: "blue" (standard), "green", "dark-blue"
class TestApp(customtkinter.CTk):
@@ -20,13 +19,13 @@ class TestApp(customtkinter.CTk):
""" gets called by self.slider_1 """
if value == 0:
- self.label_1.set_text("mode: Light")
+ self.label_1.configure(text="mode: Light")
customtkinter.set_appearance_mode("Light")
elif value == 1:
- self.label_1.set_text("mode: Dark")
+ self.label_1.configure(text="mode: Dark")
customtkinter.set_appearance_mode("Dark")
else:
- self.label_1.set_text("mode: System")
+ self.label_1.configure(text="mode: System")
customtkinter.set_appearance_mode("System")
def create_widgets_on_tk(self):
@@ -74,7 +73,7 @@ class TestApp(customtkinter.CTk):
self.progress_bar_2 = customtkinter.CTkProgressBar(master=self.ctk_frame)
self.progress_bar_2.place(relx=0.5, y=y + 320, anchor=tkinter.CENTER)
- self.slider_2 = customtkinter.CTkSlider(master=self.ctk_frame, command=lambda v: self.label_2.set_text(str(round(v, 5))))
+ self.slider_2 = customtkinter.CTkSlider(master=self.ctk_frame, command=lambda v: self.label_2.configure(text=str(round(v, 5))))
self.slider_2.place(relx=0.5, y=y + 400, anchor=tkinter.CENTER)
self.check_box_2 = customtkinter.CTkCheckBox(master=self.ctk_frame)
@@ -102,7 +101,7 @@ class TestApp(customtkinter.CTk):
self.ctk_frame_customized.configure(fg_color=("#F4F4FA", "#1E2742"))
self.label_3 = customtkinter.CTkLabel(master=self.ctk_frame_customized, text="customized", corner_radius=60,
- text_font=("times", 16))
+ font=("times", 16))
self.label_3.place(relx=0.5, y=y, anchor=tkinter.CENTER)
self.label_3.configure(fg_color=("#F4F4FA", "#333D5E"), text_color=("#373E57", "#7992C1"))
@@ -110,13 +109,13 @@ class TestApp(customtkinter.CTk):
self.frame_3.place(relx=0.5, y=y + 80, anchor=tkinter.CENTER)
self.frame_3.configure(fg_color=("#EBECF3", "#4B577E"))
- self.button_3 = customtkinter.CTkButton(master=self.ctk_frame_customized, command=lambda: x, border_width=3,
- corner_radius=20, text_font=("times", 16))
+ self.button_3 = customtkinter.CTkButton(master=self.ctk_frame_customized, command=lambda: None, border_width=3,
+ corner_radius=20, font=("times", 16))
self.button_3.place(relx=0.5, y=y + 160, anchor=tkinter.CENTER)
self.button_3.configure(border_color=("#4F90F8", "#6FADF9"), hover_color=("#3A65E8", "#4376EE"))
- self.button_3.configure(fg_color=None)
+ self.button_3.configure(fg_color="transparent")
- self.entry_3 = customtkinter.CTkEntry(master=self.ctk_frame_customized, text_font=("times", 16))
+ self.entry_3 = customtkinter.CTkEntry(master=self.ctk_frame_customized, font=("times", 16))
self.entry_3.place(relx=0.5, y=y + 240, anchor=tkinter.CENTER)
self.entry_3.configure(fg_color=("gray60", "gray5"), corner_radius=20)
self.entry_3.insert(0, "1234567890")
@@ -131,7 +130,7 @@ class TestApp(customtkinter.CTk):
self.slider_3.configure(button_color="#8AE0C3", fg_color=("#EBECF3", "#4B577E"), progress_color=("gray30", "gray10"))
self.slider_3.configure(from_=0, to=1)
- self.check_box_3 = customtkinter.CTkCheckBox(master=self.ctk_frame_customized, corner_radius=50, text_font=("times", 16))
+ self.check_box_3 = customtkinter.CTkCheckBox(master=self.ctk_frame_customized, corner_radius=50, font=("times", 16))
self.check_box_3.place(relx=0.5, y=y + 480, anchor=tkinter.CENTER)
self.check_box_3.configure(border_color="#8AE0C3")
@@ -152,7 +151,7 @@ class TestApp(customtkinter.CTk):
self.button_4 = customtkinter.CTkButton(master=self.tk_frame_customized, command=lambda: x, border_width=3)
self.button_4.place(relx=0.5, y=y + 160, anchor=tkinter.CENTER)
self.button_4.configure(border_color=("#4F90F8", "#6FADF9"), hover_color=("#3A65E8", "#4376EE"))
- self.button_4.configure(fg_color=None)
+ self.button_4.configure(fg_color="transparent")
self.entry_4 = customtkinter.CTkEntry(master=self.tk_frame_customized)
self.entry_4.place(relx=0.5, y=y + 240, anchor=tkinter.CENTER)
@@ -176,4 +175,4 @@ class TestApp(customtkinter.CTk):
if __name__ == "__main__":
test_app = TestApp()
- test_app.mainloop()
\ No newline at end of file
+ test_app.mainloop()
diff --git a/test/manual_integration_tests/test_askdialog.py b/test/manual_integration_tests/test_askdialog.py
deleted file mode 100644
index 5ab7924..0000000
--- a/test/manual_integration_tests/test_askdialog.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import tkinter
-import tkinter.messagebox
-from tkinter import filedialog as fd
-import customtkinter
-
-
-class App(customtkinter.CTk):
-
- customtkinter.set_appearance_mode("dark")
- APP_NAME = "Bulk Barcode Generator"
- WIDTH = 600
- HEIGHT = 450
-
- MAIN_COLOR = "#5ea886"
- MAIN_COLOR_DARK = "#2D5862"
- MAIN_HOVER = "#05f4b7"
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- self.title(App.APP_NAME)
- self.geometry(str(App.WIDTH) + "x" + str(App.HEIGHT))
- self.minsize(App.WIDTH, App.HEIGHT)
-
- self.protocol("WM_DELETE_WINDOW", self.on_closing)
- # ============ create two CTkFrames ============
-
- self.frame_left = customtkinter.CTkFrame(master=self,
- width=220,
- height=App.HEIGHT-40,
- corner_radius=5)
- self.frame_left.place(relx=0.38, rely=0.5, anchor=tkinter.E)
-
- self.frame_right = customtkinter.CTkFrame(master=self,
- width=350,
- height=App.HEIGHT-40,
- corner_radius=5)
- self.frame_right.place(relx=0.40, rely=0.5, anchor=tkinter.W)
-
-# # ============ frame_right ============
-
- self.button_output = customtkinter.CTkButton(master=self.frame_right, border_color=App.MAIN_COLOR,
- fg_color=None, hover_color=App.MAIN_HOVER,
- height=28, text="Output Folder", command=self.button_outputFunc,
- border_width=3, corner_radius=10, text_font=('Calibri',12))
- self.button_output.place(relx=0.05, rely=0.06, anchor=tkinter.NW)
- self.entry_output = customtkinter.CTkEntry(master=self.frame_right, width=320, height=38, corner_radius=5)
- self.entry_output.place(relx=0.05, rely=0.18, anchor=tkinter.NW)
-
- def button_outputFunc(self):
- self.entry_output.delete(0, 'end')
- filename = fd.askdirectory()
- self.entry_output.insert(0,str(filename))
- pass
-
- def on_closing(self, event=0):
- self.destroy()
-
- def start(self):
- self.mainloop()
-
-
-if __name__ == "__main__":
- app = App()
- app.start()
diff --git a/test/manual_integration_tests/test_ctk_toplevel.py b/test/manual_integration_tests/test_ctk_toplevel.py
index 88e8f1c..32bd490 100644
--- a/test/manual_integration_tests/test_ctk_toplevel.py
+++ b/test/manual_integration_tests/test_ctk_toplevel.py
@@ -8,6 +8,7 @@ class ToplevelWindow(customtkinter.CTkToplevel):
super().__init__(*args, **kwargs)
self.protocol("WM_DELETE_WINDOW", self.closing)
self.geometry("500x300")
+ self.resizable(False, False)
self.closing_event = closing_event
self.label = customtkinter.CTkLabel(self, text="ToplevelWindow")
@@ -26,6 +27,7 @@ class App(customtkinter.CTk):
def __init__(self):
super().__init__()
self.geometry("500x400")
+ self.resizable(False, False)
self.button_1 = customtkinter.CTkButton(self, text="Open CTkToplevel", command=self.open_toplevel)
self.button_1.pack(side="top", padx=40, pady=40)
diff --git a/test/manual_integration_tests/test_filedialog.py b/test/manual_integration_tests/test_filedialog.py
new file mode 100644
index 0000000..4615da8
--- /dev/null
+++ b/test/manual_integration_tests/test_filedialog.py
@@ -0,0 +1,31 @@
+import tkinter.messagebox
+import customtkinter
+
+customtkinter.set_appearance_mode("dark")
+
+
+class App(customtkinter.CTk):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.title("test filedialog")
+
+ self.button_1 = customtkinter.CTkButton(master=self, text="askopenfile", command=lambda: print(customtkinter.filedialog.askopenfile()))
+ self.button_1.pack(pady=10)
+ self.button_2 = customtkinter.CTkButton(master=self, text="askopenfiles", command=lambda: print(customtkinter.filedialog.askopenfiles()))
+ self.button_2.pack(pady=10)
+ self.button_3 = customtkinter.CTkButton(master=self, text="askdirectory", command=lambda: print(customtkinter.filedialog.askdirectory()))
+ self.button_3.pack(pady=10)
+ self.button_4 = customtkinter.CTkButton(master=self, text="asksaveasfile", command=lambda: print(customtkinter.filedialog.asksaveasfile()))
+ self.button_4.pack(pady=10)
+ self.button_5 = customtkinter.CTkButton(master=self, text="askopenfilename", command=lambda: print(customtkinter.filedialog.askopenfilename()))
+ self.button_5.pack(pady=10)
+ self.button_6 = customtkinter.CTkButton(master=self, text="askopenfilenames", command=lambda: print(customtkinter.filedialog.askopenfilenames()))
+ self.button_6.pack(pady=10)
+ self.button_7 = customtkinter.CTkButton(master=self, text="asksaveasfilename", command=lambda: print(customtkinter.filedialog.asksaveasfilename()))
+ self.button_7.pack(pady=10)
+
+
+if __name__ == "__main__":
+ app = App()
+ app.mainloop()
diff --git a/test/manual_integration_tests/test_font.py b/test/manual_integration_tests/test_font.py
new file mode 100644
index 0000000..6dc4fba
--- /dev/null
+++ b/test/manual_integration_tests/test_font.py
@@ -0,0 +1,79 @@
+import customtkinter
+
+
+app = customtkinter.CTk()
+app.geometry("1200x1000")
+app.grid_rowconfigure(0, weight=1)
+app.grid_columnconfigure((0, 1), weight=1)
+
+frame_1 = customtkinter.CTkFrame(app)
+frame_1.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
+frame_2 = customtkinter.CTkFrame(app)
+frame_2.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)
+
+def set_scaling(scaling):
+ customtkinter.set_widget_scaling(scaling)
+
+scaling_button = customtkinter.CTkSegmentedButton(frame_1, values=[0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.5, 2.0], command=set_scaling)
+scaling_button.pack(pady=(2, 10))
+
+b = customtkinter.CTkButton(frame_1, text="single name", font=("Times", ))
+b.pack(pady=2)
+b = customtkinter.CTkButton(frame_1, text="name with size", font=("Times", 18))
+b.pack(pady=2)
+b = customtkinter.CTkButton(frame_1, text="name with negative size", font=("Times", -18))
+b.pack(pady=2)
+b = customtkinter.CTkButton(frame_1, text="extra keywords", font=("Times", -18, "bold italic underline overstrike"))
+b.pack(pady=2)
+
+b = customtkinter.CTkButton(frame_1, text="object default")
+b.pack(pady=(10, 2))
+b = customtkinter.CTkButton(frame_1, text="object single name", font=customtkinter.CTkFont("Times"))
+b.pack(pady=2)
+b = customtkinter.CTkButton(frame_1, text="object with name and size", font=customtkinter.CTkFont("Times", 18))
+b.pack(pady=2)
+b = customtkinter.CTkButton(frame_1, text="object with name and negative size", font=customtkinter.CTkFont("Times", -18))
+b.pack(pady=2)
+b = customtkinter.CTkButton(frame_1, text="object with extra keywords",
+ font=customtkinter.CTkFont("Times", -18, weight="bold", slant="italic", underline=True, overstrike=True))
+b.pack(pady=2)
+
+b1 = customtkinter.CTkButton(frame_1, text="object default modified")
+b1.pack(pady=(10, 2))
+b1.cget("font").configure(size=9)
+print("test_font.py:", b1.cget("font").cget("size"), b1.cget("font").cget("family"))
+
+b2 = customtkinter.CTkButton(frame_1, text="object default overridden")
+b2.pack(pady=10)
+b2.configure(font=customtkinter.CTkFont(family="Times"))
+
+label_font = customtkinter.CTkFont(size=5)
+for i in range(30):
+ l = customtkinter.CTkLabel(frame_2, font=label_font, height=0)
+ l.grid(row=i, column=0, pady=1)
+ b = customtkinter.CTkButton(frame_2, font=label_font, height=5)
+ b.grid(row=i, column=1, pady=1)
+ c = customtkinter.CTkCheckBox(frame_2, font=label_font)
+ c.grid(row=i, column=2, pady=1)
+ c = customtkinter.CTkComboBox(frame_2, font=label_font, dropdown_font=label_font, height=15)
+ c.grid(row=i, column=3, pady=1)
+ e = customtkinter.CTkEntry(frame_2, font=label_font, height=15, placeholder_text="testtest")
+ e.grid(row=i, column=4, pady=1)
+ o = customtkinter.CTkOptionMenu(frame_2, font=label_font, height=15, width=50)
+ o.grid(row=i, column=5, pady=1)
+ r = customtkinter.CTkRadioButton(frame_2, font=label_font, height=15, width=50)
+ r.grid(row=i, column=6, pady=1)
+ s = customtkinter.CTkSwitch(frame_2, font=label_font, height=15, width=50)
+ s.grid(row=i, column=7, pady=1)
+frame_2.grid_columnconfigure((0, 1, 2, 3, 4), weight=1)
+
+def change_font():
+ import time
+ t1 = time.perf_counter()
+ label_font.configure(size=10, overstrike=True)
+ t2 = time.perf_counter()
+ print("change_font:", (t2-t1)*1000, "ms")
+
+app.after(3000, change_font)
+app.after(6000, lambda: label_font.configure(size=8, overstrike=False))
+app.mainloop()
diff --git a/test/manual_integration_tests/test_images.py b/test/manual_integration_tests/test_images.py
new file mode 100644
index 0000000..e1f9545
--- /dev/null
+++ b/test/manual_integration_tests/test_images.py
@@ -0,0 +1,43 @@
+import customtkinter
+from PIL import Image, ImageTk
+import os
+
+# load images
+file_path = os.path.dirname(os.path.realpath(__file__))
+image_1 = customtkinter.CTkImage(light_image=Image.open(file_path + "/test_images/add_folder_dark.png"),
+ dark_image=Image.open(file_path + "/test_images/add_folder_light.png"),
+ size=(30, 30))
+image_1.configure(dark_image=Image.open(file_path + "/test_images/add_folder_light.png"))
+image_2 = customtkinter.CTkImage(light_image=Image.open(file_path + "/test_images/bg_gradient.jpg"),
+ size=(30, 50))
+
+app = customtkinter.CTk()
+app.geometry("500x900")
+
+mode_switch = customtkinter.CTkSwitch(app, text="darkmode",
+ command=lambda: customtkinter.set_appearance_mode("dark" if mode_switch.get() == 1 else "light"))
+mode_switch.pack(padx=20, pady=20)
+
+scaling_button = customtkinter.CTkSegmentedButton(app, values=[0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0],
+ command=lambda v: customtkinter.set_widget_scaling(v))
+scaling_button.pack(padx=20, pady=20)
+
+button_1 = customtkinter.CTkButton(app, image=image_1)
+button_1.pack(padx=20, pady=20)
+
+button_1 = customtkinter.CTkButton(app, image=image_1, anchor="nw", compound="right", height=50, corner_radius=4)
+button_1.pack(padx=20, pady=20)
+
+label_1 = customtkinter.CTkLabel(app, text="", image=image_2, compound="right", fg_color="green", width=0)
+label_1.pack(padx=20, pady=20)
+label_1.configure(image=image_1)
+
+label_2 = customtkinter.CTkLabel(app, text="text", image=image_2, compound="right", fg_color="red", width=0, corner_radius=10)
+label_2.pack(padx=20, pady=20)
+
+label_3 = customtkinter.CTkLabel(app, image=ImageTk.PhotoImage(Image.open(file_path + "/test_images/bg_gradient.jpg").resize((300, 100))),
+ text="", compound="right", fg_color="green", width=0)
+label_3.pack(padx=20, pady=20)
+
+app.mainloop()
+
diff --git a/test/manual_integration_tests/test_images/add_folder_dark.png b/test/manual_integration_tests/test_images/add_folder_dark.png
new file mode 100644
index 0000000..dcf4f2d
Binary files /dev/null and b/test/manual_integration_tests/test_images/add_folder_dark.png differ
diff --git a/examples/test_images/add-folder.png b/test/manual_integration_tests/test_images/add_folder_light.png
similarity index 100%
rename from examples/test_images/add-folder.png
rename to test/manual_integration_tests/test_images/add_folder_light.png
diff --git a/test/manual_integration_tests/test_optionmenu_combobox.py b/test/manual_integration_tests/test_optionmenu_combobox.py
index 3c75360..351743e 100644
--- a/test/manual_integration_tests/test_optionmenu_combobox.py
+++ b/test/manual_integration_tests/test_optionmenu_combobox.py
@@ -34,7 +34,6 @@ combobox_1 = customtkinter.CTkComboBox(app, variable=variable, values=countries,
combobox_1.pack(pady=20, padx=10)
def set_new_scaling(scaling):
- customtkinter.set_spacing_scaling(scaling)
customtkinter.set_window_scaling(scaling)
customtkinter.set_widget_scaling(scaling)
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 bf7f7ba..90dcbff 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
@@ -22,7 +22,6 @@ def button_function():
def slider_function(value):
customtkinter.set_widget_scaling(value * 2)
- customtkinter.set_spacing_scaling(value * 2)
customtkinter.set_window_scaling(value * 2)
progressbar_1.set(value)
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 8f08484..3c0d8f7 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
@@ -20,12 +20,11 @@ top_tk.geometry("500x500")
def button_function():
app.geometry(f"{200}x{200}")
- print("Button click", label_1.text_label.cget("text"))
+ print("Button click", label_1.cget("text"))
def slider_function(value):
customtkinter.set_widget_scaling(value * 2)
- customtkinter.set_spacing_scaling(value * 2)
customtkinter.set_window_scaling(value * 2)
progressbar_1.set(value)
diff --git a/test/manual_integration_tests/test_scrollbar.py b/test/manual_integration_tests/test_scrollbar.py
deleted file mode 100644
index d579911..0000000
--- a/test/manual_integration_tests/test_scrollbar.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import tkinter
-import customtkinter
-
-# test with scaling
-# customtkinter.set_widget_scaling(2)
-# customtkinter.set_window_scaling(2)
-# customtkinter.set_spacing_scaling(2)
-
-customtkinter.set_appearance_mode("dark")
-
-app = customtkinter.CTk()
-app.title("test_scrollbar.py")
-app.grid_rowconfigure(0, weight=1)
-app.grid_columnconfigure((0, 2), weight=1)
-
-tk_textbox = tkinter.Text(app, highlightthickness=0, padx=5, pady=5)
-tk_textbox.grid(row=0, column=0, sticky="nsew")
-ctk_textbox_scrollbar = customtkinter.CTkScrollbar(app, command=tk_textbox.yview)
-ctk_textbox_scrollbar.grid(row=0, column=1, padx=0, sticky="ns")
-tk_textbox.configure(yscrollcommand=ctk_textbox_scrollbar.set)
-
-frame_1 = customtkinter.CTkFrame(app)
-frame_1.grid(row=0, column=2, padx=10, pady=10, sticky="nsew")
-frame_1.grid_rowconfigure((0, 1), weight=1)
-frame_1.grid_columnconfigure((0, ), weight=1)
-tk_textbox_1 = tkinter.Text(frame_1, highlightthickness=0, padx=5, pady=5)
-tk_textbox_1.grid(row=0, column=0, sticky="nsew", padx=(5, 0), pady=5)
-ctk_textbox_scrollbar_1 = customtkinter.CTkScrollbar(frame_1, command=tk_textbox_1.yview)
-ctk_textbox_scrollbar_1.grid(row=0, column=1, sticky="ns", padx=(0, 5), pady=5)
-tk_textbox_1.configure(yscrollcommand=ctk_textbox_scrollbar_1.set)
-ctk_textbox_scrollbar_1.configure(scrollbar_color="red", scrollbar_hover_color="darkred",
- border_spacing=0, width=12, fg_color="green", corner_radius=4)
-
-frame_2 = customtkinter.CTkFrame(frame_1)
-frame_2.grid(row=1, column=0, columnspan=2, padx=20, pady=20, sticky="nsew")
-frame_2.grid_rowconfigure((0, ), weight=1)
-frame_2.grid_columnconfigure((0, ), weight=1)
-tk_textbox_2 = tkinter.Text(frame_2, highlightthickness=0, padx=5, pady=5, wrap="none")
-tk_textbox_2.grid(row=0, column=0, sticky="nsew", padx=(5, 0), pady=5)
-ctk_textbox_scrollbar_2 = customtkinter.CTkScrollbar(frame_2, command=tk_textbox_2.yview)
-ctk_textbox_scrollbar_2.grid(row=0, column=1, sticky="ns", padx=(0, 5), pady=5)
-ctk_textbox_scrollbar_2_horizontal = customtkinter.CTkScrollbar(frame_2, command=tk_textbox_2.xview, orientation="horizontal")
-ctk_textbox_scrollbar_2_horizontal.grid(row=1, column=0, sticky="ew", padx=(5, 0), pady=(0, 5))
-tk_textbox_2.configure(yscrollcommand=ctk_textbox_scrollbar_2.set, xscrollcommand=ctk_textbox_scrollbar_2_horizontal.set)
-
-tk_textbox.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"]))
-tk_textbox_1.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"]))
-tk_textbox_2.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"]))
-
-tk_textbox.insert("insert", "\n".join([str(i) for i in range(100)]))
-tk_textbox_1.insert("insert", "\n".join([str(i) for i in range(1000)]))
-tk_textbox_2.insert("insert", "\n".join([str(i) + " - "*30 for i in range(10000)]))
-
-app.mainloop()
diff --git a/test/manual_integration_tests/test_segmented_button.py b/test/manual_integration_tests/test_segmented_button.py
new file mode 100644
index 0000000..79ef7ce
--- /dev/null
+++ b/test/manual_integration_tests/test_segmented_button.py
@@ -0,0 +1,81 @@
+import customtkinter
+
+
+app = customtkinter.CTk()
+app.geometry("600x950")
+
+switch_1 = customtkinter.CTkSwitch(app, text="darkmode", command=lambda: customtkinter.set_appearance_mode("dark" if switch_1.get() == 1 else "light"))
+switch_1.pack(padx=20, pady=20)
+
+seg_1 = customtkinter.CTkSegmentedButton(app, values=[])
+seg_1.configure(values=["value 1", "Value 2", "Value 42", "Value 123", "longlonglong"])
+seg_1.pack(padx=20, pady=20)
+
+frame_1 = customtkinter.CTkFrame(app, height=100)
+frame_1.pack(padx=20, pady=20, fill="x")
+
+seg_2_var = customtkinter.StringVar(value="value 1")
+
+seg_2 = customtkinter.CTkSegmentedButton(frame_1, values=["value 1", "Value 2", "Value 42"], variable=seg_2_var)
+seg_2.configure(values=[])
+seg_2.configure(values=["value 1", "Value 2", "Value 42"])
+seg_2.pack(padx=20, pady=10)
+seg_2.insert(0, "insert at 0")
+seg_2.insert(1, "insert at 1")
+
+label_seg_2 = customtkinter.CTkLabel(frame_1, textvariable=seg_2_var)
+label_seg_2.pack(padx=20, pady=10)
+
+frame_1_1 = customtkinter.CTkFrame(frame_1, height=100)
+frame_1_1.pack(padx=20, pady=10, fill="x")
+
+switch_2 = customtkinter.CTkSwitch(frame_1_1, text="change fg", command=lambda: frame_1_1.configure(fg_color="red" if switch_2.get() == 1 else "green"))
+switch_2.pack(padx=20, pady=20)
+
+seg_3 = customtkinter.CTkSegmentedButton(frame_1_1, values=["value 1", "Value 2", "Value 42"])
+seg_3.pack(padx=20, pady=10)
+
+seg_4 = customtkinter.CTkSegmentedButton(app)
+seg_4.pack(padx=20, pady=20)
+
+seg_5_var = customtkinter.StringVar(value="kfasjkfdklaj")
+seg_5 = customtkinter.CTkSegmentedButton(app, corner_radius=1000, border_width=0, unselected_color="green",
+ variable=seg_5_var)
+seg_5.pack(padx=20, pady=20)
+seg_5.configure(values=["1", "2", "3", "4"])
+seg_5.insert(0, "insert begin")
+seg_5.insert(len(seg_5.cget("values")), "insert 1")
+seg_5.insert(len(seg_5.cget("values")), "insert 2")
+seg_5.insert(len(seg_5.cget("values")), "insert 3")
+seg_5.configure(fg_color="green")
+
+seg_5.set("insert 2")
+seg_5.delete("insert 2")
+
+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,
+ dynamic_resizing=False, font=("Times", -20))
+
+app.mainloop()
diff --git a/test/manual_integration_tests/test_states.py b/test/manual_integration_tests/test_states.py
index 188aba8..e7c5daf 100644
--- a/test/manual_integration_tests/test_states.py
+++ b/test/manual_integration_tests/test_states.py
@@ -8,9 +8,9 @@ app.title("CustomTkinter Test")
def change_state(widget):
- if widget.state == tkinter.NORMAL:
+ if widget.cget("state") == tkinter.NORMAL:
widget.configure(state=tkinter.DISABLED)
- elif widget.state == tkinter.DISABLED:
+ elif widget.cget("state") == tkinter.DISABLED:
widget.configure(state=tkinter.NORMAL)
diff --git a/test/manual_integration_tests/test_string_dialog.py b/test/manual_integration_tests/test_string_dialog.py
index 76b2526..8d33f48 100644
--- a/test/manual_integration_tests/test_string_dialog.py
+++ b/test/manual_integration_tests/test_string_dialog.py
@@ -2,6 +2,8 @@ import customtkinter
customtkinter.set_appearance_mode("dark")
customtkinter.set_default_color_theme("blue")
+customtkinter.set_window_scaling(0.8)
+customtkinter.set_widget_scaling(0.8)
app = customtkinter.CTk()
app.geometry("400x300")
@@ -15,14 +17,21 @@ def change_mode():
customtkinter.set_appearance_mode("dark")
-def button_click_event():
- dialog = customtkinter.CTkInputDialog(master=None, text="Type in a number:", title="Test")
+def button_1_click_event():
+ dialog = customtkinter.CTkInputDialog(text="Type in a number:", title="Test")
print("Number:", dialog.get_input())
-button = customtkinter.CTkButton(app, text="Open Dialog", command=button_click_event)
-button.place(relx=0.5, rely=0.5, anchor=customtkinter.CENTER)
+def button_2_click_event():
+ dialog = customtkinter.CTkInputDialog(text="long text "*100, title="Test")
+ print("Number:", dialog.get_input())
+
+
+button_1 = customtkinter.CTkButton(app, text="Open Dialog", command=button_1_click_event)
+button_1.pack(pady=20)
+button_2 = customtkinter.CTkButton(app, text="Open Dialog", command=button_2_click_event)
+button_2.pack(pady=20)
c1 = customtkinter.CTkCheckBox(app, text="dark mode", command=change_mode)
-c1.place(relx=0.5, rely=0.8, anchor=customtkinter.CENTER)
+c1.pack(pady=20)
app.mainloop()
diff --git a/test/manual_integration_tests/test_tabview.py b/test/manual_integration_tests/test_tabview.py
new file mode 100644
index 0000000..73f7651
--- /dev/null
+++ b/test/manual_integration_tests/test_tabview.py
@@ -0,0 +1,34 @@
+import customtkinter
+
+app = customtkinter.CTk()
+app.geometry("800x900")
+
+tabview_1 = customtkinter.CTkTabview(app)
+tabview_1.pack(padx=20, pady=20)
+
+tab_1 = tabview_1.add("tab 1")
+tabview_1.insert(0, "tab 2")
+
+tabview_1.add("tab 42")
+tabview_1.set("tab 42")
+tabview_1.delete("tab 42")
+tabview_1.insert(0, "tab 42")
+tabview_1.delete("tab 42")
+tabview_1.insert(1, "tab 42")
+tabview_1.delete("tab 42")
+
+tabview_1.move(0, "tab 2")
+
+b2 = customtkinter.CTkButton(master=tabview_1.tab("tab 2"), text="button tab 2")
+b2.pack()
+
+# tabview_1.tab("tab 2").configure(fg_color="red")
+tabview_1.configure(state="normal")
+# tabview_1.delete("tab 1")
+
+for i in range(10):
+ for j in range(30):
+ button = customtkinter.CTkButton(tabview_1.tab("tab 1"), height=10, width=30, font=customtkinter.CTkFont(size=8))
+ button.grid(row=j, column=i, padx=2, pady=2)
+
+app.mainloop()
diff --git a/test/manual_integration_tests/test_textbox.py b/test/manual_integration_tests/test_textbox.py
index f9a5bd0..0a874bc 100644
--- a/test/manual_integration_tests/test_textbox.py
+++ b/test/manual_integration_tests/test_textbox.py
@@ -1,54 +1,102 @@
-import tkinter
import customtkinter
-# test with scaling
-# customtkinter.set_widget_scaling(2)
-# customtkinter.set_window_scaling(2)
-# customtkinter.set_spacing_scaling(2)
+#customtkinter.set_widget_scaling(0.9)
+#customtkinter.set_window_scaling(0.9)
customtkinter.set_appearance_mode("dark")
app = customtkinter.CTk()
app.title("test_scrollbar.py")
+app.geometry("800x1200")
app.grid_rowconfigure(0, weight=1)
-app.grid_columnconfigure((0, 2), weight=1)
+app.grid_columnconfigure((0, 1, 2, 3, 4), weight=1)
-tk_textbox = customtkinter.CTkTextbox(app, highlightthickness=0, padx=5, pady=5)
-tk_textbox.grid(row=0, column=0, sticky="nsew")
-ctk_textbox_scrollbar = customtkinter.CTkScrollbar(app, command=tk_textbox.yview)
-ctk_textbox_scrollbar.grid(row=0, column=1, padx=0, sticky="ns")
-tk_textbox.configure(yscrollcommand=ctk_textbox_scrollbar.set)
+textbox_1 = customtkinter.CTkTextbox(app, fg_color=None, corner_radius=0, border_spacing=0)
+textbox_1.grid(row=0, column=0, sticky="nsew")
+textbox_1.insert("0.0", "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)
-frame_1 = customtkinter.CTkFrame(app)
-frame_1.grid(row=0, column=2, padx=10, pady=10, sticky="nsew")
-frame_1.grid_rowconfigure((0, 1), weight=1)
-frame_1.grid_columnconfigure((0, ), weight=1)
-tk_textbox_1 = customtkinter.CTkTextbox(frame_1, highlightthickness=0, padx=5, pady=5)
-tk_textbox_1.grid(row=0, column=0, sticky="nsew", padx=(5, 0), pady=5)
-ctk_textbox_scrollbar_1 = customtkinter.CTkScrollbar(frame_1, command=tk_textbox_1.yview)
-ctk_textbox_scrollbar_1.grid(row=0, column=1, sticky="ns", padx=(0, 5), pady=5)
-tk_textbox_1.configure(yscrollcommand=ctk_textbox_scrollbar_1.set)
-ctk_textbox_scrollbar_1.configure(scrollbar_color="red", scrollbar_hover_color="darkred",
- border_spacing=0, width=12, fg_color="green", corner_radius=4)
+frame_1 = customtkinter.CTkFrame(app, corner_radius=0)
+frame_1.grid(row=0, column=1, sticky="nsew")
+frame_1.grid_rowconfigure((0, 1, 2, 3, 4), weight=1)
+frame_1.grid_columnconfigure(0, weight=1)
-frame_2 = customtkinter.CTkFrame(frame_1)
-frame_2.grid(row=1, column=0, columnspan=2, padx=20, pady=20, sticky="nsew")
-frame_2.grid_rowconfigure((0, ), weight=1)
-frame_2.grid_columnconfigure((0, ), weight=1)
-tk_textbox_2 = customtkinter.CTkTextbox(frame_2, highlightthickness=0, padx=5, pady=5, wrap="none")
-tk_textbox_2.grid(row=0, column=0, sticky="nsew", padx=(5, 0), pady=5)
-ctk_textbox_scrollbar_2 = customtkinter.CTkScrollbar(frame_2, command=tk_textbox_2.yview)
-ctk_textbox_scrollbar_2.grid(row=0, column=1, sticky="ns", padx=(0, 5), pady=5)
-ctk_textbox_scrollbar_2_horizontal = customtkinter.CTkScrollbar(frame_2, command=tk_textbox_2.xview, orientation="horizontal")
-ctk_textbox_scrollbar_2_horizontal.grid(row=1, column=0, sticky="ew", padx=(5, 0), pady=(0, 5))
-tk_textbox_2.configure(yscrollcommand=ctk_textbox_scrollbar_2.set, xscrollcommand=ctk_textbox_scrollbar_2_horizontal.set)
+textbox_2 = customtkinter.CTkTextbox(frame_1, wrap="none")
+textbox_2.grid(row=0, column=0, sticky="nsew", padx=20, pady=20)
+textbox_2.insert("0.0", "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)
-tk_textbox.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"]))
-tk_textbox_1.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"]))
-tk_textbox_2.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"]))
+textbox_2 = customtkinter.CTkTextbox(frame_1, wrap="none", corner_radius=30)
+textbox_2.grid(row=1, column=0, sticky="nsew", padx=20, pady=20)
+textbox_2.insert("0.0", "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)
-tk_textbox.insert("insert", "\n".join([str(i) for i in range(100)]))
-tk_textbox_1.insert("insert", "\n".join([str(i) for i in range(1000)]))
-tk_textbox_2.insert("insert", "\n".join([str(i) + " - "*30 for i in range(10000)]))
+textbox_2 = customtkinter.CTkTextbox(frame_1, wrap="none", corner_radius=0, border_width=30)
+textbox_2.grid(row=2, column=0, sticky="nsew", padx=20, pady=20)
+textbox_2.insert("0.0", "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)
+textbox_2 = customtkinter.CTkTextbox(frame_1, wrap="none", corner_radius=60, border_width=15)
+ #fg_color="blue", scrollbar_color="yellow", text_color="red")
+textbox_2.grid(row=3, column=0, sticky="nsew", padx=20, pady=20)
+textbox_2.insert("0.0", "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)
+
+textbox_2 = customtkinter.CTkTextbox(frame_1, wrap="none", corner_radius=0, border_width=0)
+textbox_2.grid(row=4, column=0, sticky="nsew", padx=20, pady=20)
+textbox_2.insert("0.0", "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)
+
+frame_2 = customtkinter.CTkFrame(app, corner_radius=0, fg_color=None)
+frame_2.grid(row=0, column=2, sticky="nsew")
+frame_2.grid_rowconfigure((0, 1, 2, 3, 4), weight=1)
+frame_2.grid_columnconfigure(0, weight=1)
+
+textbox_3 = customtkinter.CTkTextbox(frame_2)
+textbox_3.grid(row=0, column=0, sticky="nsew", padx=20, pady=20)
+textbox_3.insert("0.0", "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)
+
+textbox_3 = customtkinter.CTkTextbox(frame_2, corner_radius=30)
+textbox_3.grid(row=1, column=0, sticky="nsew", padx=20, pady=20)
+textbox_3.insert("0.0", "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)
+
+textbox_3 = customtkinter.CTkTextbox(frame_2, corner_radius=0, border_width=30)
+textbox_3.grid(row=2, column=0, sticky="nsew", padx=20, pady=20)
+textbox_3.insert("0.0", "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)
+
+textbox_3 = customtkinter.CTkTextbox(frame_2, corner_radius=60, border_width=15)
+textbox_3.grid(row=3, column=0, sticky="nsew", padx=20, pady=20)
+textbox_3.insert("0.0", "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)
+
+textbox_3 = customtkinter.CTkTextbox(frame_2, corner_radius=0, border_width=0, border_spacing=20)
+textbox_3.grid(row=4, column=0, sticky="nsew", padx=20, pady=20)
+textbox_3.insert("0.0", "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)
+
+frame_3 = customtkinter.CTkFrame(app, corner_radius=0, fg_color=None)
+frame_3.grid(row=0, column=3, sticky="nsew")
+frame_3.grid_rowconfigure((0, 1, 2, 3, 4), weight=1)
+frame_3.grid_columnconfigure(0, weight=1)
+
+textbox_3 = customtkinter.CTkTextbox(frame_3, activate_scrollbars=False)
+textbox_3.grid(row=0, column=0, sticky="nsew", padx=20, pady=20)
+textbox_3.insert("0.0", "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)
+
+textbox_3 = customtkinter.CTkTextbox(frame_3, corner_radius=10, border_width=2, activate_scrollbars=False)
+textbox_3.grid(row=1, column=0, sticky="nsew", padx=20, pady=20)
+textbox_3.insert("0.0", "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)
+
+textbox_3 = customtkinter.CTkTextbox(frame_3, corner_radius=0, border_width=2, activate_scrollbars=False)
+textbox_3.grid(row=2, column=0, sticky="nsew", padx=20, pady=20)
+textbox_3.insert("0.0", "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)
+
+textbox_3 = customtkinter.CTkTextbox(frame_3, corner_radius=0, border_width=2, activate_scrollbars=False)
+textbox_3.grid(row=3, column=0, sticky="nsew", padx=20, pady=20)
+textbox_3.insert("0.0", "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)
+
+textbox_3 = customtkinter.CTkTextbox(frame_3, corner_radius=0, border_width=0, activate_scrollbars=False, border_spacing=10)
+textbox_3.grid(row=4, column=0, sticky="nsew", padx=20, pady=20)
+textbox_3.insert("0.0", "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)
+
+textbox_4 = customtkinter.CTkTextbox(app, fg_color=None, corner_radius=0)
+textbox_4.grid(row=0, column=4, sticky="nsew")
+textbox_4.insert("0.0", "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)
+scrollbar4 = customtkinter.CTkScrollbar(app, command=textbox_4.yview)
+scrollbar4.grid(row=0, column=5, sticky="nsew")
+textbox_4.configure(yscrollcommand=scrollbar4.set)
+
+# app.after(3000, lambda: customtkinter.set_appearance_mode("light"))
app.mainloop()
diff --git a/test/manual_integration_tests/test_theme_colors.py b/test/manual_integration_tests/test_theme_colors.py
new file mode 100644
index 0000000..e1fe20c
--- /dev/null
+++ b/test/manual_integration_tests/test_theme_colors.py
@@ -0,0 +1,85 @@
+import tkinter
+import customtkinter
+
+customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light"
+customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
+
+app = customtkinter.CTk()
+app.geometry("1100x900")
+app.title("CustomTkinter simple_example.py")
+
+
+def create_all_widgets(master, state="normal"):
+ label_1 = customtkinter.CTkLabel(master=master, justify=tkinter.LEFT)
+ label_1.pack(pady=10, padx=10)
+
+ progressbar_1 = customtkinter.CTkProgressBar(master=master)
+ progressbar_1.pack(pady=10, padx=10)
+
+ button_1 = customtkinter.CTkButton(master=master, state=state, border_width=0)
+ button_1.pack(pady=10, padx=10)
+
+ slider_1 = customtkinter.CTkSlider(master=master, from_=0, to=1, state=state)
+ slider_1.pack(pady=10, padx=10)
+ slider_1.set(0.5)
+
+ entry_1 = customtkinter.CTkEntry(master=master, placeholder_text="CTkEntry", state=state)
+ entry_1.pack(pady=10, padx=10)
+
+ optionmenu_1 = customtkinter.CTkOptionMenu(master, values=["Option 1", "Option 2", "Option 42 long long long..."], state=state)
+ optionmenu_1.pack(pady=10, padx=10)
+ optionmenu_1.set("CTkOptionMenu")
+
+ combobox_1 = customtkinter.CTkComboBox(master, values=["Option 1", "Option 2", "Option 42 long long long..."], state=state)
+ combobox_1.pack(pady=10, padx=10)
+ optionmenu_1.set("CTkComboBox")
+
+ checkbox_1 = customtkinter.CTkCheckBox(master=master, state=state)
+ checkbox_1.pack(pady=10, padx=10)
+
+ radiobutton_var = tkinter.IntVar(value=1)
+
+ radiobutton_1 = customtkinter.CTkRadioButton(master=master, variable=radiobutton_var, value=1, state=state)
+ radiobutton_1.pack(pady=10, padx=10)
+
+ radiobutton_2 = customtkinter.CTkRadioButton(master=master, variable=radiobutton_var, value=2, state=state)
+ radiobutton_2.pack(pady=10, padx=10)
+
+ switch_1 = customtkinter.CTkSwitch(master=master, state=state)
+ switch_1.pack(pady=10, padx=10)
+
+ text_1 = customtkinter.CTkTextbox(master=master, width=200, height=70, state=state)
+ text_1.pack(pady=10, padx=10)
+ text_1.insert("0.0", "CTkTextbox\n\n\n\n")
+
+ segmented_button_1 = customtkinter.CTkSegmentedButton(master=master, values=["CTkSegmentedButton", "Value 2"], state=state)
+ segmented_button_1.pack(pady=10, padx=10)
+
+ tabview_1 = customtkinter.CTkTabview(master=master, width=200, height=100, state=state, border_width=2)
+ tabview_1.pack(pady=10, padx=10)
+ tabview_1.add("CTkTabview")
+ tabview_1.add("Tab 2")
+
+
+frame_0 = customtkinter.CTkFrame(master=app, fg_color="transparent")
+frame_0.grid(row=0, column=0, padx=10, pady=10)
+create_all_widgets(frame_0, state="disabled")
+
+frame_1 = customtkinter.CTkFrame(master=app, fg_color="transparent")
+frame_1.grid(row=0, column=1, padx=10, pady=10)
+create_all_widgets(frame_1)
+
+frame_2 = customtkinter.CTkFrame(master=app)
+frame_2.grid(row=0, column=2, padx=10, pady=10)
+create_all_widgets(frame_2)
+
+frame_3 = customtkinter.CTkFrame(master=app)
+frame_3.grid(row=0, column=3, padx=10, pady=10)
+frame_4 = customtkinter.CTkFrame(master=frame_3)
+frame_4.grid(row=0, column=0, padx=25, pady=25)
+create_all_widgets(frame_4)
+
+appearance_mode_button = customtkinter.CTkSegmentedButton(app, values=["light", "dark"], command=lambda v: customtkinter.set_appearance_mode(v))
+appearance_mode_button.grid(row=1, column=0, columnspan=3, padx=25, pady=25)
+
+app.mainloop()
diff --git a/test/manual_integration_tests/test_vertical_widgets.py b/test/manual_integration_tests/test_vertical_widgets.py
index 3b2feab..92bd06d 100644
--- a/test/manual_integration_tests/test_vertical_widgets.py
+++ b/test/manual_integration_tests/test_vertical_widgets.py
@@ -7,17 +7,17 @@ app.title("test_vertical_widgets")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure((0, 1, 2, 3), weight=1)
-progressbar_1 = customtkinter.CTkProgressBar(app, orient="horizontal")
+progressbar_1 = customtkinter.CTkProgressBar(app, orientation="horizontal")
progressbar_1.grid(row=0, column=0, pady=20, padx=20)
-progressbar_2 = customtkinter.CTkProgressBar(app, orient="vertical")
+progressbar_2 = customtkinter.CTkProgressBar(app, orientation="vertical")
progressbar_2.grid(row=1, column=0, pady=20, padx=20)
-slider_1 = customtkinter.CTkSlider(app, orient="horizontal", command=progressbar_1.set,
+slider_1 = customtkinter.CTkSlider(app, orientation="horizontal", command=progressbar_1.set,
button_corner_radius=3, button_length=20)
slider_1.grid(row=2, column=0, pady=20, padx=20)
-slider_2 = customtkinter.CTkSlider(app, orient="vertical", command=progressbar_2.set,
+slider_2 = customtkinter.CTkSlider(app, orientation="vertical", command=progressbar_2.set,
button_corner_radius=3, button_length=20)
slider_2.grid(row=3, column=0, pady=20, padx=20)
diff --git a/test/unit_tests/test_ctk.py b/test/unit_tests/test_ctk.py
index ab0d801..bb95d29 100644
--- a/test/unit_tests/test_ctk.py
+++ b/test/unit_tests/test_ctk.py
@@ -62,28 +62,28 @@ class TestCTk():
customtkinter.ScalingTracker.set_window_scaling(1.5)
self.root_ctk.geometry("300x400")
- assert self.root_ctk.current_width == 300 and self.root_ctk.current_height == 400
+ assert self.root_ctk._current_width == 300 and self.root_ctk._current_height == 400
assert self.root_ctk.window_scaling == 1.5 * customtkinter.ScalingTracker.get_window_dpi_scaling(self.root_ctk)
self.root_ctk.maxsize(400, 500)
self.root_ctk.geometry("500x500")
- assert self.root_ctk.current_width == 400 and self.root_ctk.current_height == 500
+ assert self.root_ctk._current_width == 400 and self.root_ctk._current_height == 500
customtkinter.ScalingTracker.set_window_scaling(1)
- assert self.root_ctk.current_width == 400 and self.root_ctk.current_height == 500
+ assert self.root_ctk._current_width == 400 and self.root_ctk._current_height == 500
print("successful")
def test_configure(self):
print(" -> test_configure: ", end="")
self.root_ctk.configure(bg="white")
- assert self.root_ctk.fg_color == "white"
+ assert self.root_ctk.cget("fg_color") == "white"
self.root_ctk.configure(background="red")
- assert self.root_ctk.fg_color == "red"
+ assert self.root_ctk.cget("fg_color") == "red"
assert self.root_ctk.cget("bg") == "red"
self.root_ctk.config(fg_color=("green", "#FFFFFF"))
- assert self.root_ctk.fg_color == ("green", "#FFFFFF")
+ assert self.root_ctk.cget("fg_color") == ("green", "#FFFFFF")
print("successful")
def test_appearance_mode(self):
diff --git a/test/unit_tests/test_ctk_entry.py b/test/unit_tests/test_ctk_entry.py
new file mode 100644
index 0000000..5c14c9a
--- /dev/null
+++ b/test/unit_tests/test_ctk_entry.py
@@ -0,0 +1,55 @@
+import customtkinter
+import time
+
+app = customtkinter.CTk()
+
+entry_1 = customtkinter.CTkEntry(app, width=100, height=25)
+entry_1.pack(padx=20, pady=20)
+entry_2 = customtkinter.CTkEntry(app, width=100, height=25)
+entry_2.pack(padx=20, pady=20)
+
+txt_var = customtkinter.StringVar(value="test")
+
+entry_1.configure(width=300,
+ height=35,
+ corner_radius=1000,
+ border_width=4,
+ bg_color="green",
+ fg_color=("red", "yellow"),
+ border_color="blue",
+ text_color=("brown", "green"),
+ placeholder_text_color="blue",
+ textvariable=txt_var,
+ placeholder_text="new_placholder",
+ font=("Times New Roman", -8, "bold"),
+ state="normal",
+ insertborderwidth=5,
+ insertwidth=10,
+ justify="right",
+ show="+")
+
+assert entry_1.cget("width") == 300
+assert entry_1.cget("height") == 35
+assert entry_1.cget("corner_radius") == 1000
+assert entry_1.cget("border_width") == 4
+assert entry_1.cget("bg_color") == "green"
+assert entry_1.cget("fg_color") == ("red", "yellow")
+assert entry_1.cget("border_color") == "blue"
+assert entry_1.cget("text_color") == ("brown", "green")
+assert entry_1.cget("placeholder_text_color") == "blue"
+assert entry_1.cget("textvariable") == txt_var
+assert entry_1.cget("placeholder_text") == "new_placholder"
+assert entry_1.cget("font") == ("Times New Roman", -8, "bold")
+assert entry_1.cget("state") == "normal"
+assert entry_1.cget("insertborderwidth") == 5
+assert entry_1.cget("insertwidth") == 10
+assert entry_1.cget("justify") == "right"
+# assert entry_1.cget("show") == "+" # somehow does not work, maybe a tkinter bug?
+
+def test_textvariable():
+ txt_var.set("test_2")
+ print(entry_1.get())
+ assert entry_1.get() == "test_2"
+
+app.after(500, test_textvariable)
+app.mainloop()