ctk_base_class.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import sys
  2. import warnings
  3. import tkinter
  4. import tkinter.ttk as ttk
  5. from typing import Union, Callable, Tuple, Any
  6. try:
  7. from typing import TypedDict
  8. except ImportError:
  9. from typing_extensions import TypedDict
  10. from .... import windows # import windows for isinstance checks
  11. from ..theme import ThemeManager
  12. from ..font import CTkFont
  13. from ..image import CTkImage
  14. from ..appearance_mode import CTkAppearanceModeBaseClass
  15. from ..scaling import CTkScalingBaseClass
  16. from ..utility import pop_from_dict_by_set, check_kwargs_empty
  17. class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
  18. """ Base class of every CTk widget, handles the dimensions, bg_color,
  19. appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """
  20. # attributes that are passed to and managed by the tkinter frame only:
  21. _valid_tk_frame_attributes: set = {"cursor"}
  22. _cursor_manipulation_enabled: bool = True
  23. def __init__(self,
  24. master: Any,
  25. width: int = 0,
  26. height: int = 0,
  27. bg_color: Union[str, Tuple[str, str]] = "transparent",
  28. **kwargs):
  29. # call init methods of super classes
  30. tkinter.Frame.__init__(self, master=master, width=width, height=height, **pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))
  31. CTkAppearanceModeBaseClass.__init__(self)
  32. CTkScalingBaseClass.__init__(self, scaling_type="widget")
  33. # check if kwargs is empty, if not raise error for unsupported arguments
  34. check_kwargs_empty(kwargs, raise_error=True)
  35. # dimensions independent of scaling
  36. self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget
  37. self._current_height = height # _current_width and _current_height are independent of the scale
  38. self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height
  39. self._desired_height = height
  40. # set width and height of tkinter.Frame
  41. super().configure(width=self._apply_widget_scaling(self._desired_width),
  42. height=self._apply_widget_scaling(self._desired_height))
  43. # save latest geometry function and kwargs
  44. class GeometryCallDict(TypedDict):
  45. function: Callable
  46. kwargs: dict
  47. self._last_geometry_manager_call: Union[GeometryCallDict, None] = None
  48. # background color
  49. 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)
  50. # set bg color of tkinter.Frame
  51. super().configure(bg=self._apply_appearance_mode(self._bg_color))
  52. # add configure callback to tkinter.Frame
  53. super().bind('<Configure>', self._update_dimensions_event)
  54. # overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well
  55. 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)):
  56. master_old_configure = self.master.config
  57. def new_configure(*args, **kwargs):
  58. if "bg" in kwargs:
  59. self.configure(bg_color=kwargs["bg"])
  60. elif "background" in kwargs:
  61. self.configure(bg_color=kwargs["background"])
  62. # args[0] is dict when attribute gets changed by widget[<attribute>] syntax
  63. elif len(args) > 0 and type(args[0]) == dict:
  64. if "bg" in args[0]:
  65. self.configure(bg_color=args[0]["bg"])
  66. elif "background" in args[0]:
  67. self.configure(bg_color=args[0]["background"])
  68. master_old_configure(*args, **kwargs)
  69. self.master.config = new_configure
  70. self.master.configure = new_configure
  71. def destroy(self):
  72. """ Destroy this and all descendants widgets. """
  73. # call destroy methods of super classes
  74. tkinter.Frame.destroy(self)
  75. CTkAppearanceModeBaseClass.destroy(self)
  76. CTkScalingBaseClass.destroy(self)
  77. def _draw(self, no_color_updates: bool = False):
  78. """ can be overridden but super method must be called """
  79. if no_color_updates is False:
  80. # Configuring color of tkinter.Frame not necessary at the moment?
  81. # Causes flickering on Windows and Linux for segmented button for some reason!
  82. # super().configure(bg=self._apply_appearance_mode(self._bg_color))
  83. pass
  84. def config(self, *args, **kwargs):
  85. raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.")
  86. def configure(self, require_redraw=False, **kwargs):
  87. """ basic configure with bg_color, width, height support, calls configure of tkinter.Frame, updates in the end """
  88. if "width" in kwargs:
  89. self._set_dimensions(width=kwargs.pop("width"))
  90. if "height" in kwargs:
  91. self._set_dimensions(height=kwargs.pop("height"))
  92. if "bg_color" in kwargs:
  93. new_bg_color = self._check_color_type(kwargs.pop("bg_color"), transparency=True)
  94. if new_bg_color == "transparent":
  95. self._bg_color = self._detect_color_of_master()
  96. else:
  97. self._bg_color = self._check_color_type(new_bg_color)
  98. require_redraw = True
  99. super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes)) # configure tkinter.Frame
  100. # if there are still items in the kwargs dict, raise ValueError
  101. check_kwargs_empty(kwargs, raise_error=True)
  102. if require_redraw:
  103. self._draw()
  104. def cget(self, attribute_name: str):
  105. """ basic cget with bg_color, width, height support, calls cget of tkinter.Frame """
  106. if attribute_name == "bg_color":
  107. return self._bg_color
  108. elif attribute_name == "width":
  109. return self._desired_width
  110. elif attribute_name == "height":
  111. return self._desired_height
  112. elif attribute_name in self._valid_tk_frame_attributes:
  113. return super().cget(attribute_name) # cget of tkinter.Frame
  114. else:
  115. raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.")
  116. def _check_font_type(self, font: any):
  117. """ check font type when passed to widget """
  118. if isinstance(font, CTkFont):
  119. return font
  120. elif type(font) == tuple and len(font) == 1:
  121. warnings.warn(f"{type(self).__name__} Warning: font {font} given without size, will be extended with default text size of current theme\n")
  122. return font[0], ThemeManager.theme["text"]["size"]
  123. elif type(font) == tuple and 2 <= len(font) <= 6:
  124. return font
  125. else:
  126. raise ValueError(f"Wrong font type {type(font)}\n" +
  127. f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 to 6 or an instance of CTkFont.\n" +
  128. f"\nUsage example:\n" +
  129. f"font=customtkinter.CTkFont(family='<name>', size=<size in px>)\n" +
  130. f"font=('<name>', <size in px>)\n")
  131. def _check_image_type(self, image: any):
  132. """ check image type when passed to widget """
  133. if image is None:
  134. return image
  135. elif isinstance(image, CTkImage):
  136. return image
  137. else:
  138. 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")
  139. return image
  140. def _update_dimensions_event(self, event):
  141. # only redraw if dimensions changed (for performance), independent of scaling
  142. if round(self._current_width) != round(self._reverse_widget_scaling(event.width)) or round(self._current_height) != round(self._reverse_widget_scaling(event.height)):
  143. self._current_width = self._reverse_widget_scaling(event.width) # adjust current size according to new size given by event
  144. self._current_height = self._reverse_widget_scaling(event.height) # _current_width and _current_height are independent of the scale
  145. self._draw(no_color_updates=True) # faster drawing without color changes
  146. def _detect_color_of_master(self, master_widget=None) -> Union[str, Tuple[str, str]]:
  147. """ detect foreground color of master widget for bg_color and transparent color """
  148. if master_widget is None:
  149. master_widget = self.master
  150. if isinstance(master_widget, (windows.widgets.core_widget_classes.CTkBaseClass, windows.CTk, windows.CTkToplevel, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame)):
  151. if master_widget.cget("fg_color") is not None and master_widget.cget("fg_color") != "transparent":
  152. return master_widget.cget("fg_color")
  153. elif isinstance(master_widget, windows.widgets.ctk_scrollable_frame.CTkScrollableFrame):
  154. return self._detect_color_of_master(master_widget.master.master.master)
  155. # if fg_color of master is None, try to retrieve fg_color from master of master
  156. elif hasattr(master_widget, "master"):
  157. return self._detect_color_of_master(master_widget.master)
  158. elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget
  159. try:
  160. ttk_style = ttk.Style()
  161. return ttk_style.lookup(master_widget.winfo_class(), 'background')
  162. except Exception:
  163. return "#FFFFFF", "#000000"
  164. else: # master is normal tkinter widget
  165. try:
  166. return master_widget.cget("bg") # try to get bg color by .cget() method
  167. except Exception:
  168. return "#FFFFFF", "#000000"
  169. def _set_appearance_mode(self, mode_string):
  170. super()._set_appearance_mode(mode_string)
  171. self._draw()
  172. super().update_idletasks()
  173. def _set_scaling(self, new_widget_scaling, new_window_scaling):
  174. super()._set_scaling(new_widget_scaling, new_window_scaling)
  175. super().configure(width=self._apply_widget_scaling(self._desired_width),
  176. height=self._apply_widget_scaling(self._desired_height))
  177. if self._last_geometry_manager_call is not None:
  178. self._last_geometry_manager_call["function"](**self._apply_argument_scaling(self._last_geometry_manager_call["kwargs"]))
  179. def _set_dimensions(self, width=None, height=None):
  180. if width is not None:
  181. self._desired_width = width
  182. if height is not None:
  183. self._desired_height = height
  184. super().configure(width=self._apply_widget_scaling(self._desired_width),
  185. height=self._apply_widget_scaling(self._desired_height))
  186. def bind(self, sequence=None, command=None, add=None):
  187. raise NotImplementedError
  188. def unbind(self, sequence=None, funcid=None):
  189. raise NotImplementedError
  190. def unbind_all(self, sequence):
  191. raise AttributeError("'unbind_all' is not allowed, because it would delete necessary internal callbacks for all widgets")
  192. def bind_all(self, sequence=None, func=None, add=None):
  193. raise AttributeError("'bind_all' is not allowed, could result in undefined behavior")
  194. def place(self, **kwargs):
  195. """
  196. Place a widget in the parent widget. Use as options:
  197. in=master - master relative to which the widget is placed
  198. in_=master - see 'in' option description
  199. x=amount - locate anchor of this widget at position x of master
  200. y=amount - locate anchor of this widget at position y of master
  201. relx=amount - locate anchor of this widget between 0.0 and 1.0 relative to width of master (1.0 is right edge)
  202. rely=amount - locate anchor of this widget between 0.0 and 1.0 relative to height of master (1.0 is bottom edge)
  203. anchor=NSEW (or subset) - position anchor according to given direction
  204. width=amount - width of this widget in pixel
  205. height=amount - height of this widget in pixel
  206. 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)
  207. 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)
  208. bordermode="inside" or "outside" - whether to take border width of master widget into account
  209. """
  210. if "width" in kwargs or "height" in kwargs:
  211. raise ValueError("'width' and 'height' arguments must be passed to the constructor of the widget, not the place method")
  212. self._last_geometry_manager_call = {"function": super().place, "kwargs": kwargs}
  213. return super().place(**self._apply_argument_scaling(kwargs))
  214. def place_forget(self):
  215. """ Unmap this widget. """
  216. self._last_geometry_manager_call = None
  217. return super().place_forget()
  218. def pack(self, **kwargs):
  219. """
  220. Pack a widget in the parent widget. Use as options:
  221. after=widget - pack it after you have packed widget
  222. anchor=NSEW (or subset) - position widget according to given direction
  223. before=widget - pack it before you will pack widget
  224. expand=bool - expand widget if parent size grows
  225. fill=NONE or X or Y or BOTH - fill widget if widget grows
  226. in=master - use master to contain this widget
  227. in_=master - see 'in' option description
  228. ipadx=amount - add internal padding in x direction
  229. ipady=amount - add internal padding in y direction
  230. padx=amount - add padding in x direction
  231. pady=amount - add padding in y direction
  232. side=TOP or BOTTOM or LEFT or RIGHT - where to add this widget.
  233. """
  234. self._last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs}
  235. return super().pack(**self._apply_argument_scaling(kwargs))
  236. def pack_forget(self):
  237. """ Unmap this widget and do not use it for the packing order. """
  238. self._last_geometry_manager_call = None
  239. return super().pack_forget()
  240. def grid(self, **kwargs):
  241. """
  242. Position a widget in the parent widget in a grid. Use as options:
  243. column=number - use cell identified with given column (starting with 0)
  244. columnspan=number - this widget will span several columns
  245. in=master - use master to contain this widget
  246. in_=master - see 'in' option description
  247. ipadx=amount - add internal padding in x direction
  248. ipady=amount - add internal padding in y direction
  249. padx=amount - add padding in x direction
  250. pady=amount - add padding in y direction
  251. row=number - use cell identified with given row (starting with 0)
  252. rowspan=number - this widget will span several rows
  253. sticky=NSEW - if cell is larger on which sides will this widget stick to the cell boundary
  254. """
  255. self._last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs}
  256. return super().grid(**self._apply_argument_scaling(kwargs))
  257. def grid_forget(self):
  258. """ Unmap this widget. """
  259. self._last_geometry_manager_call = None
  260. return super().grid_forget()