19

I am trying to get rounded buttons for my script using tkinter.

I found the following code in an answer to How to make a Button using the tkinter Canvas widget?:

from tkinter import *
import tkinter as tk

class CustomButton(tk.Canvas):
    def __init__(self, parent, width, height, color, command=None):
        tk.Canvas.__init__(self, parent, borderwidth=1, 
            relief="raised", highlightthickness=0)
        self.command = command

        padding = 4
        id = self.create_oval((padding,padding,
            width+padding, height+padding), outline=color, fill=color)
        (x0,y0,x1,y1)  = self.bbox("all")
        width = (x1-x0) + padding
        height = (y1-y0) + padding
        self.configure(width=width, height=height)
        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()
app = CustomButton()
app.mainloop()

but I get the following error:

TypeError: __init__() missing 4 required positional arguments: 'parent', 'width', 'height', and 'color'
martineau
  • 119,623
  • 25
  • 170
  • 301
Martinn Roelofse
  • 423
  • 2
  • 4
  • 13

7 Answers7

18

A very easy way to make a rounded button in tkinter is to use an image.

First create an image of what you want you button to look like save it as a .png and remove the outside background so it is rounded like the one below:

Click here to see image

Next insert the image in a button with PhotoImage like this:

self.loadimage = tk.PhotoImage(file="rounded_button.png")
self.roundedbutton = tk.Button(self, image=self.loadimage)
self.roundedbutton["bg"] = "white"
self.roundedbutton["border"] = "0"
self.roundedbutton.pack(side="top")

Ensure to use border="0" and the button border will be removed.

I added the self.roundedborder["bg"] = "white" so that the the background the background of the button is the same as the Tkinter window.

The great part is that you can use any shape you like not just the normal button shapes.

Zoe
  • 27,060
  • 21
  • 118
  • 148
Xantium
  • 11,201
  • 10
  • 62
  • 89
  • 3
    Seems nice but this does not work in my case. Because as soon as you put a Background-picture behind the buttons you will still see the border as the Button is not rounded like the picture. – Cedric Sep 25 '19 at 12:59
  • 1
    Yes it isn't infallible, for things like that you really want to consider switching to a different library. PyQT looks nice and is easy to code, if you want to go for something free and open source then WX widgets might be better for you. For anything very unique you may want to use something like PyOpenGL to code a new GUI. I've heard it's not that hard, but time consuming – Xantium Sep 25 '19 at 23:54
  • 1
    Ok thanks but I do not get paid for that... So I wont program my own UI soon ^^ – Cedric Sep 26 '19 at 05:59
14

I made this rounded rectangle button if anyone was looking for more of an apple look or something. For convenience here are the arguments:

RoundedButton(parent, width, height, cornerradius, padding, fillcolor, background, command)

Note: If the corner radius is greater than half of the width or height an error message will be sent in the terminal. Pill shapes can still be made through if you set the corner radius to exactly half of the height or width.

Finally the code:

from tkinter import *
import tkinter as tk

root = Tk()

class RoundedButton(tk.Canvas):
    def __init__(self, parent, width, height, cornerradius, padding, color, bg, command=None):
        tk.Canvas.__init__(self, parent, borderwidth=0, 
            relief="flat", highlightthickness=0, bg=bg)
        self.command = command

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

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

        rad = 2*cornerradius
        def shape():
            self.create_polygon((padding,height-cornerradius-padding,padding,cornerradius+padding,padding+cornerradius,padding,width-padding-cornerradius,padding,width-padding,cornerradius+padding,width-padding,height-cornerradius-padding,width-padding-cornerradius,height-padding,padding+cornerradius,height-padding), fill=color, outline=color)
            self.create_arc((padding,padding+rad,padding+rad,padding), start=90, extent=90, fill=color, outline=color)
            self.create_arc((width-padding-rad,padding,width-padding,padding+rad), start=0, extent=90, fill=color, outline=color)
            self.create_arc((width-padding,height-rad-padding,width-padding-rad,height-padding), start=270, extent=90, fill=color, outline=color)
            self.create_arc((padding,height-padding-rad,padding+rad,height-padding), start=180, extent=90, 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.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 test():
    print("Hello")

canvas = Canvas(root, height=300, width=500)
canvas.pack()

button = RoundedButton(root, 200, 100, 50, 2, 'red', 'white', command=test)
button.place(relx=.1, rely=.1)

root.mainloop()
Guac
  • 185
  • 1
  • 8
  • 6
    Is there a way to add text to this button? – Polydynamical Sep 01 '20 at 01:39
  • 1
    The statement `id = shape()` is a little mysterious because `id` isn't referenced anywhere else that I can see. Is it something the `tk.Canvas` class uses automatically? I'm still new to padding but shouldn't `padding` be split into `padx` and `pady`? – WinEunuuchs2Unix Jun 09 '21 at 00:17
6

Unfortunately, images don't work well when resized.

Below is an example of a rounded button using canvas which works well even if resized.

import tkinter as tk


class RoundedButton(tk.Canvas):

    def __init__(self, master=None, text:str="", radius=25, btnforeground="#000000", btnbackground="#ffffff", clicked=None, *args, **kwargs):
        super(RoundedButton, self).__init__(master, *args, **kwargs)
        self.config(bg=self.master["bg"])
        self.btnbackground = btnbackground
        self.clicked = clicked

        self.radius = radius        
        
        self.rect = self.round_rectangle(0, 0, 0, 0, tags="button", radius=radius, fill=btnbackground)
        self.text = self.create_text(0, 0, text=text, tags="button", fill=btnforeground, font=("Times", 30), justify="center")

        self.tag_bind("button", "<ButtonPress>", self.border)
        self.tag_bind("button", "<ButtonRelease>", self.border)
        self.bind("<Configure>", self.resize)
        
        text_rect = self.bbox(self.text)
        if int(self["width"]) < text_rect[2]-text_rect[0]:
            self["width"] = (text_rect[2]-text_rect[0]) + 10
        
        if int(self["height"]) < text_rect[3]-text_rect[1]:
            self["height"] = (text_rect[3]-text_rect[1]) + 10
          
    def round_rectangle(self, x1, y1, x2, y2, radius=25, update=False, **kwargs): # if update is False a new rounded rectangle's id will be returned else updates existing rounded rect.
        # source: https://stackoverflow.com/a/44100075/15993687
        points = [x1+radius, y1,
                x1+radius, y1,
                x2-radius, y1,
                x2-radius, y1,
                x2, y1,
                x2, y1+radius,
                x2, y1+radius,
                x2, y2-radius,
                x2, y2-radius,
                x2, y2,
                x2-radius, y2,
                x2-radius, y2,
                x1+radius, y2,
                x1+radius, y2,
                x1, y2,
                x1, y2-radius,
                x1, y2-radius,
                x1, y1+radius,
                x1, y1+radius,
                x1, y1]

        if not update:
            return self.create_polygon(points, **kwargs, smooth=True)
        
        else:
            self.coords(self.rect, points)

    def resize(self, event):
        text_bbox = self.bbox(self.text)

        if self.radius > event.width or self.radius > event.height:
            radius = min((event.width, event.height))

        else:
            radius = self.radius

        width, height = event.width, event.height

        if event.width < text_bbox[2]-text_bbox[0]:
            width = text_bbox[2]-text_bbox[0] + 30
        
        if event.height < text_bbox[3]-text_bbox[1]:  
            height = text_bbox[3]-text_bbox[1] + 30
        
        self.round_rectangle(5, 5, width-5, height-5, radius, update=True)

        bbox = self.bbox(self.rect)

        x = ((bbox[2]-bbox[0])/2) - ((text_bbox[2]-text_bbox[0])/2)
        y = ((bbox[3]-bbox[1])/2) - ((text_bbox[3]-text_bbox[1])/2)

        self.moveto(self.text, x, y)

    def border(self, event):
        if event.type == "4":
            self.itemconfig(self.rect, fill="#d2d6d3")
            if self.clicked is not None:
                self.clicked()

        else:
            self.itemconfig(self.rect, fill=self.btnbackground)

def func():
    print("Button pressed")

root = tk.Tk()
btn = RoundedButton(text="This is a \n rounded button", radius=100, btnbackground="#0078ff", btnforeground="#ffffff", clicked=func)
btn.pack(expand=True, fill="both")
root.mainloop()

To create this use canvas.create_rectangle() and canvas.create_text() methods and give them both of them same tag, say "button". The tag will be used when using canvas.tag_bind("tag", "<ButtonPress>")(you can also simply pass "current" as tag, which is assigned to the currently selected item by tkinter, in which case you can remove button tag).

Use canvas.tag_bind on canvas item instead of bind on canvas, this way the button color will change only if the mouse press happens inside the rounded button and not at edges.

You can scale and improve this to generate custom events when clicked inside the button, add configure method to configure button text and background, etc.

output: enter image description here

Art
  • 2,836
  • 4
  • 17
  • 34
  • How can I create smaller buttons? – Rovindu Thamuditha Dec 15 '21 at 14:16
  • 1
    @RovinduThamuditha that depends on your geometry manager. If you are using pack you might be required to remove the expand=True and fill and specify height and width in `RoundedButton` as a constructor. – Art Dec 15 '21 at 16:09
  • This is fantastic, thanks so much for this! I do have an issue with the button having a white border, which is noticeable when there is a colour other than white behind it. I've been trying to understand where it's coming from in the class. Do you have any ideas how I can fix it? – Cai Allin Mar 10 '22 at 16:06
  • @CaiAllin I am unable to reproduce the problem, I tried with blue as well as green background. Make sure you have set `bd` or `borderwidth` parameter in the canvas to 0. Also try setting relief to "flat" or check this link: https://stackoverflow.com/questions/4310489/how-do-i-remove-the-light-grey-border-around-my-canvas-widget – Art Mar 11 '22 at 00:21
  • @Art Thanks, I managed to figure it out with some help, borderwidth was the issue. Thanks again – Cai Allin Mar 11 '22 at 01:51
5

You are not passing any arguments to the constructor.

Specifically, on this line

app = CustomButton()

you need to pass the arguments that were defined in the constructor definition, namely parent, width, height and color.

Community
  • 1
  • 1
frederick99
  • 1,033
  • 11
  • 18
5

You need to create root window first (or some other widget) and give it to your CustomButton together with different parameters (see definition of __init__ method).

Try instead of app = CustomButton() the following:

app = tk.Tk()
button = CustomButton(app, 100, 25, 'red')
button.pack()
app.mainloop()
avysk
  • 1,973
  • 12
  • 18
  • 1
    Thank you. That made it run but the button isn't round – Martinn Roelofse Mar 03 '17 at 13:29
  • 3
    No, it's not. However, it's exactly what the code you "found" is supposed to be doing. It makes rectangular Canvas with raised relief and draws an oval on it. When you press/release button, it makes relief sunken / raised again. – avysk Mar 03 '17 at 13:31
2

I have had a lot of trouble finding the code that works for me. I have tried applying images to buttons and also tried the custom button styles from above.

This is the custom button code that worked for me and I am thankful for this issue on Github

Here is the code just in case :

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="raised", highlightthickness=0, bg=parent["bg"])
    self.command = command
    font_size = 10
    self.font = font.Font(size=font_size, family='Helvetica')
    self.id = None
    height = font_size + (1 * padding)
    width = self.font.measure(text)+(1*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='black', 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()

Now save this code in a file, for example, name it custombutton.py. Next import this file in your current python file (like so: from custombutton import RoundedButton) and use it like so:

RoundedButton(root, text="Some Text", border_radius=2, padding=4, command=some_function, color="#cda989")
  • How do you add a border and still keep it a rounded button? I changed the ```boarderwidth``` and it then stop being a rounded button – Lakshan Costa Jul 25 '22 at 13:00
0

If you use an image such as in @Xantium 's method, you could set the button parameter borderwidth to 0.

As in:

homebtn = tk.Button(root, image=img, borderwidth=0)
MrNiver632
  • 21
  • 2