ctk_scrollable_frame.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. from typing import Union, Tuple, Optional, Any
  2. try:
  3. from typing import Literal
  4. except ImportError:
  5. from typing_extensions import Literal
  6. import tkinter
  7. import sys
  8. from .ctk_frame import CTkFrame
  9. from .ctk_scrollbar import CTkScrollbar
  10. from .appearance_mode import CTkAppearanceModeBaseClass
  11. from .scaling import CTkScalingBaseClass
  12. from .core_widget_classes import CTkBaseClass
  13. from .ctk_label import CTkLabel
  14. from .font import CTkFont
  15. from .theme import ThemeManager
  16. class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
  17. def __init__(self,
  18. master: Any,
  19. width: int = 200,
  20. height: int = 200,
  21. corner_radius: Optional[Union[int, str]] = None,
  22. border_width: Optional[Union[int, str]] = None,
  23. bg_color: Union[str, Tuple[str, str]] = "transparent",
  24. fg_color: Optional[Union[str, Tuple[str, str]]] = None,
  25. border_color: Optional[Union[str, Tuple[str, str]]] = None,
  26. scrollbar_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
  27. scrollbar_button_color: Optional[Union[str, Tuple[str, str]]] = None,
  28. scrollbar_button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
  29. label_fg_color: Optional[Union[str, Tuple[str, str]]] = None,
  30. label_text_color: Optional[Union[str, Tuple[str, str]]] = None,
  31. label_text: str = "",
  32. label_font: Optional[Union[tuple, CTkFont]] = None,
  33. label_anchor: str = "center",
  34. orientation: Literal["vertical", "horizontal"] = "vertical"):
  35. self._orientation = orientation
  36. # dimensions independent of scaling
  37. self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height
  38. self._desired_height = height
  39. self._parent_frame = CTkFrame(master=master, width=0, height=0, corner_radius=corner_radius,
  40. border_width=border_width, bg_color=bg_color, fg_color=fg_color, border_color=border_color)
  41. self._parent_canvas = tkinter.Canvas(master=self._parent_frame, highlightthickness=0)
  42. self._set_scroll_increments()
  43. if self._orientation == "horizontal":
  44. self._scrollbar = CTkScrollbar(master=self._parent_frame, orientation="horizontal", command=self._parent_canvas.xview,
  45. fg_color=scrollbar_fg_color, button_color=scrollbar_button_color, button_hover_color=scrollbar_button_hover_color)
  46. self._parent_canvas.configure(xscrollcommand=self._scrollbar.set)
  47. elif self._orientation == "vertical":
  48. self._scrollbar = CTkScrollbar(master=self._parent_frame, orientation="vertical", command=self._parent_canvas.yview,
  49. fg_color=scrollbar_fg_color, button_color=scrollbar_button_color, button_hover_color=scrollbar_button_hover_color)
  50. self._parent_canvas.configure(yscrollcommand=self._scrollbar.set)
  51. self._label_text = label_text
  52. self._label = CTkLabel(self._parent_frame, text=label_text, anchor=label_anchor, font=label_font,
  53. corner_radius=self._parent_frame.cget("corner_radius"), text_color=label_text_color,
  54. fg_color=ThemeManager.theme["CTkScrollableFrame"]["label_fg_color"] if label_fg_color is None else label_fg_color)
  55. tkinter.Frame.__init__(self, master=self._parent_canvas, highlightthickness=0)
  56. CTkAppearanceModeBaseClass.__init__(self)
  57. CTkScalingBaseClass.__init__(self, scaling_type="widget")
  58. self._create_grid()
  59. self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
  60. height=self._apply_widget_scaling(self._desired_height))
  61. self.bind("<Configure>", lambda e: self._parent_canvas.configure(scrollregion=self._parent_canvas.bbox("all")))
  62. self._parent_canvas.bind("<Configure>", self._fit_frame_dimensions_to_canvas)
  63. self.bind_all("<MouseWheel>", self._mouse_wheel_all, add="+")
  64. self.bind_all("<KeyPress-Shift_L>", self._keyboard_shift_press_all, add="+")
  65. self.bind_all("<KeyPress-Shift_R>", self._keyboard_shift_press_all, add="+")
  66. self.bind_all("<KeyRelease-Shift_L>", self._keyboard_shift_release_all, add="+")
  67. self.bind_all("<KeyRelease-Shift_R>", self._keyboard_shift_release_all, add="+")
  68. self._create_window_id = self._parent_canvas.create_window(0, 0, window=self, anchor="nw")
  69. if self._parent_frame.cget("fg_color") == "transparent":
  70. tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
  71. self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
  72. else:
  73. tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
  74. self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
  75. self._shift_pressed = False
  76. def destroy(self):
  77. tkinter.Frame.destroy(self)
  78. CTkAppearanceModeBaseClass.destroy(self)
  79. CTkScalingBaseClass.destroy(self)
  80. def _create_grid(self):
  81. border_spacing = self._apply_widget_scaling(self._parent_frame.cget("corner_radius") + self._parent_frame.cget("border_width"))
  82. if self._orientation == "horizontal":
  83. self._parent_frame.grid_columnconfigure(0, weight=1)
  84. self._parent_frame.grid_rowconfigure(1, weight=1)
  85. self._parent_canvas.grid(row=1, column=0, sticky="nsew", padx=border_spacing, pady=(border_spacing, 0))
  86. self._scrollbar.grid(row=2, column=0, sticky="nsew", padx=border_spacing)
  87. if self._label_text is not None and self._label_text != "":
  88. self._label.grid(row=0, column=0, sticky="ew", padx=border_spacing, pady=border_spacing)
  89. else:
  90. self._label.grid_forget()
  91. elif self._orientation == "vertical":
  92. self._parent_frame.grid_columnconfigure(0, weight=1)
  93. self._parent_frame.grid_rowconfigure(1, weight=1)
  94. self._parent_canvas.grid(row=1, column=0, sticky="nsew", padx=(border_spacing, 0), pady=border_spacing)
  95. self._scrollbar.grid(row=1, column=1, sticky="nsew", pady=border_spacing)
  96. if self._label_text is not None and self._label_text != "":
  97. self._label.grid(row=0, column=0, columnspan=2, sticky="ew", padx=border_spacing, pady=border_spacing)
  98. else:
  99. self._label.grid_forget()
  100. def _set_appearance_mode(self, mode_string):
  101. super()._set_appearance_mode(mode_string)
  102. if self._parent_frame.cget("fg_color") == "transparent":
  103. tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
  104. self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
  105. else:
  106. tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
  107. self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
  108. def _set_scaling(self, new_widget_scaling, new_window_scaling):
  109. super()._set_scaling(new_widget_scaling, new_window_scaling)
  110. self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
  111. height=self._apply_widget_scaling(self._desired_height))
  112. def _set_dimensions(self, width=None, height=None):
  113. if width is not None:
  114. self._desired_width = width
  115. if height is not None:
  116. self._desired_height = height
  117. self._parent_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
  118. height=self._apply_widget_scaling(self._desired_height))
  119. def configure(self, **kwargs):
  120. if "width" in kwargs:
  121. self._set_dimensions(width=kwargs.pop("width"))
  122. if "height" in kwargs:
  123. self._set_dimensions(height=kwargs.pop("height"))
  124. if "corner_radius" in kwargs:
  125. new_corner_radius = kwargs.pop("corner_radius")
  126. self._parent_frame.configure(corner_radius=new_corner_radius)
  127. if self._label is not None:
  128. self._label.configure(corner_radius=new_corner_radius)
  129. self._create_grid()
  130. if "border_width" in kwargs:
  131. self._parent_frame.configure(border_width=kwargs.pop("border_width"))
  132. self._create_grid()
  133. if "fg_color" in kwargs:
  134. self._parent_frame.configure(fg_color=kwargs.pop("fg_color"))
  135. if self._parent_frame.cget("fg_color") == "transparent":
  136. tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
  137. self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("bg_color")))
  138. else:
  139. tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
  140. self._parent_canvas.configure(bg=self._apply_appearance_mode(self._parent_frame.cget("fg_color")))
  141. for child in self.winfo_children():
  142. if isinstance(child, CTkBaseClass):
  143. child.configure(bg_color=self._parent_frame.cget("fg_color"))
  144. if "scrollbar_fg_color" in kwargs:
  145. self._scrollbar.configure(fg_color=kwargs.pop("scrollbar_fg_color"))
  146. if "scrollbar_button_color" in kwargs:
  147. self._scrollbar.configure(button_color=kwargs.pop("scrollbar_button_color"))
  148. if "scrollbar_button_hover_color" in kwargs:
  149. self._scrollbar.configure(button_hover_color=kwargs.pop("scrollbar_button_hover_color"))
  150. if "label_text" in kwargs:
  151. self._label_text = kwargs.pop("label_text")
  152. self._label.configure(text=self._label_text)
  153. self._create_grid()
  154. if "label_font" in kwargs:
  155. self._label.configure(font=kwargs.pop("label_font"))
  156. if "label_text_color" in kwargs:
  157. self._label.configure(text_color=kwargs.pop("label_text_color"))
  158. if "label_fg_color" in kwargs:
  159. self._label.configure(fg_color=kwargs.pop("label_fg_color"))
  160. if "label_anchor" in kwargs:
  161. self._label.configure(anchor=kwargs.pop("label_anchor"))
  162. self._parent_frame.configure(**kwargs)
  163. def cget(self, attribute_name: str):
  164. if attribute_name == "width":
  165. return self._desired_width
  166. elif attribute_name == "height":
  167. return self._desired_height
  168. elif attribute_name == "label_text":
  169. return self._label_text
  170. elif attribute_name == "label_font":
  171. return self._label.cget("font")
  172. elif attribute_name == "label_text_color":
  173. return self._label.cget("_text_color")
  174. elif attribute_name == "label_fg_color":
  175. return self._label.cget("fg_color")
  176. elif attribute_name == "label_anchor":
  177. return self._label.cget("anchor")
  178. elif attribute_name.startswith("scrollbar_fg_color"):
  179. return self._scrollbar.cget("fg_color")
  180. elif attribute_name.startswith("scrollbar_button_color"):
  181. return self._scrollbar.cget("button_color")
  182. elif attribute_name.startswith("scrollbar_button_hover_color"):
  183. return self._scrollbar.cget("button_hover_color")
  184. else:
  185. return self._parent_frame.cget(attribute_name)
  186. def _fit_frame_dimensions_to_canvas(self, event):
  187. if self._orientation == "horizontal":
  188. self._parent_canvas.itemconfigure(self._create_window_id, height=self._parent_canvas.winfo_height())
  189. elif self._orientation == "vertical":
  190. self._parent_canvas.itemconfigure(self._create_window_id, width=self._parent_canvas.winfo_width())
  191. def _set_scroll_increments(self):
  192. if sys.platform.startswith("win"):
  193. self._parent_canvas.configure(xscrollincrement=1, yscrollincrement=1)
  194. elif sys.platform == "darwin":
  195. self._parent_canvas.configure(xscrollincrement=4, yscrollincrement=8)
  196. def _mouse_wheel_all(self, event):
  197. if self.check_if_master_is_canvas(event.widget):
  198. if sys.platform.startswith("win"):
  199. if self._shift_pressed:
  200. if self._parent_canvas.xview() != (0.0, 1.0):
  201. self._parent_canvas.xview("scroll", -int(event.delta / 6), "units")
  202. else:
  203. if self._parent_canvas.yview() != (0.0, 1.0):
  204. self._parent_canvas.yview("scroll", -int(event.delta / 6), "units")
  205. elif sys.platform == "darwin":
  206. if self._shift_pressed:
  207. if self._parent_canvas.xview() != (0.0, 1.0):
  208. self._parent_canvas.xview("scroll", -event.delta, "units")
  209. else:
  210. if self._parent_canvas.yview() != (0.0, 1.0):
  211. self._parent_canvas.yview("scroll", -event.delta, "units")
  212. else:
  213. if self._shift_pressed:
  214. if self._parent_canvas.xview() != (0.0, 1.0):
  215. self._parent_canvas.xview("scroll", -event.delta, "units")
  216. else:
  217. if self._parent_canvas.yview() != (0.0, 1.0):
  218. self._parent_canvas.yview("scroll", -event.delta, "units")
  219. def _keyboard_shift_press_all(self, event):
  220. self._shift_pressed = True
  221. def _keyboard_shift_release_all(self, event):
  222. self._shift_pressed = False
  223. def check_if_master_is_canvas(self, widget):
  224. if widget == self._parent_canvas:
  225. return True
  226. elif widget.master is not None:
  227. return self.check_if_master_is_canvas(widget.master)
  228. else:
  229. return False
  230. def pack(self, **kwargs):
  231. self._parent_frame.pack(**kwargs)
  232. def place(self, **kwargs):
  233. self._parent_frame.place(**kwargs)
  234. def grid(self, **kwargs):
  235. self._parent_frame.grid(**kwargs)
  236. def pack_forget(self):
  237. self._parent_frame.pack_forget()
  238. def place_forget(self, **kwargs):
  239. self._parent_frame.place_forget()
  240. def grid_forget(self, **kwargs):
  241. self._parent_frame.grid_forget()
  242. def grid_remove(self, **kwargs):
  243. self._parent_frame.grid_remove()
  244. def grid_propagate(self, **kwargs):
  245. self._parent_frame.grid_propagate()
  246. def grid_info(self, **kwargs):
  247. return self._parent_frame.grid_info()
  248. def lift(self, aboveThis=None):
  249. self._parent_frame.lift(aboveThis)
  250. def lower(self, belowThis=None):
  251. self._parent_frame.lower(belowThis)