Title: Let the user draw polygons in Pygame and Python
This example shows how to make a Pygame application that lets the user draw polygons. The user presses the left mouse button to start drawing. Each click of the left mouse button adds a new point to the polygon. As the user is drawing, the program displays the points drawn so far with a polyline. When the user clicks the right mouse button, the program closes the polygon and finalizes it so the user can start another new polygon.
There are two main pieces to this program: the PolygonSprite class that represents a polygon and the main program's event_loop method.
PolygonSprite
The following code shows the PolygonSprite class that represents a polygon.
class PolygonSprite:
def __init__(self, fg_color, bg_color, is_closed, points=[]):
self.fg_color = fg_color
self.bg_color = bg_color
self.is_closed = is_closed
self.points = points
def update(self, elapsed_seconds):
'''Update the object's position, speed, rotation, or whatever.'''
# This isn't needed for polygons that don't move.
pass
def draw(self, surface):
'''Draw the polygon/polyline on the drawing surface.'''
if self.is_closed and len(self.points) >= 3:
# It's closed. Draw a polygon.
gfxdraw.filled_polygon(surface, self.points, self.bg_color)
gfxdraw.aapolygon(surface, self.points, self.fg_color)
else:
# It's open. Draw a polygon.
pygame.draw.lines(surface, self.fg_color, closed=False,
points=self.points)
The class's constructor saves the polygon's foreground and background colors, the is_closed flag (which determines whether the shape should be drawn as a closed polygon or an open polyline), and a points list.
I've included an update method because many sprite classes need one to manage objects that move or change over time. The polygons drawn in this program don't change, so this method doesn't do anything.
The draw method draws the shape. If the is_closed flag is True, the method fills the polygon and then outlines it. If is_closed is False, the code uses pygame.draw.lines to connect the object's points without closing them to form a polygon.
That's all there is to the PolygonSprite class; it's pretty straightforward.
DrawPolygonsApp
The following code shows the beginning of the main app class.
class DrawPolygonsApp:
def __init__(self, screen_size=(400,300),
title='pygame_draw_polygons',
max_fps=30, bg_color=pygame.Color('lightblue')):
# Save parameters.
self.max_fps = max_fps
self.bg_color = bg_color
# Initialize pygame.
pygame.init()
self.surface = pygame.display.set_mode(screen_size)
pygame.display.set_caption(f'{title}')
# Make the sprite list.
self.all_sprites = []
# Initially we are not drawing a polygon.
self.new_polygon = None
# Start the event loop.
self.event_loop()
# Double-tap to end the program.
pygame.display.quit()
pygame.quit()
This code sets up some parameters including the maximum number of frames per second max_fps and the window's background color. The objects in this example don't move, so you could use fewer frames per second. The only thing that does move is the point that the use is currently drawing and that doesn't need to move quite so quickly. Setting this to 10 frames per second gives a noticeable delay, but 20 frames per second works fairly well.
After it defines those parameters, the program initializes Pygame and creates a window.
Next, the code creates an empty sprites list to hold the objects that should be drawn. It sets self.new_polygon to None because we are initially not drawing a new polygon.
The code then enters the event loop where all the fun happens. After that method returns, the code closes Pygame.
Event Loop
The following code shows the event loop that does all of the most interesting work.
def event_loop(self):
'''Process events.'''
# Set drawing parameters.
# (You could change these and/or allow the user to change them.)
new_fg_color = pygame.Color('red')
new_bg_color = pygame.Color('orange')
fg_color = pygame.Color('dark green')
bg_color = pygame.Color('light green')
# Create a clock to time frames.
clock = pygame.time.Clock()
# Run indefinitely.
running = True
while running:
##################
# Process events #
##################
for event in pygame.event.get():
if event.type == pygame.locals.QUIT:
running = False
break
elif event.type == pygame.MOUSEMOTION:
if self.new_polygon is not None:
# Add the current location to the new polygon.
self.new_polygon.points.pop()
self.new_polygon.points.append(event.pos)
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == pygame.locals.BUTTON_LEFT:
if self.new_polygon is None:
# Start a new polygon. Add the current point
# twice for start and end points.
self.new_polygon = PolygonSprite(new_fg_color,
new_bg_color, False, [event.pos, event.pos])
self.all_sprites.append(self.new_polygon)
else:
# Add the current location to the new polygon.
self.new_polygon.points.append(event.pos)
else:
# Finish the new polygon.
if self.new_polygon is not None:
# Remove the last point.
self.new_polygon.points.pop()
# If we have at least 3 points, save it.
if len(self.new_polygon.points) >= 3:
# Close and save this polygon.
self.new_polygon.is_closed = True
self.new_polygon.fg_color = fg_color
self.new_polygon.bg_color = bg_color
self.all_sprites.append(self.new_polygon)
else:
# The polygon has fewer than three points.
# Remove it from all_sprites.
self.all_sprites.remove(self.new_polygon)
self.new_polygon = None
##################
# Update sprites #
##################
# See how much time has passed since the last update.
elapsed_ticks = clock.tick(self.max_fps)
elapsed_seconds = elapsed_ticks / 1000
# Update the sprites.
self.update_sprites(elapsed_seconds)
# Draw the sprites.
self.draw_sprites()
# Update the display.
pygame.display.update()
This method first sets the colors used to draw new polygons and finished polygons. As the comment indicates, you could change the colors or let the user select them somehow.
The code then creates a clock so it can keep track of the elapsed time between frames. The objects in this example don't move so they don't really need this, but you'll need it for programs where objects move so I've included it.
The program then enters an event loop that runs as long as the program is running. Inside the loop, the code enters another loop that examines any pending events like mouse moves and clicks.
If the program sees the QUIT event, it sets running = False to end the event loop.
If the program sees a MOUSEMOTION event, the user is moving the mouse. If we are currently drawing a new polygon, the code replaces the polygon's last point with the current mouse position. (While we are drawing a new polygon, its last point is the mouse's current position.)
If the program sees the MOUSEBUTTONDOWN event, the user has clicked a mouse button. If it's the left button, the program either starts a new polygon (if we are not currently drawing one) or adds the current mouse position to the new polygon (if we are currently drawing one).
If the user clicks the right mouse button and we are drawing a polygon, the program finalizes that polygon. To do that, the code removes the polygon's final point, which is the mouse's current position. Then, if the user defined at least three points, the code sets the polygon's is_closed flag to True and gives it the final foreground and background colors. It adds the new polygon to the all_sprites list and sets self.new_polygon = None so we know we are no longer drawing a new polygon.
If the new polygon has fewer than three points, it doesn't really define a polygon so the code removes it from the all_sprites list and we never speak of it again.
After it finishes processing system events (quit and mouse events), the event loop performs two more tasks: updating the sprites and making the sprites draw themselves. To do that, it calls the update_sprites and draw_sprites methods described next.
update_sprites
The following update_sprites method simply loops through all of the program's sprites and calls their update methods. (As I mentioned earlier, the update method doesn't do anything in this program.)
def update_sprites(self, elapsed_seconds):
'''Update the sprites.'''
# Update the sprites.
for sprite in self.all_sprites:
# Update this sprite.
sprite.update(elapsed_seconds)
draw_sprites
The following draw_sprites method clears the window with its background color and then loops through all of the program's sprites and calls their draw methods so they draw themselves.
def draw_sprites(self):
'''Draw the sprites.'''
# Clear the window.
self.surface.fill(self.bg_color)
# Draw the sprites.
for sprite in self.all_sprites:
sprite.draw(self.surface)
Conclusion
This isn't a very complicated example but it does show how to handle mouse move and mouse button press events to let the user draw polygons. With some extra work, you could save and reload polygons, let the user draw other shapes like ellipses and rectangles, delete objects, and set object properties like colors and line thickness. I'll leave that to you.
Download the example to experiment with it and to see additional details.
|