25 Commits

Author SHA1 Message Date
f5fdd77584 Bump to 4.2.0 2022-05-30 17:30:27 +02:00
940ed128bd fixed geometry string scaling 2022-05-30 17:20:27 +02:00
60b13bf215 fixes for DropdownMenu 2022-05-30 16:46:36 +02:00
a50e2ea9ca added test_optionmenu.py 2022-05-30 15:48:41 +02:00
15558b4d0f added dropdown arrow with font files 2022-05-30 15:35:23 +02:00
aa46c56da9 small fixes in examples 2022-05-30 14:35:33 +02:00
cf6f513afc added click support for labels of CTkCheckBox, CTkRadioButton, CTkSwitch 2022-05-30 13:39:10 +02:00
9d618386e1 fixed scaling for DropdownMenu 2022-05-27 01:09:54 +02:00
9bd55cc159 implemented overwrite_preferred_drawing_method parameter in DrawEngine 2022-05-27 00:30:06 +02:00
8a87b6f926 updated color for blue theme 2022-05-27 00:12:37 +02:00
a1afc3056b removed scaling with ctypes shcore for Windows < 8.1 2022-05-26 19:16:01 +02:00
34da9505e9 DropDown fixes for Windows 2022-05-25 22:40:30 +02:00
91a8687736 added DropdownMenu and CTkOptionMenu 2022-05-25 22:14:38 +02:00
e96165d212 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	customtkinter/draw_engine.py
2022-05-25 18:42:42 +02:00
aa8c96a2c4 added overwrite_preferred_drawing_method parameter to DrawEngine 2022-05-25 18:40:07 +02:00
fd8135129c Merge remote-tracking branch 'origin/master'
# Conflicts:
#	test/manual_integration_tests/test_new_menu_design.py
2022-05-25 18:38:49 +02:00
5f88db11aa updated test_new_menu_design.py 2022-05-25 18:37:55 +02:00
1fed35a193 added draw_rounded_rect_with_border_vertical_split() function to DrawEngine 2022-05-25 17:04:00 +02:00
4b3b406250 updated test_new_menu_design.py 2022-05-24 13:50:34 +02:00
f49c83d2dc Bump to 4.1.0 2022-05-24 01:03:14 +02:00
4389c3e86b added configurable dimensions to some widgets 2022-05-24 01:00:58 +02:00
25297c2598 Bump to 4.0.4 2022-05-23 22:35:57 +02:00
4e155aedd6 fixed bug in fg_color detection of master 2022-05-23 22:35:38 +02:00
3a5d34cef6 Merge pull request #106 from bengy3d/linux-font-hotfix
Fixed loading fonts on linux
2022-05-23 16:27:01 +02:00
e42db49ca5 Fixed loading fonts on linux 2022-05-23 16:16:09 +02:00
32 changed files with 1171 additions and 169 deletions

View File

@ -4,14 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.1.0] - 2022-05-24
### Added
- Configure width and height for frame, button, label, progressbar, slider, entry
## [4.0.0] - 2022-05-22
### Added
- This changelog file
- Adopted semantic versioning
- Added HighDPI scaling to all widgets and geometry managers (place, pack, grid)
- Restructured CTkSettings and renamed a few manager classes
### Changed
- Orientation attribute for slider and progressbar
### Removed
- A few unnecessary tests

View File

@ -16,7 +16,8 @@ CustomTkinter is a python UI-library based on Tkinter, which provides new, moder
fully customizable widgets. They are created and used like normal Tkinter widgets and
can also be used in combination with normal Tkinter elements. The widgets
and the window colors either adapt to the system appearance or the manually set mode
('light', 'dark'). With CustomTkinter you'll get a consistent and modern look across all
('light', 'dark'), and all CustomTkinter widgets and windows support HighDPI scaling
(Windows, macOS). With CustomTkinter you'll get a consistent and modern look across all
desktop platforms (Windows, macOS, Linux).

View File

@ -1,8 +1,9 @@
__version__ = "4.0.3"
__version__ = "4.2.0"
import os
import sys
from tkinter.constants import *
from tkinter import StringVar, IntVar, DoubleVar, BooleanVar
# import manager classes
from .settings import Settings
@ -22,13 +23,16 @@ if sys.platform == "darwin":
else:
DrawEngine.preferred_drawing_method = "font_shapes"
if sys.platform.startswith("win") and sys.getwindowsversion().build < 9000: # No automatic scaling on Windows < 8.1
ScalingTracker.deactivate_automatic_dpi_awareness = True
# load Roboto fonts (used on Windows/Linux)
script_directory = os.path.dirname(os.path.abspath(__file__))
FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf"))
FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Medium.ttf"))
# load font necessary for rendering the widgets (used on Windows/Linux)
if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "CustomTkinter_shapes_font-fine.otf")) is False:
if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "CustomTkinter_shapes_font.otf")) is False:
# change draw method if font loading failed
if DrawEngine.preferred_drawing_method == "font_shapes":
sys.stderr.write("customtkinter.__init__ warning: " +
@ -47,6 +51,7 @@ from .widgets.ctk_label import CTkLabel
from .widgets.ctk_radiobutton import CTkRadioButton
from .widgets.ctk_canvas import CTkCanvas
from .widgets.ctk_switch import CTkSwitch
from .widgets.ctk_optionmenu import CTkOptionMenu
# import windows
from .windows.ctk_tk import CTk

View File

@ -1,32 +1,37 @@
{
"color": {
"window_bg_color": ["gray95", "gray12"],
"button":["#5B97D3", "#3373B8"],
"button_hover": ["#4A7BAD", "#1D538D"],
"button_border": ["gray40", "#D5D9DE"],
"checkbox_border": ["gray40", "#D5D9DE"],
"window_bg_color": ["#EBEBEC", "#212325"],
"button": ["#3B8ED0", "#1F6AA5"],
"button_hover": ["#36719F", "#144870"],
"button_border": ["#3E454A", "#949A9F"],
"checkbox_border": ["#3E454A", "#949A9F"],
"checkmark": ["white", "gray90"],
"entry": ["white", "gray24"],
"entry_border": ["gray70", "gray32"],
"entry": ["#F9F9FA", "#343638"],
"entry_border": ["#979DA2", "#565B5E"],
"entry_placeholder_text": ["gray52", "gray62"],
"frame_border": ["#A7C2E0", "#5FB4DD"],
"frame_low": ["#E3E4E5", "gray16"],
"frame_high": ["#D7D8D9", "gray22"],
"frame_border": ["#979DA2", "#1F2122"],
"frame_low": ["#D1D5D8", "#2A2D2E"],
"frame_high": ["#D7D8D9", "#343638"],
"label": [null, null],
"text": ["gray20", "#D5D9DE"],
"text": ["gray10", "#DCE4EE"],
"text_disabled": ["gray60", "#777B80"],
"text_button_disabled": ["gray40", "gray74"],
"progressbar": ["#6B6B6B", "gray0"],
"progressbar_progress": ["#5B97D3", "#3373B8"],
"progressbar": ["#939BA2", "#4A4D50"],
"progressbar_progress": ["#3B8ED0", "#1F6AA5"],
"progressbar_border": ["gray", "gray"],
"slider": ["#6B6B6B", "gray0"],
"slider_progress": ["white", "gray40"],
"slider_button": ["#5B97D3", "#3373B8"],
"slider_button_hover": ["#4A7BAD", "#1D538D"],
"switch": ["gray70", "gray35"],
"switch_progress": ["#5B97D3", "#3373B8"],
"slider": ["#939BA2", "#4A4D50"],
"slider_progress": ["white", "#AAB0B5"],
"slider_button": ["#3B8ED0", "#1F6AA5"],
"slider_button_hover": ["#36719F", "#144870"],
"switch": ["#939BA2", "#4A4D50"],
"switch_progress": ["#3B8ED0", "#1F6AA5"],
"switch_button": ["gray36", "#D5D9DE"],
"switch_button_hover": ["gray20", "gray100"]
"switch_button_hover": ["gray20", "gray100"],
"optionmenu_button": ["#36719F", "#144870"],
"optionmenu_button_hover": ["#27577D", "#203A4F"],
"dropdown_color": ["#A8ACB1", "#535353"],
"dropdown_hover": ["#D6DCE2", "#46484A"],
"dropdown_text": ["gray10", "#DCE4EE"]
},
"text": {
"macOS": {
@ -43,21 +48,21 @@
}
},
"shape": {
"button_corner_radius": 8,
"button_corner_radius": 6,
"button_border_width": 0,
"checkbox_corner_radius": 7,
"checkbox_corner_radius": 6,
"checkbox_border_width": 3,
"radiobutton_corner_radius": 1000,
"radiobutton_border_width_unchecked": 3,
"radiobutton_border_width_checked": 6,
"entry_border_width": 2,
"frame_corner_radius": 8,
"frame_corner_radius": 6,
"frame_border_width": 0,
"label_corner_radius": 8,
"progressbar_border_width": 0,
"progressbar_corner_radius": 1000,
"slider_border_width": 6,
"slider_corner_radius": 8,
"slider_corner_radius": 1000,
"slider_button_length": 0,
"slider_button_corner_radius": 1000,
"switch_border_width": 3,

View File

@ -17,9 +17,11 @@ class DrawEngine:
Functions:
- draw_rounded_rect_with_border()
- draw_rounded_rect_with_border_vertical_split()
- draw_rounded_progress_bar_with_border()
- draw_rounded_slider_with_border_and_button()
- draw_checkmark()
- draw_dropdown_arrow()
"""
@ -27,9 +29,8 @@ class DrawEngine:
def __init__(self, canvas: CTkCanvas):
self._canvas = canvas
self._existing_tags = set()
def _calc_optimal_corner_radius(self, user_corner_radius: Union[float, int]) -> Union[float, int]:
def __calc_optimal_corner_radius(self, user_corner_radius: Union[float, int]) -> Union[float, int]:
# optimize for drawing with polygon shapes
if self.preferred_drawing_method == "polygon_shapes":
if sys.platform == "darwin":
@ -53,7 +54,8 @@ class DrawEngine:
else:
return user_corner_radius
def draw_rounded_rect_with_border(self, width: int, height: int, corner_radius: Union[float, int], border_width: Union[float, int]) -> bool:
def draw_rounded_rect_with_border(self, width: int, height: int, corner_radius: Union[float, int], border_width: Union[float, int],
overwrite_preferred_drawing_method: str = None) -> bool:
""" Draws a rounded rectangle with a corner_radius and border_width on the canvas. The border elements have a 'border_parts' tag,
the main foreground elements have an 'inner_parts' tag to color the elements accordingly.
@ -67,21 +69,26 @@ class DrawEngine:
corner_radius = min(width / 2, height / 2)
border_width = round(border_width)
corner_radius = self._calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
if corner_radius >= border_width:
inner_corner_radius = corner_radius - border_width
else:
inner_corner_radius = 0
if self.preferred_drawing_method == "polygon_shapes":
return self._draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius)
elif self.preferred_drawing_method == "font_shapes":
return self._draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ())
elif self.preferred_drawing_method == "circle_shapes":
return self._draw_rounded_rect_with_border_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius)
if overwrite_preferred_drawing_method is not None:
preferred_drawing_method = overwrite_preferred_drawing_method
else:
preferred_drawing_method = self.preferred_drawing_method
def _draw_rounded_rect_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool:
if preferred_drawing_method == "polygon_shapes":
return self.__draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius)
elif preferred_drawing_method == "font_shapes":
return self.__draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ())
elif preferred_drawing_method == "circle_shapes":
return self.__draw_rounded_rect_with_border_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius)
def __draw_rounded_rect_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool:
requires_recoloring = False
# create border button parts (only if border exists)
@ -134,8 +141,8 @@ class DrawEngine:
return requires_recoloring
def _draw_rounded_rect_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
exclude_parts: tuple) -> bool:
def __draw_rounded_rect_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
exclude_parts: tuple) -> bool:
requires_recoloring = False
# create border button parts
@ -272,7 +279,7 @@ class DrawEngine:
return requires_recoloring
def _draw_rounded_rect_with_border_circle_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool:
def __draw_rounded_rect_with_border_circle_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool:
requires_recoloring = False
# border button parts
@ -346,6 +353,285 @@ class DrawEngine:
return requires_recoloring
def draw_rounded_rect_with_border_vertical_split(self, width: int, height: int, corner_radius: Union[float, int], border_width: Union[float, int],
left_section_width: int) -> bool:
""" Draws a rounded rectangle with a corner_radius and border_width on the canvas which is split at left_section_width.
The border elements have the tags 'border_parts_left', 'border_parts_lright',
the main foreground elements have an 'inner_parts_left' and inner_parts_right' tag,
to color the elements accordingly.
returns bool if recoloring is necessary """
width = math.floor(width / 2) * 2 # round (floor) current_width and current_height and restrict them to even values only
height = math.floor(height / 2) * 2
corner_radius = round(corner_radius)
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
corner_radius = min(width / 2, height / 2)
border_width = round(border_width)
corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
if corner_radius >= border_width:
inner_corner_radius = corner_radius - border_width
else:
inner_corner_radius = 0
if left_section_width > width - corner_radius * 2:
left_section_width = width - corner_radius * 2
elif left_section_width < corner_radius * 2:
left_section_width = corner_radius * 2
if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes":
return self.__draw_rounded_rect_with_border_vertical_split_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, left_section_width)
elif self.preferred_drawing_method == "font_shapes":
return self.__draw_rounded_rect_with_border_vertical_split_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, left_section_width, ())
def __draw_rounded_rect_with_border_vertical_split_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
left_section_width: int) -> bool:
requires_recoloring = False
# create border button parts (only if border exists)
if border_width > 0:
if not self._canvas.find_withtag("border_parts"):
self._canvas.create_polygon((0, 0, 0, 0), tags=("border_line_left_1", "border_parts_left", "border_parts"))
self._canvas.create_polygon((0, 0, 0, 0), tags=("border_line_right_1", "border_parts_right", "border_parts"))
self._canvas.create_rectangle((0, 0, 0, 0), tags=("border_rect_left_1", "border_parts_left", "border_parts"))
self._canvas.create_rectangle((0, 0, 0, 0), tags=("border_rect_right_1", "border_parts_right", "border_parts"))
requires_recoloring = True
self._canvas.coords("border_line_left_1",
(corner_radius,
corner_radius,
left_section_width - corner_radius,
corner_radius,
left_section_width - corner_radius,
height - corner_radius,
corner_radius,
height - corner_radius))
self._canvas.coords("border_line_right_1",
(left_section_width + corner_radius,
corner_radius,
width - corner_radius,
corner_radius,
width - corner_radius,
height - corner_radius,
left_section_width + corner_radius,
height - corner_radius))
self._canvas.coords("border_rect_left_1",
(left_section_width - corner_radius,
0,
left_section_width,
height))
self._canvas.coords("border_rect_right_1",
(left_section_width,
0,
left_section_width + corner_radius,
height))
self._canvas.itemconfig("border_line_left_1", joinstyle=tkinter.ROUND, width=corner_radius * 2)
self._canvas.itemconfig("border_line_right_1", joinstyle=tkinter.ROUND, width=corner_radius * 2)
else:
self._canvas.delete("border_parts")
# create inner button parts
if not self._canvas.find_withtag("inner_parts"):
self._canvas.create_polygon((0, 0, 0, 0), tags=("inner_line_left_1", "inner_parts_left", "inner_parts"), joinstyle=tkinter.ROUND)
self._canvas.create_polygon((0, 0, 0, 0), tags=("inner_line_right_1", "inner_parts_right", "inner_parts"), joinstyle=tkinter.ROUND)
self._canvas.create_rectangle((0, 0, 0, 0), tags=("inner_rect_left_1", "inner_parts_left", "inner_parts"))
self._canvas.create_rectangle((0, 0, 0, 0), tags=("inner_rect_right_1", "inner_parts_right", "inner_parts"))
requires_recoloring = True
self._canvas.coords("inner_line_left_1",
corner_radius,
corner_radius,
left_section_width - corner_radius,
corner_radius,
left_section_width - corner_radius,
height - corner_radius,
corner_radius,
height - corner_radius)
self._canvas.coords("inner_line_right_1",
left_section_width + corner_radius,
corner_radius,
width - corner_radius,
corner_radius,
width - corner_radius,
height - corner_radius,
left_section_width + corner_radius,
height - corner_radius)
self._canvas.coords("inner_rect_left_1",
(left_section_width - inner_corner_radius,
border_width,
left_section_width,
height - border_width))
self._canvas.coords("inner_rect_right_1",
(left_section_width,
border_width,
left_section_width + inner_corner_radius,
height - border_width))
self._canvas.itemconfig("inner_line_left_1", width=inner_corner_radius * 2)
self._canvas.itemconfig("inner_line_right_1", width=inner_corner_radius * 2)
if requires_recoloring: # new parts were added -> manage z-order
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
return requires_recoloring
def __draw_rounded_rect_with_border_vertical_split_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
left_section_width: int, exclude_parts: tuple) -> bool:
requires_recoloring = False
# create border button parts
if border_width > 0:
if corner_radius > 0:
# create canvas border corner parts if not already created, but only if needed, and delete if not needed
if not self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_a", "border_corner_part", "border_parts_left" "border_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_b", "border_corner_part", "border_parts_left", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" in exclude_parts:
self._canvas.delete("border_oval_1_a", "border_oval_1_b")
if not self._canvas.find_withtag("border_oval_2_a") and width > 2 * corner_radius and "border_oval_2" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_a", "border_corner_part", "border_parts_right", "border_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_b", "border_corner_part", "border_parts_right", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("border_oval_2_a") and (not width > 2 * corner_radius or "border_oval_2" in exclude_parts):
self._canvas.delete("border_oval_2_a", "border_oval_2_b")
if not self._canvas.find_withtag("border_oval_3_a") and height > 2 * corner_radius \
and width > 2 * corner_radius and "border_oval_3" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_a", "border_corner_part", "border_parts_right", "border_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_parts_right", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("border_oval_3_a") and (not (height > 2 * corner_radius
and width > 2 * corner_radius) or "border_oval_3" in exclude_parts):
self._canvas.delete("border_oval_3_a", "border_oval_3_b")
if not self._canvas.find_withtag("border_oval_4_a") and height > 2 * corner_radius and "border_oval_4" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_a", "border_corner_part", "border_parts_left", "border_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_b", "border_corner_part", "border_parts_left", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("border_oval_4_a") and (not height > 2 * corner_radius or "border_oval_4" in exclude_parts):
self._canvas.delete("border_oval_4_a", "border_oval_4_b")
# change position of border corner parts
self._canvas.coords("border_oval_1_a", corner_radius, corner_radius, corner_radius)
self._canvas.coords("border_oval_1_b", corner_radius, corner_radius, corner_radius)
self._canvas.coords("border_oval_2_a", width - corner_radius, corner_radius, corner_radius)
self._canvas.coords("border_oval_2_b", width - corner_radius, corner_radius, corner_radius)
self._canvas.coords("border_oval_3_a", width - corner_radius, height - corner_radius, corner_radius)
self._canvas.coords("border_oval_3_b", width - corner_radius, height - corner_radius, corner_radius)
self._canvas.coords("border_oval_4_a", corner_radius, height - corner_radius, corner_radius)
self._canvas.coords("border_oval_4_b", corner_radius, height - corner_radius, corner_radius)
else:
self._canvas.delete("border_corner_part") # delete border corner parts if not needed
# create canvas border rectangle parts if not already created
if not self._canvas.find_withtag("border_rectangle_1"):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_left_1", "border_rectangle_part", "border_parts_left", "border_parts"), width=0)
self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_left_2", "border_rectangle_part", "border_parts_left", "border_parts"), width=0)
self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_right_1", "border_rectangle_part", "border_parts_right", "border_parts"), width=0)
self._canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_right_2", "border_rectangle_part", "border_parts_right", "border_parts"), width=0)
requires_recoloring = True
# change position of border rectangle parts
self._canvas.coords("border_rectangle_left_1", (0, corner_radius, left_section_width, height - corner_radius))
self._canvas.coords("border_rectangle_left_2", (corner_radius, 0, left_section_width, height))
self._canvas.coords("border_rectangle_right_1", (left_section_width, corner_radius, width, height - corner_radius))
self._canvas.coords("border_rectangle_right_2", (corner_radius, left_section_width, width - corner_radius, height))
else:
self._canvas.delete("border_parts")
# create inner button parts
if inner_corner_radius > 0:
# create canvas border corner parts if not already created, but only if they're needed and delete if not needed
if not self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_a", "inner_corner_part", "inner_parts_left", "inner_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_b", "inner_corner_part", "inner_parts_left", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" in exclude_parts:
self._canvas.delete("inner_oval_1_a", "inner_oval_1_b")
if not self._canvas.find_withtag("inner_oval_2_a") and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_2" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_a", "inner_corner_part","inner_parts_right", "inner_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_b", "inner_corner_part", "inner_parts_right","inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_2_a") and (not width - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_2" in exclude_parts):
self._canvas.delete("inner_oval_2_a", "inner_oval_2_b")
if not self._canvas.find_withtag("inner_oval_3_a") and height - (2 * border_width) > 2 * inner_corner_radius \
and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_3" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_a", "inner_corner_part", "inner_parts_right","inner_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_b", "inner_corner_part", "inner_parts_right", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_3_a") and (not (height - (2 * border_width) > 2 * inner_corner_radius
and width - (2 * border_width) > 2 * inner_corner_radius) or "inner_oval_3" in exclude_parts):
self._canvas.delete("inner_oval_3_a", "inner_oval_3_b")
if not self._canvas.find_withtag("inner_oval_4_a") and height - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_4" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_a", "inner_corner_part", "inner_parts_left", "inner_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_b", "inner_corner_part", "inner_parts_left", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_4_a") and (not height - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_4" in exclude_parts):
self._canvas.delete("inner_oval_4_a", "inner_oval_4_b")
# change position of border corner parts
self._canvas.coords("inner_oval_1_a", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_1_b", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_2_a", width - border_width - inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_2_b", width - border_width - inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_3_a", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_3_b", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_4_a", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
self._canvas.coords("inner_oval_4_b", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius)
else:
self._canvas.delete("inner_corner_part") # delete inner corner parts if not needed
# create canvas inner rectangle parts if not already created
if not self._canvas.find_withtag("inner_rectangle_1"):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_left_1", "inner_rectangle_part", "inner_parts_left", "inner_parts"), width=0)
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_right_1", "inner_rectangle_part", "inner_parts_right", "inner_parts"), width=0)
requires_recoloring = True
if not self._canvas.find_withtag("inner_rectangle_2") and inner_corner_radius * 2 < height - (border_width * 2):
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_left_2", "inner_rectangle_part", "inner_parts_left", "inner_parts"), width=0)
self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_right_2", "inner_rectangle_part", "inner_parts_right", "inner_parts"), width=0)
requires_recoloring = True
elif self._canvas.find_withtag("inner_rectangle_2") and not inner_corner_radius * 2 < height - (border_width * 2):
self._canvas.delete("inner_rectangle_left_2")
self._canvas.delete("inner_rectangle_right_2")
# change position of inner rectangle parts
self._canvas.coords("inner_rectangle_left_1", (border_width + inner_corner_radius,
border_width,
left_section_width,
height - border_width))
self._canvas.coords("inner_rectangle_left_2", (border_width,
border_width + inner_corner_radius,
left_section_width,
height - inner_corner_radius - border_width))
self._canvas.coords("inner_rectangle_right_1", (left_section_width,
border_width,
width - border_width - inner_corner_radius,
height - border_width))
self._canvas.coords("inner_rectangle_right_2", (left_section_width,
border_width + inner_corner_radius,
width - border_width,
height - inner_corner_radius - border_width))
if requires_recoloring: # new parts were added -> manage z-order
self._canvas.tag_lower("inner_parts")
self._canvas.tag_lower("border_parts")
return requires_recoloring
def draw_rounded_progress_bar_with_border(self, width: int, height: int, corner_radius: Union[float, int], border_width: Union[float, int],
progress_value: float, orientation: str) -> bool:
""" Draws a rounded bar on the canvas, which is split in half according to the argument 'progress_value' (0 - 1).
@ -361,7 +647,7 @@ class DrawEngine:
corner_radius = min(width / 2, height / 2)
border_width = round(border_width)
corner_radius = self._calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
if corner_radius >= border_width:
inner_corner_radius = corner_radius - border_width
@ -369,16 +655,16 @@ class DrawEngine:
inner_corner_radius = 0
if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes":
return self._draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
progress_value, orientation)
return self.__draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
progress_value, orientation)
elif self.preferred_drawing_method == "font_shapes":
return self._draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
progress_value, orientation)
return self.__draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
progress_value, orientation)
def _draw_rounded_progress_bar_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
progress_value: float, orientation: str) -> bool:
def __draw_rounded_progress_bar_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
progress_value: float, orientation: str) -> bool:
requires_recoloring = self._draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius)
requires_recoloring = self.__draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius)
if corner_radius <= border_width:
bottom_right_shift = 0 # weird canvas rendering inaccuracy that has to be corrected in some cases
@ -417,8 +703,8 @@ class DrawEngine:
return requires_recoloring
def _draw_rounded_progress_bar_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
progress_value: float, orientation: str) -> bool:
def __draw_rounded_progress_bar_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
progress_value: float, orientation: str) -> bool:
requires_recoloring, requires_recoloring_2 = False, False
@ -452,8 +738,8 @@ class DrawEngine:
# horizontal orientation from the bottom
if orientation == "w":
requires_recoloring_2 = self._draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
("inner_oval_1", "inner_oval_4"))
requires_recoloring_2 = self.__draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
("inner_oval_1", "inner_oval_4"))
# set positions of progress corner parts
self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius)
@ -483,8 +769,8 @@ class DrawEngine:
# vertical orientation from the bottom
if orientation == "s":
requires_recoloring_2 = self._draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
("inner_oval_3", "inner_oval_4"))
requires_recoloring_2 = self.__draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
("inner_oval_3", "inner_oval_4"))
# set positions of progress corner parts
self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius,
@ -530,7 +816,7 @@ class DrawEngine:
button_length = round(button_length)
border_width = round(border_width)
button_corner_radius = round(button_corner_radius)
corner_radius = self._calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
corner_radius = self.__calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
if corner_radius >= border_width:
inner_corner_radius = corner_radius - border_width
@ -538,18 +824,18 @@ class DrawEngine:
inner_corner_radius = 0
if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes":
return self._draw_rounded_slider_with_border_and_button_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
button_length, button_corner_radius, slider_value, orientation)
return self.__draw_rounded_slider_with_border_and_button_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
button_length, button_corner_radius, slider_value, orientation)
elif self.preferred_drawing_method == "font_shapes":
return self._draw_rounded_slider_with_border_and_button_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
button_length, button_corner_radius, slider_value, orientation)
return self.__draw_rounded_slider_with_border_and_button_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
button_length, button_corner_radius, slider_value, orientation)
def _draw_rounded_slider_with_border_and_button_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
button_length: int, button_corner_radius: int, slider_value: float, orientation: str) -> bool:
def __draw_rounded_slider_with_border_and_button_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
button_length: int, button_corner_radius: int, slider_value: float, orientation: str) -> bool:
# draw normal progressbar
requires_recoloring = self._draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
slider_value, orientation)
requires_recoloring = self.__draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
slider_value, orientation)
# create slider button part
if not self._canvas.find_withtag("slider_parts"):
@ -583,12 +869,12 @@ class DrawEngine:
return requires_recoloring
def _draw_rounded_slider_with_border_and_button_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
button_length: int, button_corner_radius: int, slider_value: float, orientation: str) -> bool:
def __draw_rounded_slider_with_border_and_button_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
button_length: int, button_corner_radius: int, slider_value: float, orientation: str) -> bool:
# draw normal progressbar
requires_recoloring = self._draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
slider_value, orientation)
requires_recoloring = self.__draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
slider_value, orientation)
# create 4 circles (if not needed, then less)
if not self._canvas.find_withtag("slider_oval_1_a"):
@ -701,3 +987,35 @@ class DrawEngine:
self._canvas.coords("checkmark", round(width / 2), round(height / 2))
return requires_recoloring
def draw_dropdown_arrow(self, x_position: Union[int, float], y_position: Union[int, float], size: Union[int, float]) -> bool:
""" Draws a dropdown bottom facing arrow at (x_position, y_position) in a given size
returns bool if recoloring is necessary """
x_position, y_position, size = round(x_position), round(y_position), round(size)
requires_recoloring = False
if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes":
if not self._canvas.find_withtag("dropdown_arrow"):
self._canvas.create_line(0, 0, 0, 0, tags="dropdown_arrow", width=round(size / 3), joinstyle=tkinter.ROUND, capstyle=tkinter.ROUND)
self._canvas.tag_raise("dropdown_arrow")
requires_recoloring = True
self._canvas.coords("dropdown_arrow",
x_position - (size / 2),
y_position - (size / 5),
x_position,
y_position + (size / 5),
x_position + (size / 2),
y_position - (size / 5))
elif self.preferred_drawing_method == "font_shapes":
if not self._canvas.find_withtag("dropdown_arrow"):
self._canvas.create_text(0, 0, text="Y", font=("CustomTkinter_shapes_font", -size), tags="dropdown_arrow", anchor=tkinter.CENTER)
self._canvas.tag_raise("dropdown_arrow")
requires_recoloring = True
self._canvas.coords("dropdown_arrow", x_position, y_position)
return requires_recoloring

View File

@ -51,7 +51,7 @@ class FontManager:
return cls.windows_load_font(font_path, private=True, enumerable=False)
# Linux
elif sys.platform.startswith("win"):
elif sys.platform.startswith("linux"):
try:
shutil.copy(font_path, os.path.expanduser("~/.fonts/"))
return True

View File

@ -124,7 +124,7 @@ class ScalingTracker:
@classmethod
def activate_high_dpi_awareness(cls):
""" make process DPI aware, customtkinter elemets will get scaled automatically,
""" 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:
@ -135,37 +135,40 @@ class ScalingTracker:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(2)
# Microsoft Docs: https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness
else:
pass # DPI awareness on Linux not implemented
@classmethod
def get_window_dpi_scaling(cls, window) -> float:
if sys.platform == "darwin":
return 1 # scaling works automatically on macOS
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
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)
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 # DPI awareness on Linux not implemented
return 1
@classmethod
def check_dpi_scaling(cls):
# check for every window if scaling value changed
for window in cls.window_widgets_dict:
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
cls.update_scaling_callbacks_for_window(window)
if window.winfo_exists():
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
cls.update_scaling_callbacks_for_window(window)
# find an existing tkinter object for the next call of .after()
for app in cls.window_widgets_dict.keys():

View File

@ -62,6 +62,19 @@ class ThemeManager:
return cls.rgb2hex(new_rgb)
@classmethod
def get_minimal_darker(cls, color: str) -> str:
if color.startswith("#"):
color_rgb = cls.hex2rgb(color)
if color_rgb[0] > 0:
return cls.rgb2hex((color_rgb[0] - 1, color_rgb[1], color_rgb[2]))
elif color_rgb[1] > 0:
return cls.rgb2hex((color_rgb[0], color_rgb[1] - 1, color_rgb[2]))
elif color_rgb[2] > 0:
return cls.rgb2hex((color_rgb[0], color_rgb[1], color_rgb[2] - 1))
else:
return cls.rgb2hex((color_rgb[0] + 1, color_rgb[1], color_rgb[2] - 1)) # otherwise slightly lighter
@classmethod
def multiply_hex_color(cls, hex_color: str, factor: float = 1.0) -> str:
try:

View File

@ -21,7 +21,7 @@ class CTkButton(CTkBaseClass):
command=None,
textvariable=None,
width=120,
height=30,
height=28,
corner_radius="default_theme",
text_font="default_theme",
text_color="default_theme",
@ -99,7 +99,14 @@ class CTkButton(CTkBaseClass):
self.image_label = None
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height)),
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):
@ -290,6 +297,14 @@ class CTkButton(CTkBaseClass):
self.text_label.configure(textvariable=self.textvariable)
del kwargs["textvariable"]
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
super().configure(*args, **kwargs)
if require_redraw:

View File

@ -1,5 +1,6 @@
import tkinter
import sys
from typing import Union
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -49,7 +50,7 @@ class CTkCheckBox(CTkBaseClass):
# text
self.text = text
self.text_label = None
self.text_label: Union[tkinter.Label, None] = None
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
@ -85,11 +86,8 @@ class CTkCheckBox(CTkBaseClass):
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, rowspan=1)
self.draw_engine = DrawEngine(self.canvas)
if self.hover is True:
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.toggle)
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.toggle)
# set select state according to variable
@ -100,8 +98,8 @@ class CTkCheckBox(CTkBaseClass):
elif self.variable.get() == self.offvalue:
self.deselect(from_variable_callback=True)
self.set_cursor()
self.draw() # initial draw
self.set_cursor()
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
@ -164,6 +162,10 @@ class CTkCheckBox(CTkBaseClass):
self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self.text_label["anchor"] = "w"
self.text_label.bind("<Enter>", self.on_enter)
self.text_label.bind("<Leave>", self.on_leave)
self.text_label.bind("<Button-1>", self.toggle)
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self.appearance_mode)))
else:
@ -244,14 +246,22 @@ class CTkCheckBox(CTkBaseClass):
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
if self.text_label is not None:
self.text_label.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
if self.text_label is not None:
self.text_label.configure(cursor="hand2")
def set_text(self, text):
self.text = text

View File

@ -18,7 +18,7 @@ class CTkEntry(CTkBaseClass):
border_width="default_theme",
border_color="default_theme",
width=120,
height=30,
height=28,
state=tkinter.NORMAL,
**kwargs):
@ -81,6 +81,13 @@ class CTkEntry(CTkBaseClass):
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 set_placeholder(self, event=None):
if self.placeholder_text is not None:
if not self.placeholder_text_active and self.entry.get() == "":
@ -181,6 +188,14 @@ class CTkEntry(CTkBaseClass):
del kwargs["corner_radius"]
require_redraw = True
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
if "placeholder_text" in kwargs:
pass

View File

@ -1,8 +1,5 @@
import tkinter
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..settings import Settings
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
@ -16,6 +13,7 @@ class CTkFrame(CTkBaseClass):
corner_radius="default_theme",
width=200,
height=200,
overwrite_preferred_drawing_method: str = None,
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
@ -24,6 +22,7 @@ class CTkFrame(CTkBaseClass):
# color
self.border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color
# determine fg_color of frame
if fg_color == "default_theme":
if isinstance(self.master, CTkFrame):
if self.master.fg_color == ThemeManager.theme["color"]["frame_low"]:
@ -46,6 +45,7 @@ class CTkFrame(CTkBaseClass):
self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.draw_engine = DrawEngine(self.canvas)
self._overwrite_preferred_drawing_method = overwrite_preferred_drawing_method
self.bind('<Configure>', self.update_dimensions_event)
@ -68,12 +68,20 @@ class CTkFrame(CTkBaseClass):
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def set_dimensions(self, width=None, height=None):
super().set_dimensions(width, height)
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.draw()
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self.current_width),
self.apply_widget_scaling(self.current_height),
self.apply_widget_scaling(self.corner_radius),
self.apply_widget_scaling(self.border_width))
self.apply_widget_scaling(self.border_width),
overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method)
if no_color_updates is False or requires_recoloring:
if self.fg_color is None:
@ -130,6 +138,14 @@ class CTkFrame(CTkBaseClass):
require_redraw = True
del kwargs["border_width"]
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
super().configure(*args, **kwargs)
if require_redraw:

View File

@ -13,7 +13,7 @@ class CTkLabel(CTkBaseClass):
text_color="default_theme",
corner_radius="default_theme",
width=120,
height=25,
height=28,
text="CTkLabel",
text_font="default_theme",
**kwargs):
@ -69,6 +69,13 @@ class CTkLabel(CTkBaseClass):
self.draw()
def set_dimensions(self, width=None, height=None):
super().set_dimensions(width, height)
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.draw()
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self.current_width),
self.apply_widget_scaling(self.current_height),
@ -118,6 +125,14 @@ class CTkLabel(CTkBaseClass):
require_redraw = True
del kwargs["text_color"]
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
self.text_label.configure(*args, **kwargs)
if require_redraw:

View File

@ -0,0 +1,312 @@
import tkinter
import sys
from .dropdown_menu import DropdownMenu
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from ..settings import Settings
from ..draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
class CTkOptionMenu(CTkBaseClass):
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
button_color="default_theme",
button_hover_color="default_theme",
dropdown_color="default_theme",
dropdown_hover_color="default_theme",
dropdown_text_color="default_theme",
variable=None,
values=None,
command=None,
width=120,
height=28,
corner_radius="default_theme",
text_font="default_theme",
text_color="default_theme",
text_color_disabled="default_theme",
hover=True,
state=tkinter.NORMAL,
**kwargs):
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs)
# color variables
self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.button_color = ThemeManager.theme["color"]["optionmenu_button"] if button_color == "default_theme" else button_color
self.button_hover_color = ThemeManager.theme["color"]["optionmenu_button_hover"] if button_hover_color == "default_theme" else button_hover_color
self.dropdown_color = ThemeManager.theme["color"]["dropdown_color"] if dropdown_color == "default_theme" else dropdown_color
self.dropdown_hover_color = ThemeManager.theme["color"]["dropdown_hover"] if dropdown_hover_color == "default_theme" else dropdown_hover_color
self.dropdown_text_color = ThemeManager.theme["color"]["dropdown_text"] if dropdown_text_color == "default_theme" else dropdown_text_color
# shape
self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
# text and font
self.text_label = None
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
# callback and hover functionality
self.function = command
self.variable = variable
self.variable_callback_blocked = False
self.variable_callback_name = None
self.state = state
self.hover = hover
self.click_animation_running = False
if values is None:
self.values = ["CTkOptionMenu"]
else:
self.values = values
self.current_value = self.values[0]
self.dropdown_menu = None
# configure grid system (1x1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
self.draw_engine = DrawEngine(self.canvas)
# event bindings
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.clicked)
self.canvas.bind("<Button-1>", self.clicked)
self.bind('<Configure>', self.update_dimensions_event)
self.set_cursor()
self.draw() # initial draw
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.set(self.variable.get(), from_variable_callback=True)
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
if self.text_label is not None:
self.text_label.destroy()
self.text_label = None
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.draw()
def set_dimensions(self, width: int = None, height: int = None):
super().set_dimensions(width, height)
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.draw()
def draw(self, no_color_updates=False):
left_section_width = self.current_width - self.current_height
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(self.apply_widget_scaling(self.current_width),
self.apply_widget_scaling(self.current_height),
self.apply_widget_scaling(self.corner_radius),
0,
self.apply_widget_scaling(left_section_width))
requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self.apply_widget_scaling(self.current_width - (self.current_height / 2)),
self.apply_widget_scaling(self.current_height / 2),
self.apply_widget_scaling(self.current_height / 3))
if self.text_label is None:
self.text_label = tkinter.Label(master=self,
font=self.apply_font_scaling(self.text_font))
self.text_label.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="w",
padx=(max(self.apply_widget_scaling(self.corner_radius), 3),
max(self.current_width - left_section_width + 3, 3)))
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 self.current_value is not None:
self.text_label.configure(text=self.current_value)
if no_color_updates is False or requires_recoloring or requires_recoloring_2:
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.canvas.itemconfig("inner_parts_left",
outline=ThemeManager.single_color(self.fg_color, self.appearance_mode),
fill=ThemeManager.single_color(self.fg_color, self.appearance_mode))
self.canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self.button_color, self.appearance_mode),
fill=ThemeManager.single_color(self.button_color, self.appearance_mode))
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode))
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self.appearance_mode)))
self.canvas.itemconfig("dropdown_arrow",
fill=ThemeManager.single_color(self.text_color_disabled, self.appearance_mode))
else:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode))
self.canvas.itemconfig("dropdown_arrow",
fill=ThemeManager.single_color(self.text_color, self.appearance_mode))
self.text_label.configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode))
def open_dropdown_menu(self):
self.dropdown_menu = DropdownMenu(x_position=self.winfo_rootx(),
y_position=self.winfo_rooty() + self.apply_widget_scaling(self.current_height + 4),
width=self.current_width,
values=self.values,
command=self.set,
fg_color=self.dropdown_color,
button_hover_color=self.dropdown_hover_color,
button_color=self.dropdown_color,
text_color=self.dropdown_text_color)
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw() at the end
if "state" in kwargs:
self.state = kwargs["state"]
self.set_cursor()
require_redraw = True
del kwargs["state"]
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
require_redraw = True
del kwargs["fg_color"]
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "button_color" in kwargs:
self.button_color = kwargs["button_color"]
require_redraw = True
del kwargs["button_color"]
if "button_hover_color" in kwargs:
self.button_hover_color = kwargs["button_hover_color"]
require_redraw = True
del kwargs["button_hover_color"]
if "text_color" in kwargs:
self.text_color = kwargs["text_color"]
require_redraw = True
del kwargs["text_color"]
if "command" in kwargs:
self.function = kwargs["command"]
del kwargs["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["variable"]
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
self.set(self.variable.get(), from_variable_callback=True)
else:
self.variable = None
del kwargs["variable"]
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
super().configure(*args, **kwargs)
if require_redraw:
self.draw()
def set_cursor(self):
if Settings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
self.configure(cursor="arrow")
elif sys.platform.startswith("win") and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
self.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and len(self.values) > 0 and Settings.cursor_manipulation_enabled:
self.configure(cursor="hand2")
def on_enter(self, event=0):
if self.hover is True and self.state == tkinter.NORMAL:
# set color of inner button parts to hover color
self.canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self.button_hover_color, self.appearance_mode),
fill=ThemeManager.single_color(self.button_hover_color, self.appearance_mode))
def on_leave(self, event=0):
self.click_animation_running = False
if self.hover is True:
# set color of inner button parts
self.canvas.itemconfig("inner_parts_right",
outline=ThemeManager.single_color(self.button_color, self.appearance_mode),
fill=ThemeManager.single_color(self.button_color, self.appearance_mode))
def click_animation(self):
if self.click_animation_running:
self.on_enter()
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: str, from_variable_callback: bool = False):
self.current_value = value
if self.text_label is not None:
self.text_label.configure(text=self.current_value)
else:
self.draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.current_value)
self.variable_callback_blocked = False
if not from_variable_callback:
if self.function is not None:
try:
self.function(self.current_value)
except Exception:
pass
def get(self) -> str:
return self.current_value
def clicked(self, event=0):
if self.state is not tkinter.DISABLED:
self.open_dropdown_menu()
# 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)

View File

@ -81,6 +81,13 @@ class CTkProgressBar(CTkBaseClass):
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def set_dimensions(self, width=None, height=None):
super().set_dimensions(width, height)
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.draw()
def destroy(self):
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
@ -159,6 +166,14 @@ class CTkProgressBar(CTkBaseClass):
del kwargs["variable"]
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
super().configure(*args, **kwargs)
if require_redraw is True:

View File

@ -1,5 +1,6 @@
import tkinter
import sys
from typing import Callable, Union
from .ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
@ -47,7 +48,7 @@ class CTkRadioButton(CTkBaseClass):
# text
self.text = text
self.text_label = None
self.text_label: Union[tkinter.Label, None] = None
self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
@ -84,10 +85,9 @@ class CTkRadioButton(CTkBaseClass):
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.invoke)
self.canvas.bind("<Button-1>", self.invoke)
self.set_cursor()
self.draw() # initial draw
self.set_cursor()
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
@ -143,6 +143,10 @@ class CTkRadioButton(CTkBaseClass):
self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self.text_label["anchor"] = "w"
self.text_label.bind("<Enter>", self.on_enter)
self.text_label.bind("<Leave>", self.on_leave)
self.text_label.bind("<Button-1>", self.invoke)
if self.state == tkinter.DISABLED:
self.text_label.configure(fg=ThemeManager.single_color(self.text_color_disabled, self.appearance_mode))
else:
@ -229,14 +233,22 @@ class CTkRadioButton(CTkBaseClass):
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
if self.text_label is not None:
self.text_label.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
if self.text_label is not None:
self.text_label.configure(cursor="hand2")
def set_text(self, text):
self.text = text

View File

@ -109,6 +109,13 @@ class CTkSlider(CTkBaseClass):
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def set_dimensions(self, width=None, height=None):
super().set_dimensions(width, height)
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.draw()
def destroy(self):
# remove variable_callback from variable callbacks if variable exists
if self.variable is not None:
@ -310,6 +317,14 @@ class CTkSlider(CTkBaseClass):
del kwargs["variable"]
if "width" in kwargs:
self.set_dimensions(width=kwargs["width"])
del kwargs["width"]
if "height" in kwargs:
self.set_dimensions(height=kwargs["height"])
del kwargs["height"]
super().configure(*args, **kwargs)
if require_redraw:

View File

@ -94,8 +94,8 @@ class CTkSwitch(CTkBaseClass):
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.toggle)
self.set_cursor()
self.draw() # initial draw
self.set_cursor()
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
@ -126,13 +126,22 @@ class CTkSwitch(CTkBaseClass):
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
else:
if self.text_label is not None:
self.text_label.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
if self.text_label is not None:
self.text_label.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
if self.text_label is not None:
self.text_label.configure(cursor="hand2")
def draw(self, no_color_updates=False):
@ -185,6 +194,11 @@ class CTkSwitch(CTkBaseClass):
font=self.apply_font_scaling(self.text_font))
self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w")
self.text_label["anchor"] = "w"
self.text_label.bind("<Enter>", self.on_enter)
self.text_label.bind("<Leave>", self.on_leave)
self.text_label.bind("<Button-1>", self.toggle)
if self.textvariable is not None:
self.text_label.configure(textvariable=self.textvariable)

View File

@ -0,0 +1,128 @@
import customtkinter
import tkinter
import sys
from ..theme_manager import ThemeManager
from ..appearance_mode_tracker import AppearanceModeTracker
from ..scaling_tracker import ScalingTracker
class DropdownMenu(tkinter.Toplevel):
def __init__(self, *args,
fg_color="#555555",
button_color="gray50",
button_hover_color="gray35",
text_color="black",
corner_radius=6,
button_corner_radius=3,
width=120,
button_height=24,
x_position=0,
y_position=0,
x_spacing=3,
y_spacing=3,
command=None,
values=None,
**kwargs):
super().__init__(*args, **kwargs)
ScalingTracker.add_widget(self.set_scaling, self)
self.widget_scaling = ScalingTracker.get_widget_scaling(self)
self.values = values
self.command = command
# color
self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
self.fg_color = fg_color
self.button_color = button_color
self.button_hover_color = button_hover_color
self.text_color = text_color
# shape
self.width = width
self.corner_radius = corner_radius
self.button_corner_radius = button_corner_radius
self.button_height = button_height
self.geometry(f"{round(self.apply_widget_scaling(self.width))}x" +
f"{round(self.apply_widget_scaling(len(self.values) * (self.button_height + y_spacing) + y_spacing))}+" +
f"{round(x_position)}+{round(y_position)}")
self.grid_columnconfigure(0, weight=1)
if sys.platform.startswith("darwin"):
self.overrideredirect(True) # remove title-bar
self.overrideredirect(False)
self.wm_attributes("-transparent", True) # turn off window shadow
self.config(bg='systemTransparent') # transparent bg
self.frame = customtkinter.CTkFrame(self,
border_width=0,
width=self.width,
corner_radius=self.corner_radius,
fg_color=ThemeManager.single_color(self.fg_color, self.appearance_mode))
elif sys.platform.startswith("win"):
self.overrideredirect(True) # remove title-bar
self.configure(bg="#010302")
self.wm_attributes("-transparentcolor", "#010302")
self.focus()
self.frame = customtkinter.CTkFrame(self,
border_width=0,
width=self.width,
corner_radius=self.corner_radius,
fg_color=self.fg_color, overwrite_preferred_drawing_method="circle_shapes")
else:
self.overrideredirect(True) # remove title-bar
self.configure(bg="#010302")
self.wm_attributes("-transparentcolor", "#010302")
self.frame = customtkinter.CTkFrame(self,
border_width=0,
width=self.width,
corner_radius=self.corner_radius,
fg_color=self.fg_color, overwrite_preferred_drawing_method="circle_shapes")
self.frame.grid(row=0, column=0, sticky="nsew", rowspan=len(self.values) + 1)
self.frame.grid_rowconfigure(len(self.values) + 1, minsize=y_spacing) # add spacing at the bottom
self.frame.grid_columnconfigure(0, weight=1)
self.button_list = []
for index, option in enumerate(self.values):
button = customtkinter.CTkButton(self.frame,
text=option,
height=self.button_height,
width=self.width - 2 * x_spacing,
fg_color=self.button_color,
text_color=self.text_color,
hover_color=self.button_hover_color,
corner_radius=self.button_corner_radius,
command=lambda i=index: self.button_callback(i))
button.text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="w")
button.grid(row=index, column=0,
padx=x_spacing,
pady=(y_spacing, 0), sticky="ew")
self.button_list.append(button)
self.bind("<FocusOut>", self.focus_loss_event)
self.frame.canvas.bind("<Button-1>", self.focus_loss_event)
def apply_widget_scaling(self, value):
if isinstance(value, (int, float)):
return value * self.widget_scaling
else:
return value
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
return
def focus_loss_event(self, event):
self.destroy()
if sys.platform.startswith("darwin"):
self.update()
def button_callback(self, index):
self.destroy()
if sys.platform.startswith("darwin"):
self.update()
if self.command is not None:
self.command(self.values[index])

View File

@ -135,22 +135,30 @@ class CTkBaseClass(tkinter.Frame):
self.draw(no_color_updates=True) # faster drawing without color changes
def detect_color_of_master(self):
def detect_color_of_master(self, master_widget=None):
""" detect color of self.master widget to set correct bg_color """
if isinstance(self.master, CTkBaseClass) and hasattr(self.master, "fg_color"): # master is CTkFrame
return self.master.fg_color
if master_widget is None:
master_widget = self.master
elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget
if isinstance(master_widget, CTkBaseClass) and hasattr(master_widget, "fg_color"): # master is CTkFrame
if master_widget.fg_color is not None:
return master_widget.fg_color
# if fg_color of master is None, try to retrieve fg_color from master of master
elif hasattr(master_widget.master, "master"):
return self.detect_color_of_master(self.master.master)
elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget
try:
ttk_style = ttk.Style()
return ttk_style.lookup(self.master.winfo_class(), 'background')
return ttk_style.lookup(master_widget.winfo_class(), 'background')
except Exception:
return "#FFFFFF", "#000000"
else: # master is normal tkinter widget
try:
return self.master.cget("bg") # try to get bg color by .cget() method
return master_widget.cget("bg") # try to get bg color by .cget() method
except Exception:
return "#FFFFFF", "#000000"
@ -177,6 +185,15 @@ class CTkBaseClass(tkinter.Frame):
if self.last_geometry_manager_call is not None:
self.last_geometry_manager_call["function"](**self.apply_argument_scaling(self.last_geometry_manager_call["kwargs"]))
def set_dimensions(self, width=None, height=None):
if width is not None:
self.desired_width = width
if height is not None:
self.desired_height = height
super().configure(width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
def apply_widget_scaling(self, value):
if isinstance(value, (int, float)):
return value * self.widget_scaling

View File

@ -138,18 +138,7 @@ class CTk(tkinter.Tk):
self.current_height = max(self.min_height, min(numbers[1], self.max_height))
def apply_geometry_scaling(self, geometry_string):
numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers
if len(numbers) == 2:
return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\
f"{self.apply_window_scaling(numbers[1]):.0f}"
elif len(numbers) == 4:
return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\
f"{self.apply_window_scaling(numbers[1]):.0f}+" +\
f"{self.apply_window_scaling(numbers[2]):.0f}+" +\
f"{self.apply_window_scaling(numbers[3]):.0f}"
else:
return geometry_string
return re.sub(re.compile("\d+"), lambda match_obj: str(round(int(match_obj.group(0)) * self.window_scaling)), geometry_string)
def apply_window_scaling(self, value):
if isinstance(value, (int, float)):

View File

@ -16,7 +16,6 @@ class App(customtkinter.CTk):
self.title("CustomTkinter complex_example.py")
self.geometry(f"{App.WIDTH}x{App.HEIGHT}")
self.protocol("WM_DELETE_WINDOW", self.on_closing) # call .on_closing() when app gets closed
# ============ create two frames ============
@ -196,10 +195,7 @@ class App(customtkinter.CTk):
def on_closing(self, event=0):
self.destroy()
def start(self):
self.mainloop()
if __name__ == "__main__":
app = App()
app.start()
app.mainloop()

View File

@ -23,6 +23,7 @@ class App(customtkinter.CTk):
self.geometry(f"{App.WIDTH}x{App.HEIGHT}")
self.minsize(App.WIDTH, App.HEIGHT)
self.maxsize(App.WIDTH, App.HEIGHT)
self.resizable(False, False)
self.protocol("WM_DELETE_WINDOW", self.on_closing)

View File

@ -5,7 +5,7 @@ customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark",
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
app = customtkinter.CTk() # create CTk window like you do with the Tk window
app.geometry("400x480")
app.geometry("400x540")
app.title("CustomTkinter simple_example.py")
@ -23,7 +23,7 @@ def check_box_function():
y_padding = 13
frame_1 = customtkinter.CTkFrame(master=app, corner_radius=15)
frame_1 = customtkinter.CTkFrame(master=app)
frame_1.pack(pady=20, padx=60, fill="both", expand=True)
label_1 = customtkinter.CTkLabel(master=frame_1, justify=tkinter.LEFT)
@ -32,7 +32,7 @@ label_1.pack(pady=y_padding, padx=10)
progressbar_1 = customtkinter.CTkProgressBar(master=frame_1)
progressbar_1.pack(pady=y_padding, padx=10)
button_1 = customtkinter.CTkButton(master=frame_1, corner_radius=8, command=button_function)
button_1 = customtkinter.CTkButton(master=frame_1, command=button_function)
button_1.pack(pady=y_padding, padx=10)
slider_1 = customtkinter.CTkSlider(master=frame_1, command=slider_function, from_=0, to=1)
@ -53,8 +53,6 @@ radiobutton_1.pack(pady=y_padding, padx=10)
radiobutton_2 = customtkinter.CTkRadioButton(master=frame_1, variable=radiobutton_var, value=2)
radiobutton_2.pack(pady=y_padding, padx=10)
s_var = tkinter.StringVar(value="on")
switch_1 = customtkinter.CTkSwitch(master=frame_1)
switch_1.pack(pady=y_padding, padx=10)

View File

@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
github_url = "https://github.com/TomSchimansky/CustomTkinter"
[tool.tbump.version]
current = "4.0.3"
current = "4.2.0"
# Example of a semver regexp.
# Make sure this matches current_version before

View File

@ -1,8 +1,8 @@
[metadata]
name = customtkinter
version = 4.0.3
version = 4.2.0
description = Create modern looking GUIs with Python
long_description = file: README.md
long_description = '# CustomTkinter UI-Library\nhttps://github.com/TomSchimansky/CustomTkinter/blob/master/documentation_images/Windows_dark.png\n\nMore Information: https://github.com/TomSchimansky/CustomTkinter'
long_description_content_type = text/markdown
url = https://github.com/TomSchimansky/CustomTkinter
author = Tom Schimansky

View File

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

View File

@ -1,53 +1,71 @@
import customtkinter
import tkinter
# customtkinter.set_appearance_mode("light")
app = customtkinter.CTk()
app.geometry("600x500")
menu = tkinter.Menu(tearoff=0, bd=0, relief=tkinter.FLAT, activeforeground="red")
menu.add_command(label="System")
menu.add_command(label="Light")
menu.add_command(label="Dark")
import sys
class CTkMenu(tkinter.Toplevel):
def __init__(self, master, x, y, options):
super().__init__(bg="black")
super().overrideredirect(True)
#self.wm_attributes("-transparentcolor", "black")
super().geometry(f"120x{len(options) * (25 + 4) + 4}+{x}+{y}")
super().lift()
super().transient(master)
self.resizable(False, False)
super().focus_force()
self.focus()
super().__init__()
self.frame = customtkinter.CTkFrame(self, border_width=0, width=120, corner_radius=10, border_color="gray4", fg_color="#333740")
self.frame.grid(row=0, column=0, sticky="nsew", rowspan=len(options) + 2, columnspan=1)
self.overrideredirect(True)
self.geometry(f"120x{len(options) * (25 + 3) + 3}+{x}+{y}")
self.frame.grid_rowconfigure(0, minsize=2)
self.frame.grid_rowconfigure(len(options) + 1, minsize=2)
if sys.platform.startswith("darwin"):
self.overrideredirect(False)
self.wm_attributes("-transparent", True) # turn off shadow
self.config(bg='systemTransparent') # transparent bg
self.frame = customtkinter.CTkFrame(self, border_width=0, width=120, corner_radius=6, border_color="gray4", fg_color="#333740")
elif sys.platform.startswith("win"):
self.configure(bg="#FFFFF1")
self.wm_attributes("-transparent", "#FFFFF1")
self.focus()
self.frame = customtkinter.CTkFrame(self, border_width=0, width=120, corner_radius=6, border_color="gray4", fg_color="#333740",
overwrite_preferred_drawing_method="circle_shapes")
else:
self.configure(bg="#FFFFF1")
self.wm_attributes("-transparent", "#FFFFF1")
self.frame = customtkinter.CTkFrame(self, border_width=0, width=120, corner_radius=6, border_color="gray4", fg_color="#333740",
overwrite_preferred_drawing_method="circle_shapes")
self.frame.grid(row=0, column=0, sticky="nsew", rowspan=len(options) + 1, columnspan=1, ipadx=0, ipady=0)
self.frame.grid_rowconfigure(len(options) + 1, minsize=3)
self.frame.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.buttons = []
for index, option in enumerate(options):
button = customtkinter.CTkButton(self.frame, height=25, width=108, fg_color="#333740", text_color="gray74", hover_color="#272A2E", corner_radius=8)
button = customtkinter.CTkButton(self.frame, text=option, height=25, width=108, fg_color="#333740", text_color="gray74",
hover_color="gray28", corner_radius=4, command=self.button_click)
button.text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="w")
button.grid(row=index + 1, column=0, padx=4, pady=2)
button.grid(row=index, column=0, padx=(3, 3), pady=(3, 0), columnspan=1, rowspan=1, sticky="ew")
self.buttons.append(button)
# master.bind("<Configure>", self.window_drag())
self.bind("<FocusOut>", self.focus_loss_event)
self.frame.canvas.bind("<Button-1>", self.focus_loss_event)
def focus_loss_event(self, event):
print("focus loss")
self.destroy()
# self.update()
def button_click(self):
print("button press")
self.destroy()
# self.update()
app = customtkinter.CTk()
app.geometry("600x500")
def open_menu():
menu = CTkMenu(app, button.winfo_rootx(), button.winfo_rooty() + button.winfo_height() + 4, ["Option 1", "Option 2", "Point 3"])
button = customtkinter.CTkButton(command=open_menu, height=50)
button = customtkinter.CTkButton(command=open_menu, height=30, corner_radius=6)
button.pack(pady=20)
button_2 = customtkinter.CTkButton(command=open_menu, height=30, corner_radius=6)
button_2.pack(pady=60)
app.mainloop()

View File

@ -0,0 +1,26 @@
import tkinter
import customtkinter
app = customtkinter.CTk()
app.title('test_optionmenu.py')
app.geometry('400x300')
def select_callback(choice):
choice = variable.get()
print("display_selected", choice)
countries = ['Bahamas', 'Canada', 'Cuba', 'United States']
variable = tkinter.StringVar()
variable.set("test")
optionmenu_tk = tkinter.OptionMenu(app, variable, *countries, command=select_callback)
optionmenu_tk.pack(pady=10, padx=10)
optionmenu_1 = customtkinter.CTkOptionMenu(app, variable=variable, values=countries, command=select_callback)
optionmenu_1.pack(pady=10, padx=10)
optionmenu_1.set("te")
app.mainloop()

View File

@ -5,7 +5,7 @@ TEST_CONFIGURE = True
TEST_REMOVING = False
app = customtkinter.CTk() # create CTk window like you do with the Tk window (you can also use normal tkinter.Tk window)
app.geometry("400x600")
app.geometry("400x800")
app.title("Tkinter Variable Test")
txt_var = tkinter.StringVar(value="")
@ -69,6 +69,12 @@ switch_1 = customtkinter.CTkSwitch(master=app, variable=s_var, textvariable=s_va
switch_1.pack(pady=20, padx=10)
switch_1 = customtkinter.CTkSwitch(master=app, variable=s_var, textvariable=s_var, onvalue="on", offvalue="off")
switch_1.pack(pady=20, padx=10)
#switch_1.toggle()
optionmenu_var = tkinter.StringVar(value="test")
optionmenu_1 = customtkinter.CTkOptionMenu(master=app, variable=optionmenu_var, values=["Option 1", "Option 2", "Option 3"])
optionmenu_1.pack(pady=20, padx=10)
optionmenu_2 = customtkinter.CTkOptionMenu(master=app, values=["Option 1", "Option 2", "Option 3"])
optionmenu_2.pack(pady=20, padx=10)
optionmenu_2.configure(variable=optionmenu_var)
app.mainloop()