added new button grid system, fixed CTkSwitch grid positioning, moved ctk_canvas.py and draw_engine.py to widgets/core_rendering, created CTkImage

This commit is contained in:
Tom Schimansky 2022-10-29 00:42:33 +02:00
parent bf1835922b
commit 08a0835fd0
27 changed files with 261 additions and 174 deletions

View File

@ -9,6 +9,7 @@ ToDo:
- auto-scaling of images
- image tuple for light/dark mode
- change font attribute in wiki
- add new button attributes to wiki
## Unreleased - 2022-10-2
### Added

View File

@ -5,9 +5,9 @@ import sys
# import manager classes
from .appearance_mode_tracker import AppearanceModeTracker
from .theme_manager import ThemeManager
from .widgets.font.font_manager import FontManager
from .scaling_tracker import ScalingTracker
from .font_manager import FontManager
from .theme_manager import ThemeManager
from .widgets.core_rendering.draw_engine import DrawEngine
AppearanceModeTracker.init_appearance_mode()
@ -46,7 +46,7 @@ if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Cust
DrawEngine.preferred_drawing_method = "circle_shapes"
# import widgets
from .widgets.widget_base_class import CTkBaseClass
from customtkinter.widgets.core_widget_classes.widget_base_class import CTkBaseClass
from .widgets.ctk_button import CTkButton
from .widgets.ctk_checkbox import CTkCheckBox
from .widgets.ctk_entry import CTkEntry
@ -64,15 +64,13 @@ from .widgets.ctk_textbox import CTkTextbox
from .widgets.ctk_tabview import CTkTabview
from .widgets.ctk_segmented_button import CTkSegmentedButton
from .widgets.ctk_button_new_grid import CTkButtonNewGrid
# import windows
from .windows.ctk_tk import CTk
from .windows.ctk_toplevel import CTkToplevel
from .windows.ctk_input_dialog import CTkInputDialog
# util classes
from .utility.ctk_font import CTkFont
# font classes
from .widgets.font.ctk_font import CTkFont
def set_appearance_mode(mode_string: str):

View File

@ -84,10 +84,4 @@ class ThemeManager:
min(255, rgb_color[2] * factor))
return ThemeManager.rgb2hex(dark_rgb_color)
except Exception as err:
# sys.stderr.write("ERROR (CTkColorManager): failed to darken the following color: " + str(hex_color) + " " + str(err))
return hex_color
@classmethod
def set_main_color(cls, main_color, main_color_hover):
cls.MAIN_COLOR = main_color
cls.MAIN_HOVER_COLOR = main_color_hover

View File

@ -2,10 +2,10 @@ import tkinter
import sys
from typing import Union, Tuple, Callable, List
from ..theme_manager import ThemeManager
from ..appearance_mode_tracker import AppearanceModeTracker
from ..scaling_tracker import ScalingTracker
from ..utility.ctk_font import CTkFont
from ...theme_manager import ThemeManager
from ...appearance_mode_tracker import AppearanceModeTracker
from ...scaling_tracker import ScalingTracker
from ..font.ctk_font import CTkFont
class DropdownMenu(tkinter.Menu):

View File

@ -9,15 +9,15 @@ try:
except ImportError:
from typing_extensions import TypedDict
from ..windows.ctk_tk import CTk
from ..windows.ctk_toplevel import CTkToplevel
from ..appearance_mode_tracker import AppearanceModeTracker
from ..scaling_tracker import ScalingTracker
from ..theme_manager import ThemeManager
from ...windows.ctk_tk import CTk
from ...windows.ctk_toplevel import CTkToplevel
from ...appearance_mode_tracker import AppearanceModeTracker
from ...scaling_tracker import ScalingTracker
from ...theme_manager import ThemeManager
from ..font.ctk_font import CTkFont
from ..image.ctk_image import CTkImage
from ..utility.ctk_font import CTkFont
from ..utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
from ...utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
class CTkBaseClass(tkinter.Frame):

View File

@ -5,8 +5,9 @@ from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from ..utility.ctk_font import CTkFont
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont
from .image.ctk_image import CTkImage
class CTkButton(CTkBaseClass):
@ -15,12 +16,15 @@ class CTkButton(CTkBaseClass):
For detailed information check out the documentation.
"""
_image_label_spacing = 6
def __init__(self,
master: any = None,
width: int = 140,
height: int = 28,
corner_radius: Union[int, str] = "default_theme",
border_width: Union[int, str] = "default_theme",
border_spacing: int = 2,
bg_color: Union[str, Tuple[str, str], None] = None,
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
@ -36,11 +40,12 @@ class CTkButton(CTkBaseClass):
text: str = "CTkButton",
font: Union[tuple, CTkFont] = "default_theme",
textvariable: tkinter.Variable = None,
image: tkinter.PhotoImage = None,
image: Union[tkinter.PhotoImage, CTkImage] = None,
state: str = "normal",
hover: bool = True,
command: Callable = None,
compound: str = "left",
anchor: str = "center",
**kwargs):
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
@ -59,8 +64,10 @@ class CTkButton(CTkBaseClass):
self._border_width = ThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width
self._round_width_to_even_numbers = round_width_to_even_numbers # rendering options for DrawEngine
self._round_height_to_even_numbers = round_height_to_even_numbers # rendering options for DrawEngine
self._corner_radius = min(self._corner_radius, round(self._current_height/2))
self._compound = compound
self._anchor = anchor
self._border_spacing = border_spacing
# text, image
self._image = image
@ -78,21 +85,14 @@ class CTkButton(CTkBaseClass):
self._textvariable = textvariable
self._state = state
self._hover = hover
self._compound = compound
self._click_animation_running: bool = False
# configure grid system (2x2)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(1, weight=1)
# canvas
self._canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
self._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
@ -109,6 +109,8 @@ class CTkButton(CTkBaseClass):
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))
@ -131,7 +133,7 @@ class CTkButton(CTkBaseClass):
# 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")
self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew")
def destroy(self):
if isinstance(self._font, CTkFont):
@ -184,6 +186,7 @@ class CTkButton(CTkBaseClass):
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)
@ -209,12 +212,14 @@ class CTkButton(CTkBaseClass):
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._create_grid()
self._image_label.bind("<Enter>", self._on_enter)
self._image_label.bind("<Leave>", self._on_leave)
@ -235,56 +240,88 @@ class CTkButton(CTkBaseClass):
if self._image_label is not None:
self._image_label.destroy()
self._image_label = None
self._create_grid()
# create grid layout with just an image given
if self._image_label is not None and self._text_label is None:
self._image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1)) # bottom pady with +1 for rounding to even
def _create_grid(self):
""" configure grid system (5x5) """
# create grid layout with just text given
if self._image_label is None and self._text_label is not None:
self._text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
padx=self._apply_widget_scaling(self._corner_radius),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1)) # bottom pady with +1 for rounding to even
# 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
# create grid layout of image and text label in 2x2 grid system with given compound
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:
if self._compound == tkinter.LEFT or self._compound == "left":
self._image_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)), 2),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1))
self._text_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
padx=(2, max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width))),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1))
elif self._compound == tkinter.TOP or self._compound == "top":
self._image_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)),
pady=(self._apply_widget_scaling(self._border_width), 2))
self._text_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)),
pady=(2, self._apply_widget_scaling(self._border_width)))
elif self._compound == tkinter.RIGHT or self._compound == "right":
self._image_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
padx=(2, max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width))),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1))
self._text_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)), 2),
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1))
elif self._compound == tkinter.BOTTOM or self._compound == "bottom":
self._image_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)),
pady=(2, self._apply_widget_scaling(self._border_width)))
self._text_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)),
pady=(self._apply_widget_scaling(self._border_width), 2))
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:
@ -317,6 +354,7 @@ class CTkButton(CTkBaseClass):
require_redraw = True # text_label will be created in .draw()
else:
self._text_label.configure(text=self._text)
self._create_grid()
if "font" in kwargs:
if isinstance(self._font, CTkFont):
@ -334,6 +372,7 @@ class CTkButton(CTkBaseClass):
if "image" in kwargs:
self._image = kwargs.pop("image")
self._create_grid()
require_redraw = True
if "state" in kwargs:
@ -351,6 +390,10 @@ class CTkButton(CTkBaseClass):
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:
@ -358,6 +401,8 @@ class CTkButton(CTkBaseClass):
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
@ -388,6 +433,8 @@ class CTkButton(CTkBaseClass):
return self._command
elif attribute_name == "compound":
return self._compound
elif attribute_name == "anchor":
return self._anchor
else:
return super().cget(attribute_name)

View File

@ -5,8 +5,8 @@ from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from ..utility.ctk_font import CTkFont
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont
class CTkCheckBox(CTkBaseClass):

View File

@ -2,12 +2,12 @@ import tkinter
import sys
from typing import Union, Tuple, Callable, List
from .dropdown_menu import DropdownMenu
from .core_widget_classes.dropdown_menu import DropdownMenu
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from ..utility.ctk_font import CTkFont
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont
class CTkComboBox(CTkBaseClass):

View File

@ -4,8 +4,8 @@ from typing import Union, Tuple
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from ..utility.ctk_font import CTkFont
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont
from customtkinter.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty

View File

@ -3,7 +3,7 @@ from typing import Union, Tuple, List
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from .core_widget_classes.widget_base_class import CTkBaseClass
class CTkFrame(CTkBaseClass):

View File

@ -4,8 +4,8 @@ from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from ..utility.ctk_font import CTkFont
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont
from customtkinter.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty

View File

@ -5,9 +5,9 @@ from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from .dropdown_menu import DropdownMenu
from ..utility.ctk_font import CTkFont
from .core_widget_classes.widget_base_class import CTkBaseClass
from .core_widget_classes.dropdown_menu import DropdownMenu
from .font.ctk_font import CTkFont
class CTkOptionMenu(CTkBaseClass):

View File

@ -5,7 +5,7 @@ from typing import Union, Tuple
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from .core_widget_classes.widget_base_class import CTkBaseClass
class CTkProgressBar(CTkBaseClass):

View File

@ -5,8 +5,8 @@ from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from ..utility.ctk_font import CTkFont
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont
class CTkRadioButton(CTkBaseClass):

View File

@ -4,7 +4,7 @@ from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from .core_widget_classes.widget_base_class import CTkBaseClass
class CTkScrollbar(CTkBaseClass):

View File

@ -4,6 +4,7 @@ from typing import Union, Tuple, List, Dict, Callable
from ..theme_manager import ThemeManager
from .ctk_button import CTkButton
from .ctk_frame import CTkFrame
from .font.ctk_font import CTkFont
class CTkSegmentedButton(CTkFrame):
@ -29,7 +30,7 @@ class CTkSegmentedButton(CTkFrame):
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
background_corner_colors: Tuple[Union[str, Tuple[str, str]]] = None,
font: any = "default_theme",
font: Union[tuple, CTkFont] = "default_theme",
values: list = None,
variable: tkinter.Variable = None,
dynamic_resizing: bool = True,

View File

@ -5,7 +5,7 @@ from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from .core_widget_classes.widget_base_class import CTkBaseClass
class CTkSlider(CTkBaseClass):

View File

@ -5,8 +5,8 @@ from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from ..utility.ctk_font import CTkFont
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont
class CTkSwitch(CTkBaseClass):

View File

@ -2,7 +2,7 @@ from typing import Union, Tuple, Dict, List, Callable
from ..theme_manager import ThemeManager
from .ctk_frame import CTkFrame
from .widget_base_class import CTkBaseClass
from .core_widget_classes.widget_base_class import CTkBaseClass
from .ctk_segmented_button import CTkSegmentedButton
from .core_rendering.ctk_canvas import CTkCanvas
from .core_rendering.draw_engine import DrawEngine

View File

@ -5,8 +5,8 @@ from .core_rendering.ctk_canvas import CTkCanvas
from .ctk_scrollbar import CTkScrollbar
from ..theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .widget_base_class import CTkBaseClass
from ..utility.ctk_font import CTkFont
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont
from customtkinter.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty

View File

View File

@ -2,7 +2,7 @@ from tkinter.font import Font
import copy
from typing import List, Callable, Tuple
from ..theme_manager import ThemeManager
from customtkinter.theme_manager import ThemeManager
class CTkFont(Font):

View File

@ -44,7 +44,7 @@ class FontManager:
flags = (FR_PRIVATE if private else 0) | (FR_NOT_ENUM if not enumerable else 0)
num_fonts_added = add_font_resource_ex(byref(path_buffer), flags, 0)
return bool(num_fonts_added)
return bool(min(num_fonts_added, 1))
@classmethod
def load_font(cls, font_path: str) -> bool:

View File

View File

@ -0,0 +1,116 @@
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] = None):
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):
if "Image" not in dir() or "ImageTk" not in dir():
raise ImportError("CTkImage: Couldn't import PIL.Image or PIL.ImageTk. PIL must be installed.")
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[0] * 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: int) -> ImageTk.PhotoImage:
scaled_size = self._get_scaled_size(widget_scaling)
if appearance_mode == 0 and self._light_image is not None:
return self._get_scaled_light_photo_image(scaled_size)
elif appearance_mode == 0 and self._light_image is None:
return self._get_scaled_dark_photo_image(scaled_size)
elif appearance_mode == 1 and self._dark_image is not None:
return self._get_scaled_dark_photo_image(scaled_size)
elif appearance_mode == 1 and self._dark_image is None:
return self._get_scaled_light_photo_image(scaled_size)

View File

@ -33,77 +33,7 @@ class App(customtkinter.CTk):
self.chat_image = self.load_image("/test_images/chat.png", 20)
self.home_image = self.load_image("/test_images/home.png", 20)
def _pyimagingtkcall(command, photo, id):
tk = photo.tk
try:
tk.call(command, photo, id)
except tkinter.TclError:
print("_pyimagingtkcall error")
class PhotoImage:
def __init__(self, image=None, size=None, **kw):
if hasattr(image, "mode") and hasattr(image, "size"):
# got an image instead of a mode
mode = image.mode
if mode == "P":
# palette mapped data
image.apply_transparency()
image.load()
try:
mode = image.palette.mode
except AttributeError:
mode = "RGB" # default
size = image.size
kw["width"], kw["height"] = size
else:
mode = image
image = None
if mode not in ["1", "L", "RGB", "RGBA"]:
mode = Image.getmodebase(mode)
self.__mode = mode
self.__size = size
self.__photo = tkinter.PhotoImage(**kw)
self.tk = self.__photo.tk
if image:
self.paste(image)
def __del__(self):
name = self.__photo.name
self.__photo.name = None
try:
self.__photo.tk.call("image", "delete", name)
except Exception:
pass # ignore internal errors
def __str__(self):
return str(self.__photo)
def width(self):
return self.__size[0]
def height(self):
return self.__size[1]
def paste(self, im, box=None):
if box is not None:
deprecate("The box parameter", 10, None)
# convert to blittable
im.load()
image = im.im
if image.isblock() and im.mode == self.__mode:
block = image
else:
block = image.new_block(self.__mode, im.size)
image.convert2(block, image) # convert directly between buffers
_pyimagingtkcall("PyImagingPhoto", self.__photo, block.id)
pil_img = Image.open(PATH + "/test_images/add-folder.png").resize((10, 10))
image = PhotoImage(pil_img)
self.button_1 = customtkinter.CTkButton(master=self.frame_1, image=image, text="Add Folder", height=32,
self.button_1 = customtkinter.CTkButton(master=self.frame_1, image=self.settings_image, text="Add Folder", height=32,
compound="right", command=self.button_function)
self.button_1.grid(row=1, column=0, columnspan=2, padx=20, pady=(20, 10), sticky="ew")