How to make a tkinter canvas rectangle with rounded corners?

21,027

Solution 1

Offering an alternate approach to tobias's method would be to indeed do it with one polygon.

This would have the advantage of being one canvas object if you are worried about optimization, or not having to worry about a tag system for referring to a single object.

The code is a bit longer, but very basic, as it is just utilizing the idea that when smoothing a polygon, you can give the same coordinate twice to 'stop' the smooth from occuring.

This is an example of what can be done:

from tkinter import *
root = Tk()
canvas = Canvas(root)
canvas.pack()

def round_rectangle(x1, y1, x2, y2, radius=25, **kwargs):
        
    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]

    return canvas.create_polygon(points, **kwargs, smooth=True)

my_rectangle = round_rectangle(50, 50, 150, 100, radius=20, fill="blue")

root.mainloop()

Using this function, you can just provide the normal coordinates that you would to a rectangle, and then specify the 'radius' which is rounded in the corners. The use of **kwargs denotes that you can pass keyword arguments such as fill="blue", just as you usually could with a create_ method.

Although the coords look complex, it is just going around methodically to each point in the 'rectangle', giving each non-corner point twice.

If you didn't mind a rather long line of code, you could put all the coordinates on one line, making the function just 2 lines(!). This looks like:

def round_rectangle(x1, y1, x2, y2, r=25, **kwargs):    
    points = (x1+r, y1, x1+r, y1, x2-r, y1, x2-r, y1, x2, y1, x2, y1+r, x2, y1+r, x2, y2-r, x2, y2-r, x2, y2, x2-r, y2, x2-r, y2, x1+r, y2, x1+r, y2, x1, y2, x1, y2-r, x1, y2-r, x1, y1+r, x1, y1+r, x1, y1)
    return canvas.create_polygon(points, **kwargs, smooth=True)

This produces the following (Note in mind this is ONE canvas object):

Rounded rectangle produced by function


If you want to update the position of the rectangle after it has been created, you could use a function like this (if in the same scope as the original canvas object):

def update_rectangle_coords(round_rect, x1, y1, x2, y2, r=25):
    points = (x1+r, y1, x1+r, y1, x2-r, y1, x2-r, y1, x2, y1, x2, y1+r, x2, y1+r, x2, y2-r, x2, y2-r, x2, y2, x2-r, y2, x2-r, y2, x1+r, y2, x1+r, y2, x1, y2, x1, y2-r, x1, y2-r, x1, y1+r, x1, y1+r, x1, y1)
    canvas.coords(round_rect, *points)

So, to update my_rectangle's position (from the first code example), we could say:

update_rectangle_coords(my_rectangle, 20, 20, 100, 100)

Solution 2

There seems not to be a built-in method for this. The closest thing would be a polyline with smooth=1, but that still looks more like an old TV screen, with the sides also slightly curved.

Instead, you could define a helper function, combining the rounded rectangle from lines and arcs:

def rounded_rect(canvas, x, y, w, h, c):
    canvas.create_arc(x,   y,   x+2*c,   y+2*c,   start= 90, extent=90, style="arc")
    canvas.create_arc(x+w-2*c, y+h-2*c, x+w, y+h, start=270, extent=90, style="arc")
    canvas.create_arc(x+w-2*c, y,   x+w, y+2*c,   start=  0, extent=90, style="arc")
    canvas.create_arc(x,   y+h-2*c, x+2*c,   y+h, start=180, extent=90, style="arc")
    canvas.create_line(x+c, y,   x+w-c, y    )
    canvas.create_line(x+c, y+h, x+w-c, y+h  )
    canvas.create_line(x,   y+c, x,     y+h-c)
    canvas.create_line(x+w, y+c, x+w,   y+h-c)

Example:

import tkinter
root = tkinter.Tk()
canvas = tkinter.Canvas(root)
canvas.pack()
rounded_rect(canvas, 20, 20, 60, 40, 10)
root.mainloop()

enter image description here

You could also provide another **options parameter to set line width, color etc. for the individual parts, but the problem with this is that e.g. lines and arcs are using different parameters for the line color (fill and outline respectively). Also, if you want to have a filled rounded rectangle, you will have to specify that as a second method, using multiple rectangles.

Solution 3

I know this post already has an accepted answer for a rectangle. But for those looking for any polygon with rounded corners (rectangle included obviously), I made this code based on @SneakyTutle's answer.

roundPolygon(x_array, y_array, sharpness, **kwargs)

Result

Image

The logic behind this is to enable smooth and place subpoints next to the vertex. This way, only the corners are going to be rounded and the rest of the polygon is kept flat.

from tkinter import *
root = Tk()
canvas = Canvas(root, width = 1000, height = 1000)
canvas.pack()

def roundPolygon(x, y, sharpness, **kwargs):

    # The sharpness here is just how close the sub-points
    # are going to be to the vertex. The more the sharpness,
    # the more the sub-points will be closer to the vertex.
    # (This is not normalized)
    if sharpness < 2:
        sharpness = 2

    ratioMultiplier = sharpness - 1
    ratioDividend = sharpness

    # Array to store the points
    points = []

    # Iterate over the x points
    for i in range(len(x)):
        # Set vertex
        points.append(x[i])
        points.append(y[i])

        # If it's not the last point
        if i != (len(x) - 1):
            # Insert submultiples points. The more the sharpness, the more these points will be
            # closer to the vertex. 
            points.append((ratioMultiplier*x[i] + x[i + 1])/ratioDividend)
            points.append((ratioMultiplier*y[i] + y[i + 1])/ratioDividend)
            points.append((ratioMultiplier*x[i + 1] + x[i])/ratioDividend)
            points.append((ratioMultiplier*y[i + 1] + y[i])/ratioDividend)
        else:
            # Insert submultiples points.
            points.append((ratioMultiplier*x[i] + x[0])/ratioDividend)
            points.append((ratioMultiplier*y[i] + y[0])/ratioDividend)
            points.append((ratioMultiplier*x[0] + x[i])/ratioDividend)
            points.append((ratioMultiplier*y[0] + y[i])/ratioDividend)
            # Close the polygon
            points.append(x[0])
            points.append(y[0])

    return canvas.create_polygon(points, **kwargs, smooth=TRUE)

my_rectangle = roundPolygon([50, 350, 350, 50], [50, 50, 350, 350], 10 , width=5, outline="#82B366", fill="#D5E8D4")
my_triangle = roundPolygon([50, 650, 50], [400, 700, 1000], 8 , width=5, outline="#82B366", fill="#D5E8D4")

root.mainloop()

I couldn't figure a nice way to normalize the sharpness. Anyway, something between 2 and 10 will be good for any case. Feel free to change the code as you wish.

Just for visualization, for a triangle with sharpness=8 the result code for the for loop is as follows. As you might note, if sharpness is 2, the sub-points are going to be placed in the middle of the vertex.

points = [
      # Begin vertex
      x[0], y[0],
      # Between vertices
      (7*x[0] + x[1])/8, (7*y[0] + y[1])/8,
      (7*x[1] + x[0])/8, (7*y[1] + y[0])/8,
      # Vertex
      x[1], y[1],
      # Between vertices
      (7*x[1] + x[2])/8, (7*y[1] + y[2])/8,
      (7*x[2] + x[1])/8, (7*y[2] + y[1])/8,
      # Vertex
      x[2], y[2],
      # Between vertices
      (7*x[2] + x[0])/8, (7*y[2] + y[0])/8,
      (7*x[0] + x[2])/8, (7*y[0] + y[2])/8,
      # End/Begin vertex
      x[0], y[0]
    ]
Share:
21,027
Cthulhu
Author by

Cthulhu

ITCR

Updated on April 13, 2021

Comments

  • Cthulhu
    Cthulhu about 3 years

    I would like to create a rectangle with rounded corners. I'm using canvas from tkinter.

  • tobias_k
    tobias_k almost 7 years
    I'm surprised this looks so good. When I tried the same, the straight edges were slightly bent outwards, as I said like an old TV. But I was using only 12 points, not 20. +1 IMHO would better with explicit x1,y1,x2,y2 or x,y,w,h parameters, though.
  • SneakyTurtle
    SneakyTurtle almost 7 years
    @tobias_k I think the trick for this method is giving the points on each side (which are the 'radius' away from the corner) twice, so they don't smooth. Also I agree about the x1,y1,x2,y2 suggestion, edited.
  • Delrius Euphoria
    Delrius Euphoria about 3 years
    I know this is an old answer but would you be somehow able to mimic the coords function such that when we try to edit the coordinate of the existing rounded rectangle, we dont have to deal with all the polygonal points.
  • SneakyTurtle
    SneakyTurtle about 3 years
    @CoolCloud See my latest edit to the answer - we can use a similar function to how we created the rectangle to update its position.
  • WinEunuuchs2Unix
    WinEunuuchs2Unix almost 3 years
    @CoolCloud I second your motion!
  • WinEunuuchs2Unix
    WinEunuuchs2Unix almost 3 years
    @SneakyTurtle Good job. Now just need a little triangle to point at the widget the mouse was hovering over when the polygon rounded corner rectangle was drawn.
  • Delrius Euphoria
    Delrius Euphoria almost 3 years
    @WinEunuuchs2Unix Now that I understand this, the concept is pretty simple