ctk_checkbox.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. import tkinter
  2. import sys
  3. from typing import Union, Tuple, Callable, Optional, Any
  4. from .core_rendering import CTkCanvas
  5. from .theme import ThemeManager
  6. from .core_rendering import DrawEngine
  7. from .core_widget_classes import CTkBaseClass
  8. from .font import CTkFont
  9. class CTkCheckBox(CTkBaseClass):
  10. """
  11. Checkbox with rounded corners, border, variable support and hover effect.
  12. For detailed information check out the documentation.
  13. """
  14. def __init__(self,
  15. master: Any,
  16. width: int = 100,
  17. height: int = 24,
  18. checkbox_width: int = 24,
  19. checkbox_height: int = 24,
  20. corner_radius: Optional[int] = None,
  21. border_width: Optional[int] = None,
  22. bg_color: Union[str, Tuple[str, str]] = "transparent",
  23. fg_color: Optional[Union[str, Tuple[str, str]]] = None,
  24. hover_color: Optional[Union[str, Tuple[str, str]]] = None,
  25. border_color: Optional[Union[str, Tuple[str, str]]] = None,
  26. checkmark_color: Optional[Union[str, Tuple[str, str]]] = None,
  27. text_color: Optional[Union[str, Tuple[str, str]]] = None,
  28. text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None,
  29. text: str = "CTkCheckBox",
  30. font: Optional[Union[tuple, CTkFont]] = None,
  31. textvariable: Union[tkinter.Variable, None] = None,
  32. state: str = tkinter.NORMAL,
  33. hover: bool = True,
  34. command: Union[Callable[[], Any], None] = None,
  35. onvalue: Union[int, str] = 1,
  36. offvalue: Union[int, str] = 0,
  37. variable: Union[tkinter.Variable, None] = None,
  38. **kwargs):
  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. # dimensions
  42. self._checkbox_width = checkbox_width
  43. self._checkbox_height = checkbox_height
  44. # color
  45. self._fg_color = ThemeManager.theme["CTkCheckBox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
  46. self._hover_color = ThemeManager.theme["CTkCheckBox"]["hover_color"] if hover_color is None else self._check_color_type(hover_color)
  47. self._border_color = ThemeManager.theme["CTkCheckBox"]["border_color"] if border_color is None else self._check_color_type(border_color)
  48. self._checkmark_color = ThemeManager.theme["CTkCheckBox"]["checkmark_color"] if checkmark_color is None else self._check_color_type(checkmark_color)
  49. # shape
  50. self._corner_radius = ThemeManager.theme["CTkCheckBox"]["corner_radius"] if corner_radius is None else corner_radius
  51. self._border_width = ThemeManager.theme["CTkCheckBox"]["border_width"] if border_width is None else border_width
  52. # text
  53. self._text = text
  54. self._text_label: Union[tkinter.Label, None] = None
  55. self._text_color = ThemeManager.theme["CTkCheckBox"]["text_color"] if text_color is None else self._check_color_type(text_color)
  56. self._text_color_disabled = ThemeManager.theme["CTkCheckBox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled)
  57. # font
  58. self._font = CTkFont() if font is None else self._check_font_type(font)
  59. if isinstance(self._font, CTkFont):
  60. self._font.add_size_configure_callback(self._update_font)
  61. # callback and hover functionality
  62. self._command = command
  63. self._state = state
  64. self._hover = hover
  65. self._check_state = False
  66. self._onvalue = onvalue
  67. self._offvalue = offvalue
  68. self._variable: tkinter.Variable = variable
  69. self._variable_callback_blocked = False
  70. self._textvariable: tkinter.Variable = textvariable
  71. self._variable_callback_name = None
  72. # configure grid system (1x3)
  73. self.grid_columnconfigure(0, weight=0)
  74. self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
  75. self.grid_columnconfigure(2, weight=1)
  76. self.grid_rowconfigure(0, weight=1)
  77. self._bg_canvas = CTkCanvas(master=self,
  78. highlightthickness=0,
  79. width=self._apply_widget_scaling(self._desired_width),
  80. height=self._apply_widget_scaling(self._desired_height))
  81. self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
  82. self._canvas = CTkCanvas(master=self,
  83. highlightthickness=0,
  84. width=self._apply_widget_scaling(self._checkbox_width),
  85. height=self._apply_widget_scaling(self._checkbox_height))
  86. self._canvas.grid(row=0, column=0, sticky="e")
  87. self._draw_engine = DrawEngine(self._canvas)
  88. self._text_label = tkinter.Label(master=self,
  89. bd=0,
  90. padx=0,
  91. pady=0,
  92. text=self._text,
  93. justify=tkinter.LEFT,
  94. font=self._apply_font_scaling(self._font),
  95. textvariable=self._textvariable)
  96. self._text_label.grid(row=0, column=2, sticky="w")
  97. self._text_label["anchor"] = "w"
  98. # register variable callback and set state according to variable
  99. if self._variable is not None and self._variable != "":
  100. self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
  101. self._check_state = True if self._variable.get() == self._onvalue else False
  102. self._create_bindings()
  103. self._set_cursor()
  104. self._draw()
  105. def _create_bindings(self, sequence: Optional[str] = None):
  106. """ set necessary bindings for functionality of widget, will overwrite other bindings """
  107. if sequence is None or sequence == "<Enter>":
  108. self._canvas.bind("<Enter>", self._on_enter)
  109. self._text_label.bind("<Enter>", self._on_enter)
  110. if sequence is None or sequence == "<Leave>":
  111. self._canvas.bind("<Leave>", self._on_leave)
  112. self._text_label.bind("<Leave>", self._on_leave)
  113. if sequence is None or sequence == "<Button-1>":
  114. self._canvas.bind("<Button-1>", self.toggle)
  115. self._text_label.bind("<Button-1>", self.toggle)
  116. def _set_scaling(self, *args, **kwargs):
  117. super()._set_scaling(*args, **kwargs)
  118. self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
  119. self._text_label.configure(font=self._apply_font_scaling(self._font))
  120. self._canvas.delete("checkmark")
  121. self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
  122. height=self._apply_widget_scaling(self._desired_height))
  123. self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width),
  124. height=self._apply_widget_scaling(self._checkbox_height))
  125. self._draw(no_color_updates=True)
  126. def _set_dimensions(self, width: int = None, height: int = None):
  127. super()._set_dimensions(width, height)
  128. self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
  129. height=self._apply_widget_scaling(self._desired_height))
  130. def _update_font(self):
  131. """ pass font to tkinter widgets with applied font scaling and update grid with workaround """
  132. if self._text_label is not None:
  133. self._text_label.configure(font=self._apply_font_scaling(self._font))
  134. # Workaround to force grid to be resized when text changes size.
  135. # Otherwise grid will lag and only resizes if other mouse action occurs.
  136. self._bg_canvas.grid_forget()
  137. self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
  138. def destroy(self):
  139. if self._variable is not None:
  140. self._variable.trace_remove("write", self._variable_callback_name)
  141. if isinstance(self._font, CTkFont):
  142. self._font.remove_size_configure_callback(self._update_font)
  143. super().destroy()
  144. def _draw(self, no_color_updates=False):
  145. super()._draw(no_color_updates)
  146. requires_recoloring_1 = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._checkbox_width),
  147. self._apply_widget_scaling(self._checkbox_height),
  148. self._apply_widget_scaling(self._corner_radius),
  149. self._apply_widget_scaling(self._border_width))
  150. if self._check_state is True:
  151. requires_recoloring_2 = self._draw_engine.draw_checkmark(self._apply_widget_scaling(self._checkbox_width),
  152. self._apply_widget_scaling(self._checkbox_height),
  153. self._apply_widget_scaling(self._checkbox_height * 0.58))
  154. else:
  155. requires_recoloring_2 = False
  156. self._canvas.delete("checkmark")
  157. if no_color_updates is False or requires_recoloring_1 or requires_recoloring_2:
  158. self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
  159. self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
  160. if self._check_state is True:
  161. self._canvas.itemconfig("inner_parts",
  162. outline=self._apply_appearance_mode(self._fg_color),
  163. fill=self._apply_appearance_mode(self._fg_color))
  164. self._canvas.itemconfig("border_parts",
  165. outline=self._apply_appearance_mode(self._fg_color),
  166. fill=self._apply_appearance_mode(self._fg_color))
  167. if "create_line" in self._canvas.gettags("checkmark"):
  168. self._canvas.itemconfig("checkmark", fill=self._apply_appearance_mode(self._checkmark_color))
  169. else:
  170. self._canvas.itemconfig("checkmark", fill=self._apply_appearance_mode(self._checkmark_color))
  171. else:
  172. self._canvas.itemconfig("inner_parts",
  173. outline=self._apply_appearance_mode(self._bg_color),
  174. fill=self._apply_appearance_mode(self._bg_color))
  175. self._canvas.itemconfig("border_parts",
  176. outline=self._apply_appearance_mode(self._border_color),
  177. fill=self._apply_appearance_mode(self._border_color))
  178. if self._state == tkinter.DISABLED:
  179. self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
  180. else:
  181. self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
  182. self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
  183. def configure(self, require_redraw=False, **kwargs):
  184. if "corner_radius" in kwargs:
  185. self._corner_radius = kwargs.pop("corner_radius")
  186. require_redraw = True
  187. if "border_width" in kwargs:
  188. self._border_width = kwargs.pop("border_width")
  189. require_redraw = True
  190. if "checkbox_width" in kwargs:
  191. self._checkbox_width = kwargs.pop("checkbox_width")
  192. self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width))
  193. require_redraw = True
  194. if "checkbox_height" in kwargs:
  195. self._checkbox_height = kwargs.pop("checkbox_height")
  196. self._canvas.configure(height=self._apply_widget_scaling(self._checkbox_height))
  197. require_redraw = True
  198. if "text" in kwargs:
  199. self._text = kwargs.pop("text")
  200. self._text_label.configure(text=self._text)
  201. if "font" in kwargs:
  202. if isinstance(self._font, CTkFont):
  203. self._font.remove_size_configure_callback(self._update_font)
  204. self._font = self._check_font_type(kwargs.pop("font"))
  205. if isinstance(self._font, CTkFont):
  206. self._font.add_size_configure_callback(self._update_font)
  207. self._update_font()
  208. if "state" in kwargs:
  209. self._state = kwargs.pop("state")
  210. self._set_cursor()
  211. require_redraw = True
  212. if "fg_color" in kwargs:
  213. self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
  214. require_redraw = True
  215. if "hover_color" in kwargs:
  216. self._hover_color = self._check_color_type(kwargs.pop("hover_color"))
  217. require_redraw = True
  218. if "border_color" in kwargs:
  219. self._border_color = self._check_color_type(kwargs.pop("border_color"))
  220. require_redraw = True
  221. if "checkmark_color" in kwargs:
  222. self._checkmark_color = self._check_color_type(kwargs.pop("checkmark_color"))
  223. require_redraw = True
  224. if "text_color" in kwargs:
  225. self._text_color = self._check_color_type(kwargs.pop("text_color"))
  226. require_redraw = True
  227. if "text_color_disabled" in kwargs:
  228. self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled"))
  229. require_redraw = True
  230. if "hover" in kwargs:
  231. self._hover = kwargs.pop("hover")
  232. if "command" in kwargs:
  233. self._command = kwargs.pop("command")
  234. if "textvariable" in kwargs:
  235. self._textvariable = kwargs.pop("textvariable")
  236. self._text_label.configure(textvariable=self._textvariable)
  237. if "variable" in kwargs:
  238. if self._variable is not None and self._variable != "":
  239. self._variable.trace_remove("write", self._variable_callback_name) # remove old variable callback
  240. self._variable = kwargs.pop("variable")
  241. if self._variable is not None and self._variable != "":
  242. self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
  243. self._check_state = True if self._variable.get() == self._onvalue else False
  244. require_redraw = True
  245. super().configure(require_redraw=require_redraw, **kwargs)
  246. def cget(self, attribute_name: str) -> any:
  247. if attribute_name == "corner_radius":
  248. return self._corner_radius
  249. elif attribute_name == "border_width":
  250. return self._border_width
  251. elif attribute_name == "checkbox_width":
  252. return self._checkbox_width
  253. elif attribute_name == "checkbox_height":
  254. return self._checkbox_height
  255. elif attribute_name == "fg_color":
  256. return self._fg_color
  257. elif attribute_name == "hover_color":
  258. return self._hover_color
  259. elif attribute_name == "border_color":
  260. return self._border_color
  261. elif attribute_name == "checkmark_color":
  262. return self._checkmark_color
  263. elif attribute_name == "text_color":
  264. return self._text_color
  265. elif attribute_name == "text_color_disabled":
  266. return self._text_color_disabled
  267. elif attribute_name == "text":
  268. return self._text
  269. elif attribute_name == "font":
  270. return self._font
  271. elif attribute_name == "textvariable":
  272. return self._textvariable
  273. elif attribute_name == "state":
  274. return self._state
  275. elif attribute_name == "hover":
  276. return self._hover
  277. elif attribute_name == "onvalue":
  278. return self._onvalue
  279. elif attribute_name == "offvalue":
  280. return self._offvalue
  281. elif attribute_name == "variable":
  282. return self._variable
  283. else:
  284. return super().cget(attribute_name)
  285. def _set_cursor(self):
  286. if self._cursor_manipulation_enabled:
  287. if self._state == tkinter.DISABLED:
  288. if sys.platform == "darwin":
  289. self._canvas.configure(cursor="arrow")
  290. if self._text_label is not None:
  291. self._text_label.configure(cursor="arrow")
  292. elif sys.platform.startswith("win"):
  293. self._canvas.configure(cursor="arrow")
  294. if self._text_label is not None:
  295. self._text_label.configure(cursor="arrow")
  296. elif self._state == tkinter.NORMAL:
  297. if sys.platform == "darwin":
  298. self._canvas.configure(cursor="pointinghand")
  299. if self._text_label is not None:
  300. self._text_label.configure(cursor="pointinghand")
  301. elif sys.platform.startswith("win"):
  302. self._canvas.configure(cursor="hand2")
  303. if self._text_label is not None:
  304. self._text_label.configure(cursor="hand2")
  305. def _on_enter(self, event=0):
  306. if self._hover is True and self._state == tkinter.NORMAL:
  307. if self._check_state is True:
  308. self._canvas.itemconfig("inner_parts",
  309. fill=self._apply_appearance_mode(self._hover_color),
  310. outline=self._apply_appearance_mode(self._hover_color))
  311. self._canvas.itemconfig("border_parts",
  312. fill=self._apply_appearance_mode(self._hover_color),
  313. outline=self._apply_appearance_mode(self._hover_color))
  314. else:
  315. self._canvas.itemconfig("inner_parts",
  316. fill=self._apply_appearance_mode(self._hover_color),
  317. outline=self._apply_appearance_mode(self._hover_color))
  318. def _on_leave(self, event=0):
  319. if self._check_state is True:
  320. self._canvas.itemconfig("inner_parts",
  321. fill=self._apply_appearance_mode(self._fg_color),
  322. outline=self._apply_appearance_mode(self._fg_color))
  323. self._canvas.itemconfig("border_parts",
  324. fill=self._apply_appearance_mode(self._fg_color),
  325. outline=self._apply_appearance_mode(self._fg_color))
  326. else:
  327. self._canvas.itemconfig("inner_parts",
  328. fill=self._apply_appearance_mode(self._bg_color),
  329. outline=self._apply_appearance_mode(self._bg_color))
  330. self._canvas.itemconfig("border_parts",
  331. fill=self._apply_appearance_mode(self._border_color),
  332. outline=self._apply_appearance_mode(self._border_color))
  333. def _variable_callback(self, var_name, index, mode):
  334. if not self._variable_callback_blocked:
  335. if self._variable.get() == self._onvalue:
  336. self.select(from_variable_callback=True)
  337. elif self._variable.get() == self._offvalue:
  338. self.deselect(from_variable_callback=True)
  339. def toggle(self, event=0):
  340. if self._state == tkinter.NORMAL:
  341. if self._check_state is True:
  342. self._check_state = False
  343. self._draw()
  344. else:
  345. self._check_state = True
  346. self._draw()
  347. if self._variable is not None:
  348. self._variable_callback_blocked = True
  349. self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
  350. self._variable_callback_blocked = False
  351. if self._command is not None:
  352. self._command()
  353. def select(self, from_variable_callback=False):
  354. self._check_state = True
  355. self._draw()
  356. if self._variable is not None and not from_variable_callback:
  357. self._variable_callback_blocked = True
  358. self._variable.set(self._onvalue)
  359. self._variable_callback_blocked = False
  360. def deselect(self, from_variable_callback=False):
  361. self._check_state = False
  362. self._draw()
  363. if self._variable is not None and not from_variable_callback:
  364. self._variable_callback_blocked = True
  365. self._variable.set(self._offvalue)
  366. self._variable_callback_blocked = False
  367. def get(self) -> Union[int, str]:
  368. return self._onvalue if self._check_state is True else self._offvalue
  369. def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True):
  370. """ called on the tkinter.Canvas """
  371. if not (add == "+" or add is True):
  372. raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks")
  373. self._canvas.bind(sequence, command, add=True)
  374. self._text_label.bind(sequence, command, add=True)
  375. def unbind(self, sequence: str = None, funcid: str = None):
  376. """ called on the tkinter.Label and tkinter.Canvas """
  377. if funcid is not None:
  378. raise ValueError("'funcid' argument can only be None, because there is a bug in" +
  379. " tkinter and its not clear whether the internal callbacks will be unbinded or not")
  380. self._canvas.unbind(sequence, None)
  381. self._text_label.unbind(sequence, None)
  382. self._create_bindings(sequence=sequence) # restore internal callbacks for sequence
  383. def focus(self):
  384. return self._text_label.focus()
  385. def focus_set(self):
  386. return self._text_label.focus_set()
  387. def focus_force(self):
  388. return self._text_label.focus_force()