Tkinter-Designer icon indicating copy to clipboard operation
Tkinter-Designer copied to clipboard

Tkinter-Designer Support for Border Radius

Open afogel opened this issue 3 years ago • 13 comments

Border Radius (corner radius) is an important feature for modern UI buttons. It is possible to implement pill buttons natively, using a modification of code like this: https://stackoverflow.com/questions/42579927/rounded-button-tkinter-python

afogel avatar Jun 16 '21 16:06 afogel

Thanks for creating an Issue, We will be eager to solve it.

ParthJadhav avatar Jun 16 '21 16:06 ParthJadhav

You can create it using PIL, but it makes the code more complicated. To keep it simple we use native tkinter code with images. It gives the ability to have rounded buttons even with base tkinter.

Here's a code -

calculator_Example.zip

ParthJadhav avatar Jun 16 '21 16:06 ParthJadhav

Hah, yeah, I understand -- I opened it as an issue b/c while it does complicate the code, I think dynamically generated buttons are a worthwhile feature to incorporate. I managed to do it on my own, but when I start collaborating with non-technical contributors, it's valuable to be able to do so using this tool. Below was my implementation.

from tkinter import *
import tkinter as tk
import tkinter.font as font

class RoundedButton(tk.Canvas):
  def __init__(self, parent, border_radius, padding, color, text='', command=None):
    tk.Canvas.__init__(self, parent, borderwidth=0,
                       relief="flat", highlightthickness=0, bg=parent["bg"])
    self.command = command
    font_size = 16
    self.font = font.Font(size=font_size, family='Helvetica', weight="bold")
    self.id = None
    height = font_size+(2*padding)
    width = self.font.measure(text)+(4*padding)
    width = width if width >= 80 else 80

    if border_radius > 0.5*width:
      print("Error: border_radius is greater than width.")
      return None

    if border_radius > 0.5*height:
      print("Error: border_radius is greater than height.")
      return None

    rad = 2*border_radius

    def shape():
      self.create_arc((0, rad, rad, 0),
                      start=90, extent=90, fill=color, outline=color)
      self.create_arc((width-rad, 0, width,
                        rad), start=0, extent=90, fill=color, outline=color)
      self.create_arc((width, height-rad, width-rad,
                        height), start=270, extent=90, fill=color, outline=color)
      self.create_arc((0, height-rad, rad, height), start=180, extent=90, fill=color, outline=color)
      return self.create_polygon((0, height-border_radius, 0, border_radius, border_radius, 0, width-border_radius, 0, width,
                           border_radius, width, height-border_radius, width-border_radius, height, border_radius, height), fill=color, outline=color)

    id = shape()
    (x0, y0, x1, y1) = self.bbox("all")
    width = (x1-x0)
    height = (y1-y0)
    self.configure(width=width, height=height)
    self.create_text(width/2, height/2,text=text, fill='white', font= self.font)
    self.bind("<ButtonPress-1>", self._on_press)
    self.bind("<ButtonRelease-1>", self._on_release)

  def _on_press(self, event):
      self.configure(relief="sunken")

  def _on_release(self, event):
      self.configure(relief="raised")
      if self.command is not None:
          self.command()

And in a separate file:

from views.utils.rounded_button import RoundedButton

# ...
    self.start_btn = RoundedButton(
        self.controls, border_radius=3, padding=8, color="#16A765", text='Start Camera')
    self.start_btn.pack( side='left')

afogel avatar Jun 16 '21 16:06 afogel

Well, it looks great. I think we can incorporate it in Tkinter Designer. But I created dynamic text button in tkinter designer you can see it in the experimental branch.

Unlike master branch, where the whole button's image is downloaded and rendered. In the experimental the buttons background is separated from the text and only the button background image is downloaded and the text is added dynamically. But it doesn't have a documentation yet. So you wont be able to use it directly.

I'll share a link to Figma file in which I have added that functionality.

Also, I might reconsider. Your implementation could be better than our's. Let me see.

I am going to bed rn, I'll catch up tomorrow.

ParthJadhav avatar Jun 16 '21 16:06 ParthJadhav

Sounds great! Thanks :) I've got some magic numbers in this source that I put in there just for the sake of MVP, but was planning a refactor for later. Between the two of us, I think we can probably beef up the source to make it more robust, I just didn't have time for it right now.

afogel avatar Jun 16 '21 17:06 afogel

Yeah, definitely. That would be great. I'll leave this issue open...

ParthJadhav avatar Jun 16 '21 17:06 ParthJadhav

Made some PEP-8 adjustments to your implementation, @afogel. This just makes the code consistent with the project's current formatting:

from tkinter import *
import tkinter as tk
import tkinter.font as font

class RoundedButton(tk.Canvas):
    def __init__(self, parent, border_radius, padding,
                 color, text = "", command = None):

        tk.Canvas.__init__(self, parent, borderwidth = 0, relief = "flat",
                           highlightthickness = 0, bg = parent["bg"])

        self.color = color
        self.command = command
        self.id = None

        font_size = 16
        height = font_size + (2 * padding)
        width = self.font.measure(text) + (4 * padding)
        width = width if width >= 80 else 80

        self.font = font.Font(size = font_size, family = "Helvetica",
                              weight = "bold")

        if (
            border_radius > 0.5 * width
            or border_radius > 0.5 * height
        ):
            raise ValueError("Error: total border_radius must not "
                             "exceed width or height.")

        self.id = self._shape(border_radius)

        x0, y0, x1, y1 = self.bbox("all")
        self.width = (x1 - x0)
        self.height = (y1 - y0)

        self.configure(width = self.width, height = self.height)
        self.create_text(self.width / 2, self.height / 2,text = text,
                         fill = 'white', font =  self.font)

        self.bind("<ButtonPress-1>", self._on_press)
        self.bind("<ButtonRelease-1>", self._on_release)


    def _on_press(self, event):
        self.configure(relief = "sunken")


    def _on_release(self, event):
        self.configure(relief = "raised")
        if self.command is not None:
            self.command()


    def _shape(self, border_radius):
            radius = 2 * border_radius
            self.create_arc((0, radius, radius, 0),
                            start = 90, extent = 90,
                            fill = self.color, outline = self.color)

            self.create_arc((self.width - radius, 0, self.width,
                             radius), start = 0, extent = 90,
                             fill = self.color, outline = self.color)

            self.create_arc((self.width, self.height - radius,
                             self.width - radius, self.height),
                             start = 270, extent = 90, fill = self.color,
                             outline = self.color)

            self.create_arc((0, self.height - radius, radius, self.height),
                            start = 180, extent = 90, fill = self.color,
                            outline = self.color)

            return self.create_polygon(
                (
                    0, self.height - border_radius,
                    0, border_radius, border_radius,
                    0, self.width - border_radius, 0,
                    self.width, border_radius, self.width,
                    self.height - border_radius,
                    self.width - border_radius,
                    self.height, border_radius,
                    self.height
                ),
                fill = self.color,
                outline = self.color)

piccoloser avatar Jun 16 '21 18:06 piccoloser

Hey @afogel, I have created three templates which are compatible with the experimental branch.

Visit Here - Check Calculator and Two forms. (V1 file not supported)

You can select any of it and then click on duplicate. Duplicate = Fork. And then clone the experimental branch and test with that file. It creates the Buttons separated by their background and text.

Also I am thinking something crazy about making the buttons colour dynamic by using Pixel replacement or hue adjustment. I don't know if it's possible but seem's fun 😄 .

I am also thinking on how to incorporate your suggestion into Tkinter designer. Let's see.

ParthJadhav avatar Jun 17 '21 05:06 ParthJadhav

Hey @afogel ,

Did you find any way to make the buttons be rounded and transparent ? I mean a simpler way than above :)

ParthJadhav avatar Jul 31 '21 12:07 ParthJadhav

not yet! I've had to pivot projects, so my desktop development for tkinter is on hold right now -- I'll probably be turning back to it in a few weeks.

afogel avatar Aug 02 '21 12:08 afogel

not yet! I've had to pivot projects, so my desktop development for tkinter is on hold right now -- I'll probably be turning back to it in a few weeks.

Sure thing !

ParthJadhav avatar Aug 02 '21 13:08 ParthJadhav

Is this still pending? The designer interface, itself seems to be using rounded corners, but mine are still rendering with artifacts or with no background behind the border radius.

adammpkins avatar Nov 19 '21 19:11 adammpkins

Hey @adammpkins the designer used Images where there are rounded corners. Naming the elements which are rounded to -> "Image" might solve the problem. Same goes with the background.

ParthJadhav avatar Nov 20 '21 04:11 ParthJadhav