From 16859991eb88fca3f4a50f29f71731d980871dbc Mon Sep 17 00:00:00 2001 From: TomSchimansky Date: Mon, 21 Feb 2022 15:35:08 +0100 Subject: [PATCH] added CTkToplevel, fixed CTk dark titlebar changes --- customtkinter/__init__.py | 20 +++ customtkinter/assets/canvas_shapes_font.otf | Bin 0 -> 2881 bytes customtkinter/customtkinter_canvas.py | 17 ++ customtkinter/customtkinter_tk.py | 60 ++++--- customtkinter/customtkinter_toplevel.py | 167 ++++++++++++++++++++ examples/complex_example.py | 16 +- test/toplevel_test.py | 44 ++++++ 7 files changed, 302 insertions(+), 22 deletions(-) create mode 100644 customtkinter/assets/canvas_shapes_font.otf create mode 100644 customtkinter/customtkinter_canvas.py create mode 100644 customtkinter/customtkinter_toplevel.py create mode 100644 test/toplevel_test.py diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index a5897f4..956e803 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -9,6 +9,8 @@ from .customtkinter_entry import CTkEntry from .customtkinter_dialog import CTkDialog from .customtkinter_checkbox import CTkCheckBox from .customtkinter_tk import CTk +from .customtkinter_canvas import CTkCanvas +from .customtkinter_toplevel import CTkToplevel from .appearance_mode_tracker import AppearanceModeTracker from .customtkinter_color_manager import CTkColorManager @@ -17,6 +19,7 @@ from distutils.version import StrictVersion as Version import tkinter import os import sys +from ctypes import windll, byref, create_unicode_buffer, create_string_buffer def enable_macos_darkmode(): @@ -57,3 +60,20 @@ def get_appearance_mode(): def set_default_color_theme(color_string): CTkColorManager.initialize_color_theme(color_string) + + +FR_PRIVATE = 0x10 +FR_NOT_ENUM = 0x20 + + +def loadfont(fontpath: str, private=True, enumerable=False): + pathbuf = create_string_buffer(bytes(fontpath, "utf-8")) + add_font_resource_ex = windll.gdi32.AddFontResourceExA + flags = (FR_PRIVATE if private else 0) | (FR_NOT_ENUM if not enumerable else 0) + num_fonts_added = add_font_resource_ex(byref(pathbuf), flags, 0) + return bool(num_fonts_added) + + +# load custom font for rendering circles on the tkinter.Canvas with antialiasing +script_directory = os.path.dirname(os.path.abspath(__file__)) +loadfont(os.path.join(script_directory, "assets", "canvas_shapes_font.otf"), private=True) diff --git a/customtkinter/assets/canvas_shapes_font.otf b/customtkinter/assets/canvas_shapes_font.otf new file mode 100644 index 0000000000000000000000000000000000000000..328f8f36f7b25dbec57a66811955cd0a6c59af3f GIT binary patch literal 2881 zcmbVO>rPWa5T5OY7K?=%G$sl(l^9fzOQIx%A1Y8F!9bu8OuSHQr6@?HVElpd06vrP zB}{w_U%~I&-BZrAr`}Fxc6a9c=CXUXCtWR-R;62he)}qM>6}5o- zay;U`qqxL~<3T3+oHRG(5z7b$Y#9w;6kEw4M3OH0e4ku7%i@zJs1szdrx+O9c;o^0 zv>fh%YcmRt<(lWQZ7}jSreL#LKkS%2H)k2VP#*z*O0KzRoLqm1m5`iFcqFOfyQEJl z9!V)At*mZkUA&;ojIw%^)k4iGurJho&UPOcli-dLwtHt8xdfhD$0RESzmCgDh?Ri< zp+}M`z6+<=@JLD_X=QaQ3yp`H$SA8vSuND80{cSU=h%IzR-M~Bs_a*#BsHnZob1UF zUc$aq*Q>iH`{g59hkI8};3~^elb;XZwm57sI;3-i;ui`yyh5Qc(_V|nOnaxT6?sh! z&locg2cHtTin|q;G*)@MnMc^@yevpj7G+76gJ0h7QOEkf(cfwmXnX8;rOn%P7)cm0 z#^jpE5u78OBk1ND@7;C&U5t)|I#Hee z0Xm6}YUqTzc2U)?gL)X9Do!wK66nUn5dA?J_C^wbe%pZ-JhTkGUh($ literal 0 HcmV?d00001 diff --git a/customtkinter/customtkinter_canvas.py b/customtkinter/customtkinter_canvas.py new file mode 100644 index 0000000..6e24cce --- /dev/null +++ b/customtkinter/customtkinter_canvas.py @@ -0,0 +1,17 @@ +import tkinter + + +class CTkCanvas(tkinter.Canvas): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.aa_circle_canvas_ids = [] + + def create_aa_circle(self, x_pos, y_pos, radius, angle=0, fill="white", tags="") -> str: + circle_chars = ["A", "B", "C", "D", "E", "F"] + + # create a circle with a font element + circle_1 = self.create_text(x_pos, y_pos, text=circle_chars[0], anchor=tkinter.CENTER, fill=fill, + font=("TheCircle", -radius * 2), tags=tags, angle=angle) + + return circle_1 diff --git a/customtkinter/customtkinter_tk.py b/customtkinter/customtkinter_tk.py index 02d6fac..1363bab 100644 --- a/customtkinter/customtkinter_tk.py +++ b/customtkinter/customtkinter_tk.py @@ -4,6 +4,7 @@ import sys import os import platform import ctypes +import win32con from .appearance_mode_tracker import AppearanceModeTracker from .customtkinter_color_manager import CTkColorManager @@ -15,6 +16,7 @@ class CTk(tkinter.Tk): **kwargs): self.enable_macos_dark_title_bar() + super().__init__(*args, **kwargs) self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" self.fg_color = CTkColorManager.WINDOW_BG if fg_color == "CTkColorManager" else fg_color @@ -26,8 +28,6 @@ class CTk(tkinter.Tk): self.fg_color = kwargs["background"] del kwargs["background"] - super().__init__(*args, **kwargs) - AppearanceModeTracker.add(self.set_appearance_mode, self) super().configure(bg=CTkColorManager.single_color(self.fg_color, self.appearance_mode)) @@ -56,6 +56,15 @@ class CTk(tkinter.Tk): self.window_exists = True super().mainloop(*args, **kwargs) + def resizable(self, *args, **kwargs): + super().resizable(*args, **kwargs) + + if sys.platform.startswith("win"): + if self.appearance_mode == 1: + self.windows_set_titlebar_color("dark") + else: + self.windows_set_titlebar_color("light") + def config(self, *args, **kwargs): self.configure(*args, **kwargs) @@ -127,26 +136,39 @@ class CTk(tkinter.Tk): https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute """ - if self.window_exists is False: - super().withdraw() # hide window if it not already exists to not show the .update call which is needed + if sys.platform.startswith("win"): - super().update() + super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible + if not self.window_exists: + super().update() - if color_mode.lower() == "dark": - DWMWA_USE_IMMERSIVE_DARK_MODE = 20 - elif color_mode.lower() == "light": - DWMWA_USE_IMMERSIVE_DARK_MODE = 19 - else: - return + if color_mode.lower() == "dark": + value = 1 + elif color_mode.lower() == "light": + value = 0 + else: + return - set_window_attribute = ctypes.windll.dwmapi.DwmSetWindowAttribute - get_parent = ctypes.windll.user32.GetParent - hwnd = get_parent(self.winfo_id()) - rendering_policy = DWMWA_USE_IMMERSIVE_DARK_MODE - value = 2 - value = ctypes.c_int(value) - set_window_attribute(hwnd, rendering_policy, ctypes.byref(value), - ctypes.sizeof(value)) + 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) + + if self.window_exists: + self.deiconify() def set_appearance_mode(self, mode_string): if mode_string.lower() == "dark": diff --git a/customtkinter/customtkinter_toplevel.py b/customtkinter/customtkinter_toplevel.py new file mode 100644 index 0000000..b1c1f7e --- /dev/null +++ b/customtkinter/customtkinter_toplevel.py @@ -0,0 +1,167 @@ +import tkinter +from distutils.version import StrictVersion as Version +import sys +import os +import platform +import ctypes + +from .appearance_mode_tracker import AppearanceModeTracker +from .customtkinter_color_manager import CTkColorManager + + +class CTkToplevel(tkinter.Toplevel): + def __init__(self, *args, + fg_color="CTkColorManager", + **kwargs): + + self.enable_macos_dark_title_bar() + super().__init__(*args, **kwargs) + self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + + self.fg_color = CTkColorManager.WINDOW_BG if fg_color == "CTkColorManager" else fg_color + + if "bg" in kwargs: + self.fg_color = kwargs["bg"] + del kwargs["bg"] + elif "background" in kwargs: + self.fg_color = kwargs["background"] + del kwargs["background"] + + AppearanceModeTracker.add(self.set_appearance_mode, self) + super().configure(bg=CTkColorManager.single_color(self.fg_color, self.appearance_mode)) + + if sys.platform.startswith("win"): + if self.appearance_mode == 1: + self.windows_set_titlebar_color("dark") + else: + self.windows_set_titlebar_color("light") + + def destroy(self): + AppearanceModeTracker.remove(self.set_appearance_mode) + self.disable_macos_dark_title_bar() + super().destroy() + + def resizable(self, *args, **kwargs): + super().resizable(*args, **kwargs) + + if sys.platform.startswith("win"): + if self.appearance_mode == 1: + self.windows_set_titlebar_color("dark") + else: + self.windows_set_titlebar_color("light") + + def config(self, *args, **kwargs): + self.configure(*args, **kwargs) + + def configure(self, *args, **kwargs): + bg_changed = False + + if "bg" in kwargs: + self.fg_color = kwargs["bg"] + bg_changed = True + kwargs["bg"] = CTkColorManager.single_color(self.fg_color, self.appearance_mode) + elif "background" in kwargs: + self.fg_color = kwargs["background"] + bg_changed = True + kwargs["background"] = CTkColorManager.single_color(self.fg_color, self.appearance_mode) + elif "fg_color" in kwargs: + self.fg_color = kwargs["fg_color"] + kwargs["bg"] = CTkColorManager.single_color(self.fg_color, self.appearance_mode) + del kwargs["fg_color"] + bg_changed = True + + elif len(args) > 0 and type(args[0]) == dict: + if "bg" in args[0]: + self.fg_color=args[0]["bg"] + bg_changed = True + args[0]["bg"] = CTkColorManager.single_color(self.fg_color, self.appearance_mode) + elif "background" in args[0]: + self.fg_color=args[0]["background"] + bg_changed = True + args[0]["background"] = CTkColorManager.single_color(self.fg_color, self.appearance_mode) + + if bg_changed: + from .customtkinter_slider import CTkSlider + from .customtkinter_progressbar import CTkProgressBar + from .customtkinter_label import CTkLabel + from .customtkinter_frame import CTkFrame + from .customtkinter_entry import CTkEntry + from .customtkinter_checkbox import CTkCheckBox + from .customtkinter_button import CTkButton + + for child in self.winfo_children(): + if isinstance(child, (CTkFrame, CTkButton, CTkLabel, CTkSlider, CTkCheckBox, CTkEntry, CTkProgressBar)): + child.configure(bg_color=self.fg_color) + + super().configure(*args, **kwargs) + + @staticmethod + def enable_macos_dark_title_bar(): + if sys.platform == "darwin": # 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") + + @staticmethod + def disable_macos_dark_title_bar(): + if sys.platform == "darwin": # 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"): + + 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.deiconify() + + 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=CTkColorManager.single_color(self.fg_color, self.appearance_mode)) diff --git a/examples/complex_example.py b/examples/complex_example.py index ce95121..692c483 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -65,7 +65,12 @@ class App(customtkinter.CTk): self.check_box_1 = customtkinter.CTkCheckBox(master=self.frame_left, text="CTkCheckBox") - self.check_box_1.place(relx=0.5, rely=0.92, anchor=tkinter.CENTER) + self.check_box_1.place(relx=0.15, rely=0.82, anchor=tkinter.W) + + self.check_box_2 = customtkinter.CTkCheckBox(master=self.frame_left, + text="Dark Mode", + command=self.change_mode) + self.check_box_2.place(relx=0.15, rely=0.92, anchor=tkinter.W) # ============ frame_right ============ @@ -93,6 +98,7 @@ class App(customtkinter.CTk): width=250, height=12) self.progressbar.place(relx=0.5, rely=0.85, anchor=tkinter.S) + self.progressbar.set(0.65) # ============ frame_right <- ============ @@ -146,11 +152,15 @@ class App(customtkinter.CTk): corner_radius=8) self.button_5.place(relx=0.66, rely=0.92, anchor=tkinter.CENTER) - self.progressbar.set(0.65) - def button_event(self): print("Button pressed") + def change_mode(self): + if self.check_box_2.get() == 1: + customtkinter.set_appearance_mode("dark") + else: + customtkinter.set_appearance_mode("light") + def on_closing(self, event=0): self.destroy() diff --git a/test/toplevel_test.py b/test/toplevel_test.py new file mode 100644 index 0000000..96cccca --- /dev/null +++ b/test/toplevel_test.py @@ -0,0 +1,44 @@ +import tkinter +import customtkinter +import time + +customtkinter.set_appearance_mode("light") + + +class ExampleApp(customtkinter.CTk): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.geometry("400x400") + self.title("main CTk window") + time.sleep(0.5) + self.resizable(False, False) + + self.b1 = customtkinter.CTkButton(self, text="Add another window", command=self.newWindow) + self.b1.pack(side="top", padx=40, pady=40) + self.c1 = customtkinter.CTkCheckBox(self, text="dark mode", command=self.change_mode) + self.c1.pack() + self.count = 0 + + def change_mode(self): + if self.c1.get() == 1: + customtkinter.set_appearance_mode("dark") + else: + customtkinter.set_appearance_mode("light") + + def newWindow(self): + self.count += 1 + + window = customtkinter.CTkToplevel(self) + window.configure(bg=("lime", "darkgreen")) + window.title("CTkToplevel window") + window.geometry("400x200") + window.resizable(False, False) + + label = customtkinter.CTkLabel(window, text=f"This is CTkToplevel window number {self.count}") + label.pack(side="top", fill="both", expand=True, padx=40, pady=40) + + +if __name__ == "__main__": + root = ExampleApp() + time.sleep(0.5) + root.mainloop()