CustomTkinter icon indicating copy to clipboard operation
CustomTkinter copied to clipboard

Added 2 new widgets (dividers)

Open IshanJ25 opened this issue 3 years ago • 13 comments
trafficstars

I have added 2 new widgets - Horizontal and vertical dividers They are loosely based on the the CTk Frame widget, except some changes:

  • width and height are called thickness and length
  • radius is half of thickness for full rounded corners
  • no border or border color
  • no child widget allowed in the frame

image while testing: image

I kindly request the maintainers to consider this pull request for addition of divider widgets.

Dividers are very simple and effective element of a powerful user interface.

IshanJ25 avatar Jun 09 '22 06:06 IshanJ25

Cool ! Can you consider adding some padding.

felipetesc avatar Jun 15 '22 19:06 felipetesc

I don't understand how padding will work for multiple sides. Could you give some logic insight?

IshanJ25 avatar Jun 29 '22 16:06 IshanJ25

IMG

to be like this:

divider

felipetesc avatar Jun 30 '22 00:06 felipetesc

@felipetesc Update: I have been working on it recently, and much of the "padding" functionality has been implemented, but there one single problem: rounded corners on divider are lost.

What seems to be causing the problem?

screenshot: image

image

here is code used to draw this window:

from ctypes import windll
from win32api import GetMonitorInfo, MonitorFromPoint
import mycustomtkinter as ctk

###############################################
################# set scaling #################
###############################################

screensize_old = windll.user32.GetSystemMetrics(0)
windll.shcore.SetProcessDpiAwareness(1)
screensize_new = windll.user32.GetSystemMetrics(0)

work_area = GetMonitorInfo(MonitorFromPoint((0, 0))).get("Work")
screen_w, screen_h = work_area[2], work_area[3]

scale = round(screensize_old / screensize_new, 2)

ctk.set_window_scaling(scale)
ctk.set_spacing_scaling(scale)
ctk.set_widget_scaling(scale)

###############################################
###############################################


root = ctk.CTk()
root.title("VerticalDivider")
root.geometry("300x300")
root.resizable(False, False)
root.bg_color = "#323232"

div = ctk.CTkVerticalDivider(root,
                             bg_color='#000000',
                             fg_color="#ff0000",
                             length=120,
                             thickness=8,
                             pady=70,
                             padx=(80, 40))
div.pack()

root.mainloop()

IshanJ25 avatar Jul 01 '22 05:07 IshanJ25

Update:

Rounded corners problem is solved

Question:

Is there a need to implement configure method for padding?

In my opinion, there is no such need, Just some polishing and it will be completed

IshanJ25 avatar Jul 03 '22 16:07 IshanJ25

Just to let you know, so you are not left in the dark. I'm trying to add padding, or border width to the divider.

I didn't post my observations and findings, which now seems unwise since we could have figured it out together. Yet, I'm debugging your code to understand what can be done. I found some issues, which are not directly related to your widget, but mostly about how canvas draw through the class DrawEngine the divider.

Perhaps borderwidth is a better word for padding, since ctk already use it for the same purpose.

I'll start posting my findings with the objective of make your widgets to be added to ctk.

configure: if you are asking my opinion, yes, I think it should be added a if clause to check if the border_width was passed as a string paremeter to the overriden configure method. So, just like any other ui element, dividers can be changed on the fly. If one day someone decides to create a gui editor for ctk we can simply pass another value and Voilà the gui shows another theme for the available widgets

felipetesc avatar Jul 03 '22 16:07 felipetesc

Ok, I will post two sample codes, the first one for a changed CTkHorizontalDivider (we can run without your cloned ctk), and the second one contains a test:

Before looking the code though, a small explanation about its changes can save some time and help, since the changes are so small and can be easily missed):

  • I added a new constructor param: border_width = 0
  • Inside the the constructor I created a new class property: self.border_width = border_width
  • I added inside configure method a new verification for border_width:
if "border_width" in kwargs:
            self.border_width = kwargs["border_width"]
            require_redraw = True
            del kwargs["border_width"]

Modified CTkHorizontalDivider(it's your class):

import customtkinter
from customtkinter import CTkCanvas
from customtkinter import ThemeManager
from customtkinter import DrawEngine
from customtkinter import CTkBaseClass


class CTkHorizontalDivider(CTkBaseClass):
    def __init__(self, *args,
                 bg_color=None,
                 fg_color="default_theme",
                 length=160,
                 thickness=8,
                 border_width = 0,
                 overwrite_preferred_drawing_method: str = None,
                 **kwargs):

        # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass
        super().__init__(*args, bg_color=bg_color, width=length, height=thickness, border_width=0, **kwargs)

        self.border_width = border_width

        # determine fg_color
        if fg_color == "default_theme":
            if isinstance(self.master, CTkHorizontalDivider):
                if self.master.fg_color == ThemeManager.theme["color"]["frame_low"]:
                    self.fg_color = ThemeManager.theme["color"]["frame_high"]
                else:
                    self.fg_color = ThemeManager.theme["color"]["frame_low"]
            else:
                self.fg_color = ThemeManager.theme["color"]["frame_low"]
        else:
            self.fg_color = fg_color

        # shape
        self.corner_radius = corner_radius = thickness / 2

        self.canvas = CTkCanvas(master=self,
                                highlightthickness=0,
                                width=self.apply_widget_scaling(self._current_width),
                                height=self.apply_widget_scaling(self._current_height))
        self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
        self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))
        self.draw_engine = DrawEngine(self.canvas)
        self._overwrite_preferred_drawing_method = overwrite_preferred_drawing_method
        self.bind('<Configure>', self.update_dimensions_event)

        self.draw()


    def set_scaling(self, *args, **kwargs):
        super().set_scaling(*args, **kwargs)

        self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
                              height=self.apply_widget_scaling(self._desired_height))
        self.draw()

    def set_dimensions(self, width=None, height=None):
        super().set_dimensions(width, height)

        self.canvas.configure(width=self.apply_widget_scaling(self._desired_width),
                              height=self.apply_widget_scaling(self._desired_height))
        self.draw()

    def draw(self, no_color_updates=False):

        requires_recoloring = self.draw_engine.draw_rounded_rect_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),
            overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method)

        if no_color_updates is False or requires_recoloring:
            if self.fg_color is None:
                self.canvas.itemconfig("inner_parts",
                                       fill=ThemeManager.single_color(self.bg_color, self._appearance_mode),
                                       outline=ThemeManager.single_color(self.bg_color, self._appearance_mode))
            else:
                self.canvas.itemconfig("inner_parts",
                                       fill=ThemeManager.single_color(self.fg_color, self._appearance_mode),
                                       outline=ThemeManager.single_color(self.fg_color, self._appearance_mode))

            self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode))

        self.canvas.tag_lower("inner_parts")
        self.canvas.tag_lower("border_parts")

    def configure(self, *args, **kwargs):
        require_redraw = False  # some attribute changes require a call of self.draw() at the end

        if "border_width" in kwargs:
            self.border_width = kwargs["border_width"]
            require_redraw = True
            del kwargs["border_width"]

        if "fg_color" in kwargs:
            self.fg_color = kwargs["fg_color"]
            require_redraw = True
            del kwargs["fg_color"]

        if "bg_color" in kwargs:
            if kwargs["bg_color"] is None:
                self.bg_color = self.detect_color_of_master()
            else:
                self.bg_color = kwargs["bg_color"]
            require_redraw = True

            del kwargs["bg_color"]

        if "length" in kwargs:
            self.set_dimensions(width=kwargs["length"])
            del kwargs["length"]

        if "thickness" in kwargs:
            self.set_dimensions(height=kwargs["thickness"])
            del kwargs["thickness"]

        super().configure(*args, **kwargs)

        if require_redraw:
            self.draw()

Sample test:

import customtkinter
from customtkinter import CTkCanvas
from customtkinter import ThemeManager
from customtkinter import DrawEngine
from customtkinter import CTkBaseClass
from ctk_vdiv import CTkVerticalDivider
from ctk_hdiv import CTkHorizontalDivider



DARK_MODE = "dark"
customtkinter.set_appearance_mode(DARK_MODE)
customtkinter.set_default_color_theme("blue")

root = customtkinter.CTk()
root.title("Any divider with padding")
root.geometry("800x600")
root.resizable(True, True)
root.bg_color = "#323232"

frame = customtkinter.CTkFrame(root, corner_radius=10,  width = 800 , height=600 )
frame.pack()

v_div = CTkVerticalDivider(frame, border_width=4,bg_color="red", fg_color="default_theme", length=380, thickness=40)
v_div.pack()

root.mainloop()

I believe that the problem I am finding is related how radius calculation happens as you can see on the image below:

divider_test

I'm guessing this is the source of the calculation problem, but I wanted to know what do you think: self.corner_radius = corner_radius = thickness / 2

felipetesc avatar Jul 03 '22 18:07 felipetesc

maybe:

if self.boder_width != 0:
    self.corner_radius = corner_radius = (thickness / 2) - (self.boder_width*2)
else:
    self.corner_radius = corner_radius = (thickness / 2)

felipetesc avatar Jul 03 '22 18:07 felipetesc

The border_width parameter has been removed bacause a divider is just a solid, flat rounded rectangle. It only has 3 major parameter values, divided into 2 each:

  • Color (bg, fg)
  • dimensions (length, thickness)
  • padding (x, y)

i used padx and pady insted, just as in native tkinter widget (instead of passing these values through pack, they are passed when initializing the widget)

example:

import customtkinter as ctk

root = ctk.CTk()
root.title("VerticalDivider")
root.geometry("300x300")
root.resizable(False, False)

div = ctk.CTkVerticalDivider(root, fg_color="#eeeeee", bg_color='#00bbaa', length=150, thickness=10, pady=(50, 10), padx=40)
div.pack()


root.mainloop()

output: image

because padx is passed as an integer, it is taken as same from both sides (40 pixels), and pady is a tuple with 50 pixls above and 10 pixels below reserved for padding

IshanJ25 avatar Jul 04 '22 12:07 IshanJ25

I'm guessing this is the source of the calculation problem, but I wanted to know what do you think: self.corner_radius = corner_radius = thickness / 2

@felipetesc I believe it can be said that this problem is half-solved, that it works after initializing the divider but configure method is still buggy. The problem was that divider size was being redirected to whole widget size including padding dimensions, so only top left rounded corner was visible while other corners were not visible. Now that is solved, and i will commit current code soon, for further debugging. There is no problem with the corner_radius value.

IshanJ25 avatar Jul 04 '22 12:07 IshanJ25

corner_radius

The corner_radius issue it is not related to your code, but it is related to my implementation, which would need to changed, but it won't because your padx and pady solution is many times better

felipetesc avatar Jul 04 '22 17:07 felipetesc

The border_width parameter has been removed bacause a divider is just a solid, flat rounded rectangle. It only has 3 major parameter values, divided into 2 each:

  • Color (bg, fg)
  • dimensions (length, thickness)
  • padding (x, y)

i used padx and pady insted, just as in native tkinter widget (instead of passing these values through pack, they are passed when initializing the widget)

example:

import customtkinter as ctk

root = ctk.CTk()
root.title("VerticalDivider")
root.geometry("300x300")
root.resizable(False, False)

div = ctk.CTkVerticalDivider(root, fg_color="#eeeeee", bg_color='#00bbaa', length=150, thickness=10, pady=(50, 10), padx=40)
div.pack()


root.mainloop()

output: image

because padx is passed as an integer, it is taken as same from both sides (40 pixels), and pady is a tuple with 50 pixls above and 10 pixels below reserved for padding

After seeing the result image, your dividers seems quite nice ! The use of padx and pady it's the best idea to add spaces. I'll certainly use them several times .

felipetesc avatar Jul 04 '22 17:07 felipetesc

corner_radius

I will test configure

felipetesc avatar Jul 04 '22 17:07 felipetesc

@IshanJ25 How can I use the dividers? Does it require a pip installation?

DominicCulyer avatar Mar 19 '23 16:03 DominicCulyer

@IshanJ25 How can I use the dividers? Does it require a pip installation?

@DominicCulyer no, this is just an idea for new widgets, not an external package. for now you can use frame widget to show a "divider" in your GUI, I have been hardly able to give this any attention in past months due to some reasons. I will surely get back to it and try the proper implementations of these widgets as soon as I am able.

IshanJ25 avatar Mar 19 '23 19:03 IshanJ25