From 4301419228cb5a9d0327b295f89705edf75e9d0c Mon Sep 17 00:00:00 2001 From: Tom Schimansky Date: Wed, 10 Nov 2021 22:17:49 +0100 Subject: [PATCH] improved CTkSlider and CTkCheckBox rendering --- customtkinter/customtkinter_button.py | 2 - customtkinter/customtkinter_checkbox.py | 39 +++-- customtkinter/customtkinter_progressbar.py | 21 ++- customtkinter/customtkinter_slider.py | 187 +++++++++++++++------ examples/complex_example.py | 22 ++- examples/complex_example_other_style.py | 36 ++-- 6 files changed, 197 insertions(+), 110 deletions(-) diff --git a/customtkinter/customtkinter_button.py b/customtkinter/customtkinter_button.py index 546af52..4610ebc 100644 --- a/customtkinter/customtkinter_button.py +++ b/customtkinter/customtkinter_button.py @@ -94,8 +94,6 @@ class CTkButton(tkinter.Frame): self.canvas.bind("", self.clicked) self.canvas.bind("", self.clicked) - self.canvas_fg_parts = [] - self.canvas_border_parts = [] self.text_label = None self.image_label = None diff --git a/customtkinter/customtkinter_checkbox.py b/customtkinter/customtkinter_checkbox.py index 96a4926..8e8de76 100644 --- a/customtkinter/customtkinter_checkbox.py +++ b/customtkinter/customtkinter_checkbox.py @@ -44,7 +44,7 @@ class CTkCheckBox(tkinter.Frame): elif self.corner_radius*2 > self.width: self.corner_radius = self.width/2 - self.border_width = border_width + self.border_width = round(border_width) if self.corner_radius >= self.border_width: self.inner_corner_radius = self.corner_radius - self.border_width @@ -123,26 +123,31 @@ class CTkCheckBox(tkinter.Frame): else: self.canvas.configure(bg=self.bg_color) + if sys.platform == "darwin": + oval_size_corr_br = 0 + else: + oval_size_corr_br = -1 + # 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.corner_radius * 2 + oval_size_corr_br, + self.corner_radius * 2 + oval_size_corr_br)) self.canvas_border_parts.append(self.canvas.create_oval(self.width - self.corner_radius * 2, 0, - self.width, - self.corner_radius * 2)) + self.width + oval_size_corr_br, + self.corner_radius * 2 + oval_size_corr_br)) self.canvas_border_parts.append(self.canvas.create_oval(0, self.height - self.corner_radius * 2, - self.corner_radius * 2, - self.height)) + self.corner_radius * 2 + oval_size_corr_br, + self.height + oval_size_corr_br)) 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.width + oval_size_corr_br, + self.height + oval_size_corr_br)) self.canvas_border_parts.append(self.canvas.create_rectangle(0, self.corner_radius, @@ -158,20 +163,20 @@ class CTkCheckBox(tkinter.Frame): 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.border_width + self.inner_corner_radius * 2 + oval_size_corr_br, + self.border_width + self.inner_corner_radius * 2 + oval_size_corr_br)) 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.width - self.border_width + oval_size_corr_br, + self.border_width + self.inner_corner_radius * 2 + oval_size_corr_br)) 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.border_width + self.inner_corner_radius * 2 + oval_size_corr_br, + self.height-self.border_width + oval_size_corr_br)) 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.width - self.border_width + oval_size_corr_br, + self.height - self.border_width + oval_size_corr_br)) self.canvas_fg_parts.append(self.canvas.create_rectangle(self.border_width + self.inner_corner_radius, self.border_width, diff --git a/customtkinter/customtkinter_progressbar.py b/customtkinter/customtkinter_progressbar.py index 082a0e6..584cc6a 100644 --- a/customtkinter/customtkinter_progressbar.py +++ b/customtkinter/customtkinter_progressbar.py @@ -29,8 +29,8 @@ class CTkProgressBar(tkinter.Frame): self.progress_color = CTkColorManager.MAIN if progress_color is None else progress_color self.width = width - self.height = height - self.border_width = border_width + self.height = self.calc_optimal_height(height) + self.border_width = round(border_width) self.value = 0.5 self.configure(width=self.width, height=self.height) @@ -41,10 +41,6 @@ class CTkProgressBar(tkinter.Frame): height=self.height) self.canvas.place(x=0, y=0) - self.border_parts = [] - self.fg_parts = [] - self.progress_parts = [] - self.draw() # set progress @@ -56,6 +52,19 @@ class CTkProgressBar(tkinter.Frame): else: return self.master.cget("bg") + @staticmethod + def calc_optimal_height(user_height): + if sys.platform == "darwin": + return user_height # on macOS just use given value (canvas has Antialiasing) + else: + # make sure the value is always with uneven for better rendering of the ovals + if user_height == 0: + return 0 + elif user_height % 2 == 0: + return user_height + 1 + else: + return user_height + def draw(self, no_color_updates=False): # decide the drawing method diff --git a/customtkinter/customtkinter_slider.py b/customtkinter/customtkinter_slider.py index 4922749..e448c07 100644 --- a/customtkinter/customtkinter_slider.py +++ b/customtkinter/customtkinter_slider.py @@ -28,14 +28,14 @@ class CTkSlider(tkinter.Frame): self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color - self.border_color = border_color + self.border_color = self.bg_color if border_color is None else border_color self.fg_color = fg_color self.button_color = self.bg_color if button_color is None else button_color self.button_hover_color = self.bg_color if button_hover_color is None else button_hover_color self.width = width - self.height = height - self.border_width = border_width + self.height = self.calc_optimal_height(height) + self.border_width = round(border_width) self.callback_function = command self.value = 0.5 # initial value of slider in percent self.hover_state = False @@ -58,10 +58,6 @@ class CTkSlider(tkinter.Frame): self.canvas.bind("", self.clicked) self.canvas.bind("", self.clicked) - self.border_parts = [] - self.fg_parts = [] - self.button_parts = [] - self.draw() def detect_color_of_master(self): @@ -70,42 +66,137 @@ class CTkSlider(tkinter.Frame): else: return self.master.cget("bg") - def draw(self): - self.canvas.delete("all") - self.border_parts = [] - self.fg_parts = [] - self.button_parts = [] + @staticmethod + def calc_optimal_height(user_height): + if sys.platform == "darwin": + return user_height # on macOS just use given value (canvas has Antialiasing) + else: + # make sure the value is always with uneven for better rendering of the ovals + if user_height == 0: + return 0 + elif user_height % 2 == 0: + return user_height + 1 + else: + return user_height + + def draw(self, no_color_updates=False): + + # decide the drawing method + if sys.platform == "darwin": + # on macOS draw button with polygons (positions are more accurate, macOS has Antialiasing) + self.draw_with_polygon_shapes() + 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() + + if no_color_updates is False: + self.canvas.configure(bg=CTkColorManager.single_color(self.bg_color, self.appearance_mode)) + print(self.border_color) + self.canvas.itemconfig("border_parts", fill=CTkColorManager.single_color(self.border_color, self.appearance_mode)) + self.canvas.itemconfig("inner_parts", fill=CTkColorManager.single_color(self.fg_color, self.appearance_mode)) + self.canvas.itemconfig("button_parts", fill=CTkColorManager.single_color(self.button_color, self.appearance_mode)) + + def draw_with_polygon_shapes(self): + """ draw the slider parts with just three polygons that have a rounded border """ + + coordinate_shift = -1 + width_reduced = -1 + + # create border parts + if self.border_width > 0: + if not self.canvas.find_withtag("border_parts"): + self.canvas.create_line((0, 0, 0, 0), tags=("border_line_1", "border_parts")) + + self.canvas.coords("border_line_1", + (self.height / 2, + self.height / 2, + self.width - self.height / 2 + coordinate_shift, + self.height / 2)) + self.canvas.itemconfig("border_line_1", + capstyle=tkinter.ROUND, + width=self.height + width_reduced) + + # create inner button parts + if not self.canvas.find_withtag("inner_parts"): + self.canvas.create_line((0, 0, 0, 0), tags=("inner_line_1", "inner_parts")) + + self.canvas.coords("inner_line_1", + (self.height / 2, + self.height / 2, + self.width - self.height / 2 + coordinate_shift, + self.height / 2)) + self.canvas.itemconfig("inner_line_1", + capstyle=tkinter.ROUND, + width=self.height - self.border_width * 2 + width_reduced) + + # button parts + if not self.canvas.find_withtag("button_parts"): + self.canvas.create_line((0, 0, 0, 0), tags=("button_line_1", "button_parts")) + + self.canvas.coords("button_line_1", + (self.height / 2 + (self.width + coordinate_shift - self.height) * self.value, + self.height / 2, + self.height / 2 + (self.width + coordinate_shift - self.height) * self.value, + self.height / 2)) + self.canvas.itemconfig("button_line_1", + capstyle=tkinter.ROUND, + width=self.height + width_reduced) + + def draw_with_ovals_and_rects(self): + """ draw the progress bar parts with ovals and rectangles """ + + # ovals and rects are always rendered too large and need to be made smaller by -1 + oval_bottom_right_shift = -1 + rect_bottom_right_shift = -1 # 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)) + if self.border_width > 0: + if not self.canvas.find_withtag("border_parts"): + self.canvas.create_oval((0, 0, 0, 0), tags=("border_oval_1", "border_parts"), width=0) + self.canvas.create_rectangle((0, 0, 0, 0), tags=("border_rect_1", "border_parts"), width=0) + self.canvas.create_oval((0, 0, 0, 0), tags=("border_oval_2", "border_parts"), width=0) + + self.canvas.coords("border_oval_1", (0, + 0, + self.height + oval_bottom_right_shift, + self.height + oval_bottom_right_shift)) + self.canvas.coords("border_rect_1", (self.height/2, + 0, + self.width-(self.height/2) + rect_bottom_right_shift, + self.height + rect_bottom_right_shift)) + self.canvas.coords("border_oval_2", (self.width-self.height, + 0, + self.width + oval_bottom_right_shift, + self.height + oval_bottom_right_shift)) # 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 not self.canvas.find_withtag("inner_parts"): + self.canvas.create_oval((0, 0, 0, 0), tags=("inner_oval_1", "inner_parts"), width=0) + self.canvas.create_rectangle((0, 0, 0, 0), tags=("inner_rect_1", "inner_parts"), width=0) + self.canvas.create_oval((0, 0, 0, 0), tags=("inner_oval_2", "inner_parts"), width=0) - # 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)) + self.canvas.coords("inner_oval_1", (self.border_width, + self.border_width, + self.height-self.border_width + oval_bottom_right_shift, + self.height-self.border_width + oval_bottom_right_shift)) + self.canvas.coords("inner_rect_1", (self.height/2, + self.border_width, + self.width-(self.height/2 + rect_bottom_right_shift), + self.height-self.border_width + rect_bottom_right_shift)) + self.canvas.coords("inner_oval_2", (self.width-self.height+self.border_width, + self.border_width, + self.width-self.border_width + oval_bottom_right_shift, + self.height-self.border_width + oval_bottom_right_shift)) - self.canvas.configure(bg=CTkColorManager.single_color(self.bg_color, self.appearance_mode)) + # progress parts + if not self.canvas.find_withtag("button_parts"): + self.canvas.create_oval((0, 0, 0, 0), tags=("button_oval_1", "button_parts"), width=0) - for part in self.border_parts: - self.canvas.itemconfig(part, fill=CTkColorManager.single_color(self.border_color, self.appearance_mode), width=0) - - for part in self.fg_parts: - self.canvas.itemconfig(part, fill=CTkColorManager.single_color(self.fg_color, self.appearance_mode), width=0) - - for part in self.button_parts: - self.canvas.itemconfig(part, fill=CTkColorManager.single_color(self.button_color, self.appearance_mode), width=0) + self.canvas.coords("button_oval_1", + ((self.width - self.height) * self.value, + 0, + self.height + (self.width - self.height) * self.value + oval_bottom_right_shift, + self.height + oval_bottom_right_shift)) def clicked(self, event=None): self.value = event.x / self.width @@ -117,33 +208,18 @@ class CTkSlider(tkinter.Frame): self.output_value = self.from_ + (self.value * (self.to - self.from_)) - self.update() + self.draw(no_color_updates=True) if self.callback_function is not None: self.callback_function(self.output_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: - self.canvas.itemconfig(part, fill=CTkColorManager.single_color(self.button_hover_color, self.appearance_mode), width=0) - else: - self.canvas.itemconfig(part, fill=CTkColorManager.single_color(self.button_color, self.appearance_mode), width=0) - def on_enter(self, event=0): self.hover_state = True - for part in self.button_parts: - self.canvas.itemconfig(part, fill=CTkColorManager.single_color(self.button_hover_color, self.appearance_mode), width=0) + self.canvas.itemconfig("button_parts", fill=CTkColorManager.single_color(self.button_hover_color, self.appearance_mode)) def on_leave(self, event=0): self.hover_state = False - for part in self.button_parts: - self.canvas.itemconfig(part, fill=CTkColorManager.single_color(self.button_color, self.appearance_mode), width=0) + self.canvas.itemconfig("button_parts", fill=CTkColorManager.single_color(self.button_color, self.appearance_mode)) def get(self): return self.output_value @@ -151,7 +227,8 @@ class CTkSlider(tkinter.Frame): def set(self, output_value): self.output_value = output_value self.value = (self.output_value - self.from_) / (self.to - self.from_) - self.update() + + self.draw(no_color_updates=True) if self.callback_function is not None: self.callback_function(self.output_value) diff --git a/examples/complex_example.py b/examples/complex_example.py index fbdcbaf..30abaf5 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -90,27 +90,25 @@ class App(tkinter.Tk): self.progressbar = customtkinter.CTkProgressBar(master=self.frame_info, width=250, - height=16, - border_color="gray10", - progress_color="green", - border_width=2) + height=15, + border_width=3) self.progressbar.place(relx=0.5, rely=0.85, anchor=tkinter.S) # ============ frame_right <- ============ self.slider_1 = customtkinter.CTkSlider(master=self.frame_right, - width=160, - height=16, - border_width=5.5, - command=self.progressbar.set) + width=160, + height=16, + border_width=5, + command=self.progressbar.set) self.slider_1.place(x=20, rely=0.6, anchor=tkinter.W) self.slider_1.set(0.3) self.slider_2 = customtkinter.CTkSlider(master=self.frame_right, - width=160, - height=16, - border_width=5.5, - command=self.progressbar.set) + width=160, + height=16, + border_width=5, + command=self.progressbar.set) self.slider_2.place(x=20, rely=0.7, anchor=tkinter.W) self.slider_2.set(0.7) diff --git a/examples/complex_example_other_style.py b/examples/complex_example_other_style.py index dbfa5cb..9f8117a 100644 --- a/examples/complex_example_other_style.py +++ b/examples/complex_example_other_style.py @@ -40,13 +40,13 @@ class App(tkinter.Tk): self.frame_left = customtkinter.CTkFrame(master=self, width=200, height=App.HEIGHT-40, - corner_radius=0) + corner_radius=5) self.frame_left.place(relx=0.32, rely=0.5, anchor=tkinter.E) self.frame_right = customtkinter.CTkFrame(master=self, width=420, height=App.HEIGHT-40, - corner_radius=0) + corner_radius=5) self.frame_right.place(relx=0.365, rely=0.5, anchor=tkinter.W) # ============ frame_left ============ @@ -57,8 +57,8 @@ class App(tkinter.Tk): hover_color=App.MAIN_HOVER, text="CTkButton", command=self.button_event, - border_width=2, - corner_radius=0) + border_width=3, + corner_radius=5) self.button_1.place(relx=0.5, y=50, anchor=tkinter.CENTER) self.button_2 = customtkinter.CTkButton(master=self.frame_left, @@ -67,8 +67,8 @@ class App(tkinter.Tk): hover_color=App.MAIN_HOVER, text="CTkButton", command=self.button_event, - border_width=2, - corner_radius=0) + border_width=3, + corner_radius=5) self.button_2.place(relx=0.5, y=100, anchor=tkinter.CENTER) self.button_3 = customtkinter.CTkButton(master=self.frame_left, @@ -77,8 +77,8 @@ class App(tkinter.Tk): hover_color=App.MAIN_HOVER, text="CTkButton", command=self.button_event, - border_width=2, - corner_radius=0) + border_width=3, + corner_radius=5) self.button_3.place(relx=0.5, y=150, anchor=tkinter.CENTER) # ============ frame_right ============ @@ -86,7 +86,7 @@ class App(tkinter.Tk): self.frame_info = customtkinter.CTkFrame(master=self.frame_right, width=380, height=200, - corner_radius=0) + corner_radius=5) self.frame_info.place(relx=0.5, y=20, anchor=tkinter.N) # ============ frame_right -> frame_info ============ @@ -98,7 +98,7 @@ class App(tkinter.Tk): "invidunt ut labore", width=250, height=100, - corner_radius=0, + corner_radius=5, fg_color=("white", "gray20"), text_color=App.MAIN_COLOR, justify=tkinter.LEFT) @@ -108,7 +108,7 @@ class App(tkinter.Tk): progress_color=App.MAIN_COLOR, width=250, height=15, - border_width=0) + border_width=3) self.progressbar.place(relx=0.5, rely=0.85, anchor=tkinter.S) self.progressbar.set(0.65) @@ -119,7 +119,7 @@ class App(tkinter.Tk): button_hover_color=App.MAIN_HOVER, width=160, height=16, - border_width=5.5, + border_width=5, command=self.progressbar.set) self.slider_1.place(x=20, rely=0.6, anchor=tkinter.W) self.slider_1.set(0.3) @@ -129,7 +129,7 @@ class App(tkinter.Tk): button_hover_color=App.MAIN_HOVER, width=160, height=16, - border_width=5.5, + border_width=5, command=self.progressbar.set) self.slider_2.place(x=20, rely=0.7, anchor=tkinter.W) self.slider_2.set(0.7) @@ -148,14 +148,14 @@ class App(tkinter.Tk): height=28, text="CTkButton", command=self.button_event, - border_width=2, - corner_radius=0) + border_width=3, + corner_radius=5) self.button_4.place(x=310, rely=0.7, anchor=tkinter.CENTER) self.entry = customtkinter.CTkEntry(master=self.frame_right, width=120, height=28, - corner_radius=0) + corner_radius=5) self.entry.place(relx=0.33, rely=0.92, anchor=tkinter.CENTER) self.entry.insert(0, "CTkEntry") @@ -166,8 +166,8 @@ class App(tkinter.Tk): height=28, text="CTkButton", command=self.button_event, - border_width=2, - corner_radius=0) + border_width=3, + corner_radius=5) self.button_5.place(relx=0.66, rely=0.92, anchor=tkinter.CENTER) def button_event(self):