CustomTkinter/customtkinter/windows/ctk_toplevel.py

274 lines
13 KiB
Python
Raw Normal View History

import tkinter
from distutils.version import StrictVersion as Version
import sys
import os
import platform
import ctypes
from typing import Union, Tuple
from .widgets.theme.theme_manager import ThemeManager
from .widgets.scaling.scaling_base_class import CTkScalingBaseClass
from .widgets.appearance_mode.appearance_mode_base_class import CTkAppearanceModeBaseClass
from ..utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
"""
Toplevel window with dark titlebar on Windows and macOS.
For detailed information check out the documentation.
"""
_valid_tk_toplevel_arguments = {"bd", "borderwidth", "class", "container", "cursor", "height",
"highlightbackground", "highlightthickness", "menu", "relief",
"screen", "takefocus", "use", "visual", "width"}
_deactivate_macos_window_header_manipulation = False
_deactivate_windows_window_header_manipulation = False
def __init__(self, *args,
fg_color: Union[str, Tuple[str, str]] = "default_theme",
**kwargs):
self._enable_macos_dark_title_bar()
2022-10-29 22:56:00 +03:00
# call init methods of super classes
super().__init__(*args, **pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="window")
check_kwargs_empty(kwargs, raise_error=True)
self._current_width = 200 # initial window size, always without scaling
self._current_height = 200
self._min_width: int = 0
self._min_height: int = 0
self._max_width: int = 1_000_000
self._max_height: int = 1_000_000
self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs)
2022-05-02 00:29:14 +03:00
2022-10-29 22:56:00 +03:00
self._fg_color = ThemeManager.theme["color"]["window"] if fg_color == "default_theme" else fg_color
# set bg color of tkinter.Toplevel
super().configure(bg=self._apply_appearance_mode(self._fg_color))
# set title of tkinter.Toplevel
2022-02-23 00:38:40 +03:00
super().title("CTkToplevel")
self._state_before_windows_set_titlebar_color = None
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
self._withdraw_called_after_windows_set_titlebar_color = False # indicates if withdraw() was called after windows_set_titlebar_color
self._iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color
if sys.platform.startswith("win"):
if self._appearance_mode == 1:
self._windows_set_titlebar_color("dark")
else:
self._windows_set_titlebar_color("light")
self.bind('<Configure>', self._update_dimensions_event)
self.bind('<FocusIn>', self._focus_in_event)
def destroy(self):
self._disable_macos_dark_title_bar()
# call destroy methods of super classes
tkinter.Toplevel.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
def _focus_in_event(self, event):
# sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
if sys.platform == "darwin":
self.lift()
def _update_dimensions_event(self, event=None):
2022-05-02 00:29:14 +03:00
detected_width = self.winfo_width() # detect current window size
detected_height = self.winfo_height()
if self._current_width != round(detected_width / self._window_scaling) or self._current_height != round(detected_height / self._window_scaling):
self._current_width = round(detected_width / self._window_scaling) # adjust current size according to new size given by event
self._current_height = round(detected_height / self._window_scaling) # _current_width and _current_height are independent of the scale
2022-05-02 00:29:14 +03:00
def _set_scaling(self, new_widget_scaling, new_window_scaling):
self._window_scaling = new_window_scaling
2022-05-02 00:29:14 +03:00
# force new dimensions on window by using min, max, and geometry
super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height))
super().geometry(
f"{self._apply_window_scaling(self._current_width)}x" + f"{self._apply_window_scaling(self._current_height)}")
# set new scaled min and max with 400ms delay (otherwise it won't work for some reason)
self.after(400, self._set_scaled_min_max)
2022-05-02 00:29:14 +03:00
def _set_scaled_min_max(self):
if self._min_width is not None or self._min_height is not None:
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
if self._max_width is not None or self._max_height is not None:
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def geometry(self, geometry_string: str = None):
if geometry_string is not None:
super().geometry(self._apply_geometry_scaling(geometry_string))
2022-05-02 00:29:14 +03:00
# update width and height attributes
width, height, x, y = self._parse_geometry_string(geometry_string)
if width is not None and height is not None:
self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max
self._current_height = max(self._min_height, min(height, self._max_height))
else:
return self._reverse_geometry_scaling(super().geometry())
2022-05-02 00:29:14 +03:00
def withdraw(self):
if self._windows_set_titlebar_color_called:
self._withdraw_called_after_windows_set_titlebar_color = True
super().withdraw()
def iconify(self):
if self._windows_set_titlebar_color_called:
self._iconify_called_after_windows_set_titlebar_color = True
super().iconify()
def resizable(self, width: bool = None, height: bool = None):
super().resizable(width, height)
self._last_resizable_args = ([], {"width": width, "height": height})
if sys.platform.startswith("win"):
if self._appearance_mode == 1:
self.after(10, lambda: self._windows_set_titlebar_color("dark"))
else:
self.after(10, lambda: self._windows_set_titlebar_color("light"))
def minsize(self, width=None, height=None):
self._min_width = width
self._min_height = height
if self._current_width < width:
self._current_width = width
if self._current_height < height:
self._current_height = height
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height))
def maxsize(self, width=None, height=None):
self._max_width = width
self._max_height = height
if self._current_width > width:
self._current_width = width
if self._current_height > height:
self._current_height = height
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def configure(self, **kwargs):
if "fg_color" in kwargs:
self._fg_color = kwargs.pop("fg_color")
super().configure(bg=self._apply_appearance_mode(self._fg_color))
for child in self.winfo_children():
try:
child.configure(bg_color=self._fg_color)
except Exception:
pass
super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
check_kwargs_empty(kwargs)
def cget(self, attribute_name: str) -> any:
if attribute_name == "fg_color":
return self._fg_color
else:
return super().cget(attribute_name)
@classmethod
def _enable_macos_dark_title_bar(cls):
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
@classmethod
def _disable_macos_dark_title_bar(cls):
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
# This command reverts the dark-mode setting for all programs.
def _windows_set_titlebar_color(self, color_mode: str):
"""
Set the titlebar color of the window to light or dark theme on Microsoft Windows.
Credits for this function:
https://stackoverflow.com/questions/23836000/can-i-change-the-title-bar-in-tkinter/70724666#70724666
MORE INFO:
https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
"""
if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation:
self._state_before_windows_set_titlebar_color = self.state()
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
super().update()
if color_mode.lower() == "dark":
value = 1
elif color_mode.lower() == "light":
value = 0
else:
return
try:
hwnd = ctypes.windll.user32.GetParent(self.winfo_id())
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19
# try with DWMWA_USE_IMMERSIVE_DARK_MODE
if ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,
ctypes.byref(ctypes.c_int(value)),
ctypes.sizeof(ctypes.c_int(value))) != 0:
# try with DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20h1
ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1,
ctypes.byref(ctypes.c_int(value)),
ctypes.sizeof(ctypes.c_int(value)))
except Exception as err:
print(err)
self._windows_set_titlebar_color_called = True
self.after(5, self._revert_withdraw_after_windows_set_titlebar_color)
def _revert_withdraw_after_windows_set_titlebar_color(self):
""" if in a short time (5ms) after """
if self._windows_set_titlebar_color_called:
if self._withdraw_called_after_windows_set_titlebar_color:
pass # leave it withdrawed
elif self._iconify_called_after_windows_set_titlebar_color:
super().iconify()
else:
if self._state_before_windows_set_titlebar_color == "normal":
self.deiconify()
elif self._state_before_windows_set_titlebar_color == "iconic":
self.iconify()
elif self._state_before_windows_set_titlebar_color == "zoomed":
self.state("zoomed")
else:
self.state(self._state_before_windows_set_titlebar_color) # other states
self._windows_set_titlebar_color_called = False
self._withdraw_called_after_windows_set_titlebar_color = False
self._iconify_called_after_windows_set_titlebar_color = False
def _set_appearance_mode(self, mode_string):
if mode_string.lower() == "dark":
self._appearance_mode = 1
elif mode_string.lower() == "light":
self._appearance_mode = 0
if sys.platform.startswith("win"):
if self._appearance_mode == 1:
self._windows_set_titlebar_color("dark")
else:
self._windows_set_titlebar_color("light")
super().configure(bg=self._apply_appearance_mode(self._fg_color))