ctk_scrollbar.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import sys
  2. from typing import Union, Tuple, Callable, Optional, Any
  3. from .core_rendering import CTkCanvas
  4. from .theme import ThemeManager
  5. from .core_rendering import DrawEngine
  6. from .core_widget_classes import CTkBaseClass
  7. class CTkScrollbar(CTkBaseClass):
  8. """
  9. Scrollbar with rounded corners, configurable spacing.
  10. Connect to scrollable widget by passing .set() method and set command attribute.
  11. For detailed information check out the documentation.
  12. """
  13. def __init__(self,
  14. master: Any,
  15. width: Optional[Union[int, str]] = None,
  16. height: Optional[Union[int, str]] = None,
  17. corner_radius: Optional[int] = None,
  18. border_spacing: Optional[int] = None,
  19. minimum_pixel_length: int = 20,
  20. bg_color: Union[str, Tuple[str, str]] = "transparent",
  21. fg_color: Optional[Union[str, Tuple[str, str]]] = None,
  22. button_color: Optional[Union[str, Tuple[str, str]]] = None,
  23. button_hover_color: Optional[Union[str, Tuple[str, str]]] = None,
  24. hover: bool = True,
  25. command: Union[Callable, Any] = None,
  26. orientation: str = "vertical",
  27. **kwargs):
  28. # set default dimensions according to orientation
  29. if width is None:
  30. if orientation.lower() == "vertical":
  31. width = 16
  32. else:
  33. width = 200
  34. if height is None:
  35. if orientation.lower() == "horizontal":
  36. height = 16
  37. else:
  38. height = 200
  39. # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass
  40. super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
  41. # color
  42. self._fg_color = ThemeManager.theme["CTkScrollbar"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True)
  43. self._button_color = ThemeManager.theme["CTkScrollbar"]["button_color"] if button_color is None else self._check_color_type(button_color)
  44. self._button_hover_color = ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color)
  45. # shape
  46. self._corner_radius = ThemeManager.theme["CTkScrollbar"]["corner_radius"] if corner_radius is None else corner_radius
  47. self._border_spacing = ThemeManager.theme["CTkScrollbar"]["border_spacing"] if border_spacing is None else border_spacing
  48. self._hover = hover
  49. self._hover_state: bool = False
  50. self._command = command
  51. self._orientation = orientation
  52. self._start_value: float = 0 # 0 to 1
  53. self._end_value: float = 1 # 0 to 1
  54. self._minimum_pixel_length = minimum_pixel_length
  55. self._canvas = CTkCanvas(master=self,
  56. highlightthickness=0,
  57. width=self._apply_widget_scaling(self._current_width),
  58. height=self._apply_widget_scaling(self._current_height))
  59. self._canvas.place(x=0, y=0, relwidth=1, relheight=1)
  60. self._draw_engine = DrawEngine(self._canvas)
  61. self._create_bindings()
  62. self._draw()
  63. def _create_bindings(self, sequence: Optional[str] = None):
  64. """ set necessary bindings for functionality of widget, will overwrite other bindings """
  65. if sequence is None:
  66. self._canvas.tag_bind("border_parts", "<Button-1>", self._clicked)
  67. if sequence is None or sequence == "<Enter>":
  68. self._canvas.bind("<Enter>", self._on_enter)
  69. if sequence is None or sequence == "<Leave>":
  70. self._canvas.bind("<Leave>", self._on_leave)
  71. if sequence is None or sequence == "<B1-Motion>":
  72. self._canvas.bind("<B1-Motion>", self._clicked)
  73. if sequence is None or sequence == "<MouseWheel>":
  74. self._canvas.bind("<MouseWheel>", self._mouse_scroll_event)
  75. def _set_scaling(self, *args, **kwargs):
  76. super()._set_scaling(*args, **kwargs)
  77. self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
  78. height=self._apply_widget_scaling(self._desired_height))
  79. self._draw(no_color_updates=True)
  80. def _set_dimensions(self, width=None, height=None):
  81. super()._set_dimensions(width, height)
  82. self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
  83. height=self._apply_widget_scaling(self._desired_height))
  84. self._draw(no_color_updates=True)
  85. def _get_scrollbar_values_for_minimum_pixel_size(self):
  86. # correct scrollbar float values if scrollbar is too small
  87. if self._orientation == "vertical":
  88. scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_height
  89. if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_height != 0:
  90. # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
  91. interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_height)
  92. corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor
  93. corrected_start_value = self._start_value - self._start_value * interval_extend_factor
  94. return corrected_start_value, corrected_end_value
  95. else:
  96. return self._start_value, self._end_value
  97. else:
  98. scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_width
  99. if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_width != 0:
  100. # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
  101. interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_width)
  102. corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor
  103. corrected_start_value = self._start_value - self._start_value * interval_extend_factor
  104. return corrected_start_value, corrected_end_value
  105. else:
  106. return self._start_value, self._end_value
  107. def _draw(self, no_color_updates=False):
  108. super()._draw(no_color_updates)
  109. corrected_start_value, corrected_end_value = self._get_scrollbar_values_for_minimum_pixel_size()
  110. requires_recoloring = self._draw_engine.draw_rounded_scrollbar(self._apply_widget_scaling(self._current_width),
  111. self._apply_widget_scaling(self._current_height),
  112. self._apply_widget_scaling(self._corner_radius),
  113. self._apply_widget_scaling(self._border_spacing),
  114. corrected_start_value,
  115. corrected_end_value,
  116. self._orientation)
  117. if no_color_updates is False or requires_recoloring:
  118. if self._hover_state is True:
  119. self._canvas.itemconfig("scrollbar_parts",
  120. fill=self._apply_appearance_mode(self._button_hover_color),
  121. outline=self._apply_appearance_mode(self._button_hover_color))
  122. else:
  123. self._canvas.itemconfig("scrollbar_parts",
  124. fill=self._apply_appearance_mode(self._button_color),
  125. outline=self._apply_appearance_mode(self._button_color))
  126. if self._fg_color == "transparent":
  127. self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
  128. self._canvas.itemconfig("border_parts",
  129. fill=self._apply_appearance_mode(self._bg_color),
  130. outline=self._apply_appearance_mode(self._bg_color))
  131. else:
  132. self._canvas.configure(bg=self._apply_appearance_mode(self._fg_color))
  133. self._canvas.itemconfig("border_parts",
  134. fill=self._apply_appearance_mode(self._fg_color),
  135. outline=self._apply_appearance_mode(self._fg_color))
  136. self._canvas.update_idletasks()
  137. def configure(self, require_redraw=False, **kwargs):
  138. if "fg_color" in kwargs:
  139. self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True)
  140. require_redraw = True
  141. if "button_color" in kwargs:
  142. self._button_color = self._check_color_type(kwargs.pop("button_color"))
  143. require_redraw = True
  144. if "button_hover_color" in kwargs:
  145. self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color"))
  146. require_redraw = True
  147. if "hover" in kwargs:
  148. self._hover = kwargs.pop("hover")
  149. if "command" in kwargs:
  150. self._command = kwargs.pop("command")
  151. if "corner_radius" in kwargs:
  152. self._corner_radius = kwargs.pop("corner_radius")
  153. require_redraw = True
  154. if "border_spacing" in kwargs:
  155. self._border_spacing = kwargs.pop("border_spacing")
  156. require_redraw = True
  157. super().configure(require_redraw=require_redraw, **kwargs)
  158. def cget(self, attribute_name: str) -> any:
  159. if attribute_name == "corner_radius":
  160. return self._corner_radius
  161. elif attribute_name == "border_spacing":
  162. return self._border_spacing
  163. elif attribute_name == "minimum_pixel_length":
  164. return self._minimum_pixel_length
  165. elif attribute_name == "fg_color":
  166. return self._fg_color
  167. elif attribute_name == "scrollbar_color":
  168. return self._button_color
  169. elif attribute_name == "scrollbar_hover_color":
  170. return self._button_hover_color
  171. elif attribute_name == "hover":
  172. return self._hover
  173. elif attribute_name == "command":
  174. return self._command
  175. elif attribute_name == "orientation":
  176. return self._orientation
  177. else:
  178. return super().cget(attribute_name)
  179. def _on_enter(self, event=0):
  180. if self._hover is True:
  181. self._hover_state = True
  182. self._canvas.itemconfig("scrollbar_parts",
  183. outline=self._apply_appearance_mode(self._button_hover_color),
  184. fill=self._apply_appearance_mode(self._button_hover_color))
  185. def _on_leave(self, event=0):
  186. self._hover_state = False
  187. self._canvas.itemconfig("scrollbar_parts",
  188. outline=self._apply_appearance_mode(self._button_color),
  189. fill=self._apply_appearance_mode(self._button_color))
  190. def _clicked(self, event):
  191. if self._orientation == "vertical":
  192. value = self._reverse_widget_scaling(((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing)))
  193. else:
  194. value = self._reverse_widget_scaling(((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing)))
  195. current_scrollbar_length = self._end_value - self._start_value
  196. value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2)))
  197. self._start_value = value - (current_scrollbar_length / 2)
  198. self._end_value = value + (current_scrollbar_length / 2)
  199. self._draw()
  200. if self._command is not None:
  201. self._command('moveto', self._start_value)
  202. def _mouse_scroll_event(self, event=None):
  203. if self._command is not None:
  204. if sys.platform.startswith("win"):
  205. self._command('scroll', -int(event.delta/40), 'units')
  206. else:
  207. self._command('scroll', -event.delta, 'units')
  208. def set(self, start_value: float, end_value: float):
  209. self._start_value = float(start_value)
  210. self._end_value = float(end_value)
  211. self._draw()
  212. def get(self):
  213. return self._start_value, self._end_value
  214. def bind(self, sequence=None, command=None, add=True):
  215. """ called on the tkinter.Canvas """
  216. if not (add == "+" or add is True):
  217. raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
  218. self._canvas.bind(sequence, command, add=True)
  219. def unbind(self, sequence=None, funcid=None):
  220. """ called on the tkinter.Canvas, restores internal callbacks """
  221. if funcid is not None:
  222. raise ValueError("'funcid' argument can only be None, because there is a bug in" +
  223. " tkinter and its not clear whether the internal callbacks will be unbinded or not")
  224. self._canvas.unbind(sequence, None) # unbind all callbacks for sequence
  225. self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
  226. def focus(self):
  227. return self._canvas.focus()
  228. def focus_set(self):
  229. return self._canvas.focus_set()
  230. def focus_force(self):
  231. return self._canvas.focus_force()