commit d43a35e6703c08870607fca435320a5c5f9e7998 Author: Tom Schimansky Date: Thu Mar 4 18:27:46 2021 +0100 first main diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..8bc42be --- /dev/null +++ b/Readme.md @@ -0,0 +1,2 @@ +# CustomTkinter + diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py new file mode 100644 index 0000000..b08b99c --- /dev/null +++ b/customtkinter/__init__.py @@ -0,0 +1,79 @@ +from .customtkinter_button import CTkButton +from .customtkinter_slider import CTkSlider +from .customtkinter_frame import CTkFrame +from .customtkinter_progressbar import CTkProgressBar +from .customtkinter_label import CTkLabel +from .customtkinter_entry import CTkEntry + +from .appearance_mode_tracker import AppearanceModeTracker, SystemAppearanceModeListenerNoThread +from .customtkinter_color_manager import CTkColorManager + +from distutils.version import StrictVersion as Version +import tkinter +import os +import sys + + +def enable_macos_darkmode(): + if sys.platform == "darwin": # macOS + if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 + os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No") + + sys.stderr.write("WARNING (customtkinter.enable_macos_darkmode): " + + "This command forces macOS dark-mode on all programs." + + "This can cause bugs on some other programs.\n" + + "Disable it by calling customtkinter.disable_macos_darkmode at the end of the program.\n") + else: + sys.stderr.write("WARNING (customtkinter.enable_macos_darkmode): " + + "Currently this works only with anaconda python version (Tcl/Tk >= 8.6.9).\n" + + "(python.org Tcl/Tk version is only 8.6.8)\n") + else: + sys.stderr.write("WARNING (customtkinter.enable_macos_darkmode): " + + "System is not macOS, but the following: {}\n".format(sys.platform)) + + +def disable_macos_darkmode(): + if sys.platform == "darwin": # macOS + 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 set_appearance_mode(mode_string): + AppearanceModeTracker.set_appearance_mode(mode_string) + + +def get_appearance_mode(): + if AppearanceModeTracker.appearance_mode == 0: + return "Light" + elif AppearanceModeTracker.appearance_mode == 1: + return "Dark" + + +def set_theme(main_color): + CTkColorManager.set_theme(main_color) + + +def deactivate_threading(): + AppearanceModeTracker.init_listener_function(no_thread=True) + sys.stderr.write("WARNING (customtkinter.deactivate_threading): Automatic threaded search for a change of the " + + "system appearance mode is deativated now.\nYou have to update the appearance mode manually " + + "in your mainloop by calling customtkinter.update_theme() every time.\n") + + +def activate_threading(): + AppearanceModeTracker.init_listener_function() + + +def update_theme(): + if isinstance(AppearanceModeTracker.system_mode_listener, SystemAppearanceModeListenerNoThread): + AppearanceModeTracker.system_mode_listener.update() + else: + sys.stderr.write("WARNING (customtkinter.update_theme): no need to call update_theme, because " + + "customtkinter is constantly searching for a mode change in a background thread.\n") + + + + + + diff --git a/customtkinter/appearance_mode_tracker.py b/customtkinter/appearance_mode_tracker.py new file mode 100644 index 0000000..d10af1f --- /dev/null +++ b/customtkinter/appearance_mode_tracker.py @@ -0,0 +1,144 @@ +from threading import Thread +from time import sleep +import darkdetect +import sys + + +class SystemAppearanceModeListener(Thread): + """ This class checks for a system appearance change + in a loop, and if a change is detected, than the + callback function gets called. Either 'Light' or + 'Dark' is passed in the callback function. """ + + def __init__(self, callback, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setDaemon(True) + + self.appearance_mode = self.detect_appearance_mode() + self.callback_function = callback + + self.activated = True + + def activate(self): + self.activated = True + + def deactivate(self): + self.activated = False + + def get_mode(self): + return self.appearance_mode + + @staticmethod + def detect_appearance_mode(): + if sys.platform == "darwin": + if darkdetect.theme() == "Dark": + return 1 # Dark + else: + return 0 # Light + else: + return 0 # Light + + def run(self): + while True: + if self.activated: + detected_mode = self.detect_appearance_mode() + if detected_mode != self.appearance_mode: + self.appearance_mode = detected_mode + + if self.appearance_mode == 0: + self.callback_function("Light", from_listener=True) + else: + self.callback_function("Dark", from_listener=True) + sleep(0.5) + + +class SystemAppearanceModeListenerNoThread(): + def __init__(self, callback): + self.appearance_mode = self.detect_appearance_mode() + self.callback_function = callback + + self.activated = True + + def get_mode(self): + return self.appearance_mode + + @staticmethod + def detect_appearance_mode(): + if sys.platform == "darwin": + if darkdetect.theme() == "Dark": + return 1 # Dark + else: + return 0 # Light + else: + return 0 # Light + + def update(self): + detected_mode = self.detect_appearance_mode() + if detected_mode != self.appearance_mode: + self.appearance_mode = detected_mode + + if self.appearance_mode == 0: + self.callback_function("Light", from_listener=True) + else: + self.callback_function("Dark", from_listener=True) + + +class AppearanceModeTracker(): + """ This class holds a list with callback functions + of every customtkinter object that gets created. + And when either the SystemAppearanceModeListener + or the user changes the appearance_mode, all + callbacks in the list get called and the + new appearance_mode is passed over to the + customtkinter objects """ + + callback_list = [] + appearance_mode = 0 # Light (standard) + system_mode_listener = None + + @classmethod + def init_listener_function(cls, no_thread=False): + if isinstance(cls.system_mode_listener, SystemAppearanceModeListener): + cls.system_mode_listener.deactivate() + + if no_thread is True: + cls.system_mode_listener = SystemAppearanceModeListenerNoThread(cls.set_appearance_mode) + cls.appearance_mode = cls.system_mode_listener.get_mode() + else: + cls.system_mode_listener = SystemAppearanceModeListener(cls.set_appearance_mode) + cls.system_mode_listener.start() + cls.appearance_mode = cls.system_mode_listener.get_mode() + + @classmethod + def add(cls, callback): + cls.callback_list.append(callback) + + @classmethod + def get_mode(cls): + return cls.appearance_mode + + @classmethod + def set_appearance_mode(cls, mode_string, from_listener=False): + if mode_string.lower() == "dark": + cls.appearance_mode = 1 + + if not from_listener: + cls.system_mode_listener.deactivate() + + elif mode_string.lower() == "light": + cls.appearance_mode = 0 + if not from_listener: + cls.system_mode_listener.deactivate() + + elif mode_string.lower() == "system": + cls.system_mode_listener.activate() + + if cls.appearance_mode == 0: + for callback in cls.callback_list: + callback("Light") + elif cls.appearance_mode == 1: + for callback in cls.callback_list: + callback("Dark") + + +AppearanceModeTracker.init_listener_function() diff --git a/customtkinter/customtkinter_button.py b/customtkinter/customtkinter_button.py new file mode 100644 index 0000000..bb73b5a --- /dev/null +++ b/customtkinter/customtkinter_button.py @@ -0,0 +1,239 @@ +import tkinter +import sys + +from .customtkinter_frame import CTkFrame +from .appearance_mode_tracker import AppearanceModeTracker +from .customtkinter_color_manager import CTkColorManager + + +class CTkButton(tkinter.Frame): + """ tkinter custom button with border, rounded corners and hover effect """ + + def __init__(self, + bg_color=None, + fg_color=CTkColorManager.MAIN, + hover_color=CTkColorManager.MAIN_HOVER, + border_color=None, + border_width=0, + command=None, + width=120, + height=32, + corner_radius=8, + text_font=None, + text_color=CTkColorManager.TEXT, + text="CTkButton", + hover=True, + *args, **kwargs): + super().__init__(*args, **kwargs) + + if bg_color is None: + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + else: + self.bg_color = bg_color + + AppearanceModeTracker.add(self.set_appearance_mode) + + self.fg_color = fg_color + self.hover_color = hover_color + self.border_color = border_color + self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + + self.width = width + self.height = height + + if corner_radius*2 > self.height: + self.corner_radius = self.height/2 + elif corner_radius*2 > self.width: + self.corner_radius = self.width/2 + else: + self.corner_radius = corner_radius + + self.border_width = border_width + + if self.corner_radius >= self.border_width: + self.inner_corner_radius = self.corner_radius - self.border_width + else: + self.inner_corner_radius = 0 + + self.text = text + self.text_color = text_color + if text_font is None: + if sys.platform == "darwin": # macOS + self.text_font = ("Avenir", 13) + elif "win" in sys.platform: # Windows + self.text_font = ("Century Gothic", 11) + else: + self.text_font = ("TkDefaultFont") + else: + self.text_font = text_font + + self.function = command + self.hover = hover + + self.configure(width=self.width, height=self.height) + + if sys.platform == "darwin" and self.function is not None: + self.configure(cursor="pointinghand") + + self.canvas = tkinter.Canvas(master=self, + highlightthicknes=0, + width=self.width, + height=self.height) + self.canvas.place(x=0, y=0) + + if self.hover is True: + self.canvas.bind("", self.on_enter) + self.canvas.bind("", self.on_leave) + + self.canvas.bind("", self.clicked) + self.canvas.bind("", self.clicked) + + self.canvas_fg_parts = [] + self.canvas_border_parts = [] + self.text_part = None + + self.draw() + + def draw(self): + self.canvas.delete("all") + self.canvas_fg_parts = [] + self.canvas_border_parts = [] + + if type(self.bg_color) == tuple and len(self.bg_color) == 2: + self.canvas.configure(bg=self.bg_color[self.appearance_mode]) + else: + self.canvas.configure(bg=self.bg_color) + + # border button parts + if self.border_width > 0: + + if self.corner_radius > 0: + self.canvas_border_parts.append(self.canvas.create_oval(0, + 0, + self.corner_radius * 2, + self.corner_radius * 2)) + self.canvas_border_parts.append(self.canvas.create_oval(self.width - self.corner_radius * 2, + 0, + self.width, + self.corner_radius * 2)) + self.canvas_border_parts.append(self.canvas.create_oval(0, + self.height - self.corner_radius * 2, + self.corner_radius * 2, + self.height)) + self.canvas_border_parts.append(self.canvas.create_oval(self.width - self.corner_radius * 2, + self.height - self.corner_radius * 2, + self.width, + self.height)) + + self.canvas_border_parts.append(self.canvas.create_rectangle(0, + self.corner_radius, + self.width, + self.height - self.corner_radius)) + self.canvas_border_parts.append(self.canvas.create_rectangle(self.corner_radius, + 0, + self.width - self.corner_radius, + self.height)) + + # inner button parts + + if self.corner_radius > 0: + self.canvas_fg_parts.append(self.canvas.create_oval(self.border_width, + self.border_width, + self.border_width + self.inner_corner_radius * 2, + self.border_width + self.inner_corner_radius * 2)) + self.canvas_fg_parts.append(self.canvas.create_oval(self.width - self.border_width - self.inner_corner_radius * 2, + self.border_width, + self.width - self.border_width, + self.border_width + self.inner_corner_radius * 2)) + self.canvas_fg_parts.append(self.canvas.create_oval(self.border_width, + self.height - self.border_width - self.inner_corner_radius * 2, + self.border_width + self.inner_corner_radius * 2, + self.height-self.border_width)) + self.canvas_fg_parts.append(self.canvas.create_oval(self.width - self.border_width - self.inner_corner_radius * 2, + self.height - self.border_width - self.inner_corner_radius * 2, + self.width - self.border_width, + self.height - self.border_width)) + + self.canvas_fg_parts.append(self.canvas.create_rectangle(self.border_width + self.inner_corner_radius, + self.border_width, + self.width - self.border_width - self.inner_corner_radius, + self.height - self.border_width)) + self.canvas_fg_parts.append(self.canvas.create_rectangle(self.border_width, + self.border_width + self.inner_corner_radius, + self.width - self.border_width, + self.height - self.inner_corner_radius - self.border_width)) + + for part in self.canvas_fg_parts: + if type(self.fg_color) == tuple and len(self.hover_color) == 2: + self.canvas.itemconfig(part, fill=self.fg_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.fg_color, outline=self.fg_color, width=0) + + for part in self.canvas_border_parts: + if type(self.border_color) == tuple and len(self.hover_color) == 2: + self.canvas.itemconfig(part, fill=self.border_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.border_color, outline=self.border_color, width=0) + + self.text_part = self.canvas.create_text(self.width / 2, + self.height / 2, + text=self.text, + font=self.text_font) + + if type(self.text_color) == tuple and len(self.text_color) == 2: + self.canvas.itemconfig(self.text_part, fill=self.text_color[self.appearance_mode]) + else: + self.canvas.itemconfig(self.text_part, fill=self.text_color) + + self.set_text(self.text) + + def configure_color(self, bg_color=None, fg_color=None, hover_color=None, text_color=None): + if bg_color is not None: + self.bg_color = bg_color + else: + self.bg_color = self.master.cget("bg") + + if fg_color is not None: + self.fg_color = fg_color + + if hover_color is not None: + self.hover_color = hover_color + + if text_color is not None: + self.text_color = text_color + + self.draw() + + def set_text(self, text): + self.canvas.itemconfig(self.text_part, text=text) + + def on_enter(self, event=0): + for part in self.canvas_fg_parts: + if type(self.hover_color) == tuple and len(self.hover_color) == 2: + self.canvas.itemconfig(part, fill=self.hover_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.hover_color, width=0) + + def on_leave(self, event=0): + for part in self.canvas_fg_parts: + if type(self.fg_color) == tuple and len(self.hover_color) == 2: + self.canvas.itemconfig(part, fill=self.fg_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.fg_color, width=0) + + def clicked(self, event=0): + if self.function is not None: + self.function() + self.on_leave() + + 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 + + self.draw() + diff --git a/customtkinter/customtkinter_color_manager.py b/customtkinter/customtkinter_color_manager.py new file mode 100644 index 0000000..cd8603f --- /dev/null +++ b/customtkinter/customtkinter_color_manager.py @@ -0,0 +1,29 @@ + + +class CTkColorManager: + MAIN = ("#1C94CF", "#1C94CF") + MAIN_HOVER = ("#5FB4DD", "#5FB4DD") + ENTRY = ("white", "#222222") + TEXT = ("black", "white") + SLIDER_BG = ("#6B6B6B", "#222222") + PROGRESS_BG = ("#6B6B6B", "#222222") + FRAME = ("#D4D5D6", "#3F3F3F") + FRAME_2 = ("#505050", "#505050") + + @classmethod + def set_theme_color(cls, hex_color, hex_color_hover): + cls.MAIN = (hex_color, hex_color) + cls.MAIN_HOVER = (hex_color_hover, hex_color_hover) + + print(CTkColorManager.MAIN, id(CTkColorManager)) + + @classmethod + def set_theme(cls, main_color): + if main_color.lower() == "green": + cls.set_theme_color("#2EDEA4", "#82FCD4") + + elif main_color.lower() == "blue": + cls.set_theme_color("#1C94CF", "#5FB4DD") + + elif main_color.lower() == "blue": + cls.set_theme_color("#1C94CF", "#5FB4DD") diff --git a/customtkinter/customtkinter_entry.py b/customtkinter/customtkinter_entry.py new file mode 100644 index 0000000..b11aed1 --- /dev/null +++ b/customtkinter/customtkinter_entry.py @@ -0,0 +1,113 @@ +import tkinter + +from .customtkinter_frame import CTkFrame +from .appearance_mode_tracker import AppearanceModeTracker +from .customtkinter_color_manager import CTkColorManager + + +class CTkEntry(tkinter.Frame): + def __init__(self, + master=None, + bg_color=None, + fg_color=CTkColorManager.ENTRY, + text_color=CTkColorManager.TEXT, + corner_radius=10, + width=120, + height=25, + *args, + **kwargs): + super().__init__(master=master) + + AppearanceModeTracker.add(self.change_appearance_mode) + + if bg_color is None: + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + else: + self.bg_color = bg_color + + self.fg_color = fg_color + self.text_color = text_color + self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + + self.width = width + self.height = height + self.corner_radius = corner_radius + + self.configure(width=self.width, height=self.height) + + self.canvas = tkinter.Canvas(master=self, + highlightthicknes=0, + width=self.width, + height=self.height) + self.canvas.place(x=0, y=0) + + self.entry = tkinter.Entry(master=self, + bd=0, + highlightthicknes=0, + *args, **kwargs) + self.entry.place(relx=0.5, rely=0.5, relwidth=0.8, anchor=tkinter.CENTER) + + self.fg_parts = [] + + self.draw() + + def draw(self): + self.canvas.delete("all") + self.fg_parts = [] + + # frame_border + self.fg_parts.append(self.canvas.create_oval(0, 0, + self.corner_radius*2, self.corner_radius*2)) + self.fg_parts.append(self.canvas.create_oval(self.width-self.corner_radius*2, 0, + self.width, self.corner_radius*2)) + self.fg_parts.append(self.canvas.create_oval(0, self.height-self.corner_radius*2, + self.corner_radius*2, self.height)) + self.fg_parts.append(self.canvas.create_oval(self.width-self.corner_radius*2, self.height-self.corner_radius*2, + self.width, self.height)) + + self.fg_parts.append(self.canvas.create_rectangle(0, self.corner_radius, + self.width, self.height-self.corner_radius)) + self.fg_parts.append(self.canvas.create_rectangle(self.corner_radius, 0, + self.width-self.corner_radius, self.height)) + + for part in self.fg_parts: + if type(self.fg_color) == tuple: + self.canvas.itemconfig(part, fill=self.fg_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.fg_color, width=0) + + if type(self.bg_color) == tuple: + self.canvas.configure(bg=self.bg_color[self.appearance_mode]) + else: + self.canvas.configure(bg=self.bg_color) + + if type(self.fg_color) == tuple: + self.entry.configure(bg=self.fg_color[self.appearance_mode], + highlightcolor=self.fg_color[self.appearance_mode]) + else: + self.entry.configure(bg=self.fg_color, + highlightcolor=self.fg_color) + + if type(self.text_color) == tuple: + self.entry.configure(fg=self.text_color[self.appearance_mode], + insertbackground=self.text_color[self.appearance_mode]) + else: + self.entry.configure(fg=self.text_color, + insertbackground=self.text_color) + + def change_appearance_mode(self, mode_string): + if mode_string.lower() == "dark": + self.appearance_mode = 1 + elif mode_string.lower() == "light": + self.appearance_mode = 0 + + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + + self.draw() + diff --git a/customtkinter/customtkinter_frame.py b/customtkinter/customtkinter_frame.py new file mode 100644 index 0000000..16b28a2 --- /dev/null +++ b/customtkinter/customtkinter_frame.py @@ -0,0 +1,109 @@ +import tkinter + +from .appearance_mode_tracker import AppearanceModeTracker +from .customtkinter_color_manager import CTkColorManager + + +class CTkFrame(tkinter.Frame): + def __init__(self, *args, + bg_color=None, + fg_color=None, + corner_radius=10, + width=50, + height=20, + **kwargs): + super().__init__(*args, **kwargs) + + AppearanceModeTracker.add(self.change_appearance_mode) + + if bg_color is None: + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + else: + self.bg_color = bg_color + + if fg_color is None: + if isinstance(self.master, CTkFrame): + if self.master.fg_color == CTkColorManager.FRAME: + self.fg_color = CTkColorManager.FRAME_2 + else: + self.fg_color = CTkColorManager.FRAME + else: + self.fg_color = CTkColorManager.FRAME + else: + self.fg_color = fg_color + + self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + + self.width = width + self.height = height + self.corner_radius = corner_radius + + self.configure(width=self.width, height=self.height) + + self.canvas = tkinter.Canvas(master=self, + highlightthicknes=0, + width=self.width, + height=self.height) + + if type(self.bg_color) == tuple: + self.canvas.configure(bg=self.bg_color[self.appearance_mode]) + else: + self.canvas.configure(bg=self.bg_color) + + self.canvas.place(x=0, y=0) + + self.fg_parts = [] + + self.draw() + + def draw(self): + #self.canvas.delete("all") + for part in self.fg_parts: + self.canvas.delete(part) + self.fg_parts = [] + + # frame_border + self.fg_parts.append(self.canvas.create_oval(0, 0, + self.corner_radius*2, self.corner_radius*2)) + self.fg_parts.append(self.canvas.create_oval(self.width-self.corner_radius*2, 0, + self.width, self.corner_radius*2)) + self.fg_parts.append(self.canvas.create_oval(0, self.height-self.corner_radius*2, + self.corner_radius*2, self.height)) + self.fg_parts.append(self.canvas.create_oval(self.width-self.corner_radius*2, self.height-self.corner_radius*2, + self.width, self.height)) + + self.fg_parts.append(self.canvas.create_rectangle(0, self.corner_radius, + self.width, self.height-self.corner_radius)) + self.fg_parts.append(self.canvas.create_rectangle(self.corner_radius, 0, + self.width-self.corner_radius, self.height)) + + for part in self.fg_parts: + if type(self.fg_color) == tuple: + self.canvas.itemconfig(part, fill=self.fg_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.fg_color, width=0) + + if type(self.bg_color) == tuple: + self.canvas.configure(bg=self.bg_color[self.appearance_mode]) + else: + self.canvas.configure(bg=self.bg_color) + + for part in self.fg_parts: + self.canvas.tag_lower(part) + + def change_appearance_mode(self, mode_string): + if mode_string.lower() == "dark": + self.appearance_mode = 1 + elif mode_string.lower() == "light": + self.appearance_mode = 0 + + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + + self.draw() + diff --git a/customtkinter/customtkinter_label.py b/customtkinter/customtkinter_label.py new file mode 100644 index 0000000..3d8e75e --- /dev/null +++ b/customtkinter/customtkinter_label.py @@ -0,0 +1,141 @@ +import tkinter +import sys + +from .customtkinter_frame import CTkFrame +from .appearance_mode_tracker import AppearanceModeTracker +from .customtkinter_color_manager import CTkColorManager + + +class CTkLabel(tkinter.Frame): + def __init__(self, + master=None, + bg_color=None, + fg_color=None, + text_color=CTkColorManager.TEXT, + corner_radius=8, + width=120, + height=25, + text="CTkLabel", + text_font=None, + *args, + **kwargs): + super().__init__(master=master) + + AppearanceModeTracker.add(self.change_appearance_mode) + + if bg_color is None: + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + else: + self.bg_color = bg_color + + if fg_color is None: + self.fg_color = self.bg_color + else: + self.fg_color = fg_color + self.text_color = text_color + self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + + self.width = width + self.height = height + self.corner_radius = corner_radius + self.text = text + + if text_font is None: + if sys.platform == "darwin": # macOS + self.text_font = ("Avenir", 13) + elif "win" in sys.platform: # Windows + self.text_font = ("Century Gothic", 11) + else: + self.text_font = ("TkDefaultFont", 11) + else: + self.text_font = text_font + self.configure(width=self.width, height=self.height) + + self.canvas = tkinter.Canvas(master=self, + highlightthicknes=0, + width=self.width, + height=self.height) + self.canvas.place(relx=0, rely=0, anchor=tkinter.NW) + + self.text_label = tkinter.Label(master=self, + highlightthicknes=0, + bd=0, + text=self.text, + font=self.text_font, + *args, **kwargs) + self.text_label.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER) + + self.fg_parts = [] + + self.draw() + + def draw(self): + self.canvas.delete("all") + self.fg_parts = [] + + # frame_border + self.fg_parts.append(self.canvas.create_oval(0, 0, + self.corner_radius*2, self.corner_radius*2)) + self.fg_parts.append(self.canvas.create_oval(self.width-self.corner_radius*2, 0, + self.width, self.corner_radius*2)) + self.fg_parts.append(self.canvas.create_oval(0, self.height-self.corner_radius*2, + self.corner_radius*2, self.height)) + self.fg_parts.append(self.canvas.create_oval(self.width-self.corner_radius*2, self.height-self.corner_radius*2, + self.width, self.height)) + + self.fg_parts.append(self.canvas.create_rectangle(0, self.corner_radius, + self.width, self.height-self.corner_radius)) + self.fg_parts.append(self.canvas.create_rectangle(self.corner_radius, 0, + self.width-self.corner_radius, self.height)) + + if type(self.bg_color) == tuple: + self.canvas.configure(bg=self.bg_color[self.appearance_mode]) + else: + self.canvas.configure(bg=self.bg_color) + + for part in self.fg_parts: + if type(self.fg_color) == tuple: + self.canvas.itemconfig(part, fill=self.fg_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.fg_color, width=0) + + if type(self.text_color) == tuple: + self.text_label.configure(fg=self.text_color[self.appearance_mode]) + else: + self.text_label.configure(fg=self.text_color) + + if type(self.fg_color) == tuple: + self.text_label.configure(bg=self.fg_color[self.appearance_mode]) + else: + self.text_label.configure(bg=self.fg_color) + + def configure_color(self, bg_color=None, fg_color=None, text_color=None): + if bg_color is not None: + self.bg_color = bg_color + + if fg_color is not None: + self.fg_color = fg_color + + if text_color is not None: + self.text_color = text_color + + self.draw() + + def set_text(self, text): + self.text_label.configure(text=text) + + def change_appearance_mode(self, mode_string): + if mode_string.lower() == "dark": + self.appearance_mode = 1 + elif mode_string.lower() == "light": + self.appearance_mode = 0 + + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + + self.draw() diff --git a/customtkinter/customtkinter_progressbar.py b/customtkinter/customtkinter_progressbar.py new file mode 100644 index 0000000..ea5763b --- /dev/null +++ b/customtkinter/customtkinter_progressbar.py @@ -0,0 +1,145 @@ +import tkinter + +from .customtkinter_frame import CTkFrame +from .appearance_mode_tracker import AppearanceModeTracker +from .customtkinter_color_manager import CTkColorManager + + +class CTkProgressBar(tkinter.Frame): + def __init__(self, + bg_color=None, + border_color=None, + fg_color=CTkColorManager.PROGRESS_BG, + progress_color=CTkColorManager.MAIN, + function=None, + width=160, + height=20, + border_width=5, + *args, **kwargs): + super().__init__(*args, **kwargs) + + AppearanceModeTracker.add(self.change_appearance_mode) + + if bg_color is None: + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + else: + self.bg_color = bg_color + + self.border_color = border_color + self.fg_color = fg_color + self.progress_color = progress_color + self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + + self.width = width + self.height = height + self.border_width = border_width + self.value = 0.5 + + self.configure(width=self.width, height=self.height) + + self.canvas = tkinter.Canvas(master=self, + highlightthicknes=0, + width=self.width, + height=self.height) + self.canvas.place(x=0, y=0) + + self.function = function + + self.border_parts = [] + self.fg_parts = [] + self.progress_parts = [] + + self.draw() + + def draw(self): + self.canvas.delete("all") + self.border_parts = [] + self.fg_parts = [] + self.progress_parts = [] + + # frame_border + self.border_parts.append(self.canvas.create_oval(0, 0, + self.height, self.height)) + self.border_parts.append(self.canvas.create_rectangle(self.height/2, 0, + self.width-(self.height/2), self.height)) + self.border_parts.append(self.canvas.create_oval(self.width-self.height, 0, + self.width, self.height)) + + # foreground + self.fg_parts.append(self.canvas.create_oval(self.border_width, self.border_width, + self.height-self.border_width, self.height-self.border_width)) + self.fg_parts.append(self.canvas.create_rectangle(self.height/2, self.border_width, + self.width-(self.height/2), self.height-self.border_width)) + self.fg_parts.append(self.canvas.create_oval(self.width-self.height+self.border_width, self.border_width, + self.width-self.border_width, self.height-self.border_width)) + + if type(self.bg_color) == tuple: + self.canvas.configure(bg=self.bg_color[self.appearance_mode]) + else: + self.canvas.configure(bg=self.bg_color) + + for part in self.border_parts: + if type(self.border_color) == tuple: + self.canvas.itemconfig(part, fill=self.border_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.border_color, width=0) + + for part in self.fg_parts: + if type(self.fg_color) == tuple: + self.canvas.itemconfig(part, fill=self.fg_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.fg_color, width=0) + + self.set(self.value) + + def set(self, value): + self.value = value + + if self.value > 1: + self.value = 1 + elif self.value < 0: + self.value = 0 + + for part in self.progress_parts: + self.canvas.delete(part) + + # progress + self.progress_parts.append(self.canvas.create_oval(self.border_width, + self.border_width, + self.height - self.border_width, + self.height - self.border_width)) + + self.progress_parts.append(self.canvas.create_rectangle(self.height / 2, + self.border_width, + self.height / 2 + (self.width - self.height) * self.value, + self.height - self.border_width)) + + self.progress_parts.append(self.canvas.create_oval(self.height / 2 + (self.width - self.height) * self.value - (self.height) / 2 + self.border_width, + self.border_width, + self.height / 2 + (self.width - self.height) * self.value + (self.height) / 2 - self.border_width, + self.height - self.border_width)) + + for part in self.progress_parts: + if type(self.progress_color) == tuple: + self.canvas.itemconfig(part, fill=self.progress_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.progress_color, width=0) + + self.canvas.update() + self.canvas.update_idletasks() + + def change_appearance_mode(self, mode_string): + if mode_string.lower() == "dark": + self.appearance_mode = 1 + elif mode_string.lower() == "light": + self.appearance_mode = 0 + + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + + self.draw() diff --git a/customtkinter/customtkinter_slider.py b/customtkinter/customtkinter_slider.py new file mode 100644 index 0000000..2daf72a --- /dev/null +++ b/customtkinter/customtkinter_slider.py @@ -0,0 +1,186 @@ +import tkinter +import sys + +from .customtkinter_frame import CTkFrame +from .appearance_mode_tracker import AppearanceModeTracker +from .customtkinter_color_manager import CTkColorManager + + +class CTkSlider(tkinter.Frame): + def __init__(self, + bg_color=None, + border_color=None, + fg_color=CTkColorManager.SLIDER_BG, + button_color=CTkColorManager.MAIN, + button_hover_color=CTkColorManager.MAIN_HOVER, + width=160, + height=16, + border_width=5.5, + command=None, + *args, **kwargs): + super().__init__(*args, **kwargs) + + AppearanceModeTracker.add(self.change_appearance_mode) + + if bg_color is None: + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + else: + self.bg_color = bg_color + + self.border_color = border_color + self.fg_color = fg_color + self.button_color = button_color + self.button_hover_color = button_hover_color + self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + + self.width = width + self.height = height + self.border_width = border_width + self.callback_function = command + self.value = 0.5 + self.hover_state = False + + self.configure(width=self.width, height=self.height) + if sys.platform == "darwin": + self.configure(cursor="pointinghand") + + self.canvas = tkinter.Canvas(master=self, + highlightthicknes=0, + width=self.width, + height=self.height) + self.canvas.place(x=0, y=0) + + self.canvas.bind("", self.on_enter) + self.canvas.bind("", self.on_leave) + self.canvas.bind("", self.clicked) + self.canvas.bind("", self.clicked) + + self.border_parts = [] + self.fg_parts = [] + self.button_parts = [] + + self.draw() + + def draw(self): + self.canvas.delete("all") + self.border_parts = [] + self.fg_parts = [] + self.button_parts = [] + + # frame_border + self.border_parts.append(self.canvas.create_oval(0, 0, + self.height, self.height)) + self.border_parts.append(self.canvas.create_rectangle(self.height/2, 0, + self.width-(self.height/2), self.height)) + self.border_parts.append(self.canvas.create_oval(self.width-self.height, 0, + self.width, self.height)) + + # foreground + self.fg_parts.append(self.canvas.create_oval(self.border_width, self.border_width, + self.height-self.border_width, self.height-self.border_width)) + self.fg_parts.append(self.canvas.create_rectangle(self.height/2, self.border_width, + self.width-(self.height/2), self.height-self.border_width)) + self.fg_parts.append(self.canvas.create_oval(self.width-self.height+self.border_width, self.border_width, + self.width-self.border_width, self.height-self.border_width)) + + # button + self.button_parts.append(self.canvas.create_oval(self.value*self.width - self.height/2, 0, + self.value*self.width + self.height/2, self.height)) + + if type(self.bg_color) == tuple: + self.canvas.configure(bg=self.bg_color[self.appearance_mode]) + else: + self.canvas.configure(bg=self.bg_color) + + for part in self.border_parts: + if type(self.border_color) == tuple: + self.canvas.itemconfig(part, fill=self.border_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.border_color, width=0) + + for part in self.fg_parts: + if type(self.fg_color) == tuple: + self.canvas.itemconfig(part, fill=self.fg_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.fg_color, width=0) + + for part in self.button_parts: + if type(self.button_color) == tuple: + self.canvas.itemconfig(part, fill=self.button_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.button_color, width=0) + + def clicked(self, event=0): + self.value = event.x / self.width + + if self.value > 1: + self.value = 1 + if self.value < 0: + self.value = 0 + + self.update() + + if self.callback_function is not None: + self.callback_function(self.value) + + def update(self): + for part in self.button_parts: + self.canvas.delete(part) + + self.button_parts.append(self.canvas.create_oval(self.value * (self.width-self.height), 0, + self.value * (self.width-self.height) + self.height, self.height)) + + for part in self.button_parts: + if self.hover_state is True: + if type(self.button_hover_color) == tuple: + self.canvas.itemconfig(part, fill=self.button_hover_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.button_hover_color, width=0) + else: + if type(self.button_color) == tuple: + self.canvas.itemconfig(part, fill=self.button_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.button_color, width=0) + + def on_enter(self, event=0): + self.hover_state = True + for part in self.button_parts: + if type(self.button_hover_color) == tuple: + self.canvas.itemconfig(part, fill=self.button_hover_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.button_hover_color, width=0) + + def on_leave(self, event=0): + self.hover_state = False + for part in self.button_parts: + if type(self.button_color) == tuple: + self.canvas.itemconfig(part, fill=self.button_color[self.appearance_mode], width=0) + else: + self.canvas.itemconfig(part, fill=self.button_color, width=0) + + def get(self): + return self.value + + def set(self, value): + self.value = value + self.update() + + if self.callback_function is not None: + self.callback_function(self.value) + + def change_appearance_mode(self, mode_string): + if mode_string.lower() == "dark": + self.appearance_mode = 1 + elif mode_string.lower() == "light": + self.appearance_mode = 0 + + if isinstance(self.master, CTkFrame): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + + self.draw() + diff --git a/main.py b/main.py new file mode 100644 index 0000000..a1b40c2 --- /dev/null +++ b/main.py @@ -0,0 +1,39 @@ +import tkinter +import customtkinter + +customtkinter.enable_macos_darkmode() +customtkinter.deactivate_threading() + +app = tkinter.Tk() +app.geometry("400x240") +app.title("TkinterCustomButton") + + +def button_function(): + print("button pressed") + + +def slider_function(value): + progressbar_1.set(value) + + +frame_1 = customtkinter.CTkFrame(master=app, width=300, height=200, corner_radius=15) +frame_1.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER) + +label_1 = customtkinter.CTkLabel(master=frame_1) +label_1.place(relx=0.5, rely=0.1, anchor=tkinter.CENTER) + +progressbar_1 = customtkinter.CTkProgressBar(master=frame_1) +progressbar_1.place(relx=0.5, rely=0.25, anchor=tkinter.CENTER) + +button_1 = customtkinter.CTkButton(master=frame_1, corner_radius=10, command=button_function) +button_1.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER) + +slider_1 = customtkinter.CTkSlider(master=frame_1, command=slider_function) +slider_1.place(relx=0.5, rely=0.7, anchor=tkinter.CENTER) + +entry_1 = customtkinter.CTkEntry(master=frame_1) +entry_1.place(relx=0.5, rely=0.85, anchor=tkinter.CENTER) + +app.mainloop() +customtkinter.disable_macos_darkmode()