123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- import sys
- import warnings
- import tkinter
- import tkinter.ttk as ttk
- from typing import Union, Callable, Tuple, Any
- try:
- from typing import TypedDict
- except ImportError:
- from typing_extensions import TypedDict
- from .... import windows # import windows for isinstance checks
- from ..theme import ThemeManager
- from ..font import CTkFont
- from ..image import CTkImage
- from ..appearance_mode import CTkAppearanceModeBaseClass
- from ..scaling import CTkScalingBaseClass
- from ..utility import pop_from_dict_by_set, check_kwargs_empty
- class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
- """ Base class of every CTk widget, handles the dimensions, bg_color,
- appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """
- # attributes that are passed to and managed by the tkinter frame only:
- _valid_tk_frame_attributes: set = {"cursor"}
- _cursor_manipulation_enabled: bool = True
- def __init__(self,
- master: Any,
- width: int = 0,
- height: int = 0,
- bg_color: Union[str, Tuple[str, str]] = "transparent",
- **kwargs):
- # call init methods of super classes
- tkinter.Frame.__init__(self, master=master, width=width, height=height, **pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))
- CTkAppearanceModeBaseClass.__init__(self)
- CTkScalingBaseClass.__init__(self, scaling_type="widget")
- # check if kwargs is empty, if not raise error for unsupported arguments
- check_kwargs_empty(kwargs, raise_error=True)
- # dimensions independent of scaling
- self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget
- self._current_height = height # _current_width and _current_height are independent of the scale
- self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height
- self._desired_height = height
- # set width and height of tkinter.Frame
- super().configure(width=self._apply_widget_scaling(self._desired_width),
- height=self._apply_widget_scaling(self._desired_height))
- # save latest geometry function and kwargs
- class GeometryCallDict(TypedDict):
- function: Callable
- kwargs: dict
- self._last_geometry_manager_call: Union[GeometryCallDict, None] = None
- # background color
- self._bg_color: Union[str, Tuple[str, str]] = self._detect_color_of_master() if bg_color == "transparent" else self._check_color_type(bg_color, transparency=True)
- # set bg color of tkinter.Frame
- super().configure(bg=self._apply_appearance_mode(self._bg_color))
- # add configure callback to tkinter.Frame
- super().bind('<Configure>', self._update_dimensions_event)
- # overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well
- if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame, tkinter.LabelFrame, ttk.Frame, ttk.LabelFrame, ttk.Notebook)) and not isinstance(self.master, (CTkBaseClass, CTkAppearanceModeBaseClass)):
- master_old_configure = self.master.config
- def new_configure(*args, **kwargs):
- if "bg" in kwargs:
- self.configure(bg_color=kwargs["bg"])
- elif "background" in kwargs:
- self.configure(bg_color=kwargs["background"])
- # args[0] is dict when attribute gets changed by widget[<attribute>] syntax
- elif len(args) > 0 and type(args[0]) == dict:
- if "bg" in args[0]:
- self.configure(bg_color=args[0]["bg"])
- elif "background" in args[0]:
- self.configure(bg_color=args[0]["background"])
- master_old_configure(*args, **kwargs)
- self.master.config = new_configure
- self.master.configure = new_configure
- def destroy(self):
- """ Destroy this and all descendants widgets. """
- # call destroy methods of super classes
- tkinter.Frame.destroy(self)
- CTkAppearanceModeBaseClass.destroy(self)
- CTkScalingBaseClass.destroy(self)
- def _draw(self, no_color_updates: bool = False):
- """ can be overridden but super method must be called """
- if no_color_updates is False:
- # Configuring color of tkinter.Frame not necessary at the moment?
- # Causes flickering on Windows and Linux for segmented button for some reason!
- # super().configure(bg=self._apply_appearance_mode(self._bg_color))
- pass
- def config(self, *args, **kwargs):
- raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.")
- def configure(self, require_redraw=False, **kwargs):
- """ basic configure with bg_color, width, height support, calls configure of tkinter.Frame, updates in the end """
- if "width" in kwargs:
- self._set_dimensions(width=kwargs.pop("width"))
- if "height" in kwargs:
- self._set_dimensions(height=kwargs.pop("height"))
- if "bg_color" in kwargs:
- new_bg_color = self._check_color_type(kwargs.pop("bg_color"), transparency=True)
- if new_bg_color == "transparent":
- self._bg_color = self._detect_color_of_master()
- else:
- self._bg_color = self._check_color_type(new_bg_color)
- require_redraw = True
- super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes)) # configure tkinter.Frame
- # if there are still items in the kwargs dict, raise ValueError
- check_kwargs_empty(kwargs, raise_error=True)
- if require_redraw:
- self._draw()
- def cget(self, attribute_name: str):
- """ basic cget with bg_color, width, height support, calls cget of tkinter.Frame """
- if attribute_name == "bg_color":
- return self._bg_color
- elif attribute_name == "width":
- return self._desired_width
- elif attribute_name == "height":
- return self._desired_height
- elif attribute_name in self._valid_tk_frame_attributes:
- return super().cget(attribute_name) # cget of tkinter.Frame
- else:
- raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.")
- def _check_font_type(self, font: any):
- """ check font type when passed to widget """
- if isinstance(font, CTkFont):
- return font
- elif type(font) == tuple and len(font) == 1:
- warnings.warn(f"{type(self).__name__} Warning: font {font} given without size, will be extended with default text size of current theme\n")
- return font[0], ThemeManager.theme["text"]["size"]
- elif type(font) == tuple and 2 <= len(font) <= 6:
- return font
- else:
- raise ValueError(f"Wrong font type {type(font)}\n" +
- f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 to 6 or an instance of CTkFont.\n" +
- f"\nUsage example:\n" +
- f"font=customtkinter.CTkFont(family='<name>', size=<size in px>)\n" +
- f"font=('<name>', <size in px>)\n")
- def _check_image_type(self, image: any):
- """ check image type when passed to widget """
- if image is None:
- return image
- elif isinstance(image, CTkImage):
- return image
- else:
- warnings.warn(f"{type(self).__name__} Warning: Given image is not CTkImage but {type(image)}. Image can not be scaled on HighDPI displays, use CTkImage instead.\n")
- return image
- def _update_dimensions_event(self, event):
- # only redraw if dimensions changed (for performance), independent of scaling
- if round(self._current_width) != round(self._reverse_widget_scaling(event.width)) or round(self._current_height) != round(self._reverse_widget_scaling(event.height)):
- self._current_width = self._reverse_widget_scaling(event.width) # adjust current size according to new size given by event
- self._current_height = self._reverse_widget_scaling(event.height) # _current_width and _current_height are independent of the scale
- self._draw(no_color_updates=True) # faster drawing without color changes
- def _detect_color_of_master(self, master_widget=None) -> Union[str, Tuple[str, str]]:
- """ detect foreground color of master widget for bg_color and transparent color """
- if master_widget is None:
- master_widget = self.master
- if isinstance(master_widget, (windows.widgets.core_widget_classes.CTkBaseClass, windows.CTk, windows.CTkToplevel, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame)):
- if master_widget.cget("fg_color") is not None and master_widget.cget("fg_color") != "transparent":
- return master_widget.cget("fg_color")
- elif isinstance(master_widget, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame):
- return self._detect_color_of_master(master_widget.master.master.master)
- # if fg_color of master is None, try to retrieve fg_color from master of master
- elif hasattr(master_widget, "master"):
- return self._detect_color_of_master(master_widget.master)
- elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget
- try:
- ttk_style = ttk.Style()
- return ttk_style.lookup(master_widget.winfo_class(), 'background')
- except Exception:
- return "#FFFFFF", "#000000"
- else: # master is normal tkinter widget
- try:
- return master_widget.cget("bg") # try to get bg color by .cget() method
- except Exception:
- return "#FFFFFF", "#000000"
- def _set_appearance_mode(self, mode_string):
- super()._set_appearance_mode(mode_string)
- self._draw()
- super().update_idletasks()
- def _set_scaling(self, new_widget_scaling, new_window_scaling):
- super()._set_scaling(new_widget_scaling, new_window_scaling)
- super().configure(width=self._apply_widget_scaling(self._desired_width),
- height=self._apply_widget_scaling(self._desired_height))
- if self._last_geometry_manager_call is not None:
- self._last_geometry_manager_call["function"](**self._apply_argument_scaling(self._last_geometry_manager_call["kwargs"]))
- def _set_dimensions(self, width=None, height=None):
- if width is not None:
- self._desired_width = width
- if height is not None:
- self._desired_height = height
- super().configure(width=self._apply_widget_scaling(self._desired_width),
- height=self._apply_widget_scaling(self._desired_height))
- def bind(self, sequence=None, command=None, add=None):
- raise NotImplementedError
- def unbind(self, sequence=None, funcid=None):
- raise NotImplementedError
- def unbind_all(self, sequence):
- raise AttributeError("'unbind_all' is not allowed, because it would delete necessary internal callbacks for all widgets")
- def bind_all(self, sequence=None, func=None, add=None):
- raise AttributeError("'bind_all' is not allowed, could result in undefined behavior")
- def place(self, **kwargs):
- """
- Place a widget in the parent widget. Use as options:
- in=master - master relative to which the widget is placed
- in_=master - see 'in' option description
- x=amount - locate anchor of this widget at position x of master
- y=amount - locate anchor of this widget at position y of master
- relx=amount - locate anchor of this widget between 0.0 and 1.0 relative to width of master (1.0 is right edge)
- rely=amount - locate anchor of this widget between 0.0 and 1.0 relative to height of master (1.0 is bottom edge)
- anchor=NSEW (or subset) - position anchor according to given direction
- width=amount - width of this widget in pixel
- height=amount - height of this widget in pixel
- relwidth=amount - width of this widget between 0.0 and 1.0 relative to width of master (1.0 is the same width as the master)
- relheight=amount - height of this widget between 0.0 and 1.0 relative to height of master (1.0 is the same height as the master)
- bordermode="inside" or "outside" - whether to take border width of master widget into account
- """
- if "width" in kwargs or "height" in kwargs:
- raise ValueError("'width' and 'height' arguments must be passed to the constructor of the widget, not the place method")
- self._last_geometry_manager_call = {"function": super().place, "kwargs": kwargs}
- return super().place(**self._apply_argument_scaling(kwargs))
- def place_forget(self):
- """ Unmap this widget. """
- self._last_geometry_manager_call = None
- return super().place_forget()
- def pack(self, **kwargs):
- """
- Pack a widget in the parent widget. Use as options:
- after=widget - pack it after you have packed widget
- anchor=NSEW (or subset) - position widget according to given direction
- before=widget - pack it before you will pack widget
- expand=bool - expand widget if parent size grows
- fill=NONE or X or Y or BOTH - fill widget if widget grows
- in=master - use master to contain this widget
- in_=master - see 'in' option description
- ipadx=amount - add internal padding in x direction
- ipady=amount - add internal padding in y direction
- padx=amount - add padding in x direction
- pady=amount - add padding in y direction
- side=TOP or BOTTOM or LEFT or RIGHT - where to add this widget.
- """
- self._last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs}
- return super().pack(**self._apply_argument_scaling(kwargs))
- def pack_forget(self):
- """ Unmap this widget and do not use it for the packing order. """
- self._last_geometry_manager_call = None
- return super().pack_forget()
- def grid(self, **kwargs):
- """
- Position a widget in the parent widget in a grid. Use as options:
- column=number - use cell identified with given column (starting with 0)
- columnspan=number - this widget will span several columns
- in=master - use master to contain this widget
- in_=master - see 'in' option description
- ipadx=amount - add internal padding in x direction
- ipady=amount - add internal padding in y direction
- padx=amount - add padding in x direction
- pady=amount - add padding in y direction
- row=number - use cell identified with given row (starting with 0)
- rowspan=number - this widget will span several rows
- sticky=NSEW - if cell is larger on which sides will this widget stick to the cell boundary
- """
- self._last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs}
- return super().grid(**self._apply_argument_scaling(kwargs))
- def grid_forget(self):
- """ Unmap this widget. """
- self._last_geometry_manager_call = None
- return super().grid_forget()
|