From 98c4c669a6ffd420d11643b9e8584008f8ab96f1 Mon Sep 17 00:00:00 2001 From: Tom Schimansky Date: Sat, 17 Sep 2022 00:18:31 +0200 Subject: [PATCH] added indeterminate mode to CTkProgressBar --- customtkinter/draw_engine.py | 88 +++++++++--------- customtkinter/widgets/ctk_progressbar.py | 90 ++++++++++++++++--- .../complex_example_new.py | 25 +++--- .../test_progressbar_intermediate_mode.py | 47 ++++++++++ 4 files changed, 185 insertions(+), 65 deletions(-) create mode 100644 test/manual_integration_tests/test_progressbar_intermediate_mode.py diff --git a/customtkinter/draw_engine.py b/customtkinter/draw_engine.py index 51148ee..ae3360f 100644 --- a/customtkinter/draw_engine.py +++ b/customtkinter/draw_engine.py @@ -645,8 +645,8 @@ class DrawEngine: return requires_recoloring def draw_rounded_progress_bar_with_border(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], - border_width: Union[float, int], progress_value: float, orientation: str) -> bool: - """ Draws a rounded bar on the canvas, which is split in half according to the argument 'progress_value' (0 - 1). + border_width: Union[float, int], progress_value_1: float, progress_value_2: float, orientation: str) -> bool: + """ Draws a rounded bar on the canvas, and onntop sits a progress bar from value 1 to value 2 (range 0-1, left to right, bottom to top). The border elements get the 'border_parts' tag", the main elements get the 'inner_parts' tag and the progress elements get the 'progress_parts' tag. The 'orientation' argument defines from which direction the progress starts (n, w, s, e). @@ -668,13 +668,13 @@ class DrawEngine: if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes": return self.__draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, - progress_value, orientation) + progress_value_1, progress_value_2, orientation) elif self.preferred_drawing_method == "font_shapes": return self.__draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, - progress_value, orientation) + progress_value_1, progress_value_2, orientation) def __draw_rounded_progress_bar_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, - progress_value: float, orientation: str) -> bool: + progress_value_1: float, progress_value_2: float, orientation: str) -> bool: requires_recoloring = self.__draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius) @@ -691,32 +691,32 @@ class DrawEngine: if orientation == "w": self._canvas.coords("progress_line_1", + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, border_width + inner_corner_radius, + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, border_width + inner_corner_radius, - border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value, - border_width + inner_corner_radius, - border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value, + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, height - (border_width + inner_corner_radius) + bottom_right_shift, - border_width + inner_corner_radius, + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, height - (border_width + inner_corner_radius) + bottom_right_shift) elif orientation == "s": self._canvas.coords("progress_line_1", border_width + inner_corner_radius, - border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), width - (border_width + inner_corner_radius), - border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), width - (border_width + inner_corner_radius), - height - (border_width + inner_corner_radius) + bottom_right_shift, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), border_width + inner_corner_radius, - height - (border_width + inner_corner_radius) + bottom_right_shift) + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1)) self._canvas.itemconfig("progress_line_1", width=inner_corner_radius * 2) return requires_recoloring def __draw_rounded_progress_bar_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, - progress_value: float, orientation: str) -> bool: + progress_value_1: float, progress_value_2: float, orientation: str) -> bool: requires_recoloring, requires_recoloring_2 = False, False @@ -754,29 +754,33 @@ class DrawEngine: ("inner_oval_1", "inner_oval_4")) # set positions of progress corner parts - self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) - self._canvas.coords("progress_oval_1_b", border_width + inner_corner_radius, border_width + inner_corner_radius, inner_corner_radius) - self._canvas.coords("progress_oval_2_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value, + self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, border_width + inner_corner_radius, inner_corner_radius) - self._canvas.coords("progress_oval_2_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value, + self._canvas.coords("progress_oval_1_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, border_width + inner_corner_radius, inner_corner_radius) - self._canvas.coords("progress_oval_3_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value, + self._canvas.coords("progress_oval_2_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, + border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_2_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, + border_width + inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_3_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, height - border_width - inner_corner_radius, inner_corner_radius) - self._canvas.coords("progress_oval_3_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value, + self._canvas.coords("progress_oval_3_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, + height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_4_a", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, + height - border_width - inner_corner_radius, inner_corner_radius) + self._canvas.coords("progress_oval_4_b", border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, height - border_width - inner_corner_radius, inner_corner_radius) - self._canvas.coords("progress_oval_4_a", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) - self._canvas.coords("progress_oval_4_b", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) # set positions of progress rect parts self._canvas.coords("progress_rectangle_1", - border_width + inner_corner_radius, + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_1, border_width, - border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value, + border_width + inner_corner_radius + (width - 2 * border_width - 2 * inner_corner_radius) * progress_value_2, height - border_width) self._canvas.coords("progress_rectangle_2", - border_width, + border_width + 2 * inner_corner_radius + (width - 2 * inner_corner_radius - 2 * border_width) * progress_value_1, border_width + inner_corner_radius, - border_width + 2 * inner_corner_radius + (width - 2 * inner_corner_radius - 2 * border_width) * progress_value, + border_width + 2 * inner_corner_radius + (width - 2 * inner_corner_radius - 2 * border_width) * progress_value_2, height - inner_corner_radius - border_width) # vertical orientation from the bottom @@ -786,29 +790,33 @@ class DrawEngine: # set positions of progress corner parts self._canvas.coords("progress_oval_1_a", border_width + inner_corner_radius, - border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), inner_corner_radius) + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) self._canvas.coords("progress_oval_1_b", border_width + inner_corner_radius, - border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), inner_corner_radius) + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) self._canvas.coords("progress_oval_2_a", width - border_width - inner_corner_radius, - border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), inner_corner_radius) + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) self._canvas.coords("progress_oval_2_b", width - border_width - inner_corner_radius, - border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), inner_corner_radius) - self._canvas.coords("progress_oval_3_a", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) - self._canvas.coords("progress_oval_3_b", width - border_width - inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) - self._canvas.coords("progress_oval_4_a", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) - self._canvas.coords("progress_oval_4_b", border_width + inner_corner_radius, height - border_width - inner_corner_radius, inner_corner_radius) + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), inner_corner_radius) + self._canvas.coords("progress_oval_3_a", width - border_width - inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) + self._canvas.coords("progress_oval_3_b", width - border_width - inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) + self._canvas.coords("progress_oval_4_a", border_width + inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) + self._canvas.coords("progress_oval_4_b", border_width + inner_corner_radius, + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1), inner_corner_radius) # set positions of progress rect parts self._canvas.coords("progress_rectangle_1", border_width + inner_corner_radius, - border_width + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), + border_width + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), width - border_width - inner_corner_radius, - height - border_width) + border_width + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1)) self._canvas.coords("progress_rectangle_2", border_width, - border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value), + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_2), width - border_width, - height - inner_corner_radius - border_width) + border_width + inner_corner_radius + (height - 2 * border_width - 2 * inner_corner_radius) * (1 - progress_value_1)) return requires_recoloring or requires_recoloring_2 @@ -847,7 +855,7 @@ class DrawEngine: # draw normal progressbar requires_recoloring = self.__draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, - slider_value, orientation) + 0, slider_value, orientation) # create slider button part if not self._canvas.find_withtag("slider_parts"): @@ -886,7 +894,7 @@ class DrawEngine: # draw normal progressbar requires_recoloring = self.__draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, - slider_value, orientation) + 0, slider_value, orientation) # create 4 circles (if not needed, then less) if not self._canvas.find_withtag("slider_oval_1_a"): diff --git a/customtkinter/widgets/ctk_progressbar.py b/customtkinter/widgets/ctk_progressbar.py index c67f98d..f6416ef 100644 --- a/customtkinter/widgets/ctk_progressbar.py +++ b/customtkinter/widgets/ctk_progressbar.py @@ -1,4 +1,5 @@ import tkinter +import math from .ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager @@ -20,6 +21,7 @@ class CTkProgressBar(CTkBaseClass): height=None, border_width="default_theme", orient="horizontal", + mode="determinate", **kwargs): # set default dimensions according to orientation @@ -50,8 +52,14 @@ class CTkProgressBar(CTkBaseClass): # shape self.corner_radius = ThemeManager.theme["shape"]["progressbar_corner_radius"] if corner_radius == "default_theme" else corner_radius self.border_width = ThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width - self.value = 0.5 + self.determinate_value = 0.5 # range 0-1 + self.determinate_speed = 1 # range 0-1 + self.indeterminate_value = 2 # range 0-1 + self.indeterminate_width = 0.4 # range 0-1 + self.indeterminate_speed = 1 # range 0-1 to travel in 50ms + self.loop_running = False self.orient = orient + self.mode = mode # "determinate" or "indeterminate" self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) @@ -101,12 +109,26 @@ class CTkProgressBar(CTkBaseClass): else: orientation = "w" - requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.apply_widget_scaling(self._current_width), - self.apply_widget_scaling(self._current_height), - self.apply_widget_scaling(self.corner_radius), - self.apply_widget_scaling(self.border_width), - self.value, - orientation) + if self.mode == "determinate": + requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.apply_widget_scaling(self._current_width), + self.apply_widget_scaling(self._current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width), + 0, + self.determinate_value, + orientation) + else: # indeterminate mode + progress_value = (math.sin(self.indeterminate_value * math.pi / 40) + 1) / 2 + progress_value_1 = min(1.0, progress_value + (self.indeterminate_width / 2)) + progress_value_2 = max(0.0, progress_value - (self.indeterminate_width / 2)) + + requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.apply_widget_scaling(self._current_width), + self.apply_widget_scaling(self._current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width), + progress_value_1, + progress_value_2, + orientation) if no_color_updates is False or requires_recoloring: self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) @@ -155,6 +177,10 @@ class CTkProgressBar(CTkBaseClass): del kwargs["variable"] + if "mode" in kwargs: + self.mode = kwargs.pop("mode") + require_redraw = True + if "width" in kwargs: self.set_dimensions(width=kwargs["width"]) del kwargs["width"] @@ -170,16 +196,54 @@ class CTkProgressBar(CTkBaseClass): self.set(self.variable.get(), from_variable_callback=True) def set(self, value, from_variable_callback=False): - self.value = value + """ set determinate value """ + self.determinate_value = value - if self.value > 1: - self.value = 1 - elif self.value < 0: - self.value = 0 + if self.determinate_value > 1: + self.determinate_value = 1 + elif self.determinate_value < 0: + self.determinate_value = 0 self.draw(no_color_updates=True) if self.variable is not None and not from_variable_callback: self.variable_callback_blocked = True - self.variable.set(round(self.value) if isinstance(self.variable, tkinter.IntVar) else self.value) + self.variable.set(round(self.determinate_value) if isinstance(self.variable, tkinter.IntVar) else self.determinate_value) self.variable_callback_blocked = False + + def get(self): + """ get determinate value """ + return self.determinate_value + + def start(self): + """ start indeterminate mode """ + if not self.loop_running: + self.loop_running = True + self.internal_loop() + + def stop(self): + """ stop indeterminate mode """ + self.loop_running = False + + def internal_loop(self): + if self.loop_running: + if self.mode == "determinate": + self.determinate_value += self.determinate_speed / 50 + if self.determinate_value > 1: + self.determinate_value -= 1 + self.draw() + self.after(20, self.internal_loop) + else: + self.indeterminate_value += self.indeterminate_speed + self.draw() + self.after(20, self.internal_loop) + + def step(self): + if self.mode == "determinate": + self.determinate_value += self.determinate_speed / 50 + if self.determinate_value > 1: + self.determinate_value -= 1 + self.draw() + else: + self.indeterminate_value += self.indeterminate_speed + self.draw() diff --git a/test/manual_integration_tests/complex_example_new.py b/test/manual_integration_tests/complex_example_new.py index e6ef785..1c9d869 100644 --- a/test/manual_integration_tests/complex_example_new.py +++ b/test/manual_integration_tests/complex_example_new.py @@ -99,14 +99,14 @@ class App(customtkinter.CTk): self.slider_progressbar_frame.grid_rowconfigure(3, weight=1) self.progressbar_1 = customtkinter.CTkProgressBar(self.slider_progressbar_frame) self.progressbar_1.grid(row=0, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") - self.slider_1 = customtkinter.CTkSlider(self.slider_progressbar_frame) - self.slider_1.grid(row=1, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") - self.slider_2 = customtkinter.CTkSlider(self.slider_progressbar_frame, from_=0, to=4, number_of_steps=4) - self.slider_2.grid(row=2, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") - self.slider_3 = customtkinter.CTkSlider(self.slider_progressbar_frame, orient="vertical") - self.slider_3.grid(row=0, column=1, rowspan=4, padx=(10, 10), pady=(10, 10), sticky="ns") - self.progressbar_2 = customtkinter.CTkProgressBar(self.slider_progressbar_frame, orient="vertical") - self.progressbar_2.grid(row=0, column=2, rowspan=4, padx=(10, 20), pady=(10, 10), sticky="ns") + self.progressbar_2 = customtkinter.CTkProgressBar(self.slider_progressbar_frame) + self.progressbar_2.grid(row=1, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") + self.slider_1 = customtkinter.CTkSlider(self.slider_progressbar_frame, from_=0, to=1, number_of_steps=4) + self.slider_1.grid(row=2, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") + self.slider_2 = customtkinter.CTkSlider(self.slider_progressbar_frame, orient="vertical") + self.slider_2.grid(row=0, column=1, rowspan=4, padx=(10, 10), pady=(10, 10), sticky="ns") + self.progressbar_3 = customtkinter.CTkProgressBar(self.slider_progressbar_frame, orient="vertical") + self.progressbar_3.grid(row=0, column=2, rowspan=4, padx=(10, 20), pady=(10, 10), sticky="ns") # set default values self.sidebar_button_3.configure(state="disabled", text="Disabled CTkButton") @@ -119,10 +119,11 @@ class App(customtkinter.CTk): self.scaling_optionemenu.set("100%") self.optionmenu_1.set("CTkOptionmenu") self.combobox_1.set("CTkComboBox") - self.textbox.insert("1.0", - "CTkTextbox\n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.", ) - #self.textbox.tag_add("headline", "1.0", "1.end") - #self.textbox.tag_config("headline", foreground="red") + self.textbox.insert("1.0", "CTkTextbox\n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.") + self.slider_1.configure(command=self.progressbar_2.set) + self.slider_2.configure(command=self.progressbar_3.set) + self.progressbar_1.configure(mode="indeterminnate") + self.progressbar_1.start() def open_input_dialog(self): dialog = customtkinter.CTkInputDialog(master=None, text="Type in a number:", title="CTkInputDialog") diff --git a/test/manual_integration_tests/test_progressbar_intermediate_mode.py b/test/manual_integration_tests/test_progressbar_intermediate_mode.py new file mode 100644 index 0000000..e05cd1e --- /dev/null +++ b/test/manual_integration_tests/test_progressbar_intermediate_mode.py @@ -0,0 +1,47 @@ +import customtkinter +import tkinter.ttk as ttk + +app = customtkinter.CTk() +app.geometry("400x600") + +p1 = customtkinter.CTkProgressBar(app) +p1.pack(pady=20) +p2 = ttk.Progressbar(app) +p2.pack(pady=20) + +s1 = customtkinter.CTkSlider(app, command=p1.set) +s1.pack(pady=20) + + +def switch_func(): + if sw1.get() == 1: + p1.configure(mode="indeterminate") + p2.configure(mode="indeterminate") + else: + p1.configure(mode="determinate") + p2.configure(mode="determinate") + +def start(): + p1.start() + p2.start() + +def stop(): + p1.stop() + p2.stop() + +def step(): + p1.step() + p2.step(10) + + +sw1 = customtkinter.CTkSwitch(app, text="intermediate mode", command=switch_func) +sw1.pack(pady=20) + +b1 = customtkinter.CTkButton(app, text="start", command=start) +b1.pack(pady=20) +b2 = customtkinter.CTkButton(app, text="stop", command=stop) +b2.pack(pady=20) +b3 = customtkinter.CTkButton(app, text="step", command=step) +b3.pack(pady=20) + +app.mainloop()