Browse Source

ctk und so

Adrian 4 weeks ago
parent
commit
aa78643f6f
100 changed files with 12531 additions and 254 deletions
  1. 90 0
      CHANGELOG.md
  2. 21 241
      LICENSE
  3. 5 0
      MANIFEST.in
  4. 130 0
      Readme.md
  5. 88 0
      customtkinter/__init__.py
  6. BIN
      customtkinter/__pycache__/__init__.cpython-310.pyc
  7. BIN
      customtkinter/assets/fonts/CustomTkinter_shapes_font.otf
  8. BIN
      customtkinter/assets/fonts/Roboto/Roboto-Medium.ttf
  9. BIN
      customtkinter/assets/fonts/Roboto/Roboto-Regular.ttf
  10. BIN
      customtkinter/assets/icons/CustomTkinter_icon_Windows.ico
  11. 155 0
      customtkinter/assets/themes/blue.json
  12. 155 0
      customtkinter/assets/themes/dark-blue.json
  13. 155 0
      customtkinter/assets/themes/green.json
  14. 3 0
      customtkinter/windows/__init__.py
  15. 117 0
      customtkinter/windows/ctk_input_dialog.py
  16. 333 0
      customtkinter/windows/ctk_tk.py
  17. 307 0
      customtkinter/windows/ctk_toplevel.py
  18. 16 0
      customtkinter/windows/widgets/__init__.py
  19. 4 0
      customtkinter/windows/widgets/appearance_mode/__init__.py
  20. 61 0
      customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py
  21. 122 0
      customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py
  22. 12 0
      customtkinter/windows/widgets/core_rendering/__init__.py
  23. 117 0
      customtkinter/windows/widgets/core_rendering/ctk_canvas.py
  24. 1235 0
      customtkinter/windows/widgets/core_rendering/draw_engine.py
  25. 2 0
      customtkinter/windows/widgets/core_widget_classes/__init__.py
  26. 326 0
      customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py
  27. 198 0
      customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py
  28. 594 0
      customtkinter/windows/widgets/ctk_button.py
  29. 469 0
      customtkinter/windows/widgets/ctk_checkbox.py
  30. 424 0
      customtkinter/windows/widgets/ctk_combobox.py
  31. 384 0
      customtkinter/windows/widgets/ctk_entry.py
  32. 196 0
      customtkinter/windows/widgets/ctk_frame.py
  33. 291 0
      customtkinter/windows/widgets/ctk_label.py
  34. 426 0
      customtkinter/windows/widgets/ctk_optionmenu.py
  35. 312 0
      customtkinter/windows/widgets/ctk_progressbar.py
  36. 430 0
      customtkinter/windows/widgets/ctk_radiobutton.py
  37. 316 0
      customtkinter/windows/widgets/ctk_scrollable_frame.py
  38. 281 0
      customtkinter/windows/widgets/ctk_scrollbar.py
  39. 447 0
      customtkinter/windows/widgets/ctk_segmented_button.py
  40. 413 0
      customtkinter/windows/widgets/ctk_slider.py
  41. 483 0
      customtkinter/windows/widgets/ctk_switch.py
  42. 433 0
      customtkinter/windows/widgets/ctk_tabview.py
  43. 500 0
      customtkinter/windows/widgets/ctk_textbox.py
  44. 24 0
      customtkinter/windows/widgets/font/__init__.py
  45. 94 0
      customtkinter/windows/widgets/font/ctk_font.py
  46. 66 0
      customtkinter/windows/widgets/font/font_manager.py
  47. 1 0
      customtkinter/windows/widgets/image/__init__.py
  48. 122 0
      customtkinter/windows/widgets/image/ctk_image.py
  49. 7 0
      customtkinter/windows/widgets/scaling/__init__.py
  50. 159 0
      customtkinter/windows/widgets/scaling/scaling_base_class.py
  51. 206 0
      customtkinter/windows/widgets/scaling/scaling_tracker.py
  52. 9 0
      customtkinter/windows/widgets/theme/__init__.py
  53. 55 0
      customtkinter/windows/widgets/theme/theme_manager.py
  54. 1 0
      customtkinter/windows/widgets/utility/__init__.py
  55. 22 0
      customtkinter/windows/widgets/utility/utility_functions.py
  56. BIN
      documentation_images/CustomTkinter_logo_dark.png
  57. BIN
      documentation_images/CustomTkinter_logo_light.png
  58. BIN
      documentation_images/complex_example_dark_Windows.png
  59. BIN
      documentation_images/complex_example_light_macOS.png
  60. BIN
      documentation_images/image_example_dark_Windows.png
  61. BIN
      documentation_images/paypal_donate_button.png
  62. BIN
      documentation_images/scrollable_frame_example_Windows.png
  63. BIN
      documentation_images/single_button_macOS.png
  64. 164 0
      examples/complex_example.py
  65. 61 0
      examples/example_background_image.py
  66. 118 0
      examples/image_example.py
  67. 133 0
      examples/scrollable_frame_example.py
  68. 76 0
      examples/simple_example.py
  69. BIN
      examples/test_images/CustomTkinter_logo_single.png
  70. BIN
      examples/test_images/add_user_dark.png
  71. BIN
      examples/test_images/add_user_light.png
  72. BIN
      examples/test_images/bg_gradient.jpg
  73. BIN
      examples/test_images/chat_dark.png
  74. BIN
      examples/test_images/chat_light.png
  75. BIN
      examples/test_images/home_dark.png
  76. BIN
      examples/test_images/home_light.png
  77. BIN
      examples/test_images/image_icon_light.png
  78. BIN
      examples/test_images/large_test_image.png
  79. 6 1
      libs/foo/__init__.py
  80. 46 12
      main.py
  81. 395 0
      oszi.py
  82. 34 0
      pyproject.toml
  83. 3 0
      requirements.txt
  84. 39 0
      setup.cfg
  85. 51 0
      test/manual_integration_tests/simple_example_standard_tkinter.py
  86. 178 0
      test/manual_integration_tests/test_all_widgets_with_colors.py
  87. 48 0
      test/manual_integration_tests/test_button_antialiasing.py
  88. 36 0
      test/manual_integration_tests/test_configure_dimensions.py
  89. 32 0
      test/manual_integration_tests/test_ctk_behavior/test_ctk_appearance_mode_change.py
  90. 9 0
      test/manual_integration_tests/test_ctk_behavior/test_ctk_iconify_at_beginning.py
  91. 9 0
      test/manual_integration_tests/test_ctk_behavior/test_ctk_withdraw_at_beginning.py
  92. 27 0
      test/manual_integration_tests/test_ctk_behavior/test_ctk_zoomed_state.py
  93. 51 0
      test/manual_integration_tests/test_ctk_toplevel.py
  94. 35 0
      test/manual_integration_tests/test_ctk_toplevel_behavior/test_ctk_toplevel_appearance_mode_change.py
  95. 12 0
      test/manual_integration_tests/test_ctk_toplevel_behavior/test_ctk_toplevel_iconify_at_beginning.py
  96. 12 0
      test/manual_integration_tests/test_ctk_toplevel_behavior/test_ctk_toplevel_withdraw_at_beginning.py
  97. 29 0
      test/manual_integration_tests/test_ctk_toplevel_behavior/test_ctk_toplevel_zoomed_state.py
  98. 31 0
      test/manual_integration_tests/test_filedialog.py
  99. 79 0
      test/manual_integration_tests/test_font.py
  100. 0 0
      test/manual_integration_tests/test_images.py

+ 90 - 0
CHANGELOG.md

@@ -0,0 +1,90 @@
+# Changelog
+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:
+ - cursor configuring
+ - overwrite winfo methods
+ - set icon (self.call("wm", "iconphoto", self._w, tkinter.PhotoImage(file="test_images/CustomTkinter_logo_single.png")))
+ - add option to change label position for checkbox, switch, radiobutton #628
+
+
+## [5.2.0] - 2022-05-02
+### Added
+ - Mostly bug fixes
+
+## [5.1.0] - 2022-05-02
+### Added
+ - Added CTkScrollableFrame
+
+### Changed
+ - Changed license to MIT
+
+## [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()
+
+## [4.5.0] - 2022-06-23
+### Added
+ - CTkScrollbar (vertical, horizontal)
+
+## [4.4.0] - 2022-06-14
+### Changed
+ - Changed custom dropdown menu to normal tkinter.Menu because of multiple platform specific bugs
+
+## [4.3.0] - 2022-06-1
+### Added
+ - Added CTkComboBox
+ - Small fixes for new dropdown menu
+
+## [4.2.0] - 2022-05-30
+### Added
+ - CTkOptionMenu with custom dropdown menu
+ - Support for clicking on labels of CTkCheckBox, CTkRadioButton, CTkSwitch
+
+## [4.1.0] - 2022-05-24
+### Added
+ - Configure width and height for frame, button, label, progressbar, slider, entry
+
+## [4.0.0] - 2022-05-22
+### Added
+ - This changelog file
+ - Adopted semantic versioning
+ - Added HighDPI scaling to all widgets and geometry managers (place, pack, grid)
+ - Restructured CTkSettings and renamed a few manager classes
+ - Orientation attribute for slider and progressbar
+
+### Removed
+ - A few unnecessary tests

File diff suppressed because it is too large
+ 21 - 241
LICENSE


+ 5 - 0
MANIFEST.in

@@ -0,0 +1,5 @@
+include customtkinter/assets/*
+include customtkinter/assets/fonts/*
+include customtkinter/assets/fonts/Roboto/*
+include customtkinter/assets/icons/*
+include customtkinter/assets/themes/*

+ 130 - 0
Readme.md

@@ -0,0 +1,130 @@
+<p align="center">
+  <picture>
+    <source media="(prefers-color-scheme: dark)" srcset="./documentation_images/CustomTkinter_logo_dark.png">
+    <img src="./documentation_images/CustomTkinter_logo_light.png">
+  </picture>
+</p>
+
+<div align="center">
+
+![PyPI](https://img.shields.io/pypi/v/customtkinter)
+![PyPI - Downloads](https://img.shields.io/pypi/dm/customtkinter?color=green&label=downloads)
+![Downloads last 6 month](https://static.pepy.tech/personalized-badge/customtkinter?period=total&units=international_system&left_color=grey&right_color=green&left_text=downloads%20last%206%20month)
+![PyPI - License](https://img.shields.io/badge/license-MIT-blue)
+![LOC](https://tokei.rs/b1/github/tomschimansky/customtkinter?category=lines)
+
+</div>
+
+---
+
+<div align="center">
+<a href="https://www.paypal.com/donate/?hosted_button_id=LK5QAZYRN2R2A"><img src="documentation_images/paypal_donate_button.png" width=170 alt="Paypal donation button"></a>
+<h3>
+Official website: https://customtkinter.tomschimansky.com
+</h3>
+</div>
+
+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
+can also be used in combination with normal Tkinter elements. The widgets
+and the window colors either adapt to the system appearance or the manually set mode
+('light', 'dark'), and all CustomTkinter widgets and windows support HighDPI scaling
+(Windows, macOS). With CustomTkinter you'll get a consistent and modern look across all
+desktop platforms (Windows, macOS, Linux).
+
+![](documentation_images/complex_example_dark_Windows.png)
+| _`complex_example.py` on Windows 11 with dark mode and 'blue' theme_
+
+![](documentation_images/complex_example_light_macOS.png)
+| _`complex_example.py` on macOS in light mode and standard 'blue' theme_
+###
+
+
+## Installation
+Install the module with pip:
+```
+pip3 install customtkinter
+```
+**Update existing installation:** ```pip3 install customtkinter --upgrade```\
+(update as often as possible because this library is under active development)
+
+## Documentation
+
+The **official** documentation can be found here:
+
+**➡️ https://customtkinter.tomschimansky.com/documentation**.
+
+## Example Program
+To test customtkinter you can try this simple example with only a single button:
+```python
+import customtkinter
+
+customtkinter.set_appearance_mode("System")  # Modes: system (default), light, dark
+customtkinter.set_default_color_theme("blue")  # Themes: blue (default), dark-blue, green
+
+app = customtkinter.CTk()  # create CTk window like you do with the Tk window
+app.geometry("400x240")
+
+def button_function():
+    print("button pressed")
+
+# 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)
+
+app.mainloop()
+```
+which results in the following window on macOS:
+
+<img src="documentation_images/single_button_macOS.png" width="400"/>
+
+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, scaling, themes and all widgets.
+
+## More Examples and Showcase
+
+### Appearance mode change and scaling change
+
+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 appearance mode change and standard 'blue' theme_
+###
+
+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):
+
+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
+It's possible to put an image on a CTkButton. You just have to
+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:
+
+![](documentation_images/image_example_dark_Windows.png)
+| _`image_example.py` on Windows 11_
+###
+
+### Scrollable Frames
+Scrollable frames are possible in vertical or horizontal orientation and can be combined
+with any other widgets.
+![](documentation_images/scrollable_frame_example_Windows.png)
+| _`scrollable_frame_example.py` on Windows 11_
+
+### Integration of TkinterMapView widget
+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:
+
+https://user-images.githubusercontent.com/66446067/204675835-1584a8da-5acc-4993-b4a9-e70f06fa14b0.mp4
+
+| _`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

+ 88 - 0
customtkinter/__init__.py

@@ -0,0 +1,88 @@
+__version__ = "5.2.2"
+
+import os
+import sys
+from tkinter import Variable, StringVar, IntVar, DoubleVar, BooleanVar
+from tkinter.constants import *
+import tkinter.filedialog as filedialog
+
+# import manager classes
+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 base widgets
+from .windows.widgets.core_rendering import CTkCanvas
+from .windows.widgets.core_widget_classes import CTkBaseClass
+
+# import widgets
+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
+from .windows.widgets import CTkScrollableFrame
+
+# import windows
+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
+
+from .windows import ctk_tk
+
+_ = Variable, StringVar, IntVar, DoubleVar, BooleanVar, CENTER, filedialog  # prevent IDE from removing unused imports
+
+
+def set_appearance_mode(mode_string: str):
+    """ possible values: light, dark, system """
+    AppearanceModeTracker.set_appearance_mode(mode_string)
+
+
+def get_appearance_mode() -> str:
+    """ get current state of the appearance mode (light or dark) """
+    if AppearanceModeTracker.appearance_mode == 0:
+        return "Light"
+    elif AppearanceModeTracker.appearance_mode == 1:
+        return "Dark"
+
+
+def set_default_color_theme(color_string: str):
+    """ set color theme or load custom theme file by passing the path """
+    ThemeManager.load_theme(color_string)
+
+
+def set_widget_scaling(scaling_value: float):
+    """ set scaling for the widget dimensions """
+    ScalingTracker.set_widget_scaling(scaling_value)
+
+
+def set_window_scaling(scaling_value: float):
+    """ set scaling for window dimensions """
+    ScalingTracker.set_window_scaling(scaling_value)
+
+
+def deactivate_automatic_dpi_awareness():
+    """ deactivate DPI awareness of current process (windll.shcore.SetProcessDpiAwareness(0)) """
+    ScalingTracker.deactivate_automatic_dpi_awareness = True
+
+
+def set_ctk_parent_class(ctk_parent_class):
+    ctk_tk.CTK_PARENT_CLASS = ctk_parent_class

BIN
customtkinter/__pycache__/__init__.cpython-310.pyc


BIN
customtkinter/assets/fonts/CustomTkinter_shapes_font.otf


BIN
customtkinter/assets/fonts/Roboto/Roboto-Medium.ttf


BIN
customtkinter/assets/fonts/Roboto/Roboto-Regular.ttf


BIN
customtkinter/assets/icons/CustomTkinter_icon_Windows.ico


+ 155 - 0
customtkinter/assets/themes/blue.json

@@ -0,0 +1,155 @@
+{
+  "CTk": {
+    "fg_color": ["gray92", "gray14"]
+  },
+  "CTkToplevel": {
+    "fg_color": ["gray92", "gray14"]
+  },
+  "CTkFrame": {
+    "corner_radius": 6,
+    "border_width": 0,
+    "fg_color": ["gray86", "gray17"],
+    "top_fg_color": ["gray81", "gray20"],
+    "border_color": ["gray65", "gray28"]
+  },
+  "CTkButton": {
+    "corner_radius": 6,
+    "border_width": 0,
+    "fg_color": ["#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", "#1D1E1E"],
+    "border_color": ["#979DA2", "#565B5E"],
+    "text_color":["gray10", "#DCE4EE"],
+    "scrollbar_button_color": ["gray55", "gray41"],
+    "scrollbar_button_hover_color": ["gray40", "gray53"]
+  },
+  "CTkScrollableFrame": {
+    "label_fg_color": ["gray78", "gray23"]
+  },
+  "DropdownMenu": {
+    "fg_color": ["gray90", "gray20"],
+    "hover_color": ["gray75", "gray28"],
+    "text_color": ["gray10", "gray90"]
+  },
+  "CTkFont": {
+    "macOS": {
+      "family": "SF Display",
+      "size": 13,
+      "weight": "normal"
+    },
+    "Windows": {
+      "family": "Roboto",
+      "size": 13,
+      "weight": "normal"
+    },
+    "Linux": {
+      "family": "Roboto",
+      "size": 13,
+      "weight": "normal"
+    }
+  }
+}

+ 155 - 0
customtkinter/assets/themes/dark-blue.json

@@ -0,0 +1,155 @@
+{
+  "CTk": {
+    "fg_color": ["gray95", "gray10"]
+  },
+  "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"]
+  },
+  "CTkScrollableFrame": {
+    "label_fg_color": ["gray80", "gray21"]
+  },
+  "DropdownMenu": {
+    "fg_color": ["gray90", "gray20"],
+    "hover_color": ["gray75", "gray28"],
+    "text_color": ["gray14", "gray84"]
+  },
+  "CTkFont": {
+    "macOS": {
+      "family": "SF Display",
+      "size": 13,
+      "weight": "normal"
+    },
+    "Windows": {
+      "family": "Roboto",
+      "size": 13,
+      "weight": "normal"
+    },
+    "Linux": {
+      "family": "Roboto",
+      "size": 13,
+      "weight": "normal"
+    }
+  }
+}

+ 155 - 0
customtkinter/assets/themes/green.json

@@ -0,0 +1,155 @@
+{
+  "CTk": {
+    "fg_color": ["gray92", "gray14"]
+  },
+  "CTkToplevel": {
+    "fg_color": ["gray92", "gray14"]
+  },
+  "CTkFrame": {
+    "corner_radius": 6,
+    "border_width": 0,
+    "fg_color": ["gray86", "gray17"],
+    "top_fg_color": ["gray81", "gray20"],
+    "border_color": ["gray65", "gray28"]
+  },
+  "CTkButton": {
+    "corner_radius": 6,
+    "border_width": 0,
+    "fg_color": ["#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"]
+  },
+  "CTkScrollableFrame": {
+    "label_fg_color": ["gray78", "gray23"]
+  },
+  "DropdownMenu": {
+    "fg_color": ["gray90", "gray20"],
+    "hover_color": ["gray75", "gray28"],
+    "text_color": ["gray10", "gray90"]
+  },
+  "CTkFont": {
+    "macOS": {
+      "family": "SF Display",
+      "size": 13,
+      "weight": "normal"
+    },
+    "Windows": {
+      "family": "Roboto",
+      "size": 13,
+      "weight": "normal"
+    },
+    "Linux": {
+      "family": "Roboto",
+      "size": 13,
+      "weight": "normal"
+    }
+  }
+}

+ 3 - 0
customtkinter/windows/__init__.py

@@ -0,0 +1,3 @@
+from .ctk_tk import CTk
+from .ctk_toplevel import CTkToplevel
+from .ctk_input_dialog import CTkInputDialog

+ 117 - 0
customtkinter/windows/ctk_input_dialog.py

@@ -0,0 +1,117 @@
+from typing import Union, Tuple, Optional
+
+from .widgets import CTkLabel
+from .widgets import CTkEntry
+from .widgets import CTkButton
+from .widgets.theme import ThemeManager
+from .ctk_toplevel import CTkToplevel
+from .widgets.font import CTkFont
+
+
+class CTkInputDialog(CTkToplevel):
+    """
+    Dialog with extra window, message, entry widget, cancel and ok button.
+    For detailed information check out the documentation.
+    """
+
+    def __init__(self,
+                 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,
+
+                 title: str = "CTkDialog",
+                 font: Optional[Union[tuple, CTkFont]] = None,
+                 text: str = "CTkDialog"):
+
+        super().__init__(fg_color=fg_color)
+
+        self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+        self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color)
+        self._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._user_input: Union[str, None] = None
+        self._running: bool = False
+        self._title = title
+        self._text = text
+        self._font = font
+
+        self.title(self._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
+
+    def _create_widgets(self):
+        self.grid_columnconfigure((0, 1), weight=1)
+        self.rowconfigure(0, weight=1)
+
+        self._label = CTkLabel(master=self,
+                               width=300,
+                               wraplength=300,
+                               fg_color="transparent",
+                               text_color=self._text_color,
+                               text=self._text,
+                               font=self._font)
+        self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew")
+
+        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,
+                               font=self._font)
+        self._entry.grid(row=1, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew")
+
+        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',
+                                    font=self._font,
+                                    command=self._ok_event)
+        self._ok_button.grid(row=2, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew")
+
+        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',
+                                        font=self._font,
+                                        command=self._cancel_event)
+        self._cancel_button.grid(row=2, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew")
+
+        self.after(150, lambda: self._entry.focus())  # set focus to entry with slight delay, otherwise it won't work
+        self._entry.bind("<Return>", self._ok_event)
+
+    def _ok_event(self, event=None):
+        self._user_input = self._entry.get()
+        self.grab_release()
+        self.destroy()
+
+    def _on_closing(self):
+        self.grab_release()
+        self.destroy()
+
+    def _cancel_event(self):
+        self.grab_release()
+        self.destroy()
+
+    def get_input(self):
+        self.master.wait_window(self)
+        return self._user_input

+ 333 - 0
customtkinter/windows/ctk_tk.py

@@ -0,0 +1,333 @@
+import tkinter
+import sys
+import os
+import platform
+import ctypes
+from typing import Union, Tuple, Optional
+from packaging import version
+
+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
+
+CTK_PARENT_CLASS = tkinter.Tk
+
+
+class CTk(CTK_PARENT_CLASS, 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):
+
+        self._enable_macos_dark_title_bar()
+
+        # call init methods of super classes
+        CTK_PARENT_CLASS.__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)
+
+        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)
+
+        self._fg_color = ThemeManager.theme["CTk"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+
+        # set bg of tkinter.Tk
+        super().configure(bg=self._apply_appearance_mode(self._fg_color))
+
+        # set title
+        self.title("CTk")
+
+        # indicator variables
+        self._iconbitmap_method_called = False  # indicates if wm_iconbitmap method got called
+        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._block_update_dimensions_event = False
+
+        # save focus before calling withdraw
+        self.focused_widget_before_widthdraw = None
+
+        # set CustomTkinter titlebar icon (Windows only)
+        if sys.platform.startswith("win"):
+            self.after(200, self._windows_set_titlebar_icon)
+
+        # set titlebar color (Windows only)
+        if sys.platform.startswith("win"):
+            self._windows_set_titlebar_color(self._get_appearance_mode())
+
+        self.bind('<Configure>', self._update_dimensions_event)
+        self.bind('<FocusIn>', self._focus_in_event)
+
+    def destroy(self):
+        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
+        super().withdraw()
+
+    def iconify(self):
+        if self._window_exists is False:
+            self._iconify_called_before_window_exists = True
+        super().iconify()
+
+    def update(self):
+        if self._window_exists is False:
+            if sys.platform.startswith("win"):
+                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()
+
+            self._window_exists = True
+
+        super().update()
+
+    def mainloop(self, *args, **kwargs):
+        if not self._window_exists:
+            if sys.platform.startswith("win"):
+                self._windows_set_titlebar_color(self._get_appearance_mode())
+
+                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()
+
+            self._window_exists = True
+
+        super().mainloop(*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"):
+            self._windows_set_titlebar_color(self._get_appearance_mode())
+
+        return current_resizable_values
+
+    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))
+
+            # update width and height attributes
+            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))
+        else:
+            return self._reverse_geometry_scaling(super().geometry())
+
+    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():
+                try:
+                    child.configure(bg_color=self._fg_color)
+                except Exception:
+                    pass
+
+        super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_configure_arguments))
+        check_kwargs_empty(kwargs)
+
+    def cget(self, attribute_name: str) -> any:
+        if attribute_name == "fg_color":
+            return self._fg_color
+        else:
+            return super().cget(attribute_name)
+
+    def wm_iconbitmap(self, bitmap=None, default=None):
+        self._iconbitmap_method_called = True
+        super().wm_iconbitmap(bitmap, default)
+
+    def iconbitmap(self, bitmap=None, default=None):
+        self._iconbitmap_method_called = True
+        super().wm_iconbitmap(bitmap, default)
+
+    def _windows_set_titlebar_icon(self):
+        try:
+            # if not the user already called iconbitmap method, set icon
+            if not self._iconbitmap_method_called:
+                customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+                self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico"))
+        except Exception:
+            pass
+
+    @classmethod
+    def _enable_macos_dark_title_bar(cls):
+        if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation:  # macOS
+            if version.parse(platform.python_version()) < version.parse("3.10"):
+                if version.parse(tkinter.Tcl().call("info", "patchlevel")) >= version.parse("8.6.9"):  # Tcl/Tk >= 8.6.9
+                    os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
+                    # This command allows dark-mode for all programs
+
+    @classmethod
+    def _disable_macos_dark_title_bar(cls):
+        if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation:  # macOS
+            if version.parse(platform.python_version()) < version.parse("3.10"):
+                if version.parse(tkinter.Tcl().call("info", "patchlevel")) >= version.parse("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):
+        """
+        Set the titlebar color of the window to light or dark theme on Microsoft Windows.
+
+        Credits for this function:
+        https://stackoverflow.com/questions/23836000/can-i-change-the-title-bar-in-tkinter/70724666#70724666
+
+        MORE INFO:
+        https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+        """
+
+        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()
+                # 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":
+                    self.focused_widget_before_widthdraw = self.focus_get()
+                    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")
+                self.focused_widget_before_widthdraw = self.focus_get()
+                super().withdraw()
+                super().update()
+
+            if color_mode.lower() == "dark":
+                value = 1
+            elif color_mode.lower() == "light":
+                value = 0
+            else:
+                return
+
+            try:
+                hwnd = ctypes.windll.user32.GetParent(self.winfo_id())
+                DWMWA_USE_IMMERSIVE_DARK_MODE = 20
+                DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19
+
+                # try with DWMWA_USE_IMMERSIVE_DARK_MODE
+                if ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,
+                                                              ctypes.byref(ctypes.c_int(value)),
+                                                              ctypes.sizeof(ctypes.c_int(value))) != 0:
+
+                    # try with DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20h1
+                    ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1,
+                                                               ctypes.byref(ctypes.c_int(value)),
+                                                               ctypes.sizeof(ctypes.c_int(value)))
+
+            except Exception as err:
+                print(err)
+
+            if self._window_exists or True:
+                # print("window_exists -> return to original state: ", self.state_before_windows_set_titlebar_color)
+                if self._state_before_windows_set_titlebar_color == "normal":
+                    self.deiconify()
+                elif self._state_before_windows_set_titlebar_color == "iconic":
+                    self.iconify()
+                elif self._state_before_windows_set_titlebar_color == "zoomed":
+                    self.state("zoomed")
+                else:
+                    self.state(self._state_before_windows_set_titlebar_color)  # other states
+            else:
+                pass  # wait for update or mainloop to be called
+
+            if self.focused_widget_before_widthdraw is not None:
+                self.after(1, self.focused_widget_before_widthdraw.focus)
+                self.focused_widget_before_widthdraw = None
+
+    def _set_appearance_mode(self, mode_string: str):
+        super()._set_appearance_mode(mode_string)
+
+        if sys.platform.startswith("win"):
+            self._windows_set_titlebar_color(mode_string)
+
+        super().configure(bg=self._apply_appearance_mode(self._fg_color))

+ 307 - 0
customtkinter/windows/ctk_toplevel.py

@@ -0,0 +1,307 @@
+import tkinter
+from packaging import version
+import sys
+import os
+import platform
+import ctypes
+from typing import Union, Tuple, Optional
+
+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, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
+    """
+    Toplevel window with dark titlebar on Windows and macOS.
+    For detailed information check out the documentation.
+    """
+
+    _valid_tk_toplevel_arguments: set = {"master", "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: Optional[Union[str, Tuple[str, str]]] = None,
+                 **kwargs):
+
+        self._enable_macos_dark_title_bar()
+
+        # 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)
+
+        try:
+            # Set Windows titlebar icon
+            if sys.platform.startswith("win"):
+                customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+                self.after(200, lambda: self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico")))
+        except Exception:
+            pass
+
+        self._current_width = 200  # initial window size, always without scaling
+        self._current_height = 200
+        self._min_width: int = 0
+        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["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
+
+        # set bg color of tkinter.Toplevel
+        super().configure(bg=self._apply_appearance_mode(self._fg_color))
+
+        # set title of tkinter.Toplevel
+        super().title("CTkToplevel")
+
+        # indicator variables
+        self._iconbitmap_method_called = True
+        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._block_update_dimensions_event = False
+
+        # save focus before calling withdraw
+        self.focused_widget_before_widthdraw = None
+
+        # set CustomTkinter titlebar icon (Windows only)
+        if sys.platform.startswith("win"):
+            self.after(200, self._windows_set_titlebar_icon)
+
+        # set titlebar color (Windows only)
+        if sys.platform.startswith("win"):
+            self._windows_set_titlebar_color(self._get_appearance_mode())
+
+        self.bind('<Configure>', self._update_dimensions_event)
+        self.bind('<FocusIn>', self._focus_in_event)
+
+    def destroy(self):
+        self._disable_macos_dark_title_bar()
+
+        # call destroy methods of super classes
+        tkinter.Toplevel.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 = self.winfo_width()  # detect current window size
+            detected_height = self.winfo_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))
+
+            # update width and height attributes
+            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))
+        else:
+            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
+        super().withdraw()
+
+    def iconify(self):
+        if self._windows_set_titlebar_color_called:
+            self._iconify_called_after_windows_set_titlebar_color = True
+        super().iconify()
+
+    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"):
+            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))
+
+    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 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():
+                try:
+                    child.configure(bg_color=self._fg_color)
+                except Exception:
+                    pass
+
+        super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
+        check_kwargs_empty(kwargs)
+
+    def cget(self, attribute_name: str) -> any:
+        if attribute_name == "fg_color":
+            return self._fg_color
+        else:
+            return super().cget(attribute_name)
+
+    def wm_iconbitmap(self, bitmap=None, default=None):
+        self._iconbitmap_method_called = True
+        super().wm_iconbitmap(bitmap, default)
+
+    def _windows_set_titlebar_icon(self):
+        try:
+            # if not the user already called iconbitmap method, set icon
+            if not self._iconbitmap_method_called:
+                customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+                self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico"))
+        except Exception:
+            pass
+
+    @classmethod
+    def _enable_macos_dark_title_bar(cls):
+        if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation:  # macOS
+            if version.parse(platform.python_version()) < version.parse("3.10"):
+                if version.parse(tkinter.Tcl().call("info", "patchlevel")) >= version.parse("8.6.9"):  # Tcl/Tk >= 8.6.9
+                    os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
+
+    @classmethod
+    def _disable_macos_dark_title_bar(cls):
+        if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation:  # macOS
+            if version.parse(platform.python_version()) < version.parse("3.10"):
+                if version.parse(tkinter.Tcl().call("info", "patchlevel")) >= version.parse("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):
+        """
+        Set the titlebar color of the window to light or dark theme on Microsoft Windows.
+
+        Credits for this function:
+        https://stackoverflow.com/questions/23836000/can-i-change-the-title-bar-in-tkinter/70724666#70724666
+
+        MORE INFO:
+        https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+        """
+
+        if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation:
+
+            self._state_before_windows_set_titlebar_color = self.state()
+            self.focused_widget_before_widthdraw = self.focus_get()
+            super().withdraw()  # hide window so that it can be redrawn after the titlebar change so that the color change is visible
+            super().update()
+
+            if color_mode.lower() == "dark":
+                value = 1
+            elif color_mode.lower() == "light":
+                value = 0
+            else:
+                return
+
+            try:
+                hwnd = ctypes.windll.user32.GetParent(self.winfo_id())
+                DWMWA_USE_IMMERSIVE_DARK_MODE = 20
+                DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19
+
+                # try with DWMWA_USE_IMMERSIVE_DARK_MODE
+                if ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,
+                                                              ctypes.byref(ctypes.c_int(value)),
+                                                              ctypes.sizeof(ctypes.c_int(value))) != 0:
+                    # try with DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20h1
+                    ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1,
+                                                               ctypes.byref(ctypes.c_int(value)),
+                                                               ctypes.sizeof(ctypes.c_int(value)))
+
+            except Exception as err:
+                print(err)
+
+            self._windows_set_titlebar_color_called = True
+            self.after(5, self._revert_withdraw_after_windows_set_titlebar_color)
+
+            if self.focused_widget_before_widthdraw is not None:
+                self.after(10, self.focused_widget_before_widthdraw.focus)
+                self.focused_widget_before_widthdraw = None
+
+    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._withdraw_called_after_windows_set_titlebar_color:
+                pass  # leave it withdrawed
+            elif self._iconify_called_after_windows_set_titlebar_color:
+                super().iconify()
+            else:
+                if self._state_before_windows_set_titlebar_color == "normal":
+                    self.deiconify()
+                elif self._state_before_windows_set_titlebar_color == "iconic":
+                    self.iconify()
+                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._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):
+        super()._set_appearance_mode(mode_string)
+
+        if sys.platform.startswith("win"):
+            self._windows_set_titlebar_color(mode_string)
+
+        super().configure(bg=self._apply_appearance_mode(self._fg_color))

+ 16 - 0
customtkinter/windows/widgets/__init__.py

@@ -0,0 +1,16 @@
+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
+from .ctk_scrollable_frame import CTkScrollableFrame

+ 4 - 0
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()

+ 61 - 0
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() to convert tuple color
+
+    """
+    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)}")

+ 122 - 0
customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py

@@ -0,0 +1,122 @@
+import tkinter
+from typing import Callable
+#import darkdetect
+
+
+class AppearanceModeTracker:
+
+    callback_list = []
+    app_list = []
+    update_loop_running = False
+    update_loop_interval = 30  # milliseconds
+
+    appearance_mode_set_by = "system"
+    appearance_mode = 0  # Light (standard)
+
+    @classmethod
+    def init_appearance_mode(cls):
+        if cls.appearance_mode_set_by == "system":
+            new_appearance_mode = cls.detect_appearance_mode()
+
+            if new_appearance_mode != cls.appearance_mode:
+                cls.appearance_mode = new_appearance_mode
+                cls.update_callbacks()
+
+    @classmethod
+    def add(cls, callback: Callable, widget=None):
+        cls.callback_list.append(callback)
+
+        if widget is not None:
+            app = cls.get_tk_root_of_widget(widget)
+            if app not in cls.app_list:
+                cls.app_list.append(app)
+
+                if not cls.update_loop_running:
+                    app.after(cls.update_loop_interval, cls.update)
+                    cls.update_loop_running = True
+
+    @classmethod
+    def remove(cls, callback: Callable):
+        try:
+            cls.callback_list.remove(callback)
+        except ValueError:
+            return
+
+    @staticmethod
+    def detect_appearance_mode() -> int:
+        try:
+            #if darkdetect.theme() == "Dark":
+            return 0  # Dark
+            #else:
+            #return 0  # Light
+        except NameError:
+            return 0  # Light
+
+    @classmethod
+    def get_tk_root_of_widget(cls, widget):
+        current_widget = widget
+
+        while isinstance(current_widget, tkinter.Tk) is False:
+            current_widget = current_widget.master
+
+        return current_widget
+
+    @classmethod
+    def update_callbacks(cls):
+        if cls.appearance_mode == 0:
+            for callback in cls.callback_list:
+                try:
+                    callback("Light")
+                except Exception:
+                    continue
+
+        elif cls.appearance_mode == 1:
+            for callback in cls.callback_list:
+                try:
+                    callback("Dark")
+                except Exception:
+                    continue
+
+    @classmethod
+    def update(cls):
+        if cls.appearance_mode_set_by == "system":
+            new_appearance_mode = cls.detect_appearance_mode()
+
+            if new_appearance_mode != cls.appearance_mode:
+                cls.appearance_mode = new_appearance_mode
+                cls.update_callbacks()
+
+        # find an existing tkinter.Tk object for the next call of .after()
+        for app in cls.app_list:
+            try:
+                app.after(cls.update_loop_interval, cls.update)
+                return
+            except Exception:
+                continue
+
+        cls.update_loop_running = False
+
+    @classmethod
+    def get_mode(cls) -> int:
+        return cls.appearance_mode
+
+    @classmethod
+    def set_appearance_mode(cls, mode_string: str):
+        if mode_string.lower() == "dark":
+            cls.appearance_mode_set_by = "user"
+            new_appearance_mode = 1
+
+            if new_appearance_mode != cls.appearance_mode:
+                cls.appearance_mode = new_appearance_mode
+                cls.update_callbacks()
+
+        elif mode_string.lower() == "light":
+            cls.appearance_mode_set_by = "user"
+            new_appearance_mode = 0
+
+            if new_appearance_mode != cls.appearance_mode:
+                cls.appearance_mode = new_appearance_mode
+                cls.update_callbacks()
+
+        elif mode_string.lower() == "system":
+            cls.appearance_mode_set_by = "system"

+ 12 - 0
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"

+ 117 - 0
customtkinter/windows/widgets/core_rendering/ctk_canvas.py

@@ -0,0 +1,117 @@
+import tkinter
+import sys
+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()
+
+    @classmethod
+    def init_font_character_mapping(cls):
+        """ optimizations made for Windows 10, 11 only """
+
+        radius_to_char_warped = {19: 'B', 18: 'B', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'B', 12: 'B', 11: 'B',
+                                 10: 'B',
+                                 9: 'C', 8: 'D', 7: 'C', 6: 'E', 5: 'F', 4: 'G', 3: 'H', 2: 'H', 1: 'H', 0: 'A'}
+
+        radius_to_char_fine_windows_10 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C',
+                                          11: 'C', 10: 'C',
+                                          9: 'D', 8: 'D', 7: 'D', 6: 'C', 5: 'D', 4: 'G', 3: 'G', 2: 'H', 1: 'H',
+                                          0: 'A'}
+
+        radius_to_char_fine_windows_11 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C',
+                                          11: 'D', 10: 'D',
+                                          9: 'E', 8: 'F', 7: 'C', 6: 'I', 5: 'E', 4: 'G', 3: 'P', 2: 'R', 1: 'R',
+                                          0: 'A'}
+
+        radius_to_char_fine_linux = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'F', 12: 'C',
+                                          11: 'F', 10: 'C',
+                                          9: 'D', 8: 'G', 7: 'D', 6: 'F', 5: 'D', 4: 'G', 3: 'M', 2: 'H', 1: 'H',
+                                          0: 'A'}
+
+        if sys.platform.startswith("win"):
+            if sys.getwindowsversion().build > 20000:  # Windows 11
+                cls.radius_to_char_fine = radius_to_char_fine_windows_11
+            else:  # < Windows 11
+                cls.radius_to_char_fine = radius_to_char_fine_windows_10
+        elif sys.platform.startswith("linux"):  # Optimized on Kali Linux
+            cls.radius_to_char_fine = radius_to_char_fine_linux
+        else:
+            cls.radius_to_char_fine = radius_to_char_fine_windows_10
+
+    def _get_char_from_radius(self, radius: int) -> str:
+        if radius >= 20:
+            return "A"
+        else:
+            return self.radius_to_char_fine[radius]
+
+    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,
+                                    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)
+
+        return circle_1
+
+    def coords(self, tag_or_id, *args):
+
+        if type(tag_or_id) == str and "ctk_aa_circle_font_element" in self.gettags(tag_or_id):
+            coords_id = self.find_withtag(tag_or_id)[0]  # take the lowest id for the given tag
+            super().coords(coords_id, *args[:2])
+
+            if len(args) == 3:
+                super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self._get_char_from_radius(args[2]))
+
+        elif type(tag_or_id) == int and tag_or_id in self._aa_circle_canvas_ids:
+            super().coords(tag_or_id, *args[:2])
+
+            if len(args) == 3:
+                super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self._get_char_from_radius(args[2]))
+
+        else:
+            super().coords(tag_or_id, *args)
+
+    def itemconfig(self, tag_or_id, *args, **kwargs):
+        kwargs_except_outline = kwargs.copy()
+        if "outline" in kwargs_except_outline:
+            del kwargs_except_outline["outline"]
+
+        if type(tag_or_id) == int:
+            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:
+                    super().itemconfigure(configure_id, *args, **kwargs_except_outline)
+                else:
+                    super().itemconfigure(configure_id, *args, **kwargs)

File diff suppressed because it is too large
+ 1235 - 0
customtkinter/windows/widgets/core_rendering/draw_engine.py


+ 2 - 0
customtkinter/windows/widgets/core_widget_classes/__init__.py

@@ -0,0 +1,2 @@
+from .dropdown_menu import DropdownMenu
+from .ctk_base_class import CTkBaseClass

+ 326 - 0
customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py

@@ -0,0 +1,326 @@
+import sys
+import warnings
+import tkinter
+import tkinter.ttk as ttk
+from typing import Union, Callable, Tuple, Any
+
+try:
+    from typing import TypedDict
+except ImportError:
+    from typing_extensions import TypedDict
+
+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('<Configure>', 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, CTkAppearanceModeBaseClass)):
+            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[<attribute>] 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:
+            warnings.warn(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) <= 6:
+            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 to 6 or an instance of CTkFont.\n" +
+                             f"\nUsage example:\n" +
+                             f"font=customtkinter.CTkFont(family='<name>', size=<size in px>)\n" +
+                             f"font=('<name>', <size in px>)\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:
+            warnings.warn(f"{type(self).__name__} Warning: Given image is not CTkImage but {type(image)}. 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, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame)):
+            if master_widget.cget("fg_color") is not None and master_widget.cget("fg_color") != "transparent":
+                return master_widget.cget("fg_color")
+
+            elif isinstance(master_widget, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame):
+                return self._detect_color_of_master(master_widget.master.master.master)
+
+            # if fg_color of master is None, try to retrieve fg_color from master of master
+            elif hasattr(master_widget, "master"):
+                return self._detect_color_of_master(master_widget.master)
+
+        elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)):  # master is ttk widget
+            try:
+                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 bind(self, sequence=None, command=None, add=None):
+        raise NotImplementedError
+
+    def unbind(self, sequence=None, funcid=None):
+        raise NotImplementedError
+
+    def unbind_all(self, sequence):
+        raise AttributeError("'unbind_all' is not allowed, because it would delete necessary internal callbacks for all widgets")
+
+    def bind_all(self, sequence=None, func=None, add=None):
+        raise AttributeError("'bind_all' is not allowed, could result in undefined behavior")
+
+    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
+        """
+        if "width" in kwargs or "height" in kwargs:
+            raise ValueError("'width' and 'height' arguments must be passed to the constructor of the widget, not the place method")
+        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()

+ 198 - 0
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='<name>', size=<size in px>)\n" +
+                             f"font=('<name>', <size in px>)\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()

+ 594 - 0
customtkinter/windows/widgets/ctk_button.py

@@ -0,0 +1,594 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional, Any
+
+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[CTkImage, "ImageTk.PhotoImage", None] = None,
+                 state: str = "normal",
+                 hover: bool = True,
+                 command: Union[Callable[[], Any], 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
+
+        # configure cursor and initial draw
+        self._create_bindings()
+        self._set_cursor()
+        self._draw()
+
+    def _create_bindings(self, sequence: Optional[str] = None):
+        """ set necessary bindings for functionality of widget, will overwrite other bindings """
+
+        if sequence is None or sequence == "<Enter>":
+            self._canvas.bind("<Enter>", self._on_enter)
+
+            if self._text_label is not None:
+                self._text_label.bind("<Enter>", self._on_enter)
+            if self._image_label is not None:
+                self._image_label.bind("<Enter>", self._on_enter)
+
+        if sequence is None or sequence == "<Leave>":
+            self._canvas.bind("<Leave>", self._on_leave)
+
+            if self._text_label is not None:
+                self._text_label.bind("<Leave>", self._on_leave)
+            if self._image_label is not None:
+                self._image_label.bind("<Leave>", self._on_leave)
+
+        if sequence is None or sequence == "<Button-1>":
+            self._canvas.bind("<Button-1>", self._clicked)
+
+            if self._text_label is not None:
+                self._text_label.bind("<Button-1>", self._clicked)
+            if self._image_label is not None:
+                self._image_label.bind("<Button-1>", self._clicked)
+
+    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:
+            if isinstance(self._image, CTkImage):
+                self._image_label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(),
+                                                                                        self._get_appearance_mode()))
+            elif self._image is not None:
+                self._image_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 _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("<Enter>", self._on_enter)
+                self._text_label.bind("<Leave>", self._on_leave)
+                self._text_label.bind("<Button-1>", self._clicked)
+                self._text_label.bind("<Button-1>", self._clicked)
+
+            if no_color_updates is False:
+                # set text_label fg color (text color)
+                self._text_label.configure(fg=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("<Enter>", self._on_enter)
+                self._image_label.bind("<Leave>", self._on_leave)
+                self._image_label.bind("<Button-1>", self._clicked)
+                self._image_label.bind("<Button-1>", self._clicked)
+
+            if no_color_updates is False:
+                # set image_label bg color (background color of label)
+                if self._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)
+            self._update_image()
+
+        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")
+            self._set_cursor()
+
+        if "compound" in kwargs:
+            self._compound = kwargs.pop("compound")
+            require_redraw = True
+
+        if "anchor" in kwargs:
+            self._anchor = kwargs.pop("anchor")
+            self._create_grid()
+            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: Union[str, bool] = True):
+        """ called on the tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._canvas.bind(sequence, command, add=True)
+
+        if self._text_label is not None:
+            self._text_label.bind(sequence, command, add=True)
+        if self._image_label is not None:
+            self._image_label.bind(sequence, command, add=True)
+
+    def unbind(self, sequence: str = None, funcid: str = None):
+        """ called on the tkinter.Label and tkinter.Canvas """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._canvas.unbind(sequence, None)
+
+        if self._text_label is not None:
+            self._text_label.unbind(sequence, None)
+        if self._image_label is not None:
+            self._image_label.unbind(sequence, None)
+
+        self._create_bindings(sequence=sequence)  # restore internal callbacks for sequence
+
+    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()

+ 469 - 0
customtkinter/windows/widgets/ctk_checkbox.py

@@ -0,0 +1,469 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional, Any
+
+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[[], Any], 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._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"
+
+        # 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._create_bindings()
+        self._set_cursor()
+        self._draw()
+
+    def _create_bindings(self, sequence: Optional[str] = None):
+        """ set necessary bindings for functionality of widget, will overwrite other bindings """
+        if sequence is None or sequence == "<Enter>":
+            self._canvas.bind("<Enter>", self._on_enter)
+            self._text_label.bind("<Enter>", self._on_enter)
+        if sequence is None or sequence == "<Leave>":
+            self._canvas.bind("<Leave>", self._on_leave)
+            self._text_label.bind("<Leave>", self._on_leave)
+        if sequence is None or sequence == "<Button-1>":
+            self._canvas.bind("<Button-1>", self.toggle)
+            self._text_label.bind("<Button-1>", self.toggle)
+
+    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 "border_color" in kwargs:
+            self._border_color = self._check_color_type(kwargs.pop("border_color"))
+            require_redraw = True
+
+        if "checkmark_color" in kwargs:
+            self._checkmark_color = self._check_color_type(kwargs.pop("checkmark_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 "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: str = None, command: Callable = None, add: Union[str, bool] = True):
+        """ called on the tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._canvas.bind(sequence, command, add=True)
+        self._text_label.bind(sequence, command, add=True)
+
+    def unbind(self, sequence: str = None, funcid: str = None):
+        """ called on the tkinter.Label and tkinter.Canvas """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._canvas.unbind(sequence, None)
+        self._text_label.unbind(sequence, None)
+        self._create_bindings(sequence=sequence)  # restore internal callbacks for sequence
+
+    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()

+ 424 - 0
customtkinter/windows/widgets/ctk_combobox.py

@@ -0,0 +1,424 @@
+import tkinter
+import sys
+import copy
+from typing import Union, Tuple, Callable, List, Optional, Any
+
+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], Any], 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()
+        self._create_bindings()
+        self._draw()  # initial draw
+
+        if self._variable is not None:
+            self._entry.configure(textvariable=self._variable)
+
+        # insert default value
+        if self._variable is None:
+            if len(self._values) > 0:
+                self._entry.insert(0, self._values[0])
+            else:
+                self._entry.insert(0, "CTkComboBox")
+
+    def _create_bindings(self, sequence: Optional[str] = None):
+        """ set necessary bindings for functionality of widget, will overwrite other bindings """
+        if sequence is None:
+            self._canvas.tag_bind("right_parts", "<Enter>", self._on_enter)
+            self._canvas.tag_bind("dropdown_arrow", "<Enter>", self._on_enter)
+            self._canvas.tag_bind("right_parts", "<Leave>", self._on_leave)
+            self._canvas.tag_bind("dropdown_arrow", "<Leave>", self._on_leave)
+            self._canvas.tag_bind("right_parts", "<Button-1>", self._clicked)
+            self._canvas.tag_bind("dropdown_arrow", "<Button-1>", self._clicked)
+
+    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),
+                                  readonlybackground=self._apply_appearance_mode(self._fg_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 copy.copy(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=None):
+        if self._state is not tkinter.DISABLED and len(self._values) > 0:
+            self._open_dropdown_menu()
+
+    def bind(self, sequence=None, command=None, add=True):
+        """ called on the tkinter.Entry """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._entry.bind(sequence, command, add=True)
+
+    def unbind(self, sequence=None, funcid=None):
+        """ called on the tkinter.Entry """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._entry.unbind(sequence, None)  # unbind all callbacks for sequence
+        self._create_bindings(sequence=sequence)  # restore internal callbacks for sequence
+
+    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()

+ 384 - 0
customtkinter/windows/widgets/ctk_entry.py

@@ -0,0 +1,384 @@
+import tkinter
+from typing import Union, Tuple, Optional, Any
+
+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))
+
+        check_kwargs_empty(kwargs, raise_error=True)
+
+        self._create_grid()
+        self._activate_placeholder()
+        self._create_bindings()
+        self._draw()
+
+    def _create_bindings(self, sequence: Optional[str] = None):
+        """ set necessary bindings for functionality of widget, will overwrite other bindings """
+        if sequence is None or sequence == "<FocusIn>":
+            self._entry.bind("<FocusIn>", self._entry_focus_in)
+        if sequence is None or sequence == "<FocusOut>":
+            self._entry.bind("<FocusOut>", self._entry_focus_out)
+
+    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(no_color_updates=True)
+
+    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)
+
+        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:
+            self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+            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),
+                                      readonlybackground=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),
+                                      readonlybackground=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=True):
+        """ called on the tkinter.Entry """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._entry.bind(sequence, command, add=True)
+
+    def unbind(self, sequence=None, funcid=None):
+        """ called on the tkinter.Entry """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._entry.unbind(sequence, None)  # unbind all callbacks for sequence
+        self._create_bindings(sequence=sequence)  # restore internal callbacks for sequence
+
+    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 and self._entry.cget("state") != "readonly":
+            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):
+        self._entry.focus()
+
+    def focus_set(self):
+        self._entry.focus_set()
+
+    def focus_force(self):
+        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)

+ 196 - 0
customtkinter/windows/widgets/ctk_frame.py

@@ -0,0 +1,196 @@
+from typing import Union, Tuple, List, Optional, Any
+
+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=True):
+        """ called on the tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._canvas.bind(sequence, command, add=True)
+
+    def unbind(self, sequence=None, funcid=None):
+        """ called on the tkinter.Canvas """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._canvas.unbind(sequence, None)

+ 291 - 0
customtkinter/windows/widgets/ctk_label.py

@@ -0,0 +1,291 @@
+import tkinter
+from typing import Union, Tuple, Callable, Optional, Any
+
+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.
+
+    state argument will probably be removed because it has no effect
+    """
+
+    # 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_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
+
+                 text: str = "CTkLabel",
+                 font: Optional[Union[tuple, CTkFont]] = None,
+                 image: Union[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)
+
+        if text_color_disabled is None:
+            if "text_color_disabled" in ThemeManager.theme["CTkLabel"]:
+                self._text_color_disabled = ThemeManager.theme["CTkLabel"]["text_color"]
+            else:
+                self._text_color_disabled = self._text_color
+        else:
+            self._text_color_disabled = self._check_color_type(text_color_disabled)
+
+        # 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),
+                                      disabledforeground=self._apply_appearance_mode(self._text_color_disabled),
+                                      bg=self._apply_appearance_mode(self._bg_color))
+            else:
+                self._canvas.itemconfig("inner_parts",
+                                        fill=self._apply_appearance_mode(self._fg_color),
+                                        outline=self._apply_appearance_mode(self._fg_color))
+
+                self._label.configure(fg=self._apply_appearance_mode(self._text_color),
+                                      disabledforeground=self._apply_appearance_mode(self._text_color_disabled),
+                                      bg=self._apply_appearance_mode(self._fg_color))
+
+            self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
+
+    def configure(self, require_redraw=False, **kwargs):
+        if "corner_radius" in kwargs:
+            self._corner_radius = kwargs.pop("corner_radius")
+            self._create_grid()
+            require_redraw = True
+
+        if "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_color_disabled" in kwargs:
+            self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
+            require_redraw = True
+
+        if "text" in kwargs:
+            self._text = kwargs.pop("text")
+            self._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_color_disabled":
+            return self._text_color_disabled
+
+        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 = True):
+        """ called on the tkinter.Label and tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._canvas.bind(sequence, command, add=True)
+        self._label.bind(sequence, command, add=True)
+
+    def unbind(self, sequence: str = None, funcid: Optional[str] = None):
+        """ called on the tkinter.Label and tkinter.Canvas """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._canvas.unbind(sequence, None)
+        self._label.unbind(sequence, None)
+
+    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()

+ 426 - 0
customtkinter/windows/widgets/ctk_optionmenu.py

@@ -0,0 +1,426 @@
+import tkinter
+import copy
+import sys
+from typing import Union, Tuple, Callable, Optional, Any
+
+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], Any], 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)
+
+        if self._cursor_manipulation_enabled:
+            if sys.platform == "darwin":
+                self.configure(cursor="pointinghand")
+            elif sys.platform.startswith("win"):
+                self.configure(cursor="hand2")
+
+        self._create_grid()
+        if not self._dynamic_resizing:
+            self.grid_propagate(0)
+
+        self._create_bindings()
+        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_bindings(self, sequence: Optional[str] = None):
+        """ set necessary bindings for functionality of widget, will overwrite other bindings """
+        if sequence is None or sequence == "<Enter>":
+            self._canvas.bind("<Enter>", self._on_enter)
+            self._text_label.bind("<Enter>", self._on_enter)
+        if sequence is None or sequence == "<Leave>":
+            self._canvas.bind("<Leave>", self._on_leave)
+            self._text_label.bind("<Leave>", self._on_leave)
+        if sequence is None or sequence == "<Button-1>":
+            self._canvas.bind("<Button-1>", self._clicked)
+            self._text_label.bind("<Button-1>", self._clicked)
+
+    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 "text_color_disabled" in kwargs:
+            self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
+            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 "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 "variable" in kwargs:
+            if self._variable is not None:  # remove old callback
+                self._variable.trace_remove("write", self._variable_callback_name)
+
+            self._variable = kwargs.pop("variable")
+
+            if self._variable is not None and self._variable != "":
+                self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
+                self._current_value = self._variable.get()
+                self._text_label.configure(text=self._current_value)
+            else:
+                self._variable = None
+
+        if "state" in kwargs:
+            self._state = kwargs.pop("state")
+            require_redraw = True
+
+        if "hover" in kwargs:
+            self._hover = kwargs.pop("hover")
+
+        if "command" in kwargs:
+            self._command = kwargs.pop("command")
+
+        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 copy.copy(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: Union[str, bool] = True):
+        """ called on the tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._canvas.bind(sequence, command, add=True)
+        self._text_label.bind(sequence, command, add=True)
+
+    def unbind(self, sequence: str = None, funcid: str = None):
+        """ called on the tkinter.Label and tkinter.Canvas """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._canvas.unbind(sequence, None)
+        self._text_label.unbind(sequence, None)
+        self._create_bindings(sequence=sequence)  # restore internal callbacks for sequence
+
+    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()

+ 312 - 0
customtkinter/windows/widgets/ctk_progressbar.py

@@ -0,0 +1,312 @@
+import tkinter
+import math
+from typing import Union, Tuple, Optional, Callable, Any
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
+
+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: Literal["determinate", "indeterminate"] = "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
+        self._loop_after_id = 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 automatic mode """
+        if not self._loop_running:
+            self._loop_running = True
+            self._internal_loop()
+
+    def stop(self):
+        """ stop automatic mode """
+        if self._loop_after_id is not None:
+            self.after_cancel(self._loop_after_id)
+        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._loop_after_id = self.after(20, self._internal_loop)
+            else:
+                self._indeterminate_value += self._indeterminate_speed
+                self._draw()
+                self._loop_after_id = self.after(20, self._internal_loop)
+
+    def step(self):
+        """ increase progress """
+        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: str = None, command: Callable = None, add: Union[str, bool] = True):
+        """ called on the tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._canvas.bind(sequence, command, add=True)
+
+    def unbind(self, sequence: str = None, funcid: str = None):
+        """ called on the tkinter.Label and tkinter.Canvas """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._canvas.unbind(sequence, None)
+
+    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()

+ 430 - 0
customtkinter/windows/widgets/ctk_radiobutton.py

@@ -0,0 +1,430 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional, Any
+
+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, Any] = 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.grid_rowconfigure(0, 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._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"
+
+        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._create_bindings()
+        self._set_cursor()
+        self._draw()
+
+    def _create_bindings(self, sequence: Optional[str] = None):
+        """ set necessary bindings for functionality of widget, will overwrite other bindings """
+        if sequence is None or sequence == "<Enter>":
+            self._canvas.bind("<Enter>", self._on_enter)
+            self._text_label.bind("<Enter>", self._on_enter)
+        if sequence is None or sequence == "<Leave>":
+            self._canvas.bind("<Leave>", self._on_leave)
+            self._text_label.bind("<Leave>", self._on_leave)
+        if sequence is None or sequence == "<Button-1>":
+            self._canvas.bind("<Button-1>", self.invoke)
+            self._text_label.bind("<Button-1>", self.invoke)
+
+    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: str = None, command: Callable = None, add: Union[str, bool] = True):
+        """ called on the tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._canvas.bind(sequence, command, add=True)
+        self._text_label.bind(sequence, command, add=True)
+
+    def unbind(self, sequence: str = None, funcid: str = None):
+        """ called on the tkinter.Label and tkinter.Canvas """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._canvas.unbind(sequence, None)
+        self._text_label.unbind(sequence, None)
+        self._create_bindings(sequence=sequence)  # restore internal callbacks for sequence
+
+    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()

+ 316 - 0
customtkinter/windows/widgets/ctk_scrollable_frame.py

@@ -0,0 +1,316 @@
+from typing import Union, Tuple, Optional, Any
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
+import tkinter
+import sys
+
+from .ctk_frame import CTkFrame
+from .ctk_scrollbar import CTkScrollbar
+from .appearance_mode import CTkAppearanceModeBaseClass
+from .scaling import CTkScalingBaseClass
+from .core_widget_classes import CTkBaseClass
+from .ctk_label import CTkLabel
+from .font import CTkFont
+from .theme import ThemeManager
+
+
+class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
+    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,
+                 scrollbar_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+                 scrollbar_button_color: Optional[Union[str, Tuple[str, str]]] = None,
+                 scrollbar_button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
+                 label_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
+                 label_text_color: Optional[Union[str, Tuple[str, str]]] = None,
+
+                 label_text: str = "",
+                 label_font: Optional[Union[tuple, CTkFont]] = None,
+                 label_anchor: str = "center",
+                 orientation: Literal["vertical", "horizontal"] = "vertical"):
+
+        self._orientation = orientation
+
+        # dimensions independent of scaling
+        self._desired_width = width  # _desired_width and _desired_height, represent desired size set by width and height
+        self._desired_height = height
+
+        self._parent_frame = CTkFrame(master=master, width=0, height=0, corner_radius=corner_radius,
+                                      border_width=border_width, bg_color=bg_color, fg_color=fg_color, border_color=border_color)
+        self._parent_canvas = tkinter.Canvas(master=self._parent_frame, highlightthickness=0)
+        self._set_scroll_increments()
+
+        if self._orientation == "horizontal":
+            self._scrollbar = CTkScrollbar(master=self._parent_frame, orientation="horizontal", command=self._parent_canvas.xview,
+                                           fg_color=scrollbar_fg_color, button_color=scrollbar_button_color, button_hover_color=scrollbar_button_hover_color)
+            self._parent_canvas.configure(xscrollcommand=self._scrollbar.set)
+        elif self._orientation == "vertical":
+            self._scrollbar = CTkScrollbar(master=self._parent_frame, orientation="vertical", command=self._parent_canvas.yview,
+                                           fg_color=scrollbar_fg_color, button_color=scrollbar_button_color, button_hover_color=scrollbar_button_hover_color)
+            self._parent_canvas.configure(yscrollcommand=self._scrollbar.set)
+
+        self._label_text = label_text
+        self._label = CTkLabel(self._parent_frame, text=label_text, anchor=label_anchor, font=label_font,
+                               corner_radius=self._parent_frame.cget("corner_radius"), text_color=label_text_color,
+                               fg_color=ThemeManager.theme["CTkScrollableFrame"]["label_fg_color"] if label_fg_color is None else label_fg_color)
+
+        tkinter.Frame.__init__(self, master=self._parent_canvas, highlightthickness=0)
+        CTkAppearanceModeBaseClass.__init__(self)
+        CTkScalingBaseClass.__init__(self, scaling_type="widget")
+
+        self._create_grid()
+
+        self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+                                      height=self._apply_widget_scaling(self._desired_height))
+
+        self.bind("<Configure>", lambda e: self._parent_canvas.configure(scrollregion=self._parent_canvas.bbox("all")))
+        self._parent_canvas.bind("<Configure>", self._fit_frame_dimensions_to_canvas)
+        self.bind_all("<MouseWheel>", self._mouse_wheel_all, add="+")
+        self.bind_all("<KeyPress-Shift_L>", self._keyboard_shift_press_all, add="+")
+        self.bind_all("<KeyPress-Shift_R>", self._keyboard_shift_press_all, add="+")
+        self.bind_all("<KeyRelease-Shift_L>", self._keyboard_shift_release_all, add="+")
+        self.bind_all("<KeyRelease-Shift_R>", self._keyboard_shift_release_all, add="+")
+        self._create_window_id = self._parent_canvas.create_window(0, 0, window=self, anchor="nw")
+
+        if self._parent_frame.cget("fg_color") == "transparent":
+            tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
+            self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
+        else:
+            tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
+            self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
+
+        self._shift_pressed = False
+
+    def destroy(self):
+        tkinter.Frame.destroy(self)
+        CTkAppearanceModeBaseClass.destroy(self)
+        CTkScalingBaseClass.destroy(self)
+
+    def _create_grid(self):
+        border_spacing = self._apply_widget_scaling(self._parent_frame.cget("corner_radius") + self._parent_frame.cget("border_width"))
+
+        if self._orientation == "horizontal":
+            self._parent_frame.grid_columnconfigure(0, weight=1)
+            self._parent_frame.grid_rowconfigure(1, weight=1)
+            self._parent_canvas.grid(row=1, column=0, sticky="nsew", padx=border_spacing, pady=(border_spacing, 0))
+            self._scrollbar.grid(row=2, column=0, sticky="nsew", padx=border_spacing)
+
+            if self._label_text is not None and self._label_text != "":
+                self._label.grid(row=0, column=0, sticky="ew", padx=border_spacing, pady=border_spacing)
+            else:
+                self._label.grid_forget()
+
+        elif self._orientation == "vertical":
+            self._parent_frame.grid_columnconfigure(0, weight=1)
+            self._parent_frame.grid_rowconfigure(1, weight=1)
+            self._parent_canvas.grid(row=1, column=0, sticky="nsew", padx=(border_spacing, 0), pady=border_spacing)
+            self._scrollbar.grid(row=1, column=1, sticky="nsew", pady=border_spacing)
+
+            if self._label_text is not None and self._label_text != "":
+                self._label.grid(row=0, column=0, columnspan=2, sticky="ew", padx=border_spacing, pady=border_spacing)
+            else:
+                self._label.grid_forget()
+
+    def _set_appearance_mode(self, mode_string):
+        super()._set_appearance_mode(mode_string)
+
+        if self._parent_frame.cget("fg_color") == "transparent":
+            tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
+            self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
+        else:
+            tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
+            self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
+
+    def _set_scaling(self, new_widget_scaling, new_window_scaling):
+        super()._set_scaling(new_widget_scaling, new_window_scaling)
+
+        self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+                                      height=self._apply_widget_scaling(self._desired_height))
+
+    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
+
+        self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
+                                      height=self._apply_widget_scaling(self._desired_height))
+
+    def configure(self, **kwargs):
+        if "width" in kwargs:
+            self._set_dimensions(width=kwargs.pop("width"))
+
+        if "height" in kwargs:
+            self._set_dimensions(height=kwargs.pop("height"))
+
+        if "corner_radius" in kwargs:
+            new_corner_radius = kwargs.pop("corner_radius")
+            self._parent_frame.configure(corner_radius=new_corner_radius)
+            if self._label is not None:
+                self._label.configure(corner_radius=new_corner_radius)
+            self._create_grid()
+
+        if "border_width" in kwargs:
+            self._parent_frame.configure(border_width=kwargs.pop("border_width"))
+            self._create_grid()
+
+        if "fg_color" in kwargs:
+            self._parent_frame.configure(fg_color=kwargs.pop("fg_color"))
+
+            if self._parent_frame.cget("fg_color") == "transparent":
+                tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
+                self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
+            else:
+                tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
+                self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
+
+            for child in self.winfo_children():
+                if isinstance(child, CTkBaseClass):
+                    child.configure(bg_color=self._parent_frame.cget("fg_color"))
+
+        if "scrollbar_fg_color" in kwargs:
+            self._scrollbar.configure(fg_color=kwargs.pop("scrollbar_fg_color"))
+
+        if "scrollbar_button_color" in kwargs:
+            self._scrollbar.configure(button_color=kwargs.pop("scrollbar_button_color"))
+
+        if "scrollbar_button_hover_color" in kwargs:
+            self._scrollbar.configure(button_hover_color=kwargs.pop("scrollbar_button_hover_color"))
+
+        if "label_text" in kwargs:
+            self._label_text = kwargs.pop("label_text")
+            self._label.configure(text=self._label_text)
+            self._create_grid()
+
+        if "label_font" in kwargs:
+            self._label.configure(font=kwargs.pop("label_font"))
+
+        if "label_text_color" in kwargs:
+            self._label.configure(text_color=kwargs.pop("label_text_color"))
+
+        if "label_fg_color" in kwargs:
+            self._label.configure(fg_color=kwargs.pop("label_fg_color"))
+
+        if "label_anchor" in kwargs:
+            self._label.configure(anchor=kwargs.pop("label_anchor"))
+
+        self._parent_frame.configure(**kwargs)
+
+    def cget(self, attribute_name: str):
+        if attribute_name == "width":
+            return self._desired_width
+        elif attribute_name == "height":
+            return self._desired_height
+
+        elif attribute_name == "label_text":
+            return self._label_text
+        elif attribute_name == "label_font":
+            return self._label.cget("font")
+        elif attribute_name == "label_text_color":
+            return self._label.cget("_text_color")
+        elif attribute_name == "label_fg_color":
+            return self._label.cget("fg_color")
+        elif attribute_name == "label_anchor":
+            return self._label.cget("anchor")
+
+        elif attribute_name.startswith("scrollbar_fg_color"):
+            return self._scrollbar.cget("fg_color")
+        elif attribute_name.startswith("scrollbar_button_color"):
+            return self._scrollbar.cget("button_color")
+        elif attribute_name.startswith("scrollbar_button_hover_color"):
+            return self._scrollbar.cget("button_hover_color")
+
+        else:
+            return self._parent_frame.cget(attribute_name)
+
+    def _fit_frame_dimensions_to_canvas(self, event):
+        if self._orientation == "horizontal":
+            self._parent_canvas.itemconfigure(self._create_window_id, height=self._parent_canvas.winfo_height())
+        elif self._orientation == "vertical":
+            self._parent_canvas.itemconfigure(self._create_window_id, width=self._parent_canvas.winfo_width())
+
+    def _set_scroll_increments(self):
+        if sys.platform.startswith("win"):
+            self._parent_canvas.configure(xscrollincrement=1, yscrollincrement=1)
+        elif sys.platform == "darwin":
+            self._parent_canvas.configure(xscrollincrement=4, yscrollincrement=8)
+
+    def _mouse_wheel_all(self, event):
+        if self.check_if_master_is_canvas(event.widget):
+            if sys.platform.startswith("win"):
+                if self._shift_pressed:
+                    if self._parent_canvas.xview() != (0.0, 1.0):
+                        self._parent_canvas.xview("scroll", -int(event.delta / 6), "units")
+                else:
+                    if self._parent_canvas.yview() != (0.0, 1.0):
+                        self._parent_canvas.yview("scroll", -int(event.delta / 6), "units")
+            elif sys.platform == "darwin":
+                if self._shift_pressed:
+                    if self._parent_canvas.xview() != (0.0, 1.0):
+                        self._parent_canvas.xview("scroll", -event.delta, "units")
+                else:
+                    if self._parent_canvas.yview() != (0.0, 1.0):
+                        self._parent_canvas.yview("scroll", -event.delta, "units")
+            else:
+                if self._shift_pressed:
+                    if self._parent_canvas.xview() != (0.0, 1.0):
+                        self._parent_canvas.xview("scroll", -event.delta, "units")
+                else:
+                    if self._parent_canvas.yview() != (0.0, 1.0):
+                        self._parent_canvas.yview("scroll", -event.delta, "units")
+
+    def _keyboard_shift_press_all(self, event):
+        self._shift_pressed = True
+
+    def _keyboard_shift_release_all(self, event):
+        self._shift_pressed = False
+
+    def check_if_master_is_canvas(self, widget):
+        if widget == self._parent_canvas:
+            return True
+        elif widget.master is not None:
+            return self.check_if_master_is_canvas(widget.master)
+        else:
+            return False
+
+    def pack(self, **kwargs):
+        self._parent_frame.pack(**kwargs)
+
+    def place(self, **kwargs):
+        self._parent_frame.place(**kwargs)
+
+    def grid(self, **kwargs):
+        self._parent_frame.grid(**kwargs)
+
+    def pack_forget(self):
+        self._parent_frame.pack_forget()
+
+    def place_forget(self, **kwargs):
+        self._parent_frame.place_forget()
+
+    def grid_forget(self, **kwargs):
+        self._parent_frame.grid_forget()
+
+    def grid_remove(self, **kwargs):
+        self._parent_frame.grid_remove()
+
+    def grid_propagate(self, **kwargs):
+        self._parent_frame.grid_propagate()
+
+    def grid_info(self, **kwargs):
+        return self._parent_frame.grid_info()
+
+    def lift(self, aboveThis=None):
+        self._parent_frame.lift(aboveThis)
+
+    def lower(self, belowThis=None):
+        self._parent_frame.lower(belowThis)

+ 281 - 0
customtkinter/windows/widgets/ctk_scrollbar.py

@@ -0,0 +1,281 @@
+import sys
+from typing import Union, Tuple, Callable, Optional, Any
+
+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, Any] = 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"]["button_color"] if button_color is None else self._check_color_type(button_color)
+        self._button_hover_color = ThemeManager.theme["CTkScrollbar"]["button_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._create_bindings()
+        self._draw()
+
+    def _create_bindings(self, sequence: Optional[str] = None):
+        """ set necessary bindings for functionality of widget, will overwrite other bindings """
+        if sequence is None:
+            self._canvas.tag_bind("border_parts", "<Button-1>", self._clicked)
+        if sequence is None or sequence == "<Enter>":
+            self._canvas.bind("<Enter>", self._on_enter)
+        if sequence is None or sequence == "<Leave>":
+            self._canvas.bind("<Leave>", self._on_leave)
+        if sequence is None or sequence == "<B1-Motion>":
+            self._canvas.bind("<B1-Motion>", self._clicked)
+        if sequence is None or sequence == "<MouseWheel>":
+            self._canvas.bind("<MouseWheel>", self._mouse_scroll_event)
+
+    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=True):
+        """ called on the tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._canvas.bind(sequence, command, add=True)
+
+    def unbind(self, sequence=None, funcid=None):
+        """ called on the tkinter.Canvas, restores internal callbacks """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._canvas.unbind(sequence, None)  # unbind all callbacks for sequence
+        self._create_bindings(sequence=sequence)  # restore internal callbacks for sequence
+
+    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()

+ 447 - 0
customtkinter/windows/widgets/ctk_segmented_button.py

@@ -0,0 +1,447 @@
+import tkinter
+import copy
+from typing import Union, Tuple, List, Dict, Callable, Optional, Any
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import Literal
+
+from .theme import ThemeManager
+from .font import CTkFont
+from .ctk_button import CTkButton
+from .ctk_frame import CTkFrame
+from .utility import check_kwargs_empty
+
+
+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], Any], None] = None,
+                 state: str = "normal"):
+
+        super().__init__(master=master, bg_color=bg_color, width=width, height=height)
+
+        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="nsew")
+
+    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 "width" in kwargs:
+            super().configure(width=kwargs.pop("width"))
+
+        if "height" in kwargs:
+            super().configure(height=kwargs.pop("height"))
+
+        if "corner_radius" in kwargs:
+            self._sb_corner_radius = kwargs.pop("corner_radius")
+            super().configure(corner_radius=self._sb_corner_radius)
+            for button in self._buttons_dict.values():
+                button.configure(corner_radius=self._sb_corner_radius)
+
+        if "border_width" in kwargs:
+            self._sb_border_width = kwargs.pop("border_width")
+            for button in self._buttons_dict.values():
+                button.configure(border_width=self._sb_border_width)
+
+        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)
+
+        check_kwargs_empty(kwargs, raise_error=True)
+
+    def cget(self, attribute_name: str) -> any:
+        if attribute_name == "width":
+            return super().cget(attribute_name)
+        elif attribute_name == "height":
+            return super().cget(attribute_name)
+        elif attribute_name == "corner_radius":
+            return self._sb_corner_radius
+        elif attribute_name == "border_width":
+            return self._sb_border_width
+
+        elif attribute_name == "bg_color":
+            return super().cget(attribute_name)
+        elif attribute_name == "fg_color":
+            return self._sb_fg_color
+        elif attribute_name == "selected_color":
+            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 copy.copy(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:
+            raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.")
+
+    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 index(self, value: str) -> int:
+        return self._value_list.index(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}'")
+
+    def bind(self, sequence=None, command=None, add=None):
+        raise NotImplementedError
+
+    def unbind(self, sequence=None, funcid=None):
+        raise NotImplementedError
+

+ 413 - 0
customtkinter/windows/widgets/ctk_slider.py

@@ -0,0 +1,413 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional, Any
+
+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], Any], 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._create_bindings()
+        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 _create_bindings(self, sequence: Optional[str] = None):
+        """ set necessary bindings for functionality of widget, will overwrite other bindings """
+        if sequence is None or sequence == "<Enter>":
+            self._canvas.bind("<Enter>", self._on_enter)
+        if sequence is None or sequence == "<Leave>":
+            self._canvas.bind("<Leave>", self._on_leave)
+        if sequence is None or sequence == "<Button-1>":
+            self._canvas.bind("<Button-1>", self._clicked)
+        if sequence is None or sequence == "<B1-Motion>":
+            self._canvas.bind("<B1-Motion>", self._clicked)
+
+    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 "corner_radius" in kwargs:
+            self._corner_radius = kwargs.pop("corner_radius")
+            require_redraw = True
+
+        if "button_corner_radius" in kwargs:
+            self._button_corner_radius = kwargs.pop("button_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 "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"), transparency=True)
+            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 "from_" in kwargs:
+            self._from_ = kwargs.pop("from_")
+
+        if "to" in kwargs:
+            self._to = kwargs.pop("to")
+
+        if "state" in kwargs:
+            self._state = kwargs.pop("state")
+            self._set_cursor()
+            require_redraw = True
+
+        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
+
+        if "orientation" in kwargs:
+            self._orientation = kwargs.pop("orientation")
+            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 == "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: str = None, command: Callable = None, add: Union[str, bool] = True):
+        """ called on the tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._canvas.bind(sequence, command, add=True)
+
+    def unbind(self, sequence: str = None, funcid: str = None):
+        """ called on the tkinter.Label and tkinter.Canvas """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._canvas.unbind(sequence, None)
+        self._create_bindings(sequence=sequence)  # restore internal callbacks for sequence
+
+    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()

+ 483 - 0
customtkinter/windows/widgets/ctk_switch.py

@@ -0,0 +1,483 @@
+import tkinter
+import sys
+from typing import Union, Tuple, Callable, Optional, Any
+
+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, Any] = 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.grid_rowconfigure(0, 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._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"
+
+        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._create_bindings()
+        self._set_cursor()
+        self._draw()  # initial draw
+
+    def _create_bindings(self, sequence: Optional[str] = None):
+        """ set necessary bindings for functionality of widget, will overwrite other bindings """
+        if sequence is None or sequence == "<Enter>":
+            self._canvas.bind("<Enter>", self._on_enter)
+            self._text_label.bind("<Enter>", self._on_enter)
+        if sequence is None or sequence == "<Leave>":
+            self._canvas.bind("<Leave>", self._on_leave)
+            self._text_label.bind("<Leave>", self._on_leave)
+        if sequence is None or sequence == "<Button-1>":
+            self._canvas.bind("<Button-1>", self.toggle)
+            self._text_label.bind("<Button-1>", self.toggle)
+
+    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 "border_color" in kwargs:
+            self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True)
+            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 "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 "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: str = None, command: Callable = None, add: Union[str, bool] = True):
+        """ called on the tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._canvas.bind(sequence, command, add=True)
+        self._text_label.bind(sequence, command, add=True)
+
+    def unbind(self, sequence: str = None, funcid: str = None):
+        """ called on the tkinter.Label and tkinter.Canvas """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._canvas.unbind(sequence, None)
+        self._text_label.unbind(sequence, None)
+        self._create_bindings(sequence=sequence)  # restore internal callbacks for sequence
+
+    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()

+ 433 - 0
customtkinter/windows/widgets/ctk_tabview.py

@@ -0,0 +1,433 @@
+import tkinter
+from typing import Union, Tuple, Dict, List, Callable, Optional, Any
+
+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.
+    """
+
+    _outer_spacing: int = 10  # px on top or below the button
+    _outer_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, Any] = None,
+                 anchor: str = "center",
+                 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._anchor = anchor
+
+        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._outer_spacing - self._outer_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._tab_dict[self._current_name].grid_forget()
+        self._current_name = selected_name
+        self._set_grid_current_tab()
+
+        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._outer_spacing - self._outer_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._outer_spacing - self._outer_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 == "transparent":
+            self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
+        else:
+            if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
+                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._fg_color, self._fg_color, self._bg_color, self._bg_color))
+
+    def _configure_grid(self):
+        """ create 3 x 4 grid system """
+
+        if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
+            self.grid_rowconfigure(0, weight=0, minsize=self._apply_widget_scaling(self._outer_spacing))
+            self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._outer_button_overhang))
+            self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._outer_button_overhang))
+            self.grid_rowconfigure(3, weight=1)
+        else:
+            self.grid_rowconfigure(0, weight=1)
+            self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._outer_button_overhang))
+            self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._outer_button_overhang))
+            self.grid_rowconfigure(3, weight=0, minsize=self._apply_widget_scaling(self._outer_spacing))
+
+        self.grid_columnconfigure(0, weight=1)
+
+    def _set_grid_canvas(self):
+        if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
+            self._canvas.grid(row=2, rowspan=2, column=0, columnspan=1, sticky="nsew")
+        else:
+            self._canvas.grid(row=0, rowspan=2, column=0, columnspan=1, sticky="nsew")
+
+    def _set_grid_segmented_button(self):
+        """ needs to be called for changes in corner_radius, anchor """
+
+        if self._anchor.lower() in ("center", "n", "s"):
+            self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="ns")
+        elif self._anchor.lower() in ("nw", "w", "sw"):
+            self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="nsw")
+        elif self._anchor.lower() in ("ne", "e", "se"):
+            self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="nse")
+
+    def _set_grid_current_tab(self):
+        """ needs to be called for changes in corner_radius, border_width """
+        if self._anchor.lower() in ("center", "w", "nw", "n", "ne", "e", "e"):
+            self._tab_dict[self._current_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)))
+        else:
+            self._tab_dict[self._current_name].grid(row=0, 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, exclude_name=None):
+        for name, frame in self._tab_dict.items():
+            if name != exclude_name:
+                frame.grid_forget()
+
+    def _create_tab(self) -> CTkFrame:
+        new_tab = CTkFrame(self,
+                           height=0,
+                           width=0,
+                           border_width=0,
+                           corner_radius=0)
+
+        if self._fg_color == "transparent":
+            new_tab.configure(fg_color=self._apply_appearance_mode(self._bg_color),
+                              bg_color=self._apply_appearance_mode(self._bg_color))
+        else:
+            new_tab.configure(fg_color=self._apply_appearance_mode(self._fg_color),
+                              bg_color=self._apply_appearance_mode(self._fg_color))
+
+        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._outer_spacing - self._outer_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))
+                for tab in self._tab_dict.values():
+                    tab.configure(fg_color=self._apply_appearance_mode(self._bg_color),
+                                  bg_color=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))
+                for tab in self._tab_dict.values():
+                    tab.configure(fg_color=self._apply_appearance_mode(self._fg_color),
+                                  bg_color=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, cause canvas does not fill frame
+
+    def configure(self, require_redraw=False, **kwargs):
+        if "corner_radius" in kwargs:
+            self._corner_radius = kwargs.pop("corner_radius")
+            self._set_grid_segmented_button()
+            self._set_grid_current_tab()
+            self._set_grid_canvas()
+            self._configure_segmented_button_background_corners()
+            self._segmented_button.configure(corner_radius=self._corner_radius)
+        if "border_width" in kwargs:
+            self._border_width = kwargs.pop("border_width")
+            require_redraw = True
+        if "fg_color" in kwargs:
+            self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
+            self._configure_segmented_button_background_corners()
+            require_redraw = True
+        if "border_color" in kwargs:
+            self._border_color = self._check_color_type(kwargs.pop("border_color"))
+            require_redraw = True
+        if "segmented_button_fg_color" in kwargs:
+            self._segmented_button.configure(fg_color=kwargs.pop("segmented_button_fg_color"))
+        if "segmented_button_selected_color" in kwargs:
+            self._segmented_button.configure(selected_color=kwargs.pop("segmented_button_selected_color"))
+        if "segmented_button_selected_hover_color" in kwargs:
+            self._segmented_button.configure(selected_hover_color=kwargs.pop("segmented_button_selected_hover_color"))
+        if "segmented_button_unselected_color" in kwargs:
+            self._segmented_button.configure(unselected_color=kwargs.pop("segmented_button_unselected_color"))
+        if "segmented_button_unselected_hover_color" in kwargs:
+            self._segmented_button.configure(unselected_hover_color=kwargs.pop("segmented_button_unselected_hover_color"))
+        if "text_color" in kwargs:
+            self._segmented_button.configure(text_color=kwargs.pop("text_color"))
+        if "text_color_disabled" in kwargs:
+            self._segmented_button.configure(text_color_disabled=kwargs.pop("text_color_disabled"))
+
+        if "command" in kwargs:
+            self._command = kwargs.pop("command")
+        if "anchor" in kwargs:
+            self._anchor = kwargs.pop("anchor")
+            self._configure_grid()
+            self._set_grid_segmented_button()
+        if "state" in kwargs:
+            self._segmented_button.configure(state=kwargs.pop("state"))
+
+        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 == "anchor":
+            return self._anchor
+        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.append(name)
+            self._tab_dict[name] = self._create_tab()
+            self._segmented_button.insert(index, 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_current_tab()
+
+            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 index(self, name) -> int:
+        """ get index of tab with given name """
+        return self._segmented_button.index(name)
+
+    def move(self, new_index: int, name: str):
+        if 0 <= new_index < len(self._name_list):
+            if name in self._tab_dict:
+                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 rename(self, old_name: str, new_name: str):
+        if new_name in self._name_list:
+            raise ValueError(f"new_name '{new_name}' already exists")
+
+        # segmented button
+        old_index = self._segmented_button.index(old_name)
+        self._segmented_button.delete(old_name)
+        self._segmented_button.insert(old_index, new_name)
+
+        # name list
+        self._name_list.remove(old_name)
+        self._name_list.append(new_name)
+
+        # tab dictionary
+        self._tab_dict[new_name] = self._tab_dict.pop(old_name)
+
+    def delete(self, name: str):
+        """ delete tab by name """
+
+        if name in self._tab_dict:
+            self._name_list.remove(name)
+            self._tab_dict[name].grid_forget()
+            self._tab_dict.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_current_tab()
+
+            # 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._set_grid_current_tab()
+            self.after(100, lambda: self._grid_forget_all_tabs(exclude_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

+ 500 - 0
customtkinter/windows/widgets/ctk_textbox.py

@@ -0,0 +1,500 @@
+import tkinter
+from typing import Union, Tuple, Optional, Callable, Any
+
+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),
+                                     **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, None, True)
+        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 = False):
+        """ 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, button_color=self._scrollbar_button_color,
+                                            button_hover_color=self._scrollbar_button_hover_color)
+                self._y_scrollbar.configure(fg_color=self._bg_color, button_color=self._scrollbar_button_color,
+                                            button_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: str = None, command: Callable = None, add: Union[str, bool] = True):
+        """ called on the tkinter.Canvas """
+        if not (add == "+" or add is True):
+            raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
+        self._textbox.bind(sequence, command, add=True)
+
+    def unbind(self, sequence: str = None, funcid: str = None):
+        """ called on the tkinter.Label and tkinter.Canvas """
+        if funcid is not None:
+            raise ValueError("'funcid' argument can only be None, because there is a bug in" +
+                             " tkinter and its not clear whether the internal callbacks will be unbinded or not")
+        self._textbox.unbind(sequence, None)
+
+    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):
+        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)

+ 24 - 0
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"

+ 94 - 0
customtkinter/windows/widgets/font/ctk_font.py

@@ -0,0 +1,94 @@
+from tkinter.font import Font
+import copy
+from typing import List, Callable, Tuple, Optional
+try:
+    from typing import Literal
+except ImportError:
+    from typing_extensions import 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 """
+        try:
+            self._size_configure_callback_list.remove(callback)
+        except ValueError:
+            pass
+
+    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)

+ 66 - 0
customtkinter/windows/widgets/font/font_manager.py

@@ -0,0 +1,66 @@
+import sys
+import os
+import shutil
+from typing import Union
+
+
+class FontManager:
+
+    linux_font_path = "~/.fonts/"
+
+    @classmethod
+    def init_font_manager(cls):
+        # Linux
+        if sys.platform.startswith("linux"):
+            try:
+                if not os.path.isdir(os.path.expanduser(cls.linux_font_path)):
+                    os.mkdir(os.path.expanduser(cls.linux_font_path))
+                return True
+            except Exception as err:
+                sys.stderr.write("FontManager error: " + str(err) + "\n")
+                return False
+
+        # other platforms
+        else:
+            return True
+
+    @classmethod
+    def windows_load_font(cls, font_path: Union[str, bytes], private: bool = True, enumerable: bool = False) -> bool:
+        """ Function taken from: https://stackoverflow.com/questions/11993290/truly-custom-font-in-tkinter/30631309#30631309 """
+
+        from ctypes import windll, byref, create_unicode_buffer, create_string_buffer
+
+        FR_PRIVATE = 0x10
+        FR_NOT_ENUM = 0x20
+
+        if isinstance(font_path, bytes):
+            path_buffer = create_string_buffer(font_path)
+            add_font_resource_ex = windll.gdi32.AddFontResourceExA
+        elif isinstance(font_path, str):
+            path_buffer = create_unicode_buffer(font_path)
+            add_font_resource_ex = windll.gdi32.AddFontResourceExW
+        else:
+            raise TypeError('font_path must be of type bytes or str')
+
+        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(min(num_fonts_added, 1))
+
+    @classmethod
+    def load_font(cls, font_path: str) -> bool:
+        # Windows
+        if sys.platform.startswith("win"):
+            return cls.windows_load_font(font_path, private=True, enumerable=False)
+
+        # Linux
+        elif sys.platform.startswith("linux"):
+            try:
+                shutil.copy(font_path, os.path.expanduser(cls.linux_font_path))
+                return True
+            except Exception as err:
+                sys.stderr.write("FontManager error: " + str(err) + "\n")
+                return False
+
+        # macOS and others
+        else:
+            return False

+ 1 - 0
customtkinter/windows/widgets/image/__init__.py

@@ -0,0 +1 @@
+from .ctk_image import CTkImage

+ 122 - 0
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 (<width>, <height>) 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)
+
+

+ 7 - 0
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

+ 159 - 0
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 3 <= len(font) <= 6:
+                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: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
+        result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
+
+        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 <x> and <y> 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 <width> and <height> in geometry_string
+            return f"+{x}+{y}"
+
+        else:
+            return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}+{x}+{y}"
+
+    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 <x> and <y> 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 <width> and <height> in geometry_string
+            return f"+{x}+{y}"
+
+        else:
+            return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}+{x}+{y}"

+ 206 - 0
customtkinter/windows/widgets/scaling/scaling_tracker.py

@@ -0,0 +1,206 @@
+import tkinter
+import sys
+from typing import Callable
+
+
+class ScalingTracker:
+    deactivate_automatic_dpi_awareness = False
+
+    window_widgets_dict = {}  # contains window objects as keys with list of widget callbacks as elements
+    window_dpi_scaling_dict = {}  # contains window objects as keys and corresponding scaling factors
+
+    widget_scaling = 1  # user values which multiply to detected window scaling factor
+    window_scaling = 1
+
+    update_loop_running = False
+    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_window_scaling(cls, window) -> float:
+        window_root = cls.get_window_root_of_widget(window)
+        return cls.window_dpi_scaling_dict[window_root] * cls.window_scaling
+
+    @classmethod
+    def set_widget_scaling(cls, widget_scaling_factor: float):
+        cls.widget_scaling = max(widget_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)
+        cls.update_scaling_callbacks_all()
+
+    @classmethod
+    def get_window_root_of_widget(cls, widget):
+        current_widget = widget
+
+        while isinstance(current_widget, tkinter.Tk) is False and\
+                isinstance(current_widget, tkinter.Toplevel) is False:
+            current_widget = current_widget.master
+
+        return current_widget
+
+    @classmethod
+    def update_scaling_callbacks_all(cls):
+        for window, callback_list in cls.window_widgets_dict.items():
+            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.window_scaling)
+                else:
+                    set_scaling_callback(cls.widget_scaling,
+                                         cls.window_scaling)
+
+    @classmethod
+    def update_scaling_callbacks_for_window(cls, window):
+        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.window_scaling)
+            else:
+                set_scaling_callback(cls.widget_scaling,
+                                     cls.window_scaling)
+
+    @classmethod
+    def add_widget(cls, widget_callback: Callable, widget):
+        window_root = cls.get_window_root_of_widget(widget)
+
+        if window_root not in cls.window_widgets_dict:
+            cls.window_widgets_dict[window_root] = [widget_callback]
+        else:
+            cls.window_widgets_dict[window_root].append(widget_callback)
+
+        if window_root not in cls.window_dpi_scaling_dict:
+            cls.window_dpi_scaling_dict[window_root] = cls.get_window_dpi_scaling(window_root)
+
+        if not cls.update_loop_running:
+            window_root.after(100, cls.check_dpi_scaling)
+            cls.update_loop_running = True
+
+    @classmethod
+    def remove_widget(cls, widget_callback, widget):
+        window_root = cls.get_window_root_of_widget(widget)
+        try:
+            cls.window_widgets_dict[window_root].remove(widget_callback)
+        except:
+            pass
+
+    @classmethod
+    def remove_window(cls, window_callback, window):
+        try:
+            del cls.window_widgets_dict[window]
+        except:
+            pass
+
+    @classmethod
+    def add_window(cls, window_callback, window):
+        if window not in cls.window_widgets_dict:
+            cls.window_widgets_dict[window] = [window_callback]
+        else:
+            cls.window_widgets_dict[window].append(window_callback)
+
+        if window not in cls.window_dpi_scaling_dict:
+            cls.window_dpi_scaling_dict[window] = cls.get_window_dpi_scaling(window)
+
+    @classmethod
+    def activate_high_dpi_awareness(cls):
+        """ make process DPI aware, customtkinter elements will get scaled automatically,
+            only gets activated when CTk object is created """
+
+        if not cls.deactivate_automatic_dpi_awareness:
+            if sys.platform == "darwin":
+                pass  # high DPI scaling works automatically on macOS
+
+            elif sys.platform.startswith("win"):
+                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
+
+    @classmethod
+    def get_window_dpi_scaling(cls, window) -> float:
+        if not cls.deactivate_automatic_dpi_awareness:
+            if sys.platform == "darwin":
+                return 1  # scaling works automatically on macOS
+
+            elif sys.platform.startswith("win"):
+                from ctypes import windll, pointer, wintypes
+
+                DPI100pc = 96  # DPI 96 is 100% scaling
+                DPI_type = 0  # MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2
+                window_hwnd = wintypes.HWND(window.winfo_id())
+                monitor_handle = windll.user32.MonitorFromWindow(window_hwnd, wintypes.DWORD(2))  # MONITOR_DEFAULTTONEAREST = 2
+                x_dpi, y_dpi = wintypes.UINT(), wintypes.UINT()
+                windll.shcore.GetDpiForMonitor(monitor_handle, DPI_type, pointer(x_dpi), pointer(y_dpi))
+                return (x_dpi.value + y_dpi.value) / (2 * DPI100pc)
+
+            else:
+                return 1  # DPI awareness on Linux not implemented
+        else:
+            return 1
+
+    @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() 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:
+                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
+
+        cls.update_loop_running = False

+ 9 - 0
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}\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")

+ 55 - 0
customtkinter/windows/widgets/theme/theme_manager.py

@@ -0,0 +1,55 @@
+import sys
+import os
+import pathlib
+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:
+            customtkinter_path = pathlib.Path(script_directory).parent.parent.parent
+            with open(os.path.join(customtkinter_path, "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"]
+
+        # fix name inconsistencies
+        if "CTkCheckbox" in cls.theme.keys():
+            cls.theme["CTkCheckBox"] = cls.theme.pop("CTkCheckbox")
+        if "CTkRadiobutton" in cls.theme.keys():
+            cls.theme["CTkRadioButton"] = cls.theme.pop("CTkRadiobutton")
+
+    @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")

+ 1 - 0
customtkinter/windows/widgets/utility/__init__.py

@@ -0,0 +1 @@
+from .utility_functions import pop_from_dict_by_set, check_kwargs_empty

+ 22 - 0
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

BIN
documentation_images/CustomTkinter_logo_dark.png


BIN
documentation_images/CustomTkinter_logo_light.png


BIN
documentation_images/complex_example_dark_Windows.png


BIN
documentation_images/complex_example_light_macOS.png


BIN
documentation_images/image_example_dark_Windows.png


BIN
documentation_images/paypal_donate_button.png


BIN
documentation_images/scrollable_frame_example_Windows.png


BIN
documentation_images/single_button_macOS.png


+ 164 - 0
examples/complex_example.py

@@ -0,0 +1,164 @@
+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__()
+
+        # configure window
+        self.title("CustomTkinter complex_example.py")
+        self.geometry(f"{1100}x{580}")
+
+        # configure grid layout (4x4)
+        self.grid_columnconfigure(1, weight=1)
+        self.grid_columnconfigure((2, 3), weight=0)
+        self.grid_rowconfigure((0, 1, 2), weight=1)
+
+        # 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))
+
+        # 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")
+
+        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")
+
+        # create textbox
+        self.textbox = customtkinter.CTkTextbox(self, width=250)
+        self.textbox.grid(row=0, column=1, padx=(20, 0), pady=(20, 0), sticky="nsew")
+
+        # 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.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.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 slider and progressbar frame
+        self.slider_progressbar_frame = customtkinter.CTkFrame(self, fg_color="transparent")
+        self.slider_progressbar_frame.grid(row=1, column=1, 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")
+
+        # create scrollable frame
+        self.scrollable_frame = customtkinter.CTkScrollableFrame(self, label_text="CTkScrollableFrame")
+        self.scrollable_frame.grid(row=1, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
+        self.scrollable_frame.grid_columnconfigure(0, weight=1)
+        self.scrollable_frame_switches = []
+        for i in range(100):
+            switch = customtkinter.CTkSwitch(master=self.scrollable_frame, text=f"CTkSwitch {i}")
+            switch.grid(row=i, column=0, padx=10, pady=(0, 20))
+            self.scrollable_frame_switches.append(switch)
+
+        # 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, 0), padx=20, sticky="n")
+        self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame)
+        self.checkbox_2.grid(row=2, column=0, pady=(20, 0), padx=20, sticky="n")
+        self.checkbox_3 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame)
+        self.checkbox_3.grid(row=3, column=0, pady=20, padx=20, sticky="n")
+
+        # set default values
+        self.sidebar_button_3.configure(state="disabled", text="Disabled CTkButton")
+        self.checkbox_3.configure(state="disabled")
+        self.checkbox_1.select()
+        self.scrollable_frame_switches[0].select()
+        self.scrollable_frame_switches[4].select()
+        self.radio_button_3.configure(state="disabled")
+        self.appearance_mode_optionemenu.set("Dark")
+        self.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 open_input_dialog_event(self):
+        dialog = customtkinter.CTkInputDialog(text="Type in a number:", title="CTkInputDialog")
+        print("CTkInputDialog:", dialog.get_input())
+
+    def change_appearance_mode_event(self, new_appearance_mode: str):
+        customtkinter.set_appearance_mode(new_appearance_mode)
+
+    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__":
+    app = App()
+    app.mainloop()

+ 61 - 0
examples/example_background_image.py

@@ -0,0 +1,61 @@
+import customtkinter
+from PIL import Image
+import os
+
+customtkinter.set_appearance_mode("dark")
+
+
+class App(customtkinter.CTk):
+    width = 900
+    height = 600
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        self.title("CustomTkinter example_background_image.py")
+        self.geometry(f"{self.width}x{self.height}")
+        self.resizable(False, False)
+
+        # 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)
+
+        # 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))
+
+        # 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))
+
+    def login_event(self):
+        print("Login pressed - username:", self.username_entry.get(), "password:", self.password_entry.get())
+
+        self.login_frame.grid_forget()  # remove login frame
+        self.main_frame.grid(row=0, column=0, sticky="nsew", padx=100)  # show main frame
+
+    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.mainloop()

+ 118 - 0
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()
+

+ 133 - 0
examples/scrollable_frame_example.py

@@ -0,0 +1,133 @@
+import customtkinter
+import os
+from PIL import Image
+
+
+class ScrollableCheckBoxFrame(customtkinter.CTkScrollableFrame):
+    def __init__(self, master, item_list, command=None, **kwargs):
+        super().__init__(master, **kwargs)
+
+        self.command = command
+        self.checkbox_list = []
+        for i, item in enumerate(item_list):
+            self.add_item(item)
+
+    def add_item(self, item):
+        checkbox = customtkinter.CTkCheckBox(self, text=item)
+        if self.command is not None:
+            checkbox.configure(command=self.command)
+        checkbox.grid(row=len(self.checkbox_list), column=0, pady=(0, 10))
+        self.checkbox_list.append(checkbox)
+
+    def remove_item(self, item):
+        for checkbox in self.checkbox_list:
+            if item == checkbox.cget("text"):
+                checkbox.destroy()
+                self.checkbox_list.remove(checkbox)
+                return
+
+    def get_checked_items(self):
+        return [checkbox.cget("text") for checkbox in self.checkbox_list if checkbox.get() == 1]
+
+
+class ScrollableRadiobuttonFrame(customtkinter.CTkScrollableFrame):
+    def __init__(self, master, item_list, command=None, **kwargs):
+        super().__init__(master, **kwargs)
+
+        self.command = command
+        self.radiobutton_variable = customtkinter.StringVar()
+        self.radiobutton_list = []
+        for i, item in enumerate(item_list):
+            self.add_item(item)
+
+    def add_item(self, item):
+        radiobutton = customtkinter.CTkRadioButton(self, text=item, value=item, variable=self.radiobutton_variable)
+        if self.command is not None:
+            radiobutton.configure(command=self.command)
+        radiobutton.grid(row=len(self.radiobutton_list), column=0, pady=(0, 10))
+        self.radiobutton_list.append(radiobutton)
+
+    def remove_item(self, item):
+        for radiobutton in self.radiobutton_list:
+            if item == radiobutton.cget("text"):
+                radiobutton.destroy()
+                self.radiobutton_list.remove(radiobutton)
+                return
+
+    def get_checked_item(self):
+        return self.radiobutton_variable.get()
+
+
+class ScrollableLabelButtonFrame(customtkinter.CTkScrollableFrame):
+    def __init__(self, master, command=None, **kwargs):
+        super().__init__(master, **kwargs)
+        self.grid_columnconfigure(0, weight=1)
+
+        self.command = command
+        self.radiobutton_variable = customtkinter.StringVar()
+        self.label_list = []
+        self.button_list = []
+
+    def add_item(self, item, image=None):
+        label = customtkinter.CTkLabel(self, text=item, image=image, compound="left", padx=5, anchor="w")
+        button = customtkinter.CTkButton(self, text="Command", width=100, height=24)
+        if self.command is not None:
+            button.configure(command=lambda: self.command(item))
+        label.grid(row=len(self.label_list), column=0, pady=(0, 10), sticky="w")
+        button.grid(row=len(self.button_list), column=1, pady=(0, 10), padx=5)
+        self.label_list.append(label)
+        self.button_list.append(button)
+
+    def remove_item(self, item):
+        for label, button in zip(self.label_list, self.button_list):
+            if item == label.cget("text"):
+                label.destroy()
+                button.destroy()
+                self.label_list.remove(label)
+                self.button_list.remove(button)
+                return
+
+
+class App(customtkinter.CTk):
+    def __init__(self):
+        super().__init__()
+
+        self.title("CTkScrollableFrame example")
+        self.grid_rowconfigure(0, weight=1)
+        self.columnconfigure(2, weight=1)
+
+        # create scrollable checkbox frame
+        self.scrollable_checkbox_frame = ScrollableCheckBoxFrame(master=self, width=200, command=self.checkbox_frame_event,
+                                                                 item_list=[f"item {i}" for i in range(50)])
+        self.scrollable_checkbox_frame.grid(row=0, column=0, padx=15, pady=15, sticky="ns")
+        self.scrollable_checkbox_frame.add_item("new item")
+
+        # create scrollable radiobutton frame
+        self.scrollable_radiobutton_frame = ScrollableRadiobuttonFrame(master=self, width=500, command=self.radiobutton_frame_event,
+                                                                       item_list=[f"item {i}" for i in range(100)],
+                                                                       label_text="ScrollableRadiobuttonFrame")
+        self.scrollable_radiobutton_frame.grid(row=0, column=1, padx=15, pady=15, sticky="ns")
+        self.scrollable_radiobutton_frame.configure(width=200)
+        self.scrollable_radiobutton_frame.remove_item("item 3")
+
+        # create scrollable label and button frame
+        current_dir = os.path.dirname(os.path.abspath(__file__))
+        self.scrollable_label_button_frame = ScrollableLabelButtonFrame(master=self, width=300, command=self.label_button_frame_event, corner_radius=0)
+        self.scrollable_label_button_frame.grid(row=0, column=2, padx=0, pady=0, sticky="nsew")
+        for i in range(20):  # add items with images
+            self.scrollable_label_button_frame.add_item(f"image and item {i}", image=customtkinter.CTkImage(Image.open(os.path.join(current_dir, "test_images", "chat_light.png"))))
+
+    def checkbox_frame_event(self):
+        print(f"checkbox frame modified: {self.scrollable_checkbox_frame.get_checked_items()}")
+
+    def radiobutton_frame_event(self):
+        print(f"radiobutton frame modified: {self.scrollable_radiobutton_frame.get_checked_item()}")
+
+    def label_button_frame_event(self, item):
+        print(f"label button frame clicked: {item}")
+
+
+if __name__ == "__main__":
+    customtkinter.set_appearance_mode("dark")
+    app = App()
+    app.mainloop()

+ 76 - 0
examples/simple_example.py

@@ -0,0 +1,76 @@
+import customtkinter
+import tkinterDnD
+
+customtkinter.set_ctk_parent_class(tkinterDnD.Tk)
+
+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("400x780")
+app.title("CustomTkinter simple_example.py")
+
+print(type(app), isinstance(app, tkinterDnD.Tk))
+
+def button_callback():
+    print("Button click", combobox_1.get())
+
+
+def slider_callback(value):
+    progressbar_1.set(value)
+
+
+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=customtkinter.LEFT)
+label_1.pack(pady=10, padx=10)
+
+progressbar_1 = customtkinter.CTkProgressBar(master=frame_1)
+progressbar_1.pack(pady=10, padx=10)
+
+button_1 = customtkinter.CTkButton(master=frame_1, command=button_callback)
+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=10, padx=10)
+slider_1.set(0.5)
+
+entry_1 = customtkinter.CTkEntry(master=frame_1, placeholder_text="CTkEntry")
+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=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=10, padx=10)
+combobox_1.set("CTkComboBox")
+
+checkbox_1 = customtkinter.CTkCheckBox(master=frame_1)
+checkbox_1.pack(pady=10, padx=10)
+
+radiobutton_var = customtkinter.IntVar(value=1)
+
+radiobutton_1 = customtkinter.CTkRadioButton(master=frame_1, variable=radiobutton_var, value=1)
+radiobutton_1.pack(pady=10, padx=10)
+
+radiobutton_2 = customtkinter.CTkRadioButton(master=frame_1, variable=radiobutton_var, value=2)
+radiobutton_2.pack(pady=10, padx=10)
+
+switch_1 = customtkinter.CTkSwitch(master=frame_1)
+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=300)
+tabview_1.pack(pady=10, padx=10)
+tabview_1.add("CTkTabview")
+tabview_1.add("Tab 2")
+
+app.mainloop()

BIN
examples/test_images/CustomTkinter_logo_single.png


BIN
examples/test_images/add_user_dark.png


BIN
examples/test_images/add_user_light.png


BIN
examples/test_images/bg_gradient.jpg


BIN
examples/test_images/chat_dark.png


BIN
examples/test_images/chat_light.png


BIN
examples/test_images/home_dark.png


BIN
examples/test_images/home_light.png


BIN
examples/test_images/image_icon_light.png


BIN
examples/test_images/large_test_image.png


+ 6 - 1
libs/foo/__init__.py

@@ -13,7 +13,7 @@ class foo:
 
 
     ##############################           Constructor          ##############################
-    def __init__(self, led_Pin_timer:Pin, led_Pin_button:Pin, button:Pin):
+    def __init__(self, led_Pin_button:Pin, button:Pin, led_Pin_timer:Pin = Pin(3),):
         
         self.__led_pin_timer = self.__check_pin(led_Pin_timer)
         self.__led_pin_button = self.__check_pin(led_Pin_button)
@@ -32,6 +32,11 @@ class foo:
     # zum Aufruf privater Variablen
     @property
     def button(self):
+        """button
+
+        Returns:
+            _button_: _description_
+        """
         return self.__button
 
     # bei Veränderung der Variable wird die folgende Funktion ausgeführt

+ 46 - 12
main.py

@@ -8,25 +8,59 @@ Main File to run on the pico.
 
 ##############################                 Modules                ##############################
 
-from machine import Pin
-from time import sleep_ms
 from libs.foo import foo
-
+import time
+from machine import Pin, UART, ADC
+import machine
 ##############################            Global Variables            ##############################
 
-baz = foo(Pin(0), Pin(1), Pin(20))
+baz = foo(Pin(1), Pin(20), led_Pin_timer=Pin(5))
 
 ##############################                 Main                   ##############################
 
 def main():
-    """
-    Main program
-    """
-
-    ##########################           Local Variables            ############################
-
-    led = Pin("LED", Pin.OUT)
+    # Pin Definitions (Assuming these are correct)
+    output_pin = machine.Pin(26, machine.Pin.IN)
+    lo_minus_pin = machine.Pin(27, machine.Pin.IN)
+    lo_plus_pin = machine.Pin(8, machine.Pin.IN)
     
+    # Configure ADC
+    adc = ADC(output_pin)
+    print("version_1")
+    # Function to map ADC value to desired range
+    def map_value(value, in_min, in_max, out_min, out_max):
+        return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
+    
+    # Function to read and process ECG signal
+    def read_ecg():
+        adc_value = adc.read_u16()
+        signal = map_value(adc_value, 0, 65535, 0, 100)  # Example mapping
+        return signal
+    
+    # Configure UART for 11520 baud rate
+    # uart = UART(0, baudrate=11520, tx=Pin(0), rx=Pin(1))  # Adjust pins as needed
+    k=0
+    while True:
+        
+        printlist=[]
+        for i in range(100):
+            ecg_signal = read_ecg()
+            printlist.append(ecg_signal)
+        # Transmit the ECG signal
+        #uart.write(str(ecg_signal) + "\n")  # Send data with newline character
+        for ecg_signal in printlist:
+            #k=k+1
+            #print(k)
+            print(ecg_signal)
+        # Optional delay between readings
+        #time.sleep(0.00001)  # Adjust delay as needed
+
+"""
+   Main program
+   
+   ##########################           Local Variables            ############################
+   led = Pin("LED", Pin.OUT)
+   
     ##########################                Code                  ############################
     
     # test example class
@@ -39,7 +73,7 @@ def main():
         sleep_ms(500)
         led.toggle()
         sleep_ms(500)
-        
+"""
 
 ##############################               Functions                ##############################
 

+ 395 - 0
oszi.py

@@ -0,0 +1,395 @@
+import customtkinter as ctk
+import time
+import math
+import random
+import csv
+import tkinter as tk
+import customtkinter as ctk
+import time
+import serial
+import time
+
+
+## cc maximilian scheinast-peter
+## last update 11.04.2024
+## dieses programm funktioniert ähnlich wie ein digitales Oszi zur Darstellung von Vitalkurven
+## Attribution-NonCommercial license
+
+
+
+def get_data():
+    # Configure serial port (adjust port name as needed)
+    ser = serial.Serial('/dev/ttyACM0', 115200)
+
+    # Wait for the serial connection to establish
+    ser.timeout = 2
+
+    #ser.write(b'testcom.py\n')  # Send command to start the script
+    while True:
+        
+        # Read data from serial port
+        data = ser.readline().decode().strip()
+
+        # Check if data is not empty
+        #print(data)
+        if data:
+            try:
+                # Attempt to convert data to float
+                value = float(data)
+                timestamp = time.time()
+                return timestamp, value
+            except ValueError:
+                print("Invalid data format:", data)
+        else:
+            print("Empty data received")
+        
+        # Add a small delay to prevent rapid looping
+        time.sleep(0.001)
+
+
+
+def wait_for_next_millisecond():
+    """Waits until the next full millisecond."""
+    current_time = time.time()
+    next_millisecond = (int(current_time * 1000) + 1) / 1000
+    time.sleep(next_millisecond - current_time)
+
+
+# Function to generate realistic ECG data
+
+
+class EKGApp:
+    def __init__(self, master):
+        self.master = master
+        master.title("EKG Visualization")
+
+        # Main Frame
+        main_frame = ctk.CTkFrame(master)
+        main_frame.pack(fill=ctk.BOTH, expand=True)
+
+        # Single Sweep Canvas
+        self.single_canvas = ctk.CTkCanvas(main_frame, width=600, height=300)
+        self.single_canvas.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
+
+        # Multi Sweep Canvas
+        self.multi_canvas = ctk.CTkCanvas(main_frame, width=600, height=300)
+        self.multi_canvas.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
+
+        # Input Frame
+        input_frame = ctk.CTkFrame(main_frame)
+        input_frame.pack(side=ctk.RIGHT, fill=ctk.Y)
+
+        # Data Structures
+        self.single_data = []
+        self.multi_data = []
+        self.logged_data = []
+
+        # Time Variables
+        self.start_time = time.time()
+        self.multi_sweep_duration = 10  # Initial Laufbanddauer
+
+        # Trigger Variables
+        self.trigger_level = 50
+        self.trigger_armed = False
+        self.trigger_paused = False
+        self.trigger_timestamp = None
+        self.last_trigger_timestamp = None
+        self.last_value = 0
+        self.trigger_count = 0
+
+        # Cooldown Variables
+        self.cooldown_time = 0
+        self.cooldown_active = False
+
+        # Logging Variables
+        self.logging_active = False
+
+        # Batching Variables
+        self.batch_size = 5
+        self.current_batch = []
+
+        # Input Fields and Buttons (with consistent size)
+        self.create_input_fields(input_frame)
+
+        # Logging Frame
+        logging_frame = ctk.CTkFrame(master)
+        logging_frame.pack(fill=ctk.X)
+        self.create_logging_buttons(logging_frame)
+
+        # Update Data Periodically
+        self.update_data()
+
+        # Resize Handling
+        self.single_canvas.bind("<Configure>", self.on_resize)
+        self.multi_canvas.bind("<Configure>", self.on_resize)
+
+    def create_input_fields(self, frame):
+        # Consistent width for all input elements
+        input_width = 150
+
+        # Trigger Level
+        ctk.CTkLabel(frame, text="Trigger Level:").pack()
+        self.trigger_entry = ctk.CTkEntry(frame, width=input_width)
+        self.trigger_entry.insert(0, str(self.trigger_level))
+        self.trigger_entry.pack()
+
+        # Flank Selection
+        ctk.CTkLabel(frame, text="Flanke:").pack()
+        self.flank_var = ctk.StringVar(value="Steigende Flanke")
+        self.flank_combobox = ctk.CTkComboBox(frame, variable=self.flank_var,
+                                                values=["Steigende Flanke", "Fallende Flanke"], width=input_width)
+        self.flank_combobox.pack()
+
+        # Cooldown
+        ctk.CTkLabel(frame, text="Cooldown (Sekunden):").pack()
+        self.cooldown_entry = ctk.CTkEntry(frame, width=input_width)
+        self.cooldown_entry.insert(0, "0")
+        self.cooldown_entry.pack()
+        self.cooldown_button = ctk.CTkButton(frame, text="Cooldown setzen", command=self.set_cooldown,
+                                                width=input_width)
+        self.cooldown_button.pack()
+
+        # Ax Time (X-Achsen-Zeit)
+        ctk.CTkLabel(frame, text="Ax-Zeit (Sekunden):").pack()
+        self.ax_time_entry = ctk.CTkEntry(frame, width=input_width)
+        self.ax_time_entry.insert(0, str(self.multi_sweep_duration))  # Initial value
+        self.ax_time_entry.pack()
+        self.ax_time_button = ctk.CTkButton(frame, text="Ax-Zeit setzen", command=self.set_ax_time, width=input_width)
+        self.ax_time_button.pack()
+
+        # Trigger Buttons
+        self.trigger_button = ctk.CTkButton(frame, text="Trigger starten", command=self.toggle_trigger,
+                                            width=input_width)
+        self.trigger_button.pack()
+        self.pause_button = ctk.CTkButton(frame, text="Trigger pausieren", command=self.pause_trigger,
+                                            state=ctk.DISABLED, width=input_width)
+        self.pause_button.pack()
+
+        # Multi Sweep Duration (Laufbanddauer)
+        ctk.CTkLabel(frame, text="Laufbanddauer (Sekunden):").pack()
+        self.duration_entry = ctk.CTkEntry(frame, width=input_width)
+        self.duration_entry.insert(0, "30")
+        self.duration_entry.pack()
+        self.duration_button = ctk.CTkButton(frame, text="Dauer setzen", command=self.set_duration, width=input_width)
+        self.duration_button.pack()
+
+    def set_ax_time(self):
+
+        try:
+            self.multi_sweep_duration = float(self.ax_time_entry.get())
+        except ValueError:
+            pass
+
+    def create_logging_buttons(self, frame):
+        # Consistent width for logging buttons
+        button_width = 120
+
+        self.start_logging_button = ctk.CTkButton(frame, text="Start Logging", command=self.start_logging,
+                                                    width=button_width)
+        self.start_logging_button.pack(side=ctk.LEFT)
+        self.end_logging_button = ctk.CTkButton(frame, text="End Logging", command=self.end_logging, state=ctk.DISABLED,
+                                                width=button_width)
+        self.end_logging_button.pack(side=ctk.LEFT)
+        self.save_data_button = ctk.CTkButton(frame, text="Save Data", command=self.save_data, state=ctk.DISABLED,
+                                                width=button_width)
+        self.save_data_button.pack(side=ctk.LEFT)
+
+    def set_cooldown(self):
+        try:
+            self.cooldown_time = float(self.cooldown_entry.get())
+        except ValueError:
+            pass
+
+    def set_duration(self):
+        try:
+            self.multi_sweep_duration = float(self.duration_entry.get())
+        except ValueError:
+            pass
+
+    def toggle_trigger(self):
+        if self.trigger_armed:
+            self.trigger_armed = False
+            self.trigger_button.configure(text="Trigger starten")
+            self.pause_button.configure(state=ctk.DISABLED)
+            self.single_data = []
+            self.draw_single_canvas()
+        else:
+            try:
+                self.trigger_level = float(self.trigger_entry.get())
+            except ValueError:
+                pass
+            self.trigger_armed = True
+            self.trigger_button.configure(text="Trigger stoppen")
+            self.pause_button.configure(state=ctk.NORMAL)
+            self.trigger_count = 0
+
+    def pause_trigger(self):
+        self.trigger_paused = not self.trigger_paused
+        if self.trigger_paused:
+            self.pause_button.configure(text="Trigger fortsetzen")
+        else:
+            self.pause_button.configure(text="Trigger pausieren")
+
+    def start_logging(self):
+        self.logging_active = True
+        self.logged_data = []
+        self.start_logging_button.configure(state=ctk.DISABLED)
+        self.end_logging_button.configure(state=ctk.NORMAL)
+
+    def end_logging(self):
+        self.logging_active = False
+        self.start_logging_button.configure(state=ctk.NORMAL)
+        self.end_logging_button.configure(state=ctk.DISABLED)
+        self.save_data_button.configure(state=ctk.NORMAL)
+
+    def save_data(self):
+        # Popup for file name
+        ####print(123)
+        file_name = ctk.CTkInputDialog(text="Enter file name:", title="Save Data").get_input()
+        if file_name:
+            try:
+                with open(f"{file_name}.csv", "w", newline="", encoding='utf-8') as csvfile:
+                    writer = csv.writer(csvfile)
+                    writer.writerow(["Timestamp", "Value"])
+                    writer.writerows(self.logged_data)
+                self.save_data_button.configure(state=ctk.DISABLED)
+            except Exception as e:
+                print(f"Error saving data: {e}")
+
+    def update_data(self):
+
+        # Get EKG Data
+        timestamp, value = get_data()
+
+        # Collect data points into batches
+        self.current_batch.append((timestamp, value))
+
+        if len(self.current_batch) == self.batch_size:
+            # Process the batch (e.g., plot it)
+            #self.multi_data.append((timestamp, value))
+            self.process_batch(self.current_batch)
+            
+            # Reset the current batch
+            self.current_batch = []
+
+
+
+        # Update Multi Sweep Data
+        #self.multi_data.append((timestamp, value))
+        try:
+            if timestamp - self.multi_data[0][0] > self.multi_sweep_duration:
+                self.multi_data.pop(0)
+        except:
+            pass
+        # Trigger Logic
+        if self.trigger_armed and not self.trigger_paused and not self.cooldown_active:
+            condition = (
+                        value >= self.trigger_level and self.last_value < self.trigger_level) if self.flank_var.get() == "Steigende Flanke" else (
+                        value <= self.trigger_level and self.last_value > self.trigger_level)
+            if condition:
+                self.last_trigger_timestamp = timestamp
+                self.single_data = []
+                self.trigger_count = 0
+                if self.cooldown_time > 0:
+                    self.cooldown_active = True
+                    self.master.after(int(self.cooldown_time * 1000), self.end_cooldown)
+
+        # Update Single Sweep Data
+        if self.last_trigger_timestamp is not None and self.trigger_armed and not self.trigger_paused:
+            self.single_data.append((timestamp, value))
+
+        # Update Logged Data
+        if self.logging_active:
+            self.logged_data.append((timestamp, value))
+
+        
+        
+
+        # Store last value and update trigger count
+        self.last_value = value
+        if self.trigger_armed and not self.trigger_paused:
+            self.trigger_count += 1
+
+    
+
+        # Call again after 1ms
+        self.master.after(1,self.update_data)
+
+    def end_cooldown(self):
+        self.cooldown_active = False
+
+    def process_batch(self, batch):
+        global i
+        i=i+1
+        #print(i)
+        # Example: Calculate average value for the batch
+        total_value = sum(value for _, value in batch)
+        average_value = total_value / len(batch)
+        for timestamp, value in batch:
+            self.multi_data.append((timestamp, value))
+        ###print(f"Average value for batch: {average_value}")
+        self.draw_multi_canvas()
+        # Draw Canvases
+        self.draw_single_canvas()
+        # You can implement your own logic here, such as plotting the batch
+        # on a separate canvas or performing other calculations.
+
+    def draw_single_canvas(self):
+        self.single_canvas.delete("all")
+        if not self.single_data or self.last_trigger_timestamp is None:
+            return
+        
+        # Prepare coordinates for create_line
+        coords = []
+        x_scale = self.single_canvas.winfo_width() / self.multi_sweep_duration
+        y_scale = self.single_canvas.winfo_height() / 100
+        last_x, last_y = None, None
+        
+        for timestamp, value in self.single_data:
+            x = (timestamp - self.last_trigger_timestamp) * x_scale
+            y = self.single_canvas.winfo_height() - (value * self.single_canvas.winfo_height() / 100)
+            if last_x is not None:
+                coords.extend([last_x, last_y, x, y])
+            last_x, last_y = x, y
+
+        # Draw the lines
+        if coords:
+            self.single_canvas.create_line(*coords, fill="blue")
+
+
+    def draw_multi_canvas(self):
+        self.multi_canvas.delete("all")
+        if not self.multi_data:
+            return
+
+        duration = max(self.multi_data[-1][0] - self.multi_data[0][0], 0.001)
+        x_scale = self.multi_canvas.winfo_width() / duration
+        y_scale = self.multi_canvas.winfo_height() / 100
+
+        coords = []
+        for timestamp, value in self.multi_data:
+            x = (timestamp - self.multi_data[0][0]) * x_scale
+            y = self.multi_canvas.winfo_height() - (value * y_scale)
+            coords.extend([x, y])
+
+        self.multi_canvas.create_line(*coords, fill="blue")
+        trigger_y = self.multi_canvas.winfo_height() - (self.trigger_level * y_scale)
+        self.multi_canvas.create_line(self.multi_canvas.winfo_width() - 10, trigger_y, self.multi_canvas.winfo_width(), trigger_y + 5, fill="red")
+        self.multi_canvas.create_line(self.multi_canvas.winfo_width() - 10, trigger_y, self.multi_canvas.winfo_width(), trigger_y - 5, fill="red")
+
+    def draw_time_axis(self, canvas, start_time, duration):
+        for i in range(int(duration) + 1):
+            x = i * canvas.winfo_width() / duration
+            canvas.create_line(x, 0, x, canvas.winfo_height(), fill="gray", dash=(2, 2))
+            canvas.create_text(x, canvas.winfo_height() - 10, text=f"{i:.1f}s", anchor=ctk.N)
+
+    def on_resize(self, event):
+        self.draw_single_canvas()
+        self.draw_multi_canvas()
+i=0
+if __name__ == "__main__":
+    #ctk.set_appearance_mode("dark")  # Modes: "System" (standard), "Dark", "Light"
+    ctk.set_default_color_theme("blue")  # Themes: "blue" (standard), "green", "dark-blue"
+    root = ctk.CTk()
+    app = EKGApp(root)
+    root.mainloop()

+ 34 - 0
pyproject.toml

@@ -0,0 +1,34 @@
+[build-system]
+requires = ["setuptools>=42", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.tbump]
+github_url = "https://github.com/TomSchimansky/CustomTkinter"
+
+[tool.tbump.version]
+current = "5.2.2"
+
+# Example of a semver regexp.
+# Make sure this matches current_version before
+# using tbump
+regex = '''
+  (?P<major>\d+)
+  \.
+  (?P<minor>\d+)
+  \.
+  (?P<patch>\d+)
+  '''
+
+[tool.tbump.git]
+message_template = "Bump to {new_version}"
+tag_template = "v{new_version}"
+
+# For each file to patch, add a [[tool.tbump.file]] config
+# section containing the path of the file, relative to the
+# tbump.toml location.
+[[tool.tbump.file]]
+src = "setup.cfg"
+
+[[tool.tbump.file]]
+src = "customtkinter/__init__.py"
+search = "__version__ = \"{current_version}\""

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
+darkdetect~=0.3.1
+typing-extensions~=4.4.0
+packaging

+ 39 - 0
setup.cfg

@@ -0,0 +1,39 @@
+[metadata]
+name = customtkinter
+version = 5.2.2
+description = Create modern looking GUIs with Python
+long_description = A modern and customizable python UI-library based on Tkinter: https://customtkinter.tomschimansky.com
+long_description_content_type = text/markdown
+url = https://customtkinter.tomschimansky.com
+author = Tom Schimansky
+license = Creative Commons Zero v1.0 Universal
+license_file = LICENSE
+classifiers =
+    License :: OSI Approved :: MIT License
+    Operating System :: OS Independent
+    Programming Language :: Python :: 3 :: Only
+
+[project.urls]
+homepage = https://customtkinter.tomschimansky.com
+documentation = https://customtkinter.tomschimansky.com/documentation
+repository = https://github.com/tomschimansky/customtkinter
+
+[options]
+python_requires = >=3.7
+packages =
+    customtkinter
+    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
+    customtkinter.windows.widgets.utility
+install_requires =
+    darkdetect
+    typing_extensions; python_version<="3.7"
+    packaging
+include_package_data = True

+ 51 - 0
test/manual_integration_tests/simple_example_standard_tkinter.py

@@ -0,0 +1,51 @@
+import tkinter.ttk as ttk
+import tkinter
+
+app = tkinter.Tk()
+app.geometry("400x350")
+app.title("simple_example_standard_tkinter.py")
+
+def button_function():
+    print("button pressed")
+
+
+def slider_function(value):
+    progressbar_1["value"] = value
+
+
+s = ttk.Style()
+s.configure("TRadiobutton", fg="red")
+
+y_padding = 6
+
+frame_1 = tkinter.Frame(master=app, width=300, height=260, bg="lightgray")
+frame_1.pack(padx=60, pady=20, fill="both", expand=True)
+
+label_1 = tkinter.Label(master=frame_1, text="Label", bg="lightgray")
+label_1.pack(pady=y_padding, padx=10)
+
+progressbar_1 = ttk.Progressbar(master=frame_1, style='black.Horizontal.TProgressbar', length=150)
+progressbar_1.pack(pady=y_padding, padx=10)
+progressbar_1["value"] = 50
+
+button_1 = tkinter.Button(master=frame_1, command=button_function, text="Button", highlightbackground="lightgray")
+button_1.pack(pady=y_padding, padx=10)
+
+slider_1 = tkinter.Scale(master=frame_1, command=slider_function, orient="horizontal", bg="lightgray", length=150)
+slider_1.pack(pady=y_padding, padx=10)
+
+entry_1 = tkinter.Entry(master=frame_1, highlightbackground="lightgray", width=10)
+entry_1.pack(pady=y_padding, padx=10)
+
+checkbox_1 = tkinter.Checkbutton(master=frame_1, bg=frame_1.cget("bg"), text="CheckButton")
+checkbox_1.pack(pady=y_padding, padx=10)
+
+radiobutton_var = tkinter.IntVar()
+
+radiobutton_1 = ttk.Radiobutton(master=frame_1, variable=radiobutton_var, value=1, text="Radiobutton")
+radiobutton_1.pack(pady=y_padding, padx=10)
+
+radiobutton_2 = ttk.Radiobutton(master=frame_1, variable=radiobutton_var, value=2, text="Radiobutton")
+radiobutton_2.pack(pady=y_padding, padx=10)
+
+app.mainloop()

+ 178 - 0
test/manual_integration_tests/test_all_widgets_with_colors.py

@@ -0,0 +1,178 @@
+import tkinter
+import customtkinter
+
+customtkinter.set_appearance_mode("System")  # Other: "Dark", "Light"
+
+
+class TestApp(customtkinter.CTk):
+    def __init__(self):
+        super().__init__()
+        self.geometry(f"{1400}x{700}")
+        self.title("CustomTkinter complete test")
+
+        self.create_widgets_on_tk()
+        self.create_widgets_on_ctk_frame()
+        self.create_widgets_on_ctk_frame_customized()
+        self.create_widgets_on_tk_frame_customized()
+
+    def change_appearance_mode(self, value):
+        """ gets called by self.slider_1 """
+
+        if value == 0:
+            self.label_1.configure(text="mode: Light")
+            customtkinter.set_appearance_mode("Light")
+        elif value == 1:
+            self.label_1.configure(text="mode: Dark")
+            customtkinter.set_appearance_mode("Dark")
+        else:
+            self.label_1.configure(text="mode: System")
+            customtkinter.set_appearance_mode("System")
+
+    def create_widgets_on_tk(self):
+        x, y = 150, 80
+
+        self.label_1 = customtkinter.CTkLabel(master=self, text="widgets_on_tk", fg_color="gray50")
+        self.label_1.place(x=x, y=y, anchor=tkinter.CENTER)
+
+        self.frame_1 = customtkinter.CTkFrame(master=self, width=200, height=60)
+        self.frame_1.place(x=x, y=y+80, anchor=tkinter.CENTER)
+
+        self.button_1 = customtkinter.CTkButton(master=self)
+        self.button_1.place(x=x, y=y + 160, anchor=tkinter.CENTER)
+
+        self.entry_1 = customtkinter.CTkEntry(master=self)
+        self.entry_1.place(x=x, y=y + 240, anchor=tkinter.CENTER)
+
+        self.progress_bar_1 = customtkinter.CTkProgressBar(master=self)
+        self.progress_bar_1.place(x=x, y=y + 320, anchor=tkinter.CENTER)
+
+        self.slider_1 = customtkinter.CTkSlider(master=self, command=self.change_appearance_mode, from_=0, to=2, number_of_steps=2)
+        self.slider_1.place(x=x, y=y + 400, anchor=tkinter.CENTER)
+
+        self.check_box_1 = customtkinter.CTkCheckBox(master=self)
+        self.check_box_1.place(x=x, y=y + 480, anchor=tkinter.CENTER)
+
+    def create_widgets_on_ctk_frame(self):
+        x, y = 450, 40
+
+        self.ctk_frame = customtkinter.CTkFrame(master=self, width=300, height=600)
+        self.ctk_frame.place(x=x, y=y, anchor=tkinter.N)
+
+        self.label_2 = customtkinter.CTkLabel(master=self.ctk_frame, text="create_widgets_on_ctk_frame", fg_color="gray50", width=200)
+        self.label_2.place(relx=0.5, y=y, anchor=tkinter.CENTER)
+
+        self.frame_2 = customtkinter.CTkFrame(master=self.ctk_frame, width=200, height=60)
+        self.frame_2.place(relx=0.5, y=y + 80, anchor=tkinter.CENTER)
+
+        self.button_2 = customtkinter.CTkButton(master=self.ctk_frame, border_width=3)
+        self.button_2.place(relx=0.5, y=y + 160, anchor=tkinter.CENTER)
+
+        self.entry_2 = customtkinter.CTkEntry(master=self.ctk_frame)
+        self.entry_2.place(relx=0.5, y=y + 240, anchor=tkinter.CENTER)
+
+        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.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)
+        self.check_box_2.place(relx=0.5, y=y + 480, anchor=tkinter.CENTER)
+
+    def change_frame_color(self, value):
+        """ gets called by self.slider_3 """
+
+        def rgb2hex(rgb_color: tuple) -> str:
+            return "#{:02x}{:02x}{:02x}".format(round(rgb_color[0]), round(rgb_color[1]), round(rgb_color[2]))
+
+        col_1 = rgb2hex((100, 50, value * 250))
+        col_2 = rgb2hex((20, value * 250, 50))
+
+        self.ctk_frame_customized.configure(fg_color=col_1)
+        self.tk_frame_customized.configure(bg=col_1)
+        self.configure(bg=col_2)
+        self.progress_bar_3.set(value)
+
+    def create_widgets_on_ctk_frame_customized(self):
+        x, y = 800, 40
+
+        self.ctk_frame_customized = customtkinter.CTkFrame(master=self, width=300, height=600)
+        self.ctk_frame_customized.place(x=x, y=y, anchor=tkinter.N)
+        self.ctk_frame_customized.configure(fg_color=("#F4F4FA", "#1E2742"))
+
+        self.label_3 = customtkinter.CTkLabel(master=self.ctk_frame_customized, text="customized", corner_radius=60,
+                                              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"))
+
+        self.frame_3 = customtkinter.CTkFrame(master=self.ctk_frame_customized, width=200, height=60)
+        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: 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="transparent")
+
+        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")
+        self.entry_3.focus_set()
+
+        self.progress_bar_3 = customtkinter.CTkProgressBar(master=self.ctk_frame_customized, height=16, fg_color=("#EBECF3", "#4B577E"))
+        self.progress_bar_3.place(relx=0.5, y=y + 320, anchor=tkinter.CENTER)
+        self.progress_bar_3.configure(progress_color="#8AE0C3", border_width=3, border_color=("gray60", "#4B577E"))
+
+        self.slider_3 = customtkinter.CTkSlider(master=self.ctk_frame_customized, command=self.change_frame_color, from_=0, to=10)
+        self.slider_3.place(relx=0.5, y=y + 400, anchor=tkinter.CENTER)
+        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, 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")
+
+    def create_widgets_on_tk_frame_customized(self):
+        x, y = 1150, 40
+
+        self.tk_frame_customized = tkinter.Frame(master=self, width=300, height=600, bg="darkred")
+        self.tk_frame_customized.place(x=x, y=y, anchor=tkinter.N)
+
+        self.label_4 = customtkinter.CTkLabel(master=self.tk_frame_customized, text="customized", corner_radius=6)
+        self.label_4.place(relx=0.5, y=y, anchor=tkinter.CENTER)
+        self.label_4.configure(fg_color=("#F4F4FA", "#333D5E"), text_color=("#373E57", "#7992C1"))
+
+        self.frame_4 = customtkinter.CTkFrame(master=self.tk_frame_customized, width=200, height=60)
+        self.frame_4.place(relx=0.5, y=y + 80, anchor=tkinter.CENTER)
+        self.frame_4.configure(fg_color=("#EBECF3", "#4B577E"))
+
+        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="transparent")
+
+        self.entry_4 = customtkinter.CTkEntry(master=self.tk_frame_customized)
+        self.entry_4.place(relx=0.5, y=y + 240, anchor=tkinter.CENTER)
+        self.entry_4.configure(fg_color=("gray60", "gray5"))
+        self.entry_4.insert(0, "1234567890")
+        self.entry_4.focus_set()
+
+        self.progress_bar_4 = customtkinter.CTkProgressBar(master=self.tk_frame_customized, height=16, fg_color=("#EBECF3", "#4B577E"))
+        self.progress_bar_4.place(relx=0.5, y=y + 320, anchor=tkinter.CENTER)
+        self.progress_bar_4.configure(progress_color="#8AE0C3", border_width=3, border_color=("gray60", "#4B577E"))
+
+        self.slider_4 = customtkinter.CTkSlider(master=self.tk_frame_customized, command=self.change_frame_color, from_=0, to=10)
+        self.slider_4.place(relx=0.5, y=y + 400, anchor=tkinter.CENTER)
+        self.slider_4.configure(button_color="#8AE0C3", fg_color=("#EBECF3", "#4B577E"), progress_color=("gray30", "gray10"))
+        self.slider_4.configure(from_=0, to=1)
+
+        self.check_box_4 = customtkinter.CTkCheckBox(master=self.tk_frame_customized)
+        self.check_box_4.place(relx=0.5, y=y + 480, anchor=tkinter.CENTER)
+        self.check_box_4.configure(border_color="#8AE0C3")
+
+
+if __name__ == "__main__":
+    test_app = TestApp()
+    test_app.mainloop()

+ 48 - 0
test/manual_integration_tests/test_button_antialiasing.py

@@ -0,0 +1,48 @@
+import customtkinter
+
+customtkinter.set_default_color_theme("blue")
+customtkinter.set_appearance_mode("dark")
+
+app = customtkinter.CTk()
+app.geometry("600x1000")
+
+app.grid_columnconfigure(0, weight=1)
+app.grid_columnconfigure(1, weight=1)
+app.grid_columnconfigure(2, weight=1)
+app.grid_columnconfigure(3, weight=1)
+
+f1 = customtkinter.CTkFrame(app, fg_color="gray10", corner_radius=0)
+f1.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
+f1.grid_columnconfigure(0, weight=1)
+
+f2 = customtkinter.CTkFrame(app, fg_color="gray10", corner_radius=0)
+f2.grid(row=0, column=1, rowspan=1, columnspan=1, sticky="nsew")
+f2.grid_columnconfigure(0, weight=1)
+
+f3 = customtkinter.CTkFrame(app, fg_color="gray85", corner_radius=0)
+f3.grid(row=0, column=2, rowspan=1, columnspan=1, sticky="nsew")
+f3.grid_columnconfigure(0, weight=1)
+
+f4 = customtkinter.CTkFrame(app, fg_color="gray90", corner_radius=0)
+f4.grid(row=0, column=3, rowspan=1, columnspan=1, sticky="nsew")
+f4.grid_columnconfigure(0, weight=1)
+
+for i in range(0, 16, 1):
+    b = customtkinter.CTkButton(f1, corner_radius=i, height=30, border_width=1, text=f"{i} {i-1}",
+                                border_color="white", fg_color=None, text_color="white")
+    # b = tkinter.Button(f1,  text=f"{i} {i-2}", width=20)
+    b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew")
+
+    b = customtkinter.CTkButton(f2, corner_radius=i, height=30, border_width=0, text=f"{i}",
+                                fg_color="#228da8")
+    b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew")
+
+    b = customtkinter.CTkButton(f3, corner_radius=i, height=30, border_width=1, text=f"{i} {i-1}",
+                                fg_color=None, border_color="gray20", text_color="black")
+    b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew")
+
+    b = customtkinter.CTkButton(f4, corner_radius=i, height=30, border_width=0, text=f"{i}",
+                                border_color="gray10", fg_color="#228da8")
+    b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew")
+
+app.mainloop()

+ 36 - 0
test/manual_integration_tests/test_configure_dimensions.py

@@ -0,0 +1,36 @@
+import customtkinter
+import random
+
+
+app = customtkinter.CTk()
+app.geometry("400x400")
+
+
+def button_callback():
+    button_1.configure(width=random.randint(30, 200), height=random.randint(30, 60))
+    frame_1.configure(width=random.randint(30, 200), height=random.randint(30, 200))
+    label_1.configure(width=random.randint(30, 200), height=random.randint(30, 40))
+    entry_1.configure(width=random.randint(30, 200), height=random.randint(30, 40))
+    progressbar_1.configure(width=random.randint(30, 200), height=random.randint(10, 16))
+    slider_1.configure(width=random.randint(30, 200), height=random.randint(14, 20))
+
+
+button_1 = customtkinter.CTkButton(app, text="button_1", command=button_callback)
+button_1.pack(pady=10)
+
+frame_1 = customtkinter.CTkFrame(app)
+frame_1.pack(pady=10)
+
+label_1 = customtkinter.CTkLabel(app, fg_color="green")
+label_1.pack(pady=10)
+
+entry_1 = customtkinter.CTkEntry(app, placeholder_text="placeholder")
+entry_1.pack(pady=10)
+
+progressbar_1 = customtkinter.CTkProgressBar(app)
+progressbar_1.pack(pady=10)
+
+slider_1 = customtkinter.CTkSlider(app)
+slider_1.pack(pady=10)
+
+app.mainloop()

+ 32 - 0
test/manual_integration_tests/test_ctk_behavior/test_ctk_appearance_mode_change.py

@@ -0,0 +1,32 @@
+import customtkinter
+import sys
+
+customtkinter.set_appearance_mode("dark")
+
+
+app = customtkinter.CTk()
+app.geometry("400x240")
+
+
+def change_appearance_mode():
+    # test appearance mode change while withdrawn
+    app.after(500, app.withdraw)
+    app.after(1500, lambda: customtkinter.set_appearance_mode("light"))
+    app.after(2500, app.deiconify)
+
+    # test appearance mode change while iconified
+    app.after(3500, app.iconify)
+    app.after(4500, lambda: customtkinter.set_appearance_mode("dark"))
+    app.after(5500, app.deiconify)
+
+    if sys.platform.startswith("win"):
+        # test appearance mode change while zoomed
+        app.after(6500, lambda: app.state("zoomed"))
+        app.after(7500, lambda: customtkinter.set_appearance_mode("light"))
+        app.after(8500, lambda: app.state("normal"))
+
+
+button_1 = customtkinter.CTkButton(app, text="start test", command=change_appearance_mode)
+button_1.pack(pady=20, padx=20)
+
+app.mainloop()

+ 9 - 0
test/manual_integration_tests/test_ctk_behavior/test_ctk_iconify_at_beginning.py

@@ -0,0 +1,9 @@
+import customtkinter
+
+app = customtkinter.CTk()
+app.geometry("400x240")
+
+app.iconify()
+app.after(2000, app.deiconify)
+
+app.mainloop()

+ 9 - 0
test/manual_integration_tests/test_ctk_behavior/test_ctk_withdraw_at_beginning.py

@@ -0,0 +1,9 @@
+import customtkinter
+
+app = customtkinter.CTk()
+app.geometry("400x240")
+
+app.withdraw()
+app.after(2000, app.deiconify)
+
+app.mainloop()

+ 27 - 0
test/manual_integration_tests/test_ctk_behavior/test_ctk_zoomed_state.py

@@ -0,0 +1,27 @@
+import customtkinter
+
+customtkinter.set_appearance_mode("dark")
+
+
+app = customtkinter.CTk()
+app.geometry("400x240")
+
+
+def change_appearance_mode():
+    # test zoom with withdraw
+    app.after(1000, lambda: app.state("zoomed"))
+    app.after(2000, app.withdraw)
+    app.after(3000, app.deiconify)
+    app.after(4000, lambda: app.state("normal"))
+
+    # test zoom with iconify
+    app.after(5000, lambda: app.state("zoomed"))
+    app.after(6000, app.iconify)
+    app.after(7000, app.deiconify)
+    app.after(8000, lambda: app.state("normal"))
+
+
+button_1 = customtkinter.CTkButton(app, text="start test", command=change_appearance_mode)
+button_1.pack(pady=20, padx=20)
+
+app.mainloop()

+ 51 - 0
test/manual_integration_tests/test_ctk_toplevel.py

@@ -0,0 +1,51 @@
+import customtkinter
+
+customtkinter.set_appearance_mode("dark")
+
+
+class ToplevelWindow(customtkinter.CTkToplevel):
+    def __init__(self, *args, closing_event=None, **kwargs):
+        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")
+        self.label.pack(padx=20, pady=20)
+
+        self.button_1 = customtkinter.CTkButton(self, text="set dark", command=lambda: customtkinter.set_appearance_mode("dark"))
+        self.button_1.pack(side="top", padx=40, pady=40)
+
+    def closing(self):
+        self.destroy()
+        if self.closing_event is not None:
+            self.closing_event()
+
+
+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)
+        self.button_2 = customtkinter.CTkButton(self, text="iconify toplevel", command=lambda: self.toplevel_window.iconify())
+        self.button_2.pack(side="top", padx=40, pady=40)
+        self.button_3 = customtkinter.CTkButton(self, text="set light", command=lambda: customtkinter.set_appearance_mode("light"))
+        self.button_3.pack(side="top", padx=40, pady=40)
+
+        self.toplevel_window = None
+
+    def open_toplevel(self):
+        if self.toplevel_window is None:  # create toplevel window only if not already open
+            self.toplevel_window = ToplevelWindow(self, closing_event=self.toplevel_close_event)
+
+    def toplevel_close_event(self):
+        self.toplevel_window = None
+
+
+if __name__ == "__main__":
+    app = App()
+    app.mainloop()

+ 35 - 0
test/manual_integration_tests/test_ctk_toplevel_behavior/test_ctk_toplevel_appearance_mode_change.py

@@ -0,0 +1,35 @@
+import customtkinter
+import sys
+
+customtkinter.set_appearance_mode("dark")
+
+
+app = customtkinter.CTk()
+app.geometry("400x400+300+300")
+
+toplevel = customtkinter.CTkToplevel(app)
+toplevel.geometry("350x240+800+300")
+
+
+def change_appearance_mode():
+    # test appearance mode change while withdrawn
+    app.after(500, toplevel.withdraw)
+    app.after(1500, lambda: customtkinter.set_appearance_mode("light"))
+    app.after(2500, toplevel.deiconify)
+
+    # test appearance mode change while iconified
+    app.after(3500, toplevel.iconify)
+    app.after(4500, lambda: customtkinter.set_appearance_mode("dark"))
+    app.after(5500, toplevel.deiconify)
+
+    if sys.platform.startswith("win"):
+        # test appearance mode change while zoomed
+        app.after(6500, lambda: toplevel.state("zoomed"))
+        app.after(7500, lambda: customtkinter.set_appearance_mode("light"))
+        app.after(8500, lambda: toplevel.state("normal"))
+
+
+button_1 = customtkinter.CTkButton(app, text="start test", command=change_appearance_mode)
+button_1.pack(pady=20, padx=20)
+
+app.mainloop()

+ 12 - 0
test/manual_integration_tests/test_ctk_toplevel_behavior/test_ctk_toplevel_iconify_at_beginning.py

@@ -0,0 +1,12 @@
+import customtkinter
+
+app = customtkinter.CTk()
+app.geometry("400x400+300+300")
+
+toplevel = customtkinter.CTkToplevel(app)
+toplevel.geometry("350x240+800+300")
+
+toplevel.iconify()
+toplevel.after(2000, toplevel.deiconify)
+
+app.mainloop()

+ 12 - 0
test/manual_integration_tests/test_ctk_toplevel_behavior/test_ctk_toplevel_withdraw_at_beginning.py

@@ -0,0 +1,12 @@
+import customtkinter
+
+app = customtkinter.CTk()
+app.geometry("400x400+300+300")
+
+toplevel = customtkinter.CTkToplevel(app)
+toplevel.geometry("350x240+800+300")
+
+toplevel.withdraw()
+toplevel.after(2000, toplevel.deiconify)
+
+app.mainloop()

+ 29 - 0
test/manual_integration_tests/test_ctk_toplevel_behavior/test_ctk_toplevel_zoomed_state.py

@@ -0,0 +1,29 @@
+import customtkinter
+
+customtkinter.set_appearance_mode("dark")
+
+app = customtkinter.CTk()
+app.geometry("400x400+300+300")
+
+toplevel = customtkinter.CTkToplevel(app)
+toplevel.geometry("350x240+800+300")
+
+
+def change_appearance_mode():
+    # test zoom with withdraw
+    app.after(1000, lambda: toplevel.state("zoomed"))
+    app.after(2000, toplevel.withdraw)
+    app.after(3000, toplevel.deiconify)
+    app.after(4000, lambda: toplevel.state("normal"))
+
+    # test zoom with iconify
+    app.after(5000, lambda: toplevel.state("zoomed"))
+    app.after(6000, toplevel.iconify)
+    app.after(7000, toplevel.deiconify)
+    app.after(8000, lambda: toplevel.state("normal"))
+
+
+button_1 = customtkinter.CTkButton(app, text="start test", command=change_appearance_mode)
+button_1.pack(pady=20, padx=20)
+
+app.mainloop()

+ 31 - 0
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()

+ 79 - 0
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()

+ 0 - 0
test/manual_integration_tests/test_images.py


Some files were not shown because too many files changed in this diff