We first discussed buttons and their implementation in Chapter 9. Recall that they work by detecting whether the mouse cursor lies within a specific rectangular region when a mouse button is clicked, and if so, then a specific action will be instigated.
Let’s think of a button as a graphical interface object, which is best implemented as a class. Then we can create many instances of buttons anywhere we like while a degree of consistency in how they are handled. What we can do to a generic button is to
Draw it at a specific location.
Label it so the user knows what it will do.
Determine whether the mouse coordinates are within the button.
We therefore need to provide a button with a position, a pair of (x,y) coordinates at which it will be drawn, a size, which can be given as a width and height, a text label that can be empty, a color, and an image that can be drawn within the window (an icon). In Python, it might be valuable to specify a surface on which the button is drawn, which is usually (but not always) the monitor’s screen.
Operations on a button will be implemented as methods. What is needed? We need to be able to set and get the label, color, and image. We need to know if the button is armed (the cursor is within the button area). We need to draw the button.
The following code is one implementation of a button.
The two most important functions need some annotation. First is armed, which determines whether the button can be turned on with a mouse click (is the cursor in the button): def armed(self):
Finally, each button has a draw method that renders it to the screen. Here’s what they look like:
The armed button is outlined in green. All buttons have a double outline and possibly an image indicating their function.
if self.armed(): # An ARMED button is drawn in green
pygame.draw.rect(self.scr, (120, 220, 100),
pygame.Rect(self.x, self.y, self.width, self.height))
else: # Unarmed button is drawn in black.
pygame.draw.rect(self.scr, (0, 0, 0),
pygame.Rect(self.x, self.y, self.width, self.height), 1)
# The second, inner, rectangle in the outline.
pygame.draw.rect(self.scr, (0, 0, 0),
self.width-6, self.height-6), 1)
# Grey fill
pygame.Rect(self.x+4, self.y+4, self.width-8, self.height-8))
# If there is an image specified then render it in the button.
if self.image is not None:
self.scr.blit(self.image, (self.x+4, self.y+4))
The entire Button class is only about 40 lines of code. Each one is given a destination Surface, a position, and a size then it is first created. Images and colors can be modified after instantiation. The setup for the circle but, as one example, is as follows:
# Create a surface for the button image
imageCircle = pygame.Surface((42, 42))
# Make the background of the button grey.
imageCircle.fill((200, 200, 200))
# Draw a circle
pygame.draw.circle(imageCircle, BLACK, (20, 20), 10, 2)
# Instantiate the button.
circleButton = Button(screen, 80, 750, 50, 50)
# Place the image into the button circleButton.set image(imageCircle)
Each button has to be drawn every time the screen is refreshed.
And, finally, each button needs to be tested every time a mouse button is depressed to see if it has been invoked. We have written a function that checks all of the buttons:
and to check the circleButton:
mode = CIRCLE
When circleButton is pressed, it sets a global mode variable to CIRCLE, and that indicates what is to be drawn from now on.
Whenever the mouse is positioned within the drawing area, drawing can be activated by pressing the mouse button. Whatever drawing state the program is in – line, circle, rectangle, text, or point – dictates what can be drawn. If the program is in LINE mode and the mouse is depressed, the mouse can be moved to a new position. Wherever the mouse button is released is the end point of the line. As the mouse is moved, a line is drawn temporarily on the screen to show what the like would look like. When the button is released, the line is drawn.
If the line is in circle mode, then when the mouse is depressed it defines the center of a circle. As the mouse is move, the radius of the circle changes and the circle is drawn temporarily on the screen and changes radius with the mouse position. When the mouse button is released, the circle becomes permanent.
The main loop of the Pygame program tests for events like the mouse button and key presses and releases. In this program, the mouse press usually establishes a starting point for an object, like a line or a circle. The user drags the mouse to another location and releases it, indicating a terminal position on the screen for the object being drawn. Doing the drawing is not difficult given the two mouse positions specified by the user. In the main loop, one must recall the point at which the mouse button was pressed and then when it is released the item can be drawn.
This is the obvious way to structure the program, but if it were done, it would be hard to remove items that have been drawn. Each line or rectangle is a different object on the screen. If the last item draw is to be erased, all of the other objects should be drawn except the last one. We have to remember all of the objects and their parameters. Moreover, if you think about it, they have to be redrawn whenever the screen is updated. Instead of drawing each item as the user defines them, we should save all of the user’s drawing instructions someplace and then use them to draw the entire image each screen update. This is more complicated at the outset in the design phase, but later simplifies implementation.
Let’s look at the main drawing operations and construct a data structure that can store them.
For a rectangle, the mouse button is depressed at the upper left (or perhaps lower right) corner of the polygon. When the mouse is released, the cursor position defines the opposite corner. Other factors that influence the drawing are the current color and the line thickness. All of these items must be saved. A possible structure is as follows:
[102, [x0,y0], [x1,y1], [r,g,b], t]]
This is a list, and the first element, in this case 102, is a code that indicates what is being drawn. 102 is the code for a rectangle. This is followed by the start point, the end point the color, and the line thickness. This list, which we’ll call a drawing directive or DD, has the following structure:
[Integer List List List Integer] = [code, start point, size, color, line thickness]
The drawing directive for a line is much the same, but the third component is not a size but an endpoint for the line. The user presses the mouse button to define the start point and releases it to define the end point. These two points define the line.
[100 [x0,y0] [x1,y1] [r,g,b] [t]]
The DD for a circle has the same structure. The point where the mouse button is pressed defines the center of the circle. When the button is released, that point is used to calculate the radius, because that’s how Pygame specifies a circle. The radius is the distance between the points where the button was pressed and where it was released. That point is stored as the second coordinate and will be used to calculate the radius, which is a floating point number. So for a circle, we have
[101, [x0,y0], [x1,y1] ,[r,g,b], t]
For text, a mouse click (a press and a release) defines the starting point for the text. The user then types text from the keyboard, which is saved as a string to be drawn at that point. This string can be stored in the DD:
[103, [x0,y0], “String”, [r,g,b], t]
Erasing is like defining a rectangle. Mouse button down to begin defining a rectangular area to be erased, button up to finish the definition. Everything in that region will be set to the background color.
[104, [x0,y0], [x1,y1], [r,g,b], t]
This defines what can draw at this stage of the design. When the user draws something, the program creates the drawing direction for that and places it at the end of a list, which is named backstack. The entire drawing can be created by starting at element 0 of backstack and drawing each of the items through to the end of the list. This is actually done by the program. Each time though the main Pygame look the screen is cleared and all of the items are redrawn.
The backstack is a list of instances of a class named Mode, which implements the drawing directive. Assuming that backstack contains all of the items drawn, in proper order, then redrawing them can be done as follows:
for i in range(0, backstack.N): # For each DD
op = backstack.get(i) # Get the item
if k == LINE: # If the code is dfor drawing a
# line, do it
pygame.draw.line(screen, c, (a, a), (b,b), t)
elif k == CIRCLE: # if the code is for a circle,
# draw it.
r = round(distance(a, b)) # a is center,
# b is release point
if r > t: # Radius is distance
# between the two
pygame.draw.circle(screen, c, (a, a),r, t)
elif k == RECTANGLE: # If the code is for a
# rectangle, draw it
pygame.draw.rect(screen, c, (a, a, b-a, b-a), t)
elif k == TEXT: # If code is for TEXT
elif k == ERASE: # If code is for erasing, draw a
# filled box
pygame.draw.rect(screen, (255, 255, 255),
(a, a, b-a, b-a), 0)
We’ve left the drawing of text until last. Drawing text requires a font and a known size. In Pygame, text uses a font that has been initialized by the programmer. We could use the text() function invented in Chapter 7. A different font variable would be used for each font in cases where many sizes are needed. We could, for example, use the following:
times20 = pygame.font.SysFont(‘Times Roman’, 20)
times30 = pygame.font.SysFont(‘Times Roman’, 30)
Source: Parker James R. (2021), Python: An Introduction to Programming, Mercury Learning and Information; Second edition.