3

I came across this interesting question (How to make a tkinter canvas rectangle with rounded corners?) related to creating rounded rectangles in Tkinter and specifically, this answer by Francisco Gomes (modified a bit):

def roundPolygon(x, y, sharpness):

    # 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])

When I adapted this code to work with my graphics library, it worked well enough! but when I create a 'stretched-square' (a non-square rectangle), the roundness becomes stretched too:

enter image description here

So how can I change this code to remove the stretched roundness and keep it a constant radius?

Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80
Bhavye Mathur
  • 1,024
  • 1
  • 7
  • 25

1 Answers1

4

Here is one approach that uses the built in tcl tk primitives canvas.create_line, and canvas.create_arc to build rectangles of various sizes, and proportions with round corners (arc of a circle).

The corners radii is expressed as a proportion of the shortest side of the rectangle (0.0 --> 0.5), and can be parametrized.

The function make_round_corners_rect returns a tuple containing all canvas item ids as fragments of the rectangle entity. All fragments are tagged with their companions' ids, so accessing the entire object is possible with only one fragment id.


enter image description here


#! python3

import math
import tkinter as tk
from tkinter import TclError


def make_round_corners_rect(canvas, x0, y0, x1, y1, ratio=0.2, npts=12):

    if x0 > x1:
        x0, x1 = x1, x0
    if y0 > y1:
        y0, y1 = y1, y0
        
    r = min(x1 - x0, y1 - y0) * ratio
    
    items = []

    topleft = x0, y0
    tld = x0, y0 + r
    tlr = x0 + r, y0
    item = canvas.create_arc(x0, y0, x0+2*r, y0+2*r, start=90, extent=90, fill='', outline='black', style=tk.ARC)
    items.append(item)
    
    top_right = x1, y0
    trl = x1 - r, y0
    trd = x1, y0 + r
    item = canvas.create_line(*tlr, *trl, fill='black')
    items.append(item)
    item = canvas.create_arc(x1-2*r, y0, x1, y0+2*r, start=0, extent=90, fill='', outline='black', style=tk.ARC)
    items.append(item)

    bot_right = x1, y1
    bru = x1, y1 - r
    brl = x1 - r, y1
    item = canvas.create_line(*trd, *bru, fill='black')
    items.append(item)
    item = canvas.create_arc(x1-2*r, y1-2*r, x1, y1, start=270, extent=90, fill='', outline='black', style=tk.ARC)
    items.append(item)

    bot_left = x0, y1
    blr = x0 + r, y1
    blu = x0, y1 - r
    item = canvas.create_line(*brl, *blr, fill='black')
    items.append(item)
    item = canvas.create_arc(x0, y1-2*r, x0+2*r, y1, start=180, extent=90, fill='', outline='black', style=tk.ARC)
    items.append(item)
    item = canvas.create_line(*blu, *tld, fill='black')
    items.append(item)
    
    items = tuple(items)
    print(items)
    
    for item_ in items:
        for _item in items:
            canvas.addtag_withtag(item_, _item)

    return items


if __name__ == '__main__':

    root = tk.Tk()
    canvas = tk.Canvas(root, width=500, height=500)
    canvas.pack(expand=True, fill=tk.BOTH)

    TL = 100, 100
    BR = 400, 200
    make_round_corners_rect(canvas, *TL, *BR)
    
    TL = 100, 300
    BR = 400, 400
    make_round_corners_rect(canvas, *TL, *BR, ratio = .3)

    TL = 300, 50
    BR = 350, 450
    that_rect = make_round_corners_rect(canvas, *TL, *BR, ratio=.4)
    for fragment in that_rect:
        canvas.itemconfig(fragment, width=4)
        try:
            canvas.itemconfig(fragment, outline='blue')
        except TclError:
            canvas.itemconfig(fragment, fill='blue')

    
    TL = 150, 50
    BR = 200, 450
    make_round_corners_rect(canvas, *TL, *BR, ratio=.07)
    
    TL = 30, 30
    BR = 470, 470
    that_rect = make_round_corners_rect(canvas, *TL, *BR, ratio=.3)
    for fragment in that_rect:
        canvas.itemconfig(fragment, dash=(3, 3))
   
    TL = 20, 20
    BR = 480, 480
    make_round_corners_rect(canvas, *TL, *BR, ratio=.1)
    

    root.mainloop()

The next step, (left to the reader as an exercise), is to encapsulate the round rectangles in a class.


Edit: how to fill a rounded corners rectangle:

It is a bit involved, and in the long run, probably requires an approach where all points are explicitly defined, and the shape is formed as a polygon, instead of the aggregation of tkinter primitives. In this edit, the rounded corners rectangle is filled with two overlapping rectangles, and four disks; it allows to create a filled/unfilled shape, but not to change that property after creation - although it would not require too much work to be able to do this too. (collecting the canvas ids, and turning them on/off on demand, in conjunction with the outline property); however, as mentioned earlier, this would make more sense to encapsulate all this behavior in a class that mimicks the behavior of tk.canvas.items.

def make_round_corners_rect(canvas, x0, y0, x1, y1, ratio=0.2, npts=12, filled=False, fillcolor=''):
    ...
    if filled:
        canvas.create_rectangle(x0+r, y0, x1-r, y1, fill=fillcolor, outline='')
        canvas.create_rectangle(x0, y0+r, x1, y1-r, fill=fillcolor, outline='')
        canvas.create_oval(x0, y0, x0+2*r, y0+2*r, fill=fillcolor, outline='')
        canvas.create_oval(x1-2*r, y0, x1, y0+2*r, fill=fillcolor, outline='')
        canvas.create_oval(x1-2*r, y1-2*r, x1, y1, fill=fillcolor, outline='')
        canvas.create_oval(x0, y1-2*r, x0+2*r, y1, fill=fillcolor, outline='')
    ...

if __name__ == '__main__':
    ...
    TL = 100, 300
    BR = 400, 400
    make_round_corners_rect(canvas, *TL, *BR, ratio=.3, filled=True, fillcolor='cyan')
    ...

enter image description here


Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80
  • Thank you! Now I'll probably be able to generalize this for any polygon. And about encapsulating this in a class - this is what I've been working on. Tkinter I wld say has a steep learning curve, so, for the past 2 years (on & off) I've tried creating a library built on top of Tk to make everything easier. https://github.com/BhavyeMathur/goopy if you want to take a look at it) :) – Bhavye Mathur Jul 03 '20 at 15:24
  • You are welcome, Glad I could help. I had a look at your lib, it is an ambitious project; you indeed put a lot of work into it. Maybe you would gain by setting priorities and tightening the focus of where you want to take it? – Reblochon Masque Jul 03 '20 at 15:47
  • Yes indeed. I've been trying to get together a To-Do list and my main priority right now is to test what I've already done. While developing it for use in my own projects, I never bothered to test parts of it I wasn't using so there are a lot of bugs. For the future though, I just want it to be something that people could use to make non-graphic heavy 2D games and applications. – Bhavye Mathur Jul 03 '20 at 15:58
  • Related to this question, how would I fill the rounded rectangle with a specific colour? – Bhavye Mathur Jul 03 '20 at 17:50
  • It is a bit involved, and in the long run, probably requires an approach where all points are explicitly defined, and the shape is formed as a polygon. I posted an edit where the rectangle is filled with two overlapping rectangles, and four disks; it allows to createa filled/unfilled shape, but not to change that property after creation - although it would not require too much work to be able to do this too. – Reblochon Masque Jul 04 '20 at 01:19