ctk_toplevel.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import tkinter
  2. from packaging import version
  3. import sys
  4. import os
  5. import platform
  6. import ctypes
  7. from typing import Union, Tuple, Optional
  8. from .widgets.theme import ThemeManager
  9. from .widgets.scaling import CTkScalingBaseClass
  10. from .widgets.appearance_mode import CTkAppearanceModeBaseClass
  11. from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
  12. class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
  13. """
  14. Toplevel window with dark titlebar on Windows and macOS.
  15. For detailed information check out the documentation.
  16. """
  17. _valid_tk_toplevel_arguments: set = {"master", "bd", "borderwidth", "class", "container", "cursor", "height",
  18. "highlightbackground", "highlightthickness", "menu", "relief",
  19. "screen", "takefocus", "use", "visual", "width"}
  20. _deactivate_macos_window_header_manipulation: bool = False
  21. _deactivate_windows_window_header_manipulation: bool = False
  22. def __init__(self, *args,
  23. fg_color: Optional[Union[str, Tuple[str, str]]] = None,
  24. **kwargs):
  25. self._enable_macos_dark_title_bar()
  26. # call init methods of super classes
  27. super().__init__(*args, **pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
  28. CTkAppearanceModeBaseClass.__init__(self)
  29. CTkScalingBaseClass.__init__(self, scaling_type="window")
  30. check_kwargs_empty(kwargs, raise_error=True)
  31. try:
  32. # Set Windows titlebar icon
  33. if sys.platform.startswith("win"):
  34. customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  35. self.after(200, lambda: self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico")))
  36. except Exception:
  37. pass
  38. self._current_width = 200 # initial window size, always without scaling
  39. self._current_height = 200
  40. self._min_width: int = 0
  41. self._min_height: int = 0
  42. self._max_width: int = 1_000_000
  43. self._max_height: int = 1_000_000
  44. self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
  45. self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color)
  46. # set bg color of tkinter.Toplevel
  47. super().configure(bg=self._apply_appearance_mode(self._fg_color))
  48. # set title of tkinter.Toplevel
  49. super().title("CTkToplevel")
  50. # indicator variables
  51. self._iconbitmap_method_called = True
  52. self._state_before_windows_set_titlebar_color = None
  53. self._windows_set_titlebar_color_called = False # indicates if windows_set_titlebar_color was called, stays True until revert_withdraw_after_windows_set_titlebar_color is called
  54. self._withdraw_called_after_windows_set_titlebar_color = False # indicates if withdraw() was called after windows_set_titlebar_color
  55. self._iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color
  56. self._block_update_dimensions_event = False
  57. # save focus before calling withdraw
  58. self.focused_widget_before_widthdraw = None
  59. # set CustomTkinter titlebar icon (Windows only)
  60. if sys.platform.startswith("win"):
  61. self.after(200, self._windows_set_titlebar_icon)
  62. # set titlebar color (Windows only)
  63. if sys.platform.startswith("win"):
  64. self._windows_set_titlebar_color(self._get_appearance_mode())
  65. self.bind('<Configure>', self._update_dimensions_event)
  66. self.bind('<FocusIn>', self._focus_in_event)
  67. def destroy(self):
  68. self._disable_macos_dark_title_bar()
  69. # call destroy methods of super classes
  70. tkinter.Toplevel.destroy(self)
  71. CTkAppearanceModeBaseClass.destroy(self)
  72. CTkScalingBaseClass.destroy(self)
  73. def _focus_in_event(self, event):
  74. # sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
  75. if sys.platform == "darwin":
  76. self.lift()
  77. def _update_dimensions_event(self, event=None):
  78. if not self._block_update_dimensions_event:
  79. detected_width = self.winfo_width() # detect current window size
  80. detected_height = self.winfo_height()
  81. if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height):
  82. self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event
  83. self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale
  84. def _set_scaling(self, new_widget_scaling, new_window_scaling):
  85. super()._set_scaling(new_widget_scaling, new_window_scaling)
  86. # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work.
  87. super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
  88. super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
  89. super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}")
  90. # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window)
  91. self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11)
  92. def block_update_dimensions_event(self):
  93. self._block_update_dimensions_event = False
  94. def unblock_update_dimensions_event(self):
  95. self._block_update_dimensions_event = False
  96. def _set_scaled_min_max(self):
  97. if self._min_width is not None or self._min_height is not None:
  98. super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
  99. if self._max_width is not None or self._max_height is not None:
  100. super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
  101. def geometry(self, geometry_string: str = None):
  102. if geometry_string is not None:
  103. super().geometry(self._apply_geometry_scaling(geometry_string))
  104. # update width and height attributes
  105. width, height, x, y = self._parse_geometry_string(geometry_string)
  106. if width is not None and height is not None:
  107. self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max
  108. self._current_height = max(self._min_height, min(height, self._max_height))
  109. else:
  110. return self._reverse_geometry_scaling(super().geometry())
  111. def withdraw(self):
  112. if self._windows_set_titlebar_color_called:
  113. self._withdraw_called_after_windows_set_titlebar_color = True
  114. super().withdraw()
  115. def iconify(self):
  116. if self._windows_set_titlebar_color_called:
  117. self._iconify_called_after_windows_set_titlebar_color = True
  118. super().iconify()
  119. def resizable(self, width: bool = None, height: bool = None):
  120. current_resizable_values = super().resizable(width, height)
  121. self._last_resizable_args = ([], {"width": width, "height": height})
  122. if sys.platform.startswith("win"):
  123. self.after(10, lambda: self._windows_set_titlebar_color(self._get_appearance_mode()))
  124. return current_resizable_values
  125. def minsize(self, width=None, height=None):
  126. self._min_width = width
  127. self._min_height = height
  128. if self._current_width < width:
  129. self._current_width = width
  130. if self._current_height < height:
  131. self._current_height = height
  132. super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
  133. def maxsize(self, width=None, height=None):
  134. self._max_width = width
  135. self._max_height = height
  136. if self._current_width > width:
  137. self._current_width = width
  138. if self._current_height > height:
  139. self._current_height = height
  140. super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
  141. def configure(self, **kwargs):
  142. if "fg_color" in kwargs:
  143. self._fg_color = self._check_color_type(kwargs.pop("fg_color"))
  144. super().configure(bg=self._apply_appearance_mode(self._fg_color))
  145. for child in self.winfo_children():
  146. try:
  147. child.configure(bg_color=self._fg_color)
  148. except Exception:
  149. pass
  150. super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
  151. check_kwargs_empty(kwargs)
  152. def cget(self, attribute_name: str) -> any:
  153. if attribute_name == "fg_color":
  154. return self._fg_color
  155. else:
  156. return super().cget(attribute_name)
  157. def wm_iconbitmap(self, bitmap=None, default=None):
  158. self._iconbitmap_method_called = True
  159. super().wm_iconbitmap(bitmap, default)
  160. def _windows_set_titlebar_icon(self):
  161. try:
  162. # if not the user already called iconbitmap method, set icon
  163. if not self._iconbitmap_method_called:
  164. customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  165. self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico"))
  166. except Exception:
  167. pass
  168. @classmethod
  169. def _enable_macos_dark_title_bar(cls):
  170. if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
  171. if version.parse(platform.python_version()) < version.parse("3.10"):
  172. if version.parse(tkinter.Tcl().call("info", "patchlevel")) >= version.parse("8.6.9"): # Tcl/Tk >= 8.6.9
  173. os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
  174. @classmethod
  175. def _disable_macos_dark_title_bar(cls):
  176. if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
  177. if version.parse(platform.python_version()) < version.parse("3.10"):
  178. if version.parse(tkinter.Tcl().call("info", "patchlevel")) >= version.parse("8.6.9"): # Tcl/Tk >= 8.6.9
  179. os.system("defaults delete -g NSRequiresAquaSystemAppearance")
  180. # This command reverts the dark-mode setting for all programs.
  181. def _windows_set_titlebar_color(self, color_mode: str):
  182. """
  183. Set the titlebar color of the window to light or dark theme on Microsoft Windows.
  184. Credits for this function:
  185. https://stackoverflow.com/questions/23836000/can-i-change-the-title-bar-in-tkinter/70724666#70724666
  186. MORE INFO:
  187. https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
  188. """
  189. if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation:
  190. self._state_before_windows_set_titlebar_color = self.state()
  191. self.focused_widget_before_widthdraw = self.focus_get()
  192. super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
  193. super().update()
  194. if color_mode.lower() == "dark":
  195. value = 1
  196. elif color_mode.lower() == "light":
  197. value = 0
  198. else:
  199. return
  200. try:
  201. hwnd = ctypes.windll.user32.GetParent(self.winfo_id())
  202. DWMWA_USE_IMMERSIVE_DARK_MODE = 20
  203. DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19
  204. # try with DWMWA_USE_IMMERSIVE_DARK_MODE
  205. if ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,
  206. ctypes.byref(ctypes.c_int(value)),
  207. ctypes.sizeof(ctypes.c_int(value))) != 0:
  208. # try with DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20h1
  209. ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1,
  210. ctypes.byref(ctypes.c_int(value)),
  211. ctypes.sizeof(ctypes.c_int(value)))
  212. except Exception as err:
  213. print(err)
  214. self._windows_set_titlebar_color_called = True
  215. self.after(5, self._revert_withdraw_after_windows_set_titlebar_color)
  216. if self.focused_widget_before_widthdraw is not None:
  217. self.after(10, self.focused_widget_before_widthdraw.focus)
  218. self.focused_widget_before_widthdraw = None
  219. def _revert_withdraw_after_windows_set_titlebar_color(self):
  220. """ if in a short time (5ms) after """
  221. if self._windows_set_titlebar_color_called:
  222. if self._withdraw_called_after_windows_set_titlebar_color:
  223. pass # leave it withdrawed
  224. elif self._iconify_called_after_windows_set_titlebar_color:
  225. super().iconify()
  226. else:
  227. if self._state_before_windows_set_titlebar_color == "normal":
  228. self.deiconify()
  229. elif self._state_before_windows_set_titlebar_color == "iconic":
  230. self.iconify()
  231. elif self._state_before_windows_set_titlebar_color == "zoomed":
  232. self.state("zoomed")
  233. else:
  234. self.state(self._state_before_windows_set_titlebar_color) # other states
  235. self._windows_set_titlebar_color_called = False
  236. self._withdraw_called_after_windows_set_titlebar_color = False
  237. self._iconify_called_after_windows_set_titlebar_color = False
  238. def _set_appearance_mode(self, mode_string):
  239. super()._set_appearance_mode(mode_string)
  240. if sys.platform.startswith("win"):
  241. self._windows_set_titlebar_color(mode_string)
  242. super().configure(bg=self._apply_appearance_mode(self._fg_color))