add font rendering to button, not finished yet

This commit is contained in:
TomSchimansky 2022-02-22 14:36:36 +01:00
parent 5007d16df3
commit 5bbcb1c617
9 changed files with 171 additions and 199 deletions

View File

@ -61,14 +61,12 @@ def set_default_color_theme(color_string):
CTkColorManager.initialize_color_theme(color_string)
if sys.platform.startswith("win"):
def load_font_windows(fontpath: str, private=True, enumerable=False):
from ctypes import windll, byref, create_string_buffer
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)
@ -76,6 +74,7 @@ if sys.platform.startswith("win"):
return bool(num_fonts_added)
if sys.platform.startswith("win"):
# 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)
print("load_font_windows:", load_font_windows(os.path.join(script_directory, "assets", "CustomTkinter_shapes_font-Regular.otf"), private=True))

View File

@ -10,7 +10,7 @@ if Version(darkdetect.__version__) < Version("0.3.1"):
exit()
class AppearanceModeTracker():
class AppearanceModeTracker:
callback_list = []
root_tk_list = []

View File

@ -1,172 +0,0 @@
from threading import Thread
import time
import sys
from distutils.version import StrictVersion as Version
import darkdetect
if Version(darkdetect.__version__) < Version("0.3.1"):
sys.stderr.write("WARNING: You have to update the darkdetect library: pip3 install --upgrade darkdetect\n")
if sys.platform != "darwin":
exit()
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
def set_mode(self, appearance_mode):
self.appearance_mode = appearance_mode
@staticmethod
def detect_appearance_mode():
try:
if darkdetect.theme() == "Dark":
return 1 # Dark
else:
return 0 # Light
except NameError:
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)
time.sleep(0.5)
else:
while self.activated is False:
time.sleep(0.05)
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():
try:
if darkdetect.theme() == "Dark":
return 1 # Dark
else:
return 0 # Light
except NameError:
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 remove(cls, callback):
cls.callback_list.remove(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
cls.system_mode_listener.set_mode(1)
if not from_listener:
cls.system_mode_listener.deactivate()
elif mode_string.lower() == "light":
cls.appearance_mode = 0
cls.system_mode_listener.set_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:
try:
callback("Light")
except Exception:
print("error callback")
continue
elif cls.appearance_mode == 1:
for callback in cls.callback_list:
try:
callback("Dark")
except Exception:
print("error callback")
continue
AppearanceModeTracker.init_listener_function()

View File

@ -5,6 +5,7 @@ from .customtkinter_tk import CTk
from .customtkinter_frame import CTkFrame
from .appearance_mode_tracker import AppearanceModeTracker
from .customtkinter_color_manager import CTkColorManager
from .customtkinter_canvas import CTkCanvas
class CTkButton(tkinter.Frame):
@ -107,8 +108,8 @@ class CTkButton(tkinter.Frame):
if sys.platform == "darwin" and self.function is not None:
self.configure(cursor="pointinghand") # other cursor when hovering over button with command
self.canvas = tkinter.Canvas(master=self,
highlightthicknes=0,
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width,
height=self.height)
self.canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
@ -157,6 +158,8 @@ class CTkButton(tkinter.Frame):
if sys.platform == "darwin":
return user_corner_radius # on macOS just use given value (canvas has Antialiasing)
else:
draw_method = "font"
if draw_method == "shapes":
user_corner_radius = 0.5 * round(user_corner_radius / 0.5) # round to 0.5 steps
# make sure the value is always with .5 at the end for smoother corners
@ -166,6 +169,8 @@ class CTkButton(tkinter.Frame):
return user_corner_radius + 0.5
else:
return user_corner_radius
elif draw_method == "font":
return round(user_corner_radius)
def draw(self, no_color_updates=False):
self.canvas.configure(bg=CTkColorManager.single_color(self.bg_color, self.appearance_mode))
@ -174,6 +179,9 @@ class CTkButton(tkinter.Frame):
if sys.platform == "darwin":
# on macOS draw button with polygons (positions are more accurate, macOS has Antialiasing)
self.draw_with_polygon_shapes()
elif sys.platform.startswith("win"):
#self.draw_with_ovals_and_rects()
self.draw_with_font_shapes_and_rects()
else:
# on Windows and other draw with ovals (corner_radius can be optimised to look better than with polygons)
self.draw_with_ovals_and_rects()
@ -438,6 +446,92 @@ class CTkButton(tkinter.Frame):
self.width - self.border_width + rect_bottom_right_shift,
self.height - self.inner_corner_radius - self.border_width + rect_bottom_right_shift))
def draw_with_font_shapes_and_rects(self):
# create border button parts
if self.border_width > 0:
if self.corner_radius > 0:
# create canvas border corner parts if not already created
if not self.canvas.find_withtag("border_oval_1_a"):
self.canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
self.canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
self.canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
self.canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
self.canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
self.canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
self.canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
self.canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
# change position of border corner parts
self.canvas.coords("border_oval_1_a", self.corner_radius, self.corner_radius, self.corner_radius)
self.canvas.coords("border_oval_1_b", self.corner_radius, self.corner_radius, self.corner_radius)
self.canvas.coords("border_oval_2_a", self.width - self.corner_radius, self.corner_radius, self.corner_radius)
self.canvas.coords("border_oval_2_b", self.width - self.corner_radius, self.corner_radius, self.corner_radius)
self.canvas.coords("border_oval_3_a", self.width - self.corner_radius, self.height - self.corner_radius, self.corner_radius)
self.canvas.coords("border_oval_3_b", self.width - self.corner_radius, self.height - self.corner_radius, self.corner_radius)
self.canvas.coords("border_oval_4_a", self.corner_radius, self.height - self.corner_radius, self.corner_radius)
self.canvas.coords("border_oval_4_b", self.corner_radius, self.height - self.corner_radius, self.corner_radius)
else:
self.canvas.delete("border_corner_part") # delete border corner parts if not needed
# create canvas border rectangle parts if not already created
if not self.canvas.find_withtag("border_rectangle_1"):
self.canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_1", "border_rectangle_part", "border_parts"))
self.canvas.create_rectangle(0, 0, 0, 0, tags=("border_rectangle_2", "border_rectangle_part", "border_parts"))
# change position of border rectangle parts
self.canvas.coords("border_rectangle_1", (0,
self.corner_radius,
self.width -1,
self.height - self.corner_radius -1))
self.canvas.coords("border_rectangle_2", (self.corner_radius,
0,
self.width - self.corner_radius -1,
self.height -1))
# create inner button parts
if self.inner_corner_radius > 0:
# create canvas border corner parts if not already created
if not self.canvas.find_withtag("inner_corner_part"):
self.canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
self.canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
self.canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
self.canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
self.canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
self.canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
self.canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
self.canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
#print(self.corner_radius, self.border_width, self.inner_corner_radius)
# change position of border corner parts
self.canvas.coords("inner_oval_1_a", self.border_width + self.inner_corner_radius, self.border_width + self.inner_corner_radius, self.inner_corner_radius)
self.canvas.coords("inner_oval_1_b", self.border_width + self.inner_corner_radius, self.border_width + self.inner_corner_radius, self.inner_corner_radius)
self.canvas.coords("inner_oval_2_a", self.width - self.border_width - self.inner_corner_radius, self.border_width + self.inner_corner_radius, self.inner_corner_radius)
self.canvas.coords("inner_oval_2_b", self.width - self.border_width - self.inner_corner_radius, self.border_width + self.inner_corner_radius, self.inner_corner_radius)
self.canvas.coords("inner_oval_3_a", self.width - self.border_width - self.inner_corner_radius, self.height - self.border_width - self.inner_corner_radius, self.inner_corner_radius)
self.canvas.coords("inner_oval_3_b", self.width - self.border_width - self.inner_corner_radius, self.height - self.border_width - self.inner_corner_radius, self.inner_corner_radius)
self.canvas.coords("inner_oval_4_a", self.border_width + self.inner_corner_radius, self.height - self.border_width - self.inner_corner_radius, self.inner_corner_radius)
self.canvas.coords("inner_oval_4_b", self.border_width + self.inner_corner_radius, self.height - self.border_width - self.inner_corner_radius, self.inner_corner_radius)
else:
self.canvas.delete("inner_corner_part") # delete inner corner parts if not needed
# create canvas inner rectangle parts if not already created
if not self.canvas.find_withtag("inner_rectangle_part"):
self.canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_1", "inner_rectangle_part", "inner_parts"))
self.canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_2", "inner_rectangle_part", "inner_parts"))
# change position of inner rectangle parts
self.canvas.coords("inner_rectangle_1", (self.border_width + self.inner_corner_radius,
self.border_width,
self.width - self.border_width - self.inner_corner_radius -1,
self.height - self.border_width -1))
self.canvas.coords("inner_rectangle_2", (self.border_width,
self.border_width + self.inner_corner_radius,
self.width - self.border_width -1,
self.height - self.inner_corner_radius - self.border_width -1))
def config(self, *args, **kwargs):
self.configure(*args, **kwargs)

View File

@ -7,11 +7,61 @@ class CTkCanvas(tkinter.Canvas):
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"]
def get_char_from_radius(self, radius):
if radius >= 10:
char = "B"
elif radius >= 6:
char = "D"
elif radius >= 3:
char = "H"
else:
char = "H"
return char
def create_aa_circle(self, x_pos, y_pos, radius, angle=0, fill="white", tags="", anchor=tkinter.CENTER) -> str:
# 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)
circle_1 = self.create_text(x_pos, y_pos, text=self.get_char_from_radius(radius), anchor=anchor, fill=fill,
font=("CustomTkinter_shapes_font", -radius * 2), tags=tags, angle=angle)
self.addtag_withtag("ctk_aa_circle_font_element", circle_1)
self.aa_circle_canvas_ids.append(circle_1)
return circle_1
def coords(self, tag_or_id, *args):
if type(tag_or_id) == str and "ctk_aa_circle_font_element" in self.gettags(tag_or_id):
coords_id = self.find_withtag(tag_or_id)[0] # take the lowest id for the given tag
super().coords(coords_id, *args[:2])
if len(args) == 3:
super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self.get_char_from_radius(args[2]))
elif type(tag_or_id) == int and tag_or_id in self.aa_circle_canvas_ids:
super().coords(tag_or_id, *args[:2])
if len(args) == 3:
super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self.get_char_from_radius(args[2]))
else:
super().coords(tag_or_id, *args)
def itemconfig(self, tag_or_id, *args, **kwargs):
kwargs_except_outline = kwargs.copy()
if "outline" in kwargs_except_outline:
del kwargs_except_outline["outline"]
if type(tag_or_id) == int:
if tag_or_id in self.aa_circle_canvas_ids:
super().itemconfigure(tag_or_id, *args, **kwargs_except_outline)
else:
super().itemconfigure(tag_or_id, *args, **kwargs)
else:
configure_ids = self.find_withtag(tag_or_id)
for configure_id in configure_ids:
if configure_id in self.aa_circle_canvas_ids:
super().itemconfigure(configure_id, *args, **kwargs_except_outline)
else:
super().itemconfigure(configure_id, *args, **kwargs)

View File

@ -23,8 +23,8 @@ class CTkColorManager:
if theme_name.lower() == "blue":
cls.WINDOW_BG = ("#ECECEC", "#323232") # macOS standard light and dark window bg colors
cls.MAIN = ("#1C94CF", "#1C94CF")
cls.MAIN_HOVER = ("#5FB4DD", "#5FB4DD")
cls.MAIN = ("#64A1D2", "#1C94CF")
cls.MAIN_HOVER = ("#A7C2E0", "#5FB4DD")
cls.ENTRY = ("white", "#222222")
cls.TEXT = ("black", "white")
cls.PLACEHOLDER_TEXT = ("gray52", "gray62")

View File

@ -96,6 +96,7 @@ class CTkDialog:
if __name__ == "__main__":
import customtkinter
customtkinter.set_appearance_mode("System")
customtkinter.set_default_color_theme("dark-blue")
app = customtkinter.CTk()
app.geometry("400x300")

View File

@ -4,7 +4,7 @@ import customtkinter
import sys
customtkinter.set_appearance_mode("Light") # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("dark-blue") # Themes: "blue" (standard), "green", "dark-blue"
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
class App(customtkinter.CTk):