Title: Draw rounded rectangles in tkinter with Python
My previous post, Find corner arcs for two points in Python, explained how to find corner arcs connecting two points. This post uses that technique to make rounded rectangles. This isn't too hard; it's mostly just a lot of simple but tedious work.
The following section describes the find_rounded_rect function, which finds the corner arcs for a rounded rectangle. The section after that describes the draw_rounded_rect method that draws the rounded rectangle.
find_rounded_rect
The following method finds the corner arcs needed to draw a rounded rectangle.
def find_rounded_rect(xmin, ymin, xmax, ymax, x_radius, y_radius):
'''Return arc and line definitions to draw a rounded rectangle.'''
# Make lists to hold results.
arc_rects = []
start_angles = []
# UL corner.
ul_x1 = xmin
ul_y1 = ymin + y_radius
ul_x2 = xmin + x_radius
ul_y2 = ymin
rect1, start_angle11, sweep11, rect2, start_angle12, sweep12 = \
find_corner_arcs((ul_x1, ul_y1), (ul_x2, ul_y2))
arc_rects.append(rect2)
start_angles.append(start_angle12)
# UR corner.
ur_x1 = xmax - x_radius
ur_y1 = ymin
ur_x2 = xmax
ur_y2 = ymin + y_radius
rect1, start_angle1, sweep1, rect2, start_angle2, sweep2 = \
find_corner_arcs((ur_x1, ur_y1), (ur_x2, ur_y2))
arc_rects.append(rect1)
start_angles.append(start_angle1)
# LL corner.
ll_x1 = xmin + x_radius
ll_y1 = ymax
ll_x2 = xmin
ll_y2 = ymax - y_radius
rect1, start_angle1, sweep1, rect2, start_angle2, sweep2 = \
find_corner_arcs((ll_x1, ll_y1), (ll_x2, ll_y2))
arc_rects.append(rect2)
start_angles.append(start_angle2)
# lr corner.
lr_x1 = xmax
lr_y1 = ymax - y_radius
lr_x2 = xmax - x_radius
lr_y2 = ymax
rect1, start_angle1, sweep1, rect2, start_angle2, sweep2 = \
find_corner_arcs((lr_x1, lr_y1), (lr_x2, lr_y2))
arc_rects.append(rect1)
start_angles.append(start_angle1)
# Define the segments.
segments = [
((ul_x2, ul_y2), (ur_x1, ur_y1)),
((ur_x2, ur_y2), (lr_x1, lr_y1)),
((lr_x2, lr_y2), (ll_x1, ll_y1)),
((ll_x2, ll_y2), (ul_x1, ul_y1)),
]
# Define the interior rectangle.
interior_rect = (ul_x2, ul_y1, lr_x2, lr_y1)
# Define the side rectangles.
side_rectangles = [
(ul_x2, ul_y2, ur_x1, ur_y2),
(ur_x1, ur_y2, lr_x1, lr_y1),
(ll_x1, ll_y2, lr_x2, lr_y2),
(ul_x1, ul_y1, ll_x1, ll_y2),
]
return arc_rects, start_angles, segments, interior_rect, side_rectangles
The function first makes lists to hold the resulting arcs and their start angles. (They all have 90° sweep angles.)
For each corner, it calculates the locations of the two points that define the corner arc as shown in the picture on the right. It then calls the find_corner_arcs function described in the earlier post to find the 90° arcs that connect the two points. That function returns two arcs: one above the line connecting the two points and one below that line. The code appends the appropriate arc data to the arc_rects and start_angles lists. (Figuring out which of the arcs is the one we want isn't magic. I just tried them until I found the right ones.)
The code repeats this process four times to find the rectangle's four corner arcs. It then creates a segments list that defines the line segments that connect the four corner arcs.
The function then defines the rectangle inside all of the corner arcs, and the rectangles that lie along the rounded rectangle's sides.
Finally, the function returns the arc rectangles, start angles, side segments, interior rectangle, and side rectangles.
draw_rounded_rect
The following draw_rounded_rect function draws a rounded rectangle.
def draw_rounded_rect(canvas, point1, point2, x_radius, y_radius,
outline='', fill='', thickness=1):
'''Draw a rounded rectangle.'''
# Find the rounded rectangle parameters.
xmin = min(point1[0], point2[0])
ymin = min(point1[1], point2[1])
xmax = max(point1[0], point2[0])
ymax = max(point1[1], point2[1])
# Find the corner arcs and side segments.
arc_rects, start_angles, segments, interior_rect, side_rectangles = \
find_rounded_rect(xmin, ymin, xmax, ymax, x_radius, y_radius)
# Fill.
if fill:
for i in range(4):
canvas.create_arc(arc_rects[i], start=start_angles[i], extent=90,
style=tk.PIESLICE, fill=fill, outline='')
canvas.create_rectangle(interior_rect, fill=fill, outline='')
for rect in side_rectangles:
canvas.create_rectangle(rect, fill=fill, outline='')
# Outline.
if outline:
for i in range(4):
canvas.create_arc(arc_rects[i], start=start_angles[i], extent=90,
style=tk.ARC, outline=outline, width=thickness)
canvas.create_line(segments[i][0], segments[i][1],
fill=outline, width=thickness)
This function first calls the previous find_rounded_rect function to get the parameters it needs to draw the rounded rectangle.
Next, if the fill parameter is not an empty string, the function fills the rounded rectangle. It loops through the arcs and fills them. It fills the interior rectangle and then loops through the side rectangles to fill them. (For an interesting experiment, try filling the various parts of the rounded rectangle with different colors. For example, you can fill the arcs and the interior rectangle with one color and the side rectangles with another color to make a checkerboard pattern.)
After it has filled the rounded rectangle, the code checks whether the outline parameter is a blank string and, if it is not, the function outlines the rounded rectangle. To do that, it loops through the arcs and line segments and draws them with the desired color and line width.
Conclusion
The result isn't perfect but it's pretty good. The fit between the arcs and segments sometimes isn't quite as smooth as I would like. It doesn't allow you to use different radii for different corners and it can't handle weird special cases like when the radii are larger than half of the rectangle's width/height. (Try it and see what happens.)
Overall, however, it does a reasonable job. Download the example to experiment with it and to see additional details.
|