Browse Source

ajout ctk et finctiob calcul qui marche

master
BARRAUX Arthur 2 years ago
parent
commit
781143c679
  1. 81
      customtkinter/__init__.py
  2. BIN
      customtkinter/__pycache__/__init__.cpython-310.pyc
  3. BIN
      customtkinter/assets/.DS_Store
  4. BIN
      customtkinter/assets/fonts/CustomTkinter_shapes_font.otf
  5. BIN
      customtkinter/assets/fonts/Roboto/Roboto-Medium.ttf
  6. BIN
      customtkinter/assets/fonts/Roboto/Roboto-Regular.ttf
  7. BIN
      customtkinter/assets/icons/.DS_Store
  8. BIN
      customtkinter/assets/icons/CustomTkinter_icon_Windows.ico
  9. 152
      customtkinter/assets/themes/blue.json
  10. 152
      customtkinter/assets/themes/dark-blue.json
  11. 152
      customtkinter/assets/themes/green.json
  12. 3
      customtkinter/windows/__init__.py
  13. BIN
      customtkinter/windows/__pycache__/__init__.cpython-310.pyc
  14. BIN
      customtkinter/windows/__pycache__/ctk_input_dialog.cpython-310.pyc
  15. BIN
      customtkinter/windows/__pycache__/ctk_tk.cpython-310.pyc
  16. BIN
      customtkinter/windows/__pycache__/ctk_toplevel.cpython-310.pyc
  17. 110
      customtkinter/windows/ctk_input_dialog.py
  18. 316
      customtkinter/windows/ctk_tk.py
  19. 299
      customtkinter/windows/ctk_toplevel.py
  20. 15
      customtkinter/windows/widgets/__init__.py
  21. BIN
      customtkinter/windows/widgets/__pycache__/__init__.cpython-310.pyc
  22. BIN
      customtkinter/windows/widgets/__pycache__/ctk_button.cpython-310.pyc
  23. BIN
      customtkinter/windows/widgets/__pycache__/ctk_checkbox.cpython-310.pyc
  24. BIN
      customtkinter/windows/widgets/__pycache__/ctk_combobox.cpython-310.pyc
  25. BIN
      customtkinter/windows/widgets/__pycache__/ctk_entry.cpython-310.pyc
  26. BIN
      customtkinter/windows/widgets/__pycache__/ctk_frame.cpython-310.pyc
  27. BIN
      customtkinter/windows/widgets/__pycache__/ctk_label.cpython-310.pyc
  28. BIN
      customtkinter/windows/widgets/__pycache__/ctk_optionmenu.cpython-310.pyc
  29. BIN
      customtkinter/windows/widgets/__pycache__/ctk_progressbar.cpython-310.pyc
  30. BIN
      customtkinter/windows/widgets/__pycache__/ctk_radiobutton.cpython-310.pyc
  31. BIN
      customtkinter/windows/widgets/__pycache__/ctk_scrollbar.cpython-310.pyc
  32. BIN
      customtkinter/windows/widgets/__pycache__/ctk_segmented_button.cpython-310.pyc
  33. BIN
      customtkinter/windows/widgets/__pycache__/ctk_slider.cpython-310.pyc
  34. BIN
      customtkinter/windows/widgets/__pycache__/ctk_switch.cpython-310.pyc
  35. BIN
      customtkinter/windows/widgets/__pycache__/ctk_tabview.cpython-310.pyc
  36. BIN
      customtkinter/windows/widgets/__pycache__/ctk_textbox.cpython-310.pyc
  37. 4
      customtkinter/windows/widgets/appearance_mode/__init__.py
  38. BIN
      customtkinter/windows/widgets/appearance_mode/__pycache__/__init__.cpython-310.pyc
  39. BIN
      customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_base_class.cpython-310.pyc
  40. BIN
      customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_tracker.cpython-310.pyc
  41. 61
      customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py
  42. 135
      customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py
  43. 12
      customtkinter/windows/widgets/core_rendering/__init__.py
  44. BIN
      customtkinter/windows/widgets/core_rendering/__pycache__/__init__.cpython-310.pyc
  45. BIN
      customtkinter/windows/widgets/core_rendering/__pycache__/ctk_canvas.cpython-310.pyc
  46. BIN
      customtkinter/windows/widgets/core_rendering/__pycache__/draw_engine.cpython-310.pyc
  47. 117
      customtkinter/windows/widgets/core_rendering/ctk_canvas.py
  48. 1235
      customtkinter/windows/widgets/core_rendering/draw_engine.py
  49. 2
      customtkinter/windows/widgets/core_widget_classes/__init__.py
  50. BIN
      customtkinter/windows/widgets/core_widget_classes/__pycache__/__init__.cpython-310.pyc
  51. BIN
      customtkinter/windows/widgets/core_widget_classes/__pycache__/ctk_base_class.cpython-310.pyc
  52. BIN
      customtkinter/windows/widgets/core_widget_classes/__pycache__/dropdown_menu.cpython-310.pyc
  53. 325
      customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py
  54. 198
      customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py
  55. 589
      customtkinter/windows/widgets/ctk_button.py
  56. 461
      customtkinter/windows/widgets/ctk_checkbox.py
  57. 424
      customtkinter/windows/widgets/ctk_combobox.py
  58. 384
      customtkinter/windows/widgets/ctk_entry.py
  59. 196
      customtkinter/windows/widgets/ctk_frame.py
  60. 272
      customtkinter/windows/widgets/ctk_label.py
  61. 422
      customtkinter/windows/widgets/ctk_optionmenu.py
  62. 312
      customtkinter/windows/widgets/ctk_progressbar.py
  63. 430
      customtkinter/windows/widgets/ctk_radiobutton.py
  64. 281
      customtkinter/windows/widgets/ctk_scrollbar.py
  65. 421
      customtkinter/windows/widgets/ctk_segmented_button.py
  66. 397
      customtkinter/windows/widgets/ctk_slider.py
  67. 475
      customtkinter/windows/widgets/ctk_switch.py
  68. 370
      customtkinter/windows/widgets/ctk_tabview.py
  69. 500
      customtkinter/windows/widgets/ctk_textbox.py
  70. 24
      customtkinter/windows/widgets/font/__init__.py
  71. BIN
      customtkinter/windows/widgets/font/__pycache__/__init__.cpython-310.pyc
  72. BIN
      customtkinter/windows/widgets/font/__pycache__/ctk_font.cpython-310.pyc
  73. BIN
      customtkinter/windows/widgets/font/__pycache__/font_manager.cpython-310.pyc
  74. 91
      customtkinter/windows/widgets/font/ctk_font.py
  75. 66
      customtkinter/windows/widgets/font/font_manager.py
  76. 1
      customtkinter/windows/widgets/image/__init__.py
  77. BIN
      customtkinter/windows/widgets/image/__pycache__/__init__.cpython-310.pyc
  78. BIN
      customtkinter/windows/widgets/image/__pycache__/ctk_image.cpython-310.pyc
  79. 122
      customtkinter/windows/widgets/image/ctk_image.py
  80. 7
      customtkinter/windows/widgets/scaling/__init__.py
  81. BIN
      customtkinter/windows/widgets/scaling/__pycache__/__init__.cpython-310.pyc
  82. BIN
      customtkinter/windows/widgets/scaling/__pycache__/scaling_base_class.cpython-310.pyc
  83. BIN
      customtkinter/windows/widgets/scaling/__pycache__/scaling_tracker.cpython-310.pyc
  84. 159
      customtkinter/windows/widgets/scaling/scaling_base_class.py
  85. 206
      customtkinter/windows/widgets/scaling/scaling_tracker.py
  86. 9
      customtkinter/windows/widgets/theme/__init__.py
  87. BIN
      customtkinter/windows/widgets/theme/__pycache__/__init__.cpython-310.pyc
  88. BIN
      customtkinter/windows/widgets/theme/__pycache__/theme_manager.cpython-310.pyc
  89. 47
      customtkinter/windows/widgets/theme/theme_manager.py
  90. 1
      customtkinter/windows/widgets/utility/__init__.py
  91. BIN
      customtkinter/windows/widgets/utility/__pycache__/__init__.cpython-310.pyc
  92. BIN
      customtkinter/windows/widgets/utility/__pycache__/utility_functions.cpython-310.pyc
  93. 22
      customtkinter/windows/widgets/utility/utility_functions.py
  94. 134
      main.py

81
customtkinter/__init__.py

@ -0,0 +1,81 @@
__version__ = "5.0.5"
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
# 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
_ = 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

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

Binary file not shown.

BIN
customtkinter/assets/.DS_Store

Binary file not shown.

BIN
customtkinter/assets/fonts/CustomTkinter_shapes_font.otf

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
customtkinter/assets/icons/.DS_Store

Binary file not shown.

BIN
customtkinter/assets/icons/CustomTkinter_icon_Windows.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

152
customtkinter/assets/themes/blue.json

@ -0,0 +1,152 @@
{
"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"]
},
"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"
}
}
}

152
customtkinter/assets/themes/dark-blue.json

@ -0,0 +1,152 @@
{
"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"]
},
"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"
}
}
}

152
customtkinter/assets/themes/green.json

@ -0,0 +1,152 @@
{
"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"]
},
"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
customtkinter/windows/__init__.py

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

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

Binary file not shown.

BIN
customtkinter/windows/__pycache__/ctk_input_dialog.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/__pycache__/ctk_tk.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/__pycache__/ctk_toplevel.cpython-310.pyc

Binary file not shown.

110
customtkinter/windows/ctk_input_dialog.py

@ -0,0 +1,110 @@
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
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",
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._text = text
self.title(title)
self.lift() # lift window on top
self.attributes("-topmost", True) # stay on top
self.protocol("WM_DELETE_WINDOW", self._on_closing)
self.after(10, self._create_widgets) # create widgets with slight delay, to avoid white flickering of background
self.resizable(False, False)
self.grab_set() # make other windows not clickable
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,)
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)
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',
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',
command=self._ok_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

316
customtkinter/windows/ctk_tk.py

@ -0,0 +1,316 @@
import tkinter
from distutils.version import StrictVersion as 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 CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
"""
Main app window with dark titlebar on Windows and macOS.
For detailed information check out the documentation.
"""
_valid_tk_constructor_arguments: set = {"screenName", "baseName", "className", "useTk", "sync", "use"}
_valid_tk_configure_arguments: set = {'bd', 'borderwidth', 'class', 'menu', 'relief', 'screen',
'use', 'container', 'cursor', 'height',
'highlightthickness', 'padx', 'pady', 'takefocus', 'visual', 'width'}
_deactivate_macos_window_header_manipulation: bool = False
_deactivate_windows_window_header_manipulation: bool = False
def __init__(self,
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
**kwargs):
self._enable_macos_dark_title_bar()
# call init methods of super classes
tkinter.Tk.__init__(self, **pop_from_dict_by_set(kwargs, self._valid_tk_constructor_arguments))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="window")
check_kwargs_empty(kwargs, raise_error=True)
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
# 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:
self._window_exists = True
if sys.platform.startswith("win"):
if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists:
# print("window dont exists -> deiconify in update")
self.deiconify()
super().update()
def mainloop(self, *args, **kwargs):
if not self._window_exists:
self._window_exists = True
if 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 mainloop")
self.deiconify()
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 _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(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
# This command allows dark-mode for all programs
@classmethod
def _disable_macos_dark_title_bar(cls):
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
# This command reverts the dark-mode setting for all programs.
def _windows_set_titlebar_color(self, color_mode: str):
"""
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":
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")
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:
# 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
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))

299
customtkinter/windows/ctk_toplevel.py

@ -0,0 +1,299 @@
import tkinter
from distutils.version import StrictVersion as 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 = {"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
# 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(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
@classmethod
def _disable_macos_dark_title_bar(cls):
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
# This command reverts the dark-mode setting for all programs.
def _windows_set_titlebar_color(self, color_mode: str):
"""
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()
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)
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))

15
customtkinter/windows/widgets/__init__.py

@ -0,0 +1,15 @@
from .ctk_button import CTkButton
from .ctk_checkbox import CTkCheckBox
from .ctk_combobox import CTkComboBox
from .ctk_entry import CTkEntry
from .ctk_frame import CTkFrame
from .ctk_label import CTkLabel
from .ctk_optionmenu import CTkOptionMenu
from .ctk_progressbar import CTkProgressBar
from .ctk_radiobutton import CTkRadioButton
from .ctk_scrollbar import CTkScrollbar
from .ctk_segmented_button import CTkSegmentedButton
from .ctk_slider import CTkSlider
from .ctk_switch import CTkSwitch
from .ctk_tabview import CTkTabview
from .ctk_textbox import CTkTextbox

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

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_button.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_checkbox.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_combobox.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_entry.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_frame.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_label.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_optionmenu.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_progressbar.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_radiobutton.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_scrollbar.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_segmented_button.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_slider.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_switch.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_tabview.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/__pycache__/ctk_textbox.cpython-310.pyc

Binary file not shown.

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

BIN
customtkinter/windows/widgets/appearance_mode/__pycache__/__init__.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_base_class.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/appearance_mode/__pycache__/appearance_mode_tracker.cpython-310.pyc

Binary file not shown.

61
customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py

@ -0,0 +1,61 @@
from typing import Union, Tuple, List
from .appearance_mode_tracker import AppearanceModeTracker
class CTkAppearanceModeBaseClass:
"""
Super-class that manages the appearance mode. Methods:
- destroy() must be called when sub-class is destroyed
- _set_appearance_mode() abstractmethod, gets called when appearance mode changes, must be overridden
- _apply_appearance_mode()
"""
def __init__(self):
AppearanceModeTracker.add(self._set_appearance_mode, self)
self.__appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
def destroy(self):
AppearanceModeTracker.remove(self._set_appearance_mode)
def _set_appearance_mode(self, mode_string: str):
""" can be overridden but super method must be called at the beginning """
if mode_string.lower() == "dark":
self.__appearance_mode = 1
elif mode_string.lower() == "light":
self.__appearance_mode = 0
def _get_appearance_mode(self) -> str:
""" get appearance mode as a string, 'light' or 'dark' """
if self.__appearance_mode == 0:
return "light"
else:
return "dark"
def _apply_appearance_mode(self, color: Union[str, Tuple[str, str], List[str]]) -> str:
"""
color can be either a single hex color string or a color name or it can be a
tuple color with (light_color, dark_color). The functions returns
always a single color string
"""
if isinstance(color, (tuple, list)):
return color[self.__appearance_mode]
else:
return color
@staticmethod
def _check_color_type(color: any, transparency: bool = False):
if color is None:
raise ValueError(f"color is None, for transparency set color='transparent'")
elif isinstance(color, (tuple, list)) and (color[0] == "transparent" or color[1] == "transparent"):
raise ValueError(f"transparency is not allowed in tuple color {color}, use 'transparent'")
elif color == "transparent" and transparency is False:
raise ValueError(f"transparency is not allowed for this attribute")
elif isinstance(color, str):
return color
elif isinstance(color, (tuple, list)) and len(color) == 2 and isinstance(color[0], str) and isinstance(color[1], str):
return color
else:
raise ValueError(f"color {color} must be string ('transparent' or 'color-name' or 'hex-color') or tuple of two strings, not {type(color)}")

135
customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py

@ -0,0 +1,135 @@
import sys
import tkinter
from distutils.version import StrictVersion as Version
from typing import Callable
try:
import darkdetect
if Version(darkdetect.__version__) < Version("0.3.1"):
sys.stderr.write("WARNING: You have to upgrade the darkdetect library: pip3 install --upgrade darkdetect\n")
if sys.platform != "darwin":
exit()
except ImportError as err:
raise err
except Exception:
sys.stderr.write("customtkinter.appearance_mode_tracker warning: failed to 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 1 # 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
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"

BIN
customtkinter/windows/widgets/core_rendering/__pycache__/__init__.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/core_rendering/__pycache__/ctk_canvas.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/core_rendering/__pycache__/draw_engine.cpython-310.pyc

Binary file not shown.

117
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)

1235
customtkinter/windows/widgets/core_rendering/draw_engine.py

File diff suppressed because it is too large

2
customtkinter/windows/widgets/core_widget_classes/__init__.py

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

BIN
customtkinter/windows/widgets/core_widget_classes/__pycache__/__init__.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/core_widget_classes/__pycache__/ctk_base_class.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/core_widget_classes/__pycache__/dropdown_menu.cpython-310.pyc

Binary file not shown.

325
customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py

@ -0,0 +1,325 @@
import sys
import warnings
import tkinter
import tkinter.ttk as ttk
from typing import Union, Callable, Tuple
try:
from typing import TypedDict
except ImportError:
from typing_extensions import TypedDict
# removed due to circular import
# from ...ctk_tk import CTk
# from ...ctk_toplevel import CTkToplevel
from .... import windows # import windows for isinstance checks
from ..theme import ThemeManager
from ..font import CTkFont
from ..image import CTkImage
from ..appearance_mode import CTkAppearanceModeBaseClass
from ..scaling import CTkScalingBaseClass
from ..utility import pop_from_dict_by_set, check_kwargs_empty
class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
""" Base class of every CTk widget, handles the dimensions, bg_color,
appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """
# attributes that are passed to and managed by the tkinter frame only:
_valid_tk_frame_attributes: set = {"cursor"}
_cursor_manipulation_enabled: bool = True
def __init__(self,
master: any,
width: int = 0,
height: int = 0,
bg_color: Union[str, Tuple[str, str]] = "transparent",
**kwargs):
# call init methods of super classes
tkinter.Frame.__init__(self, master=master, width=width, height=height, **pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="widget")
# check if kwargs is empty, if not raise error for unsupported arguments
check_kwargs_empty(kwargs, raise_error=True)
# dimensions independent of scaling
self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget
self._current_height = height # _current_width and _current_height are independent of the scale
self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height
self._desired_height = height
# set width and height of tkinter.Frame
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
# save latest geometry function and kwargs
class GeometryCallDict(TypedDict):
function: Callable
kwargs: dict
self._last_geometry_manager_call: Union[GeometryCallDict, None] = None
# background color
self._bg_color: Union[str, Tuple[str, str]] = self._detect_color_of_master() if bg_color == "transparent" else self._check_color_type(bg_color, transparency=True)
# set bg color of tkinter.Frame
super().configure(bg=self._apply_appearance_mode(self._bg_color))
# add configure callback to tkinter.Frame
super().bind('<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):
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)}. " +
f"Image can not be scaled on HighDPI displays, use CTkImage instead.\n")
return image
def _update_dimensions_event(self, event):
# only redraw if dimensions changed (for performance), independent of scaling
if round(self._current_width) != round(self._reverse_widget_scaling(event.width)) or round(self._current_height) != round(self._reverse_widget_scaling(event.height)):
self._current_width = self._reverse_widget_scaling(event.width) # adjust current size according to new size given by event
self._current_height = self._reverse_widget_scaling(event.height) # _current_width and _current_height are independent of the scale
self._draw(no_color_updates=True) # faster drawing without color changes
def _detect_color_of_master(self, master_widget=None) -> Union[str, Tuple[str, str]]:
""" detect foreground color of master widget for bg_color and transparent color """
if master_widget is None:
master_widget = self.master
if isinstance(master_widget, (windows.widgets.core_widget_classes.CTkBaseClass, windows.CTk, windows.CTkToplevel)):
if master_widget.cget("fg_color") is not None and master_widget.cget("fg_color") != "transparent":
return master_widget.cget("fg_color")
# if fg_color of master is None, try to retrieve fg_color from master of master
elif hasattr(master_widget.master, "master"):
return self._detect_color_of_master(master_widget.master)
elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget
try:
ttk_style = ttk.Style()
return ttk_style.lookup(master_widget.winfo_class(), 'background')
except Exception:
return "#FFFFFF", "#000000"
else: # master is normal tkinter widget
try:
return master_widget.cget("bg") # try to get bg color by .cget() method
except Exception:
return "#FFFFFF", "#000000"
def _set_appearance_mode(self, mode_string):
super()._set_appearance_mode(mode_string)
self._draw()
super().update_idletasks()
def _set_scaling(self, new_widget_scaling, new_window_scaling):
super()._set_scaling(new_widget_scaling, new_window_scaling)
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
if self._last_geometry_manager_call is not None:
self._last_geometry_manager_call["function"](**self._apply_argument_scaling(self._last_geometry_manager_call["kwargs"]))
def _set_dimensions(self, width=None, height=None):
if width is not None:
self._desired_width = width
if height is not None:
self._desired_height = height
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
def 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
"""
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
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()

589
customtkinter/windows/widgets/ctk_button.py

@ -0,0 +1,589 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
from .image import CTkImage
class CTkButton(CTkBaseClass):
"""
Button with rounded corners, border, hover effect, image support, click command and textvariable.
For detailed information check out the documentation.
"""
_image_label_spacing: int = 6
def __init__(self,
master: any,
width: int = 140,
height: int = 28,
corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
border_spacing: int = 2,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
hover_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None,
round_width_to_even_numbers: bool = True,
round_height_to_even_numbers: bool = True,
text: str = "CTkButton",
font: Optional[Union[tuple, CTkFont]] = None,
textvariable: Union[tkinter.Variable, None] = None,
image: Union[CTkImage, None] = None,
state: str = "normal",
hover: bool = True,
command: Union[Callable[[], None], None] = None,
compound: str = "left",
anchor: str = "center",
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# shape
self._corner_radius: int = ThemeManager.theme["CTkButton"]["corner_radius"] if corner_radius is None else corner_radius
self._corner_radius = min(self._corner_radius, round(self._current_height / 2))
self._border_width: int = ThemeManager.theme["CTkButton"]["border_width"] if border_width is None else border_width
self._border_spacing: int = border_spacing
# color
self._fg_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
self._hover_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
self._border_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._text_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
# rendering options
self._background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = background_corner_colors # rendering options for DrawEngine
self._round_width_to_even_numbers: bool = round_width_to_even_numbers # rendering options for DrawEngine
self._round_height_to_even_numbers: bool = round_height_to_even_numbers # rendering options for DrawEngine
# text, font
self._text = text
self._text_label: Union[tkinter.Label, None] = None
self._textvariable: tkinter.Variable = textvariable
self._font: Union[tuple, CTkFont] = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# image
self._image = self._check_image_type(image)
self._image_label: Union[tkinter.Label, None] = None
if isinstance(self._image, CTkImage):
self._image.add_configure_callback(self._update_image)
# other
self._state: str = state
self._hover: bool = hover
self._command: Callable = command
self._compound: str = compound
self._anchor: str = anchor
self._click_animation_running: bool = False
# canvas and draw engine
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew")
self._draw_engine = DrawEngine(self._canvas)
self._draw_engine.set_round_to_even_numbers(self._round_width_to_even_numbers, self._round_height_to_even_numbers) # rendering options
# 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:
self._image_label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(),
self._get_appearance_mode()))
def destroy(self):
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
if self._background_corner_colors is not None:
self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height))
self._canvas.itemconfig("background_corner_top_left", fill=self._apply_appearance_mode(self._background_corner_colors[0]))
self._canvas.itemconfig("background_corner_top_right", fill=self._apply_appearance_mode(self._background_corner_colors[1]))
self._canvas.itemconfig("background_corner_bottom_right", fill=self._apply_appearance_mode(self._background_corner_colors[2]))
self._canvas.itemconfig("background_corner_bottom_left", fill=self._apply_appearance_mode(self._background_corner_colors[3]))
else:
self._canvas.delete("background_parts")
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width))
if no_color_updates is False or requires_recoloring:
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
# set color for the button border parts (outline)
self._canvas.itemconfig("border_parts",
outline=self._apply_appearance_mode(self._border_color),
fill=self._apply_appearance_mode(self._border_color))
# set color for inner button parts
if self._fg_color == "transparent":
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(self._bg_color),
fill=self._apply_appearance_mode(self._bg_color))
else:
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(self._fg_color),
fill=self._apply_appearance_mode(self._fg_color))
# create text label if text given
if self._text is not None and self._text != "":
if self._text_label is None:
self._text_label = tkinter.Label(master=self,
font=self._apply_font_scaling(self._font),
text=self._text,
padx=0,
pady=0,
borderwidth=1,
textvariable=self._textvariable)
self._create_grid()
self._text_label.bind("<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")
if "compound" in kwargs:
self._compound = kwargs.pop("compound")
require_redraw = True
if "anchor" in kwargs:
self._anchor = kwargs.pop("anchor")
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "border_spacing":
return self._border_spacing
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "hover_color":
return self._hover_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text_color_disabled":
return self._text_color_disabled
elif attribute_name == "background_corner_colors":
return self._background_corner_colors
elif attribute_name == "text":
return self._text
elif attribute_name == "font":
return self._font
elif attribute_name == "textvariable":
return self._textvariable
elif attribute_name == "image":
return self._image
elif attribute_name == "state":
return self._state
elif attribute_name == "hover":
return self._hover
elif attribute_name == "command":
return self._command
elif attribute_name == "compound":
return self._compound
elif attribute_name == "anchor":
return self._anchor
else:
return super().cget(attribute_name)
def _set_cursor(self):
if self._cursor_manipulation_enabled:
if self._state == tkinter.DISABLED:
if sys.platform == "darwin" and self._command is not None:
self.configure(cursor="arrow")
elif sys.platform.startswith("win") and self._command is not None:
self.configure(cursor="arrow")
elif self._state == tkinter.NORMAL:
if sys.platform == "darwin" and self._command is not None:
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and self._command is not None:
self.configure(cursor="hand2")
def _on_enter(self, event=None):
if self._hover is True and self._state == "normal":
if self._hover_color is None:
inner_parts_color = self._fg_color
else:
inner_parts_color = self._hover_color
# set color of inner button parts to hover color
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(inner_parts_color),
fill=self._apply_appearance_mode(inner_parts_color))
# set text_label bg color to button hover color
if self._text_label is not None:
self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
# set image_label bg color to button hover color
if self._image_label is not None:
self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
def _on_leave(self, event=None):
self._click_animation_running = False
if self._fg_color == "transparent":
inner_parts_color = self._bg_color
else:
inner_parts_color = self._fg_color
# set color of inner button parts
self._canvas.itemconfig("inner_parts",
outline=self._apply_appearance_mode(inner_parts_color),
fill=self._apply_appearance_mode(inner_parts_color))
# set text_label bg color (label color)
if self._text_label is not None:
self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
# set image_label bg color (image bg color)
if self._image_label is not None:
self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
def _click_animation(self):
if self._click_animation_running:
self._on_enter()
def _clicked(self, event=None):
if self._state != tkinter.DISABLED:
# click animation: change color with .on_leave() and back to normal after 100ms with click_animation()
self._on_leave()
self._click_animation_running = True
self.after(100, self._click_animation)
if self._command is not None:
self._command()
def invoke(self):
""" calls command function if button is not disabled """
if self._state != tkinter.DISABLED:
if self._command is not None:
return self._command()
def bind(self, sequence: str = None, command: Callable = None, add: 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()

461
customtkinter/windows/widgets/ctk_checkbox.py

@ -0,0 +1,461 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
class CTkCheckBox(CTkBaseClass):
"""
Checkbox with rounded corners, border, variable support and hover effect.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 100,
height: int = 24,
checkbox_width: int = 24,
checkbox_height: int = 24,
corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
hover_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Optional[Union[str, Tuple[str, str]]] = None,
checkmark_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
text: str = "CTkCheckBox",
font: Optional[Union[tuple, CTkFont]] = None,
textvariable: Union[tkinter.Variable, None] = None,
state: str = tkinter.NORMAL,
hover: bool = True,
command: Union[Callable[[], None], None] = None,
onvalue: Union[int, str] = 1,
offvalue: Union[int, str] = 0,
variable: Union[tkinter.Variable, None] = None,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# dimensions
self._checkbox_width = checkbox_width
self._checkbox_height = checkbox_height
# color
self._fg_color = ThemeManager.theme["CTkCheckbox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._hover_color = ThemeManager.theme["CTkCheckbox"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
self._border_color = ThemeManager.theme["CTkCheckbox"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._checkmark_color = ThemeManager.theme["CTkCheckbox"]["checkmark_color"] if checkmark_color is None else self._check_color_type(checkmark_color)
# shape
self._corner_radius = ThemeManager.theme["CTkCheckbox"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkCheckbox"]["border_width"] if border_width is None else border_width
# text
self._text = text
self._text_label: Union[tkinter.Label, None] = None
self._text_color = ThemeManager.theme["CTkCheckbox"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkCheckbox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# callback and hover functionality
self._command = command
self._state = state
self._hover = hover
self._check_state = False
self._onvalue = onvalue
self._offvalue = offvalue
self._variable: tkinter.Variable = variable
self._variable_callback_blocked = False
self._textvariable: tkinter.Variable = textvariable
self._variable_callback_name = None
# configure grid system (1x3)
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=1)
self.grid_rowconfigure(0, weight=1)
self._bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._checkbox_width),
height=self._apply_widget_scaling(self._checkbox_height))
self._canvas.grid(row=0, column=0, sticky="e")
self._draw_engine = DrawEngine(self._canvas)
self._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 "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "command" in kwargs:
self._command = kwargs.pop("command")
if "textvariable" in kwargs:
self._textvariable = kwargs.pop("textvariable")
self._text_label.configure(textvariable=self._textvariable)
if "variable" in kwargs:
if self._variable is not None and self._variable != "":
self._variable.trace_remove("write", self._variable_callback_name) # remove old variable callback
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._onvalue else False
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "checkbox_width":
return self._checkbox_width
elif attribute_name == "checkbox_height":
return self._checkbox_height
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "hover_color":
return self._hover_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "checkmark_color":
return self._checkmark_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text_color_disabled":
return self._text_color_disabled
elif attribute_name == "text":
return self._text
elif attribute_name == "font":
return self._font
elif attribute_name == "textvariable":
return self._textvariable
elif attribute_name == "state":
return self._state
elif attribute_name == "hover":
return self._hover
elif attribute_name == "onvalue":
return self._onvalue
elif attribute_name == "offvalue":
return self._offvalue
elif attribute_name == "variable":
return self._variable
else:
return super().cget(attribute_name)
def _set_cursor(self):
if self._cursor_manipulation_enabled:
if self._state == tkinter.DISABLED:
if sys.platform == "darwin":
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif sys.platform.startswith("win"):
self._canvas.configure(cursor="arrow")
if self._text_label is not None:
self._text_label.configure(cursor="arrow")
elif self._state == tkinter.NORMAL:
if sys.platform == "darwin":
self._canvas.configure(cursor="pointinghand")
if self._text_label is not None:
self._text_label.configure(cursor="pointinghand")
elif sys.platform.startswith("win"):
self._canvas.configure(cursor="hand2")
if self._text_label is not None:
self._text_label.configure(cursor="hand2")
def _on_enter(self, event=0):
if self._hover is True and self._state == tkinter.NORMAL:
if self._check_state is True:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._hover_color),
outline=self._apply_appearance_mode(self._hover_color))
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._hover_color),
outline=self._apply_appearance_mode(self._hover_color))
else:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._hover_color),
outline=self._apply_appearance_mode(self._hover_color))
def _on_leave(self, event=0):
if self._check_state is True:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
else:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=self._apply_appearance_mode(self._bg_color))
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._border_color),
outline=self._apply_appearance_mode(self._border_color))
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
if self._variable.get() == self._onvalue:
self.select(from_variable_callback=True)
elif self._variable.get() == self._offvalue:
self.deselect(from_variable_callback=True)
def toggle(self, event=0):
if self._state == tkinter.NORMAL:
if self._check_state is True:
self._check_state = False
self._draw()
else:
self._check_state = True
self._draw()
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
self._variable_callback_blocked = False
if self._command is not None:
self._command()
def select(self, from_variable_callback=False):
self._check_state = True
self._draw()
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._onvalue)
self._variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
self._check_state = False
self._draw()
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._offvalue)
self._variable_callback_blocked = False
def get(self) -> Union[int, str]:
return self._onvalue if self._check_state is True else self._offvalue
def bind(self, sequence: 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
customtkinter/windows/widgets/ctk_combobox.py

@ -0,0 +1,424 @@
import tkinter
import sys
import copy
from typing import Union, Tuple, Callable, List, Optional
from .core_widget_classes import DropdownMenu
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
class CTkComboBox(CTkBaseClass):
"""
Combobox with dropdown menu, rounded corners, border, variable support.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 140,
height: int = 28,
corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Optional[Union[str, Tuple[str, str]]] = None,
button_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
font: Optional[Union[tuple, CTkFont]] = None,
dropdown_font: Optional[Union[tuple, CTkFont]] = None,
values: Optional[List[str]] = None,
state: str = tkinter.NORMAL,
hover: bool = True,
variable: Union[tkinter.Variable, None] = None,
command: Union[Callable[[str], None], None] = None,
justify: str = "left",
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# shape
self._corner_radius = ThemeManager.theme["CTkComboBox"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkComboBox"]["border_width"] if border_width is None else border_width
# color
self._fg_color = ThemeManager.theme["CTkComboBox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._border_color = ThemeManager.theme["CTkComboBox"]["border_color"] if border_color is None else self._check_color_type(border_color)
self._button_color = ThemeManager.theme["CTkComboBox"]["button_color"] if button_color is None else self._check_color_type(button_color)
self._button_hover_color = ThemeManager.theme["CTkComboBox"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
self._text_color = ThemeManager.theme["CTkComboBox"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkComboBox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# callback and hover functionality
self._command = command
self._variable = variable
self._state = state
self._hover = hover
if values is None:
self._values = ["CTkComboBox"]
else:
self._values = values
self._dropdown_menu = DropdownMenu(master=self,
values=self._values,
command=self._dropdown_callback,
fg_color=dropdown_fg_color,
hover_color=dropdown_hover_color,
text_color=dropdown_text_color,
font=dropdown_font)
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self.draw_engine = DrawEngine(self._canvas)
self._entry = tkinter.Entry(master=self,
state=self._state,
width=1,
bd=0,
justify=justify,
highlightthickness=0,
font=self._apply_font_scaling(self._font))
self._create_grid()
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
customtkinter/windows/widgets/ctk_entry.py

@ -0,0 +1,384 @@
import tkinter
from typing import Union, Tuple, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
from .utility import pop_from_dict_by_set, check_kwargs_empty
class CTkEntry(CTkBaseClass):
"""
Entry with rounded corners, border, textvariable support, focus and placeholder.
For detailed information check out the documentation.
"""
_minimum_x_padding = 6 # minimum padding between tkinter entry and frame border
# attributes that are passed to and managed by the tkinter entry only:
_valid_tk_entry_attributes = {"exportselection", "insertborderwidth", "insertofftime",
"insertontime", "insertwidth", "justify", "selectborderwidth",
"show", "takefocus", "validate", "validatecommand", "xscrollcommand"}
def __init__(self,
master: any,
width: int = 140,
height: int = 28,
corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
placeholder_text_color: Optional[Union[str, Tuple[str, str]]] = None,
textvariable: Union[tkinter.Variable, None] = None,
placeholder_text: Union[str, None] = None,
font: Optional[Union[tuple, CTkFont]] = None,
state: str = tkinter.NORMAL,
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height)
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
# color
self._fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
self._text_color = ThemeManager.theme["CTkEntry"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._placeholder_text_color = ThemeManager.theme["CTkEntry"]["placeholder_text_color"] if placeholder_text_color is None else self._check_color_type(placeholder_text_color)
self._border_color = ThemeManager.theme["CTkEntry"]["border_color"] if border_color is None else self._check_color_type(border_color)
# shape
self._corner_radius = ThemeManager.theme["CTkEntry"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkEntry"]["border_width"] if border_width is None else border_width
# text and state
self._is_focused: bool = True
self._placeholder_text = placeholder_text
self._placeholder_text_active = False
self._pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back
self._textvariable = textvariable
self._state = state
self._textvariable_callback_name: str = ""
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
if not (self._textvariable is None or self._textvariable == ""):
self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._draw_engine = DrawEngine(self._canvas)
self._entry = tkinter.Entry(master=self,
bd=0,
width=1,
highlightthickness=0,
font=self._apply_font_scaling(self._font),
state=self._state,
textvariable=self._textvariable,
**pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes))
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()
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
self._entry.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._canvas.grid_forget()
self._canvas.grid(column=0, row=0, sticky="nswe")
def destroy(self):
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width))
if requires_recoloring or no_color_updates is False:
if self._apply_appearance_mode(self._fg_color) == "transparent":
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=self._apply_appearance_mode(self._bg_color))
self._entry.configure(bg=self._apply_appearance_mode(self._bg_color),
disabledbackground=self._apply_appearance_mode(self._bg_color),
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):
return self._entry.focus()
def focus_set(self):
return self._entry.focus_set()
def focus_force(self):
return self._entry.focus_force()
def index(self, index):
return self._entry.index(index)
def icursor(self, index):
return self._entry.icursor(index)
def select_adjust(self, index):
return self._entry.select_adjust(index)
def select_from(self, index):
return self._entry.icursor(index)
def select_clear(self):
return self._entry.select_clear()
def select_present(self):
return self._entry.select_present()
def select_range(self, start_index, end_index):
return self._entry.select_range(start_index, end_index)
def select_to(self, index):
return self._entry.select_to(index)
def xview(self, index):
return self._entry.xview(index)
def xview_moveto(self, f):
return self._entry.xview_moveto(f)
def xview_scroll(self, number, what):
return self._entry.xview_scroll(number, what)

196
customtkinter/windows/widgets/ctk_frame.py

@ -0,0 +1,196 @@
from typing import Union, Tuple, List, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
class CTkFrame(CTkBaseClass):
"""
Frame with rounded corners and border.
Default foreground colors are set according to theme.
To make the frame transparent set fg_color=None.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 200,
height: int = 200,
corner_radius: Optional[Union[int, str]] = None,
border_width: Optional[Union[int, str]] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Optional[Union[str, Tuple[str, str]]] = None,
background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None,
overwrite_preferred_drawing_method: Union[str, None] = None,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self._border_color = ThemeManager.theme["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color)
# determine fg_color of frame
if fg_color is None:
if isinstance(self.master, CTkFrame):
if self.master._fg_color == ThemeManager.theme["CTkFrame"]["fg_color"]:
self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"]
else:
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
else:
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
else:
self._fg_color = self._check_color_type(fg_color, transparency=True)
self._background_corner_colors = background_corner_colors # rendering options for DrawEngine
# shape
self._corner_radius = ThemeManager.theme["CTkFrame"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkFrame"]["border_width"] if border_width is None else border_width
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._current_width),
height=self._apply_widget_scaling(self._current_height))
self._canvas.place(x=0, y=0, relwidth=1, relheight=1)
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
self._draw_engine = DrawEngine(self._canvas)
self._overwrite_preferred_drawing_method = overwrite_preferred_drawing_method
self._draw(no_color_updates=True)
def winfo_children(self) -> List[any]:
"""
winfo_children of CTkFrame without self.canvas widget,
because it's not a child but part of the CTkFrame itself
"""
child_widgets = super().winfo_children()
try:
child_widgets.remove(self._canvas)
return child_widgets
except ValueError:
return child_widgets
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw()
def _set_dimensions(self, width=None, height=None):
super()._set_dimensions(width, height)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw()
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
if not self._canvas.winfo_exists():
return
if self._background_corner_colors is not None:
self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height))
self._canvas.itemconfig("background_corner_top_left", fill=self._apply_appearance_mode(self._background_corner_colors[0]))
self._canvas.itemconfig("background_corner_top_right", fill=self._apply_appearance_mode(self._background_corner_colors[1]))
self._canvas.itemconfig("background_corner_bottom_right", fill=self._apply_appearance_mode(self._background_corner_colors[2]))
self._canvas.itemconfig("background_corner_bottom_left", fill=self._apply_appearance_mode(self._background_corner_colors[3]))
else:
self._canvas.delete("background_parts")
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width),
overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method)
if no_color_updates is False or requires_recoloring:
if self._fg_color == "transparent":
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=self._apply_appearance_mode(self._bg_color))
else:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._border_color),
outline=self._apply_appearance_mode(self._border_color))
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
# self._canvas.tag_lower("inner_parts") # maybe unnecessary, I don't know ???
# self._canvas.tag_lower("border_parts")
def configure(self, require_redraw=False, **kwargs):
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
# check if CTk widgets are children of the frame and change their bg_color to new frame fg_color
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self._fg_color)
if "bg_color" in kwargs:
# pass bg_color change to children if fg_color is "transparent"
if self._fg_color == "transparent":
for child in self.winfo_children():
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self._fg_color)
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
if "background_corner_colors" in kwargs:
self._background_corner_colors = kwargs.pop("background_corner_colors")
require_redraw = True
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "background_corner_colors":
return self._background_corner_colors
else:
return super().cget(attribute_name)
def bind(self, sequence=None, command=None, add=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)

272
customtkinter/windows/widgets/ctk_label.py

@ -0,0 +1,272 @@
import tkinter
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
from .image import CTkImage
from .utility import pop_from_dict_by_set, check_kwargs_empty
class CTkLabel(CTkBaseClass):
"""
Label with rounded corners. Default is fg_color=None (transparent fg_color).
For detailed information check out the documentation.
"""
# attributes that are passed to and managed by the tkinter entry only:
_valid_tk_label_attributes = {"cursor", "justify", "padx", "pady",
"textvariable", "state", "takefocus", "underline"}
def __init__(self,
master: any,
width: int = 0,
height: int = 28,
corner_radius: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text: str = "CTkLabel",
font: Optional[Union[tuple, CTkFont]] = None,
image: Union[CTkImage, None] = None,
compound: str = "center",
anchor: str = "center", # label anchor: center, n, e, s, w
wraplength: int = 0,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height)
# color
self._fg_color = ThemeManager.theme["CTkLabel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(text_color)
# shape
self._corner_radius = ThemeManager.theme["CTkLabel"]["corner_radius"] if corner_radius is None else corner_radius
# text
self._anchor = anchor
self._text = text
self._wraplength = wraplength
# image
self._image = self._check_image_type(image)
self._compound = compound
if isinstance(self._image, CTkImage):
self._image.add_configure_callback(self._update_image)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.grid(row=0, column=0, sticky="nswe")
self._draw_engine = DrawEngine(self._canvas)
self._label = tkinter.Label(master=self,
highlightthickness=0,
padx=0,
pady=0,
borderwidth=0,
anchor=self._anchor,
compound=self._compound,
wraplength=self._apply_widget_scaling(self._wraplength),
text=self._text,
font=self._apply_font_scaling(self._font))
self._label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes))
check_kwargs_empty(kwargs, raise_error=True)
self._create_grid()
self._update_image()
self._draw()
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height))
self._label.configure(font=self._apply_font_scaling(self._font))
self._label.configure(wraplength=self._apply_widget_scaling(self._wraplength))
self._create_grid()
self._update_image()
self._draw(no_color_updates=True)
def _set_appearance_mode(self, mode_string):
super()._set_appearance_mode(mode_string)
self._update_image()
def _set_dimensions(self, width=None, height=None):
super()._set_dimensions(width, height)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._create_grid()
self._draw()
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
self._label.configure(font=self._apply_font_scaling(self._font))
# Workaround to force grid to be resized when text changes size.
# Otherwise grid will lag and only resizes if other mouse action occurs.
self._canvas.grid_forget()
self._canvas.grid(row=0, column=0, sticky="nswe")
def _update_image(self):
if isinstance(self._image, CTkImage):
self._label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(),
self._get_appearance_mode()))
elif self._image is not None:
self._label.configure(image=self._image)
def destroy(self):
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
super().destroy()
def _create_grid(self):
""" configure grid system (1x1) """
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
self._label.grid(row=0, column=0, sticky=text_label_grid_sticky,
padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height / 2))))
def _draw(self, no_color_updates=False):
super()._draw(no_color_updates)
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height),
self._apply_widget_scaling(self._corner_radius),
0)
if no_color_updates is False or requires_recoloring:
if self._apply_appearance_mode(self._fg_color) == "transparent":
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=self._apply_appearance_mode(self._bg_color))
self._label.configure(fg=self._apply_appearance_mode(self._text_color),
bg=self._apply_appearance_mode(self._bg_color))
else:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
self._label.configure(fg=self._apply_appearance_mode(self._text_color),
bg=self._apply_appearance_mode(self._fg_color))
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
self._create_grid()
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
if "text_color" in kwargs:
self._text_color = self._check_color_type(kwargs.pop("text_color"))
require_redraw = True
if "text" in kwargs:
self._text = kwargs.pop("text")
self._label.configure(text=self._text)
if "font" in kwargs:
if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font)
self._font = self._check_font_type(kwargs.pop("font"))
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
self._update_font()
if "image" in kwargs:
if isinstance(self._image, CTkImage):
self._image.remove_configure_callback(self._update_image)
self._image = self._check_image_type(kwargs.pop("image"))
if isinstance(self._image, CTkImage):
self._image.add_configure_callback(self._update_image)
self._update_image()
if "compound" in kwargs:
self._compound = kwargs.pop("compound")
self._label.configure(compound=self._compound)
if "anchor" in kwargs:
self._anchor = kwargs.pop("anchor")
self._label.configure(anchor=self._anchor)
self._create_grid()
if "wraplength" in kwargs:
self._wraplength = kwargs.pop("wraplength")
self._label.configure(wraplength=self._apply_widget_scaling(self._wraplength))
self._label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes)) # configure tkinter.Label
super().configure(require_redraw=require_redraw, **kwargs) # configure CTkBaseClass
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text":
return self._text
elif attribute_name == "font":
return self._font
elif attribute_name == "image":
return self._image
elif attribute_name == "compound":
return self._compound
elif attribute_name == "anchor":
return self._anchor
elif attribute_name == "wraplength":
return self._wraplength
elif attribute_name in self._valid_tk_label_attributes:
return self._label.cget(attribute_name) # cget of tkinter.Label
else:
return super().cget(attribute_name) # cget of CTkBaseClass
def bind(self, sequence: str = None, command: Callable = None, add: str = 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()

422
customtkinter/windows/widgets/ctk_optionmenu.py

@ -0,0 +1,422 @@
import tkinter
import copy
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .core_widget_classes import DropdownMenu
from .font import CTkFont
class CTkOptionMenu(CTkBaseClass):
"""
Optionmenu with rounded corners, dropdown menu, variable support, command.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 140,
height: int = 28,
corner_radius: Optional[Union[int]] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
button_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
dropdown_text_color: Optional[Union[str, Tuple[str, str]]] = None,
font: Optional[Union[tuple, CTkFont]] = None,
dropdown_font: Optional[Union[tuple, CTkFont]] = None,
values: Optional[list] = None,
variable: Union[tkinter.Variable, None] = None,
state: str = tkinter.NORMAL,
hover: bool = True,
command: Union[Callable[[str], None], None] = None,
dynamic_resizing: bool = True,
anchor: str = "w",
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color variables
self._fg_color = ThemeManager.theme["CTkOptionMenu"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._button_color = ThemeManager.theme["CTkOptionMenu"]["button_color"] if button_color is None else self._check_color_type(button_color)
self._button_hover_color = ThemeManager.theme["CTkOptionMenu"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
# shape
self._corner_radius = ThemeManager.theme["CTkOptionMenu"]["corner_radius"] if corner_radius is None else corner_radius
# text and font
self._text_color = ThemeManager.theme["CTkOptionMenu"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkOptionMenu"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# callback and hover functionality
self._command = command
self._variable = variable
self._variable_callback_blocked: bool = False
self._variable_callback_name: Union[str, None] = None
self._state = state
self._hover = hover
self._dynamic_resizing = dynamic_resizing
if values is None:
self._values = ["CTkOptionMenu"]
else:
self._values = values
if len(self._values) > 0:
self._current_value = self._values[0]
else:
self._current_value = "CTkOptionMenu"
self._dropdown_menu = DropdownMenu(master=self,
values=self._values,
command=self._dropdown_callback,
fg_color=dropdown_fg_color,
hover_color=dropdown_hover_color,
text_color=dropdown_text_color,
font=dropdown_font)
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._draw_engine = DrawEngine(self._canvas)
self._text_label = tkinter.Label(master=self,
font=self._apply_font_scaling(self._font),
anchor=anchor,
padx=0,
pady=0,
borderwidth=1,
text=self._current_value)
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 "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 "command" in kwargs:
self._command = kwargs.pop("command")
if "variable" in kwargs:
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._current_value = self._variable.get()
self._text_label.configure(text=self._current_value)
else:
self._variable = None
if "values" in kwargs:
self._values = kwargs.pop("values")
self._dropdown_menu.configure(values=self._values)
if "dropdown_font" in kwargs:
self._dropdown_menu.configure(font=kwargs.pop("dropdown_font"))
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "state" in kwargs:
self._state = kwargs.pop("state")
require_redraw = True
if "dynamic_resizing" in kwargs:
self._dynamic_resizing = kwargs.pop("dynamic_resizing")
if not self._dynamic_resizing:
self.grid_propagate(0)
else:
self.grid_propagate(1)
if "anchor" in kwargs:
self._text_label.configure(anchor=kwargs.pop("anchor"))
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "button_color":
return self._button_color
elif attribute_name == "button_hover_color":
return self._button_hover_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text_color_disabled":
return self._text_color_disabled
elif attribute_name == "dropdown_fg_color":
return self._dropdown_menu.cget("fg_color")
elif attribute_name == "dropdown_hover_color":
return self._dropdown_menu.cget("hover_color")
elif attribute_name == "dropdown_text_color":
return self._dropdown_menu.cget("text_color")
elif attribute_name == "font":
return self._font
elif attribute_name == "dropdown_font":
return self._dropdown_menu.cget("font")
elif attribute_name == "values":
return 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
customtkinter/windows/widgets/ctk_progressbar.py

@ -0,0 +1,312 @@
import tkinter
import math
from typing import Union, Tuple, Optional, Callable
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
customtkinter/windows/widgets/ctk_radiobutton.py

@ -0,0 +1,430 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
class CTkRadioButton(CTkBaseClass):
"""
Radiobutton with rounded corners, border, label, variable support, command.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 100,
height: int = 22,
radiobutton_width: int = 22,
radiobutton_height: int = 22,
corner_radius: Optional[int] = None,
border_width_unchecked: Optional[int] = None,
border_width_checked: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
hover_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
text: str = "CTkRadioButton",
font: Optional[Union[tuple, CTkFont]] = None,
textvariable: Union[tkinter.Variable, None] = None,
variable: Union[tkinter.Variable, None] = None,
value: Union[int, str] = 0,
state: str = tkinter.NORMAL,
hover: bool = True,
command: Union[Callable, None] = None,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# dimensions
self._radiobutton_width = radiobutton_width
self._radiobutton_height = radiobutton_height
# color
self._fg_color = ThemeManager.theme["CTkRadiobutton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._hover_color = ThemeManager.theme["CTkRadiobutton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
self._border_color = ThemeManager.theme["CTkRadiobutton"]["border_color"] if border_color is None else self._check_color_type(border_color)
# shape
self._corner_radius = ThemeManager.theme["CTkRadiobutton"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width_unchecked = ThemeManager.theme["CTkRadiobutton"]["border_width_unchecked"] if border_width_unchecked is None else border_width_unchecked
self._border_width_checked = ThemeManager.theme["CTkRadiobutton"]["border_width_checked"] if border_width_checked is None else border_width_checked
# text
self._text = text
self._text_label: Union[tkinter.Label, None] = None
self._text_color = ThemeManager.theme["CTkRadiobutton"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkRadiobutton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# callback and control variables
self._command = command
self._state = state
self._hover = hover
self._check_state: bool = False
self._value = value
self._variable: tkinter.Variable = variable
self._variable_callback_blocked: bool = False
self._textvariable = textvariable
self._variable_callback_name: Union[str, None] = None
# configure grid system (3x1)
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=1)
self.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()

281
customtkinter/windows/widgets/ctk_scrollbar.py

@ -0,0 +1,281 @@
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
class CTkScrollbar(CTkBaseClass):
"""
Scrollbar with rounded corners, configurable spacing.
Connect to scrollable widget by passing .set() method and set command attribute.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: Optional[Union[int, str]] = None,
height: Optional[Union[int, str]] = None,
corner_radius: Optional[int] = None,
border_spacing: Optional[int] = None,
minimum_pixel_length: int = 20,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
button_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
hover: bool = True,
command: Union[Callable, None] = None,
orientation: str = "vertical",
**kwargs):
# set default dimensions according to orientation
if width is None:
if orientation.lower() == "vertical":
width = 16
else:
width = 200
if height is None:
if orientation.lower() == "horizontal":
height = 16
else:
height = 200
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self._fg_color = ThemeManager.theme["CTkScrollbar"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
self._button_color = ThemeManager.theme["CTkScrollbar"]["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()

421
customtkinter/windows/widgets/ctk_segmented_button.py

@ -0,0 +1,421 @@
import tkinter
import copy
from typing import Union, Tuple, List, Dict, Callable, Optional
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
class CTkSegmentedButton(CTkFrame):
"""
Segmented button with corner radius, border width, variable support.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 140,
height: int = 28,
corner_radius: Optional[int] = None,
border_width: int = 3,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
selected_color: Optional[Union[str, Tuple[str, str]]] = None,
selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
unselected_color: Optional[Union[str, Tuple[str, str]]] = None,
unselected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None,
font: Optional[Union[tuple, CTkFont]] = None,
values: Optional[list] = None,
variable: Union[tkinter.Variable, None] = None,
dynamic_resizing: bool = True,
command: Union[Callable[[str], None], None] = None,
state: str = "normal",
**kwargs):
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
self._sb_fg_color = ThemeManager.theme["CTkSegmentedButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._sb_selected_color = ThemeManager.theme["CTkSegmentedButton"]["selected_color"] if selected_color is None else self._check_color_type(selected_color)
self._sb_selected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["selected_hover_color"] if selected_hover_color is None else self._check_color_type(selected_hover_color)
self._sb_unselected_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_color"] if unselected_color is None else self._check_color_type(unselected_color)
self._sb_unselected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_hover_color"] if unselected_hover_color is None else self._check_color_type(unselected_hover_color)
self._sb_text_color = ThemeManager.theme["CTkSegmentedButton"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._sb_text_color_disabled = ThemeManager.theme["CTkSegmentedButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
self._sb_corner_radius = ThemeManager.theme["CTkSegmentedButton"]["corner_radius"] if corner_radius is None else corner_radius
self._sb_border_width = ThemeManager.theme["CTkSegmentedButton"]["border_width"] if border_width is None else border_width
self._background_corner_colors = background_corner_colors # rendering options for DrawEngine
self._command: Callable[[str], None] = command
self._font = CTkFont() if font is None else font
self._state = state
self._buttons_dict: Dict[str, CTkButton] = {} # mapped from value to button object
if values is None:
self._value_list: List[str] = ["CTkSegmentedButton"]
else:
self._value_list: List[str] = values # Values ordered like buttons rendered on widget
self._dynamic_resizing = dynamic_resizing
if not self._dynamic_resizing:
self.grid_propagate(False)
self._check_unique_values(self._value_list)
self._current_value: str = ""
if len(self._value_list) > 0:
self._create_buttons_from_values()
self._create_button_grid()
self._variable = variable
self._variable_callback_blocked: bool = False
self._variable_callback_name: Union[str, None] = None
if self._variable is not None:
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.set(self._variable.get(), from_variable_callback=True)
super().configure(corner_radius=self._sb_corner_radius, fg_color="transparent")
def destroy(self):
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
super().destroy()
def _set_dimensions(self, width: int = None, height: int = None):
super()._set_dimensions(width, height)
for button in self._buttons_dict.values():
button.configure(height=height)
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self.set(self._variable.get(), from_variable_callback=True)
def _get_index_by_value(self, value: str):
for index, value_from_list in enumerate(self._value_list):
if value_from_list == value:
return index
raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
def _configure_button_corners_for_index(self, index: int):
if index == 0 and len(self._value_list) == 1:
if self._background_corner_colors is None:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
else:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=self._background_corner_colors)
elif index == 0:
if self._background_corner_colors is None:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._sb_fg_color, self._sb_fg_color, self._bg_color))
else:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._background_corner_colors[0], self._sb_fg_color, self._sb_fg_color, self._background_corner_colors[3]))
elif index == len(self._value_list) - 1:
if self._background_corner_colors is None:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._bg_color, self._bg_color, self._sb_fg_color))
else:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._background_corner_colors[1], self._background_corner_colors[2], self._sb_fg_color))
else:
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._sb_fg_color, self._sb_fg_color, self._sb_fg_color))
def _unselect_button_by_value(self, value: str):
if value in self._buttons_dict:
self._buttons_dict[value].configure(fg_color=self._sb_unselected_color,
hover_color=self._sb_unselected_hover_color)
def _select_button_by_value(self, value: str):
if self._current_value is not None and self._current_value != "":
self._unselect_button_by_value(self._current_value)
self._current_value = value
self._buttons_dict[value].configure(fg_color=self._sb_selected_color,
hover_color=self._sb_selected_hover_color)
def _create_button(self, index: int, value: str) -> CTkButton:
new_button = CTkButton(self,
width=0,
height=self._current_height,
corner_radius=self._sb_corner_radius,
border_width=self._sb_border_width,
fg_color=self._sb_unselected_color,
border_color=self._sb_fg_color,
hover_color=self._sb_unselected_hover_color,
text_color=self._sb_text_color,
text_color_disabled=self._sb_text_color_disabled,
text=value,
font=self._font,
state=self._state,
command=lambda v=value: self.set(v, from_button_callback=True),
background_corner_colors=None,
round_width_to_even_numbers=False,
round_height_to_even_numbers=False) # DrawEngine rendering option (so that theres no gap between buttons)
return new_button
@staticmethod
def _check_unique_values(values: List[str]):
""" raises exception if values are not unique """
if len(values) != len(set(values)):
raise ValueError("CTkSegmentedButton values are not unique")
def _create_button_grid(self):
# remove minsize from every grid cell in the first row
number_of_columns, _ = self.grid_size()
for n in range(number_of_columns):
self.grid_columnconfigure(n, weight=1, minsize=0)
self.grid_rowconfigure(0, weight=1)
for index, value in enumerate(self._value_list):
self.grid_columnconfigure(index, weight=1, minsize=self._current_height)
self._buttons_dict[value].grid(row=0, column=index, sticky="ew")
def _create_buttons_from_values(self):
assert len(self._buttons_dict) == 0
assert len(self._value_list) > 0
for index, value in enumerate(self._value_list):
self._buttons_dict[value] = self._create_button(index, value)
self._configure_button_corners_for_index(index)
def configure(self, **kwargs):
if "bg_color" in kwargs:
super().configure(bg_color=kwargs.pop("bg_color"))
if len(self._buttons_dict) > 0:
self._configure_button_corners_for_index(0)
if len(self._buttons_dict) > 1:
max_index = len(self._buttons_dict) - 1
self._configure_button_corners_for_index(max_index)
if "fg_color" in kwargs:
self._sb_fg_color = self._check_color_type(kwargs.pop("fg_color"))
for index, button in enumerate(self._buttons_dict.values()):
button.configure(border_color=self._sb_fg_color)
self._configure_button_corners_for_index(index)
if "selected_color" in kwargs:
self._sb_selected_color = self._check_color_type(kwargs.pop("selected_color"))
if self._current_value in self._buttons_dict:
self._buttons_dict[self._current_value].configure(fg_color=self._sb_selected_color)
if "selected_hover_color" in kwargs:
self._sb_selected_hover_color = self._check_color_type(kwargs.pop("selected_hover_color"))
if self._current_value in self._buttons_dict:
self._buttons_dict[self._current_value].configure(hover_color=self._sb_selected_hover_color)
if "unselected_color" in kwargs:
self._sb_unselected_color = self._check_color_type(kwargs.pop("unselected_color"))
for value, button in self._buttons_dict.items():
if value != self._current_value:
button.configure(fg_color=self._sb_unselected_color)
if "unselected_hover_color" in kwargs:
self._sb_unselected_hover_color = self._check_color_type(kwargs.pop("unselected_hover_color"))
for value, button in self._buttons_dict.items():
if value != self._current_value:
button.configure(hover_color=self._sb_unselected_hover_color)
if "text_color" in kwargs:
self._sb_text_color = self._check_color_type(kwargs.pop("text_color"))
for button in self._buttons_dict.values():
button.configure(text_color=self._sb_text_color)
if "text_color_disabled" in kwargs:
self._sb_text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
for button in self._buttons_dict.values():
button.configure(text_color_disabled=self._sb_text_color_disabled)
if "background_corner_colors" in kwargs:
self._background_corner_colors = kwargs.pop("background_corner_colors")
for i in range(len(self._buttons_dict)):
self._configure_button_corners_for_index(i)
if "font" in kwargs:
self._font = kwargs.pop("font")
for button in self._buttons_dict.values():
button.configure(font=self._font)
if "values" in kwargs:
for button in self._buttons_dict.values():
button.destroy()
self._buttons_dict.clear()
self._value_list = kwargs.pop("values")
self._check_unique_values(self._value_list)
if len(self._value_list) > 0:
self._create_buttons_from_values()
self._create_button_grid()
if self._current_value in self._value_list:
self._select_button_by_value(self._current_value)
if "variable" in kwargs:
if self._variable is not None: # remove old callback
self._variable.trace_remove("write", self._variable_callback_name)
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.set(self._variable.get(), from_variable_callback=True)
else:
self._variable = None
if "dynamic_resizing" in kwargs:
self._dynamic_resizing = kwargs.pop("dynamic_resizing")
if not self._dynamic_resizing:
self.grid_propagate(False)
else:
self.grid_propagate(True)
if "command" in kwargs:
self._command = kwargs.pop("command")
if "state" in kwargs:
self._state = kwargs.pop("state")
for button in self._buttons_dict.values():
button.configure(state=self._state)
super().configure(**kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._sb_corner_radius
elif attribute_name == "border_width":
return self._sb_border_width
elif attribute_name == "fg_color":
return self._sb_fg_color
elif attribute_name == "selected_color":
return self._sb_selected_color
elif attribute_name == "selected_hover_color":
return self._sb_selected_hover_color
elif attribute_name == "unselected_color":
return self._sb_unselected_color
elif attribute_name == "unselected_hover_color":
return self._sb_unselected_hover_color
elif attribute_name == "text_color":
return self._sb_text_color
elif attribute_name == "text_color_disabled":
return self._sb_text_color_disabled
elif attribute_name == "font":
return self._font
elif attribute_name == "values":
return 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:
return super().cget(attribute_name)
def set(self, value: str, from_variable_callback: bool = False, from_button_callback: bool = False):
if value == self._current_value:
return
elif value in self._buttons_dict:
self._select_button_by_value(value)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(value)
self._variable_callback_blocked = False
else:
if self._current_value in self._buttons_dict:
self._unselect_button_by_value(self._current_value)
self._current_value = value
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(value)
self._variable_callback_blocked = False
if from_button_callback:
if self._command is not None:
self._command(self._current_value)
def get(self) -> str:
return self._current_value
def insert(self, index: int, value: str):
if value not in self._buttons_dict:
if value != "":
self._value_list.insert(index, value)
self._buttons_dict[value] = self._create_button(index, value)
self._configure_button_corners_for_index(index)
if index > 0:
self._configure_button_corners_for_index(index - 1)
if index < len(self._buttons_dict) - 1:
self._configure_button_corners_for_index(index + 1)
self._create_button_grid()
if value == self._current_value:
self._select_button_by_value(self._current_value)
else:
raise ValueError(f"CTkSegmentedButton can not insert value ''")
else:
raise ValueError(f"CTkSegmentedButton can not insert value '{value}', already part of the values")
def move(self, new_index: int, value: str):
if 0 <= new_index < len(self._value_list):
if value in self._buttons_dict:
self.delete(value)
self.insert(new_index, value)
else:
raise ValueError(f"CTkSegmentedButton has no value named '{value}'")
else:
raise ValueError(f"CTkSegmentedButton new_index {new_index} not in range of value list with len {len(self._value_list)}")
def delete(self, value: str):
if value in self._buttons_dict:
self._buttons_dict[value].destroy()
self._buttons_dict.pop(value)
index_to_remove = self._get_index_by_value(value)
self._value_list.pop(index_to_remove)
# removed index was outer right element
if index_to_remove == len(self._buttons_dict) and len(self._buttons_dict) > 0:
self._configure_button_corners_for_index(index_to_remove - 1)
# removed index was outer left element
if index_to_remove == 0 and len(self._buttons_dict) > 0:
self._configure_button_corners_for_index(0)
#if index_to_remove <= len(self._buttons_dict) - 1:
# self._configure_button_corners_for_index(index_to_remove)
self._create_button_grid()
else:
raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
def bind(self, sequence=None, command=None, add=None):
raise NotImplementedError
def unbind(self, sequence=None, funcid=None):
raise NotImplementedError

397
customtkinter/windows/widgets/ctk_slider.py

@ -0,0 +1,397 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
class CTkSlider(CTkBaseClass):
"""
Slider with rounded corners, border, number of steps, variable support, vertical orientation.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: Optional[int] = None,
height: Optional[int] = None,
corner_radius: Optional[int] = None,
button_corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
button_length: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Union[str, Tuple[str, str]] = "transparent",
progress_color: Optional[Union[str, Tuple[str, str]]] = None,
button_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
from_: int = 0,
to: int = 1,
state: str = "normal",
number_of_steps: Union[int, None] = None,
hover: bool = True,
command: Union[Callable[[float], None], None] = None,
variable: Union[tkinter.Variable, None] = None,
orientation: str = "horizontal",
**kwargs):
# set default dimensions according to orientation
if width is None:
if orientation.lower() == "vertical":
width = 16
else:
width = 200
if height is None:
if orientation.lower() == "vertical":
height = 200
else:
height = 16
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self._border_color = self._check_color_type(border_color, transparency=True)
self._fg_color = ThemeManager.theme["CTkSlider"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
self._progress_color = ThemeManager.theme["CTkSlider"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True)
self._button_color = ThemeManager.theme["CTkSlider"]["button_color"] if button_color is None else self._check_color_type(button_color)
self._button_hover_color = ThemeManager.theme["CTkSlider"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
# shape
self._corner_radius = ThemeManager.theme["CTkSlider"]["corner_radius"] if corner_radius is None else corner_radius
self._button_corner_radius = ThemeManager.theme["CTkSlider"]["button_corner_radius"] if button_corner_radius is None else button_corner_radius
self._border_width = ThemeManager.theme["CTkSlider"]["border_width"] if border_width is None else border_width
self._button_length = ThemeManager.theme["CTkSlider"]["button_length"] if button_length is None else button_length
self._value: float = 0.5 # initial value of slider in percent
self._orientation = orientation
self._hover_state: bool = False
self._hover = hover
self._from_ = from_
self._to = to
self._number_of_steps = number_of_steps
self._output_value = self._from_ + (self._value * (self._to - self._from_))
if self._corner_radius < self._button_corner_radius:
self._corner_radius = self._button_corner_radius
# callback and control variables
self._command = command
self._variable: tkinter.Variable = variable
self._variable_callback_blocked: bool = False
self._variable_callback_name: Union[bool, None] = None
self._state = state
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe")
self._draw_engine = DrawEngine(self._canvas)
self._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 "state" in kwargs:
self._state = kwargs.pop("state")
self._set_cursor()
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
require_redraw = True
if "progress_color" in kwargs:
self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True)
require_redraw = True
if "button_color" in kwargs:
self._button_color = self._check_color_type(kwargs.pop("button_color"))
require_redraw = True
if "button_hover_color" in kwargs:
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True)
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "from_" in kwargs:
self._from_ = kwargs.pop("from_")
if "to" in kwargs:
self._to = kwargs.pop("to")
if "number_of_steps" in kwargs:
self._number_of_steps = kwargs.pop("number_of_steps")
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "command" in kwargs:
self._command = kwargs.pop("command")
if "variable" in kwargs:
if self._variable is not None:
self._variable.trace_remove("write", self._variable_callback_name)
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self.set(self._variable.get(), from_variable_callback=True)
else:
self._variable = None
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "button_corner_radius":
return self._button_corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "button_length":
return self._button_length
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "progress_color":
return self._progress_color
elif attribute_name == "button_color":
return self._button_color
elif attribute_name == "button_hover_color":
return self._button_hover_color
elif attribute_name == "from_":
return self._from_
elif attribute_name == "to":
return self._to
elif attribute_name == "state":
return self._state
elif attribute_name == "number_of_steps":
return self._number_of_steps
elif attribute_name == "hover":
return self._hover
elif attribute_name == "command":
return self._command
elif attribute_name == "variable":
return self._variable
elif attribute_name == "orientation":
return self._orientation
else:
return super().cget(attribute_name)
def _clicked(self, event=None):
if self._state == "normal":
if self._orientation.lower() == "horizontal":
self._value = self._reverse_widget_scaling(event.x / self._current_width)
else:
self._value = 1 - self._reverse_widget_scaling(event.y / self._current_height)
if self._value > 1:
self._value = 1
if self._value < 0:
self._value = 0
self._output_value = self._round_to_step_size(self._from_ + (self._value * (self._to - self._from_)))
self._value = (self._output_value - self._from_) / (self._to - self._from_)
self._draw(no_color_updates=False)
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
self._variable_callback_blocked = False
if self._command is not None:
self._command(self._output_value)
def _on_enter(self, event=0):
if self._hover is True and self._state == "normal":
self._hover_state = True
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_hover_color),
outline=self._apply_appearance_mode(self._button_hover_color))
def _on_leave(self, event=0):
self._hover_state = False
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_color),
outline=self._apply_appearance_mode(self._button_color))
def _round_to_step_size(self, value) -> float:
if self._number_of_steps is not None:
step_size = (self._to - self._from_) / self._number_of_steps
value = self._to - (round((self._to - value) / step_size) * step_size)
return value
else:
return value
def get(self) -> float:
return self._output_value
def set(self, output_value, from_variable_callback=False):
if self._from_ < self._to:
if output_value > self._to:
output_value = self._to
elif output_value < self._from_:
output_value = self._from_
else:
if output_value < self._to:
output_value = self._to
elif output_value > self._from_:
output_value = self._from_
self._output_value = self._round_to_step_size(output_value)
self._value = (self._output_value - self._from_) / (self._to - self._from_)
self._draw(no_color_updates=False)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
self._variable_callback_blocked = False
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
self.set(self._variable.get(), from_variable_callback=True)
def bind(self, sequence: 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()

475
customtkinter/windows/widgets/ctk_switch.py

@ -0,0 +1,475 @@
import tkinter
import sys
from typing import Union, Tuple, Callable, Optional
from .core_rendering import CTkCanvas
from .theme import ThemeManager
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .font import CTkFont
class CTkSwitch(CTkBaseClass):
"""
Switch with rounded corners, border, label, command, variable support.
For detailed information check out the documentation.
"""
def __init__(self,
master: any,
width: int = 100,
height: int = 24,
switch_width: int = 36,
switch_height: int = 18,
corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
button_length: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Union[str, Tuple[str, str]] = "transparent",
progress_color: Optional[Union[str, Tuple[str, str]]] = None,
button_color: Optional[Union[str, Tuple[str, str]]] = None,
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
text: str = "CTkSwitch",
font: Optional[Union[tuple, CTkFont]] = None,
textvariable: Union[tkinter.Variable, None] = None,
onvalue: Union[int, str] = 1,
offvalue: Union[int, str] = 0,
variable: Union[tkinter.Variable, None] = None,
hover: bool = True,
command: Union[Callable, None] = None,
state: str = tkinter.NORMAL,
**kwargs):
# transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# dimensions
self._switch_width = switch_width
self._switch_height = switch_height
# color
self._border_color = self._check_color_type(border_color, transparency=True)
self._fg_color = ThemeManager.theme["CTkSwitch"]["fg_Color"] if fg_color is None else self._check_color_type(fg_color)
self._progress_color = ThemeManager.theme["CTkSwitch"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True)
self._button_color = ThemeManager.theme["CTkSwitch"]["button_color"] if button_color is None else self._check_color_type(button_color)
self._button_hover_color = ThemeManager.theme["CTkSwitch"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
self._text_color = ThemeManager.theme["CTkSwitch"]["text_color"] if text_color is None else self._check_color_type(text_color)
self._text_color_disabled = ThemeManager.theme["CTkSwitch"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
# text
self._text = text
self._text_label = None
# font
self._font = CTkFont() if font is None else self._check_font_type(font)
if isinstance(self._font, CTkFont):
self._font.add_size_configure_callback(self._update_font)
# shape
self._corner_radius = ThemeManager.theme["CTkSwitch"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkSwitch"]["border_width"] if border_width is None else border_width
self._button_length = ThemeManager.theme["CTkSwitch"]["button_length"] if button_length is None else button_length
self._hover_state: bool = False
self._check_state: bool = False # True if switch is activated
self._hover = hover
self._state = state
self._onvalue = onvalue
self._offvalue = offvalue
# callback and control variables
self._command = command
self._variable = variable
self._variable_callback_blocked = False
self._variable_callback_name = None
self._textvariable = textvariable
# configure grid system (3x1)
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=1)
self.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 "progress_color" in kwargs:
self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True)
require_redraw = True
if "button_color" in kwargs:
self._button_color = self._check_color_type(kwargs.pop("button_color"))
require_redraw = True
if "button_hover_color" in kwargs:
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True)
require_redraw = True
if "hover" in kwargs:
self._hover = kwargs.pop("hover")
if "command" in kwargs:
self._command = kwargs.pop("command")
if "textvariable" in kwargs:
self._textvariable = kwargs.pop("textvariable")
self._text_label.configure(textvariable=self._textvariable)
if "variable" in kwargs:
if self._variable is not None and self._variable != "":
self._variable.trace_remove("write", self._variable_callback_name)
self._variable = kwargs.pop("variable")
if self._variable is not None and self._variable != "":
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
self._check_state = True if self._variable.get() == self._onvalue else False
require_redraw = True
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "button_length":
return self._button_length
elif attribute_name == "switch_width":
return self._switch_width
elif attribute_name == "switch_height":
return self._switch_height
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "progress_color":
return self._progress_color
elif attribute_name == "button_color":
return self._button_color
elif attribute_name == "button_hover_color":
return self._button_hover_color
elif attribute_name == "text_color":
return self._text_color
elif attribute_name == "text_color_disabled":
return self._text_color_disabled
elif attribute_name == "text":
return self._text
elif attribute_name == "font":
return self._font
elif attribute_name == "textvariable":
return self._textvariable
elif attribute_name == "onvalue":
return self._onvalue
elif attribute_name == "offvalue":
return self._offvalue
elif attribute_name == "variable":
return self._variable
elif attribute_name == "hover":
return self._hover
elif attribute_name == "command":
return self._command
elif attribute_name == "state":
return self._state
else:
return super().cget(attribute_name)
def toggle(self, event=None):
if self._state is not tkinter.DISABLED:
if self._check_state is True:
self._check_state = False
else:
self._check_state = True
self._draw(no_color_updates=True)
if self._variable is not None:
self._variable_callback_blocked = True
self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
self._variable_callback_blocked = False
if self._command is not None:
self._command()
def select(self, from_variable_callback=False):
if self._state is not tkinter.DISABLED or from_variable_callback:
self._check_state = True
self._draw(no_color_updates=True)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._onvalue)
self._variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
if self._state is not tkinter.DISABLED or from_variable_callback:
self._check_state = False
self._draw(no_color_updates=True)
if self._variable is not None and not from_variable_callback:
self._variable_callback_blocked = True
self._variable.set(self._offvalue)
self._variable_callback_blocked = False
def get(self) -> Union[int, str]:
return self._onvalue if self._check_state is True else self._offvalue
def _on_enter(self, event=0):
if self._hover is True and self._state == "normal":
self._hover_state = True
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_hover_color),
outline=self._apply_appearance_mode(self._button_hover_color))
def _on_leave(self, event=0):
self._hover_state = False
self._canvas.itemconfig("slider_parts",
fill=self._apply_appearance_mode(self._button_color),
outline=self._apply_appearance_mode(self._button_color))
def _variable_callback(self, var_name, index, mode):
if not self._variable_callback_blocked:
if self._variable.get() == self._onvalue:
self.select(from_variable_callback=True)
elif self._variable.get() == self._offvalue:
self.deselect(from_variable_callback=True)
def bind(self, sequence: 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()

370
customtkinter/windows/widgets/ctk_tabview.py

@ -0,0 +1,370 @@
import tkinter
from typing import Union, Tuple, Dict, List, Callable, Optional
from .theme import ThemeManager
from .ctk_frame import CTkFrame
from .core_rendering import CTkCanvas
from .core_rendering import DrawEngine
from .core_widget_classes import CTkBaseClass
from .ctk_segmented_button import CTkSegmentedButton
class CTkTabview(CTkBaseClass):
"""
Tabview...
For detailed information check out the documentation.
"""
_top_spacing: int = 10 # px on top of the buttons
_top_button_overhang: int = 8 # px
_button_height: int = 26
_segmented_button_border_width: int = 3
def __init__(self,
master: any,
width: int = 300,
height: int = 250,
corner_radius: Optional[int] = None,
border_width: Optional[int] = None,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Optional[Union[str, Tuple[str, str]]] = None,
border_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_selected_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_unselected_color: Optional[Union[str, Tuple[str, str]]] = None,
segmented_button_unselected_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color: Optional[Union[str, Tuple[str, str]]] = None,
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
command: Union[Callable, None] = None,
state: str = "normal",
**kwargs):
# transfer some functionality to CTkFrame
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
# color
self._border_color = ThemeManager.theme["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color)
# determine fg_color of frame
if fg_color is None:
if isinstance(self.master, (CTkFrame, CTkTabview)):
if self.master.cget("fg_color") == ThemeManager.theme["CTkFrame"]["fg_color"]:
self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"]
else:
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
else:
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"]
else:
self._fg_color = self._check_color_type(fg_color, transparency=True)
# shape
self._corner_radius = ThemeManager.theme["CTkFrame"]["corner_radius"] if corner_radius is None else corner_radius
self._border_width = ThemeManager.theme["CTkFrame"]["border_width"] if border_width is None else border_width
self._canvas = CTkCanvas(master=self,
bg=self._apply_appearance_mode(self._bg_color),
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height - self._top_spacing - self._top_button_overhang))
self._draw_engine = DrawEngine(self._canvas)
self._segmented_button = CTkSegmentedButton(self,
values=[],
height=self._button_height,
fg_color=segmented_button_fg_color,
selected_color=segmented_button_selected_color,
selected_hover_color=segmented_button_selected_hover_color,
unselected_color=segmented_button_unselected_color,
unselected_hover_color=segmented_button_unselected_hover_color,
text_color=text_color,
text_color_disabled=text_color_disabled,
corner_radius=corner_radius,
border_width=self._segmented_button_border_width,
command=self._segmented_button_callback,
state=state)
self._configure_segmented_button_background_corners()
self._configure_grid()
self._set_grid_canvas()
self._tab_dict: Dict[str, CTkFrame] = {}
self._name_list: List[str] = [] # list of unique tab names in order of tabs
self._current_name: str = ""
self._command = command
self._draw()
def _segmented_button_callback(self, selected_name):
self._current_name = selected_name
self._grid_forget_all_tabs()
self._set_grid_tab_by_name(self._current_name)
if self._command is not None:
self._command()
def winfo_children(self) -> List[any]:
"""
winfo_children of CTkTabview without canvas and segmented button widgets,
because it's not a child but part of the CTkTabview itself
"""
child_widgets = super().winfo_children()
try:
child_widgets.remove(self._canvas)
child_widgets.remove(self._segmented_button)
return child_widgets
except ValueError:
return child_widgets
def _set_scaling(self, *args, **kwargs):
super()._set_scaling(*args, **kwargs)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height - self._top_spacing - self._top_button_overhang))
self._configure_grid()
self._draw(no_color_updates=True)
def _set_dimensions(self, width=None, height=None):
super()._set_dimensions(width, height)
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height - self._top_spacing - self._top_button_overhang))
self._draw()
def _configure_segmented_button_background_corners(self):
""" needs to be called for changes in fg_color, bg_color """
if self._fg_color is not None:
self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._fg_color, self._fg_color))
else:
self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
def _configure_tab_background_corners_by_name(self, name: str):
""" needs to be called for changes in fg_color, bg_color, border_width """
self._tab_dict[name].configure(background_corner_colors=None)
def _configure_grid(self):
""" create 3 x 4 grid system """
self.grid_rowconfigure(0, weight=0, minsize=self._apply_widget_scaling(self._top_spacing))
self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._top_button_overhang))
self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._top_button_overhang))
self.grid_rowconfigure(3, weight=1)
self.grid_columnconfigure(0, weight=1)
def _set_grid_canvas(self):
self._canvas.grid(row=2, rowspan=2, column=0, columnspan=1, sticky="nsew")
def _set_grid_segmented_button(self):
""" needs to be called for changes in corner_radius """
self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="ns")
def _set_grid_tab_by_name(self, name: str):
""" needs to be called for changes in corner_radius, border_width """
self._tab_dict[name].grid(row=3, column=0, sticky="nsew",
padx=self._apply_widget_scaling(max(self._corner_radius, self._border_width)),
pady=self._apply_widget_scaling(max(self._corner_radius, self._border_width)))
def _grid_forget_all_tabs(self):
for frame in self._tab_dict.values():
frame.grid_forget()
def _create_tab(self) -> CTkFrame:
new_tab = CTkFrame(self,
height=0,
width=0,
fg_color=self._fg_color,
border_width=0,
corner_radius=self._corner_radius)
return new_tab
def _draw(self, no_color_updates: bool = False):
super()._draw(no_color_updates)
if not self._canvas.winfo_exists():
return
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width),
self._apply_widget_scaling(self._current_height - self._top_spacing - self._top_button_overhang),
self._apply_widget_scaling(self._corner_radius),
self._apply_widget_scaling(self._border_width))
if no_color_updates is False or requires_recoloring:
if self._fg_color == "transparent":
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._bg_color),
outline=self._apply_appearance_mode(self._bg_color))
else:
self._canvas.itemconfig("inner_parts",
fill=self._apply_appearance_mode(self._fg_color),
outline=self._apply_appearance_mode(self._fg_color))
self._canvas.itemconfig("border_parts",
fill=self._apply_appearance_mode(self._border_color),
outline=self._apply_appearance_mode(self._border_color))
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._bg_color)) # configure bg color of tkinter.Frame, cuase canvas does not fill frame
def configure(self, require_redraw=False, **kwargs):
if "corner_radius" in kwargs:
self._corner_radius = kwargs.pop("corner_radius")
require_redraw = True
if "border_width" in kwargs:
self._border_width = kwargs.pop("border_width")
require_redraw = True
if "fg_color" in kwargs:
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
require_redraw = True
if "border_color" in kwargs:
self._border_color = self._check_color_type(kwargs.pop("border_color"))
require_redraw = True
if "segmented_button_fg_color" in kwargs:
self._segmented_button.configure(fg_color=kwargs.pop("segmented_button_fg_color"))
if "segmented_button_selected_color" in kwargs:
self._segmented_button.configure(selected_color=kwargs.pop("segmented_button_selected_color"))
if "segmented_button_selected_hover_color" in kwargs:
self._segmented_button.configure(selected_hover_color=kwargs.pop("segmented_button_selected_hover_color"))
if "segmented_button_unselected_color" in kwargs:
self._segmented_button.configure(unselected_color=kwargs.pop("segmented_button_unselected_color"))
if "segmented_button_unselected_hover_color" in kwargs:
self._segmented_button.configure(unselected_hover_color=kwargs.pop("segmented_button_unselected_hover_color"))
if "text_color" in kwargs:
self._segmented_button.configure(text_color=kwargs.pop("text_color"))
if "text_color_disabled" in kwargs:
self._segmented_button.configure(text_color_disabled=kwargs.pop("text_color_disabled"))
if "command" in kwargs:
self._command = kwargs.pop("command")
if "state" in kwargs:
self._segmented_button.configure(state=kwargs.pop("state"))
super().configure(require_redraw=require_redraw, **kwargs)
def cget(self, attribute_name: str):
if attribute_name == "corner_radius":
return self._corner_radius
elif attribute_name == "border_width":
return self._border_width
elif attribute_name == "fg_color":
return self._fg_color
elif attribute_name == "border_color":
return self._border_color
elif attribute_name == "segmented_button_fg_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "segmented_button_selected_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "segmented_button_selected_hover_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "segmented_button_unselected_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "segmented_button_unselected_hover_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "text_color":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "text_color_disabled":
return self._segmented_button.cget(attribute_name)
elif attribute_name == "command":
return self._command
elif attribute_name == "state":
return self._segmented_button.cget(attribute_name)
else:
return super().cget(attribute_name)
def tab(self, name: str) -> CTkFrame:
""" returns reference to the tab with given name """
if name in self._tab_dict:
return self._tab_dict[name]
else:
raise ValueError(f"CTkTabview has no tab named '{name}'")
def insert(self, index: int, name: str) -> CTkFrame:
""" creates new tab with given name at position index """
if name not in self._tab_dict:
# if no tab exists, set grid for segmented button
if len(self._tab_dict) == 0:
self._set_grid_segmented_button()
self._name_list.insert(index, name)
self._tab_dict[name] = self._create_tab()
self._segmented_button.insert(index, name)
self._configure_tab_background_corners_by_name(name)
# if created tab is only tab select this tab
if len(self._tab_dict) == 1:
self._current_name = name
self._segmented_button.set(self._current_name)
self._grid_forget_all_tabs()
self._set_grid_tab_by_name(self._current_name)
return self._tab_dict[name]
else:
raise ValueError(f"CTkTabview already has tab named '{name}'")
def add(self, name: str) -> CTkFrame:
""" appends new tab with given name """
return self.insert(len(self._tab_dict), name)
def move(self, new_index: int, name: str):
if 0 <= new_index < len(self._name_list):
if name in self._tab_dict:
self._segmented_button.move(new_index, name)
else:
raise ValueError(f"CTkTabview has no name '{name}'")
else:
raise ValueError(f"CTkTabview new_index {new_index} not in range of name list with len {len(self._name_list)}")
def delete(self, name: str):
""" delete tab by name """
if name in self._tab_dict:
self._name_list.remove(name)
self._tab_dict[name].grid_forget()
self._tab_dict.pop(name)
self._segmented_button.delete(name)
# set current_name to '' and remove segmented button if no tab is left
if len(self._name_list) == 0:
self._current_name = ""
self._segmented_button.grid_forget()
# if only one tab left, select this tab
elif len(self._name_list) == 1:
self._current_name = self._name_list[0]
self._segmented_button.set(self._current_name)
self._grid_forget_all_tabs()
self._set_grid_tab_by_name(self._current_name)
# more tabs are left
else:
# if current_name is deleted tab, select first tab at position 0
if self._current_name == name:
self.set(self._name_list[0])
else:
raise ValueError(f"CTkTabview has no tab named '{name}'")
def set(self, name: str):
""" select tab by name """
if name in self._tab_dict:
self._current_name = name
self._segmented_button.set(name)
self._grid_forget_all_tabs()
self._set_grid_tab_by_name(name)
else:
raise ValueError(f"CTkTabview has no tab named '{name}'")
def get(self) -> str:
""" returns name of selected tab, returns empty string if no tab selected """
return self._current_name

500
customtkinter/windows/widgets/ctk_textbox.py

@ -0,0 +1,500 @@
import tkinter
from typing import Union, Tuple, Optional, Callable
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
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"

BIN
customtkinter/windows/widgets/font/__pycache__/__init__.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/font/__pycache__/ctk_font.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/font/__pycache__/font_manager.cpython-310.pyc

Binary file not shown.

91
customtkinter/windows/widgets/font/ctk_font.py

@ -0,0 +1,91 @@
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 """
self._size_configure_callback_list.remove(callback)
def create_scaled_tuple(self, font_scaling: float) -> Tuple[str, int, str]:
""" return scaled tuple representation of font in the form (family: str, size: int, style: str)"""
return self._family, round(-abs(self._size) * font_scaling), self._tuple_style_string
def config(self, *args, **kwargs):
raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.")
def configure(self, **kwargs):
if "size" in kwargs:
self._size = kwargs.pop("size")
super().configure(size=-abs(self._size))
if "family" in kwargs:
super().configure(family=kwargs.pop("family"))
self._family = super().cget("family")
super().configure(**kwargs)
# update style string for create_scaled_tuple() method
self._tuple_style_string = f"{super().cget('weight')} {super().cget('slant')} {'underline' if super().cget('underline') else ''} {'overstrike' if super().cget('overstrike') else ''}"
# call all functions registered with add_size_configure_callback()
for callback in self._size_configure_callback_list:
callback()
def cget(self, attribute_name: str) -> any:
if attribute_name == "size":
return self._size
if attribute_name == "family":
return self._family
else:
return super().cget(attribute_name)
def copy(self) -> "CTkFont":
return copy.deepcopy(self)

66
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
customtkinter/windows/widgets/image/__init__.py

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

BIN
customtkinter/windows/widgets/image/__pycache__/__init__.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/image/__pycache__/ctk_image.cpython-310.pyc

Binary file not shown.

122
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
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

BIN
customtkinter/windows/widgets/scaling/__pycache__/__init__.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/scaling/__pycache__/scaling_base_class.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/scaling/__pycache__/scaling_tracker.cpython-310.pyc

Binary file not shown.

159
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
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
customtkinter/windows/widgets/theme/__init__.py

@ -0,0 +1,9 @@
from .theme_manager import ThemeManager
# load default blue theme
try:
ThemeManager.load_theme("blue")
except FileNotFoundError as err:
raise FileNotFoundError(f"{err}\n\nThe .json theme file for CustomTkinter could not be found.\n" +
f"If packaging with pyinstaller was used, have a look at the wiki:\n" +
f"https://github.com/TomSchimansky/CustomTkinter/wiki/Packaging#windows-pyinstaller-auto-py-to-exe")

BIN
customtkinter/windows/widgets/theme/__pycache__/__init__.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/theme/__pycache__/theme_manager.cpython-310.pyc

Binary file not shown.

47
customtkinter/windows/widgets/theme/theme_manager.py

@ -0,0 +1,47 @@
import sys
import os
import json
from typing import List, Union
class ThemeManager:
theme: dict = {} # contains all the theme data
_built_in_themes: List[str] = ["blue", "green", "dark-blue", "sweetkind"]
_currently_loaded_theme: Union[str, None] = None
@classmethod
def load_theme(cls, theme_name_or_path: str):
script_directory = os.path.dirname(os.path.abspath(__file__))
if theme_name_or_path in cls._built_in_themes:
with open(os.path.join(script_directory, "../../../assets", "themes", f"{theme_name_or_path}.json"), "r") as f:
cls.theme = json.load(f)
else:
with open(theme_name_or_path, "r") as f:
cls.theme = json.load(f)
# store theme path for saving
cls._currently_loaded_theme = theme_name_or_path
# filter theme values for platform
for key in cls.theme.keys():
# check if values for key differ on platforms
if "macOS" in cls.theme[key].keys():
if sys.platform == "darwin":
cls.theme[key] = cls.theme[key]["macOS"]
elif sys.platform.startswith("win"):
cls.theme[key] = cls.theme[key]["Windows"]
else:
cls.theme[key] = cls.theme[key]["Linux"]
@classmethod
def save_theme(cls):
if cls._currently_loaded_theme is not None:
if cls._currently_loaded_theme not in cls._built_in_themes:
with open(cls._currently_loaded_theme, "r") as f:
json.dump(cls.theme, f, indent=2)
else:
raise ValueError(f"cannot modify builtin theme '{cls._currently_loaded_theme}'")
else:
raise ValueError(f"cannot save theme, no theme is loaded")

1
customtkinter/windows/widgets/utility/__init__.py

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

BIN
customtkinter/windows/widgets/utility/__pycache__/__init__.cpython-310.pyc

Binary file not shown.

BIN
customtkinter/windows/widgets/utility/__pycache__/utility_functions.cpython-310.pyc

Binary file not shown.

22
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

134
main.py

@ -1,4 +1,5 @@
from data_structure import * from data_structure import *
import customtkinter as ctk
class Expression: class Expression:
"""manipule les expression sous forme d'arbre""" """manipule les expression sous forme d'arbre"""
@ -19,46 +20,152 @@ class Expression:
return True return True
return False return False
def evalue(self) -> float: def evalue(self, x=0) -> float:
"""renvoie le résultat de l'expression""" """renvoie le résultat de l'expression"""
if self.est_feuille(): if self.est_feuille():
if self.val == 'x':
return x
return float(self.val) return float(self.val)
if self.val == '+': if self.val == '+':
return self.fils_gauche.evalue() + self.fils_droit.evalue() return self.fils_gauche.evalue(x) + self.fils_droit.evalue(x)
if self.val == '*': if self.val == '*':
return self.fils_gauche.evalue() * self.fils_droit.evalue() return self.fils_gauche.evalue(x) * self.fils_droit.evalue(x)
if self.val == '/': if self.val == '/':
return self.fils_gauche.evalue() / self.fils_droit.evalue() return self.fils_gauche.evalue(x) / self.fils_droit.evalue(x)
if self.val == '^': if self.val == '^':
return self.fils_gauche.evalue() ** self.fils_droit.evalue() return self.fils_gauche.evalue(x) ** self.fils_droit.evalue(x)
if self.val == '-': if self.val == '-':
return self.fils_gauche.evalue() - self.fils_droit.evalue() return self.fils_gauche.evalue(x) - self.fils_droit.evalue(x)
def valeurs_de_fonction(self):
"""calcul les 100 premieres valeurs"""
result = []
for i in range(-100, 100):
result.append((i, self.evalue(i)))
return result
class App(ctk.CTk):
def __init__(self) -> None:
super().__init__()
self.grid_columnconfigure(0, weight=0)
# Calcul Frame
self.calcul_frame = ctk.CTkFrame(self,corner_radius=0, fg_color="transparent")
self.screen = ctk.CTkTextbox(self.calcul_frame)
self.screen.pack(padx=20, pady=20, fill="both", expand=True)
self.numbers = []
for i, val in enumerate(["(", ")", "/", "^", 7,8,9,"*", 4, 5, 6, "-", 1, 2, 3, "+", "clear", 0, '.', "exe"]): # add numbers button
self.numbers.append(ctk.CTkButton(self, text=val))
if val == "clear":
self.numbers[i]._command = self.textbox_clear
elif val == "exe":
self.numbers[i]._command = self.calculate
else:
self.numbers[i]._command = lambda x=val: self.add_value(x)
self.numbers[i].grid(row=1+i//4, column=i%4+1, sticky="NSEW", padx=5, pady=5)
# Fonction Frame
self.fonction_frame = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent")
self.fonction_screen = ctk.CTkCanvas(self.fonction_frame)
self.fonction_screen.pack(padx=20, pady=20, fill="both", expand=True)
# navigation menu
self.navigation_frame = ctk.CTkFrame(self, corner_radius=0)
self.navigation_frame.grid_rowconfigure(4, weight=1)
self.navigation_frame.grid(row=0, rowspan=10, column=0, sticky="nsew")
self.titre = ctk.CTkLabel(self.navigation_frame, text="Wx Calculator",
compound="left", font=ctk.CTkFont(size=15, weight="bold"))
self.titre.grid(row=0, column=0, padx=20, pady=20)
self.home_button = ctk.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Calcul",
fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"),
anchor="w", command=lambda: self.select_frame_by_name("Calcul"))
self.home_button.grid(row=1, column=0, sticky="ew")
self.function_button = ctk.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Fonction",
fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"),
anchor="w", command=lambda: self.select_frame_by_name("Fonction"))
self.function_button.grid(row=2, column=0, sticky="ew")
self.appearance_mode_menu = ctk.CTkOptionMenu(self.navigation_frame, values=["Light", "Dark", "System"],
command=self.change_appearance_mode_event)
self.appearance_mode_menu.grid(row=4, column=0, padx=20, pady=20, sticky="s")
# select default frame
self.select_frame_by_name("Calcul")
def select_frame_by_name(self, name):
# set button color for selected button
self.home_button.configure(fg_color=("gray75", "gray25") if name == "Calcul" else "transparent")
self.function_button.configure(fg_color=("gray75", "gray25") if name == "Fonction" else "transparent")
# show selected frame
if name == "Calcul":
self.calcul_frame.grid(columnspan=4, column=1, row=0, sticky="nsew")
else:
self.calcul_frame.grid_forget()
if name == "Fonction":
self.fonction_frame.grid(columnspan=4, column=1, row=0, sticky="nsew")
else:
self.fonction_frame.grid_forget()
def change_appearance_mode_event(self, new_appearance_mode):
ctk.set_appearance_mode(new_appearance_mode)
def add_value(self, value) -> None:
self.screen.insert('end', value)
def textbox_clear(self):
self.screen.delete("0.0", "end")
def draw_framing(self):
pass
def calculate(self) -> None:
exp = list(self.screen.get("0.0", "end").strip())
exp = inf2npi(exp)
result = npi2tree(exp).evalue()
self.textbox_clear()
self.screen.insert("end", result)
def npi2tree(expr: list) -> Expression: def npi2tree(expr: list) -> Expression:
"""renvoie l'arbre formé a partir de l'expression donnée""" """renvoie l'arbre formé a partir de l'expression donnée"""
pile = Pile_chaine() pile = Pile_chaine()
for val in expr: for val in expr:
if not val.isdigit(): if not val.isdigit() and val != "x":
# on inverse pour avoir les nombres dans le bon ordre # on inverse pour avoir les nombres dans le bon ordre
nombre2, nombre1 = pile.depiler(), pile.depiler() nombre2, nombre1 = pile.depiler(), pile.depiler()
pile.empiler(Expression(val, nombre1, nombre2)) pile.empiler(Expression(val, nombre1, nombre2))
else: else:
pile.empiler(Expression(int(val), None, None)) pile.empiler(Expression(val, None, None))
return pile.sommet() return pile.sommet()
def inf2npi(expr: list) -> list: def inf2npi(expr: list) -> list:
"""Transforme une expression infixé en notation polonaise inversée"""
operator_stack = Pile_chaine() operator_stack = Pile_chaine()
operator_priority = { operator_priority = {
'+': 1, '+': 1,
'-': 1, '-': 1,
'*': 2, '*': 2,
'^': 2,
'/': 2, '/': 2,
'(': 0, '(': 0,
')': 0 ')': 0
} }
output = [] output = []
for val in expr: for val in expr:
if val.isdigit(): if val.isdigit() or val == 'x':
output.append(val) output.append(val)
else: else:
if operator_stack.est_vide() or ( val == '(' or operator_priority[val] > operator_priority[operator_stack.sommet()]): if operator_stack.est_vide() or ( val == '(' or operator_priority[val] > operator_priority[operator_stack.sommet()]):
@ -82,7 +189,10 @@ def inf2npi(expr: list) -> list:
return output return output
# [3, '-', 6, '*', 4, '+', 3] # [3, '-', 6, '*', 4, '+', 3]
exp = inf2npi(list('6*(4+3)')) exp = inf2npi(list('x^2'))
print(exp) print(npi2tree(exp).evalue(2))
print(npi2tree(exp).valeurs_de_fonction())
print(npi2tree(exp)) gui = App()
gui.mainloop()
Loading…
Cancel
Save