Designing a Button UI Module for Pygame

Pygame is a 2D graphics and gaming library for Python. It's pretty nifty because it essentially gives you a blank window that you can draw any shapes or lines or images you want on it. But it doesn't come with any UI elements like buttons, scrollbars, or check boxes. This post will go through not only creating a button class for Pygame, but also the reasoning behind why I've set up the code as it is. This is more of a "how to create a module other people can use" tutorial than a UI or Pygame tutorial.

This tutorial assumes you know the basics of Pygame and Python programming. If you don't, it's probably easy enough to follow anyway.

A button is a common user interface (UI) control that is used in many software applications. It seems simple enough: there's a button on the window and you click on it and something happens. But there's a lot of details we should plan out ahead of time. Remember, we want to make a generic button class so that other programmers can use this in their games and programs. Once you've read through the process here, you'll be familiar with how to make your own modules for UI elements.

Designing a UI button class is good a good programming practice exercise.

Download the PygButton module from PyPI by running "pip install pygbutton". You can also look at just the pygbutton.py file itself.

Instead of text, the user will be able to specify images for the three different states. We'll call these image-based buttons.

The button's visibility can be toggled on and off.

And it's always a good idea to come up with a list of things we specifically won't implement (to avoid feature creep each time we think, "Hey, it'd be cool if we could..."). These features could always be implemented later.

Must be rectangular (i.e. can't be oval).

No transparency.

No more than the three states.

No hotkeys attached to them, or keyboard focus.

No special "double click" event (it'll just be two click events).

For now, the highlight state looks identical to the normal state for text-based buttons.

A button is either text-based or image-based, there's no hybrid.

No "disabled" state.

Only one font & color at a time for text-based buttons.

The text caption will always be centered, not left- or right-aligned.

(But you can add these features to your own code if you want.)

Design Details

Whenever you're designing something, always do a prior art search first. Looking at how buttons on a web page work is a good case to examine, for example.

The buttons have three states and can have a different appearance for each state.

The "normal" state is what the button looks like when it has not been clicked and the mouse is not over it.

The "highlight" state is what the button looks like when the mouse is hovering over it, but not clicking it. We can use this to add some kind of highlighting behavior when the mouse glides over the button. For normal text-based buttons, this state will look identical to the normal state.

The "down" state is what the button looks like when it is being clicked down.

There are also six different "button events" that the buttons can produce based on the Pygame mouse events that are passed to them:

Enter - When a MOUSEMOTION event has told the button that the mouse is over the button when previously it wasn't.

Exit - When a MOUSEMOTION event has told the button that the mouse is no longer over the button when previously it was.

Move - When the button has received a MOUSEMOTION event.

Down - When the mouse is pressed down on the button.

Up - When the mouse is released on the button.

Click - When the mouse was pressed down on the button and released over the button. (Releasing the mouse off of the button does not trigger the click event.)

(Note: The buttons won't produce Pygame USEREVENTS. I didn't see a significant need for them.)

As to how the mouse button looks, I'll be using the Windows look-and-feel of buttons. Here's what they look like zoomed in:

Notice that the 3D appearance is caused by drawing these black, gray, and white outlines. These lines don't change if the background color of the button changes.

What the API will Look Like

Before diving into coding, we need a concrete plan for how other programmers will use this module. It doesn't matter how sophisticated your library is, if it is opaque, difficult to learn, and inconsistent no one will want to learn it and it will not be used. It's important to get these details right the first time, because making changes (like changing a function name or getting rid of a class) later on could break other people's code that uses your library. This means they won't adopt newer versions and new features (since the newer version breaks their code), which further limits the popularity of your module.

The button's API will have three main parts: the constructor function that creates it, the function that draws the button to a pygame.Surface object (to display it on the screen), and a handleEvent() method that we can pass pygame.Event objects to so it knows what is happening in the program. The code will roughly look like this:

Before we start coding, we should write out the method names and parameters for the PygButton class first. This will help cement what we want to code before we start coding:

def __init__(self, rect=None, caption='', bgcolor=LIGHTGRAY, fgcolor=BLACK, font=None, normal=None, down=None, highlight=None) - The constructor. Note that pretty much everything has a default argument. If the user just wants asimple button, we shouldn't have to make her write out tons of boilerplate code. Let's just supply default values.

def handleEvent(self, eventObj) - Changes the button's state if the Pygame event passed is relevant.

def draw(self, surfaceObj) - Draws the button (in its current state) to the surfaceObj surface.

def mouseClick(self, event) - Called when the button has a click event. (These methods don't do anything in the PygButton class, but you can override this class to implement code in these methods.)

def mouseEnter(self, event) - Called when the button has a "mouse enter" event.

def mouseExit(self, event) - Called when the button has a "mouse exit" event.

def mouseMove(self, event) - Called when the button has a "mouse move" event.

def mouseDown(self, event) - Called when the button has a mouse button down event.

def mouseUp(self, event) - Called when the button has a mouse button up event.

def setSurfaces(self, normalSurface, downSurface=None, highlightSurface=None) - Let's the user specify either image filenames or pygame.Surface objects to use for each of the states. (This sets the button to be an image-based button.)

And here are some properties that we'd like to set for the PygButton class. Whenever you think you'll need a get and set method for something (i.e. getCaption() and setCaption() instead of just a caption property), this is a strong indication that a property would be better instead.

caption - The string for the text caption in the center of the button.

rect - A pygame.Rect object which gives the position and size of the button.

visible - A boolean that sets the button to visible (True) or invisible (False).

fgcolor - An RGB tuple or pygame.Color object for the text (foreground) color.

bgcolor - An RGB tuple or pygame.Color object for the background color.

font - A pygame.font.Font object for the font (and size) to use for the text caption.

Setting any of these properties (other than rect) will result in the button becoming a text-based button if it was previously an image-based button. Setting the rect property of an image-based button simply resizes the images.

Note that we don't have properties for setting the normal, down, and highlight Surfaces. This is because when we switch from a normal text-based button (which uses the caption, fgcolor, bgcolor, and font properties) to an image-based button, we want to set the images for all three Surfaces at the same time (even though we have defaults for the down and highlight surfaces.)

The Preamble Code

Here's the code that goes at the top of the pygbutton.py file. It imports Pygame and calls the init() function for the fonts and creates a few constants that we'll use in the module.

For image-based buttons, the setSurfaces() method is called, which handles the default images for the Down and Highlight state if they are unspecified. It also checks that the images are the same size. Note that the user can specify either pygame.Surface objects or string filename values.

Note that the PygButton class also stores the original images in the origSurfaceNormal, origSurfaceDown, and origSurfaceHighlight member variables. This is so that when the code does a resize, we are resizing the original images. The button could be resized multiple times, and this would result in poor quality if we tried to resize and previously resized image. (The same way a photocopy of a photocopy of a photocopy reduces the image quality.)

The draw() Method

The draw() method is straightforward since it only copies the surfaceNormal, surfaceDown, and surfaceHighlight properties to the passed pygame.Surface object. The draw() method is called whenever the button's current state needs to be drawn to a Surface object. Drawing the buttons themselves will be handled by the _update() method.

The _update() method will be called whenever the appearance of the buttons has been modified. This happens when the text, background color, size, etc. of the button has changed. This is why the name of _update() begins with an underscore; it's only called by the class's code itself. It shouldn't be called by the user.

The Event Callback Methods

There are two ways that we can execute code in response to button-related events. The first is to have a method in the PygButton class (and its subclasses) get called that contains the code we want to run.

We'll just put stub functions for these methods. Any subclasses that inherit from PygButton can override these methods and use any code they want. But for now, they do nothing:

def mouseClick(self, event):
pass # This class is meant to be overridden.
def mouseEnter(self, event):
pass # This class is meant to be overridden.
def mouseMove(self, event):
pass # This class is meant to be overridden.
def mouseExit(self, event):
pass # This class is meant to be overridden.
def mouseDown(self, event):
pass # This class is meant to be overridden.
def mouseUp(self, event):
pass # This class is meant to be overridden.

The handleEvent() Method

Whenever our program calls pygame.event.get_events() to retrieve all the events generated (for keyboard, mouse, etc. events) we should pass them to handleEvent() so the buttons can update their state. The second way to execute code in response to button events is with the return value of handleEvent().

The handleEvent() method has been set up so that it returns a list of all button events that have happened due to the normal Pygame events passed to handleEvent(). So if a mouse move Pygame event has happened over the button (when previously the mouse cursor wasn't over the button), the handleEvent() method will return the list ['enter', 'move'].

The caller of handleEvent() can perform any actions in response to these events.

PygButton Properties

Instead of having simple member variables for the caption, rect, visible, fgcolor, bgcolor, and font, we can use Python properties instead. This is better, because each time these values get updated we need to run some code that updates the Surface objects that hold the button's look. In other languages, this would require the use of bulky get and set methods. Python's property() function lets us assign methods to be called whenever the member variables need to be get or set.

Example Programs

This is a very important step. We need to accompany the module with some example programs that show how simple it is to actually use the module in a working program. I would even say that including example programs is more important than having documentation (for smaller libraries, at least.)

6 thoughts on “Designing a Button UI Module for Pygame”

There have been quite a few GUI/widget libraries for Pygame (tho I don't remember any ever reaching version 1.0). I really
like the simplicity of your approach tho -and it came at exactly the right time for me (The thought of developing your own widgets is quite demotivating so thanks a lot!)
Things like this should really come packaged with Pygame.

Hi, there. I am very new to pygame and python and I am struggling with creating a button module.

I have created a module which blits an image onto the screen. When the cursor is over the image and mouse button 1 is clicked, this image is replaced by a nother image which is the same but just smaller. This simulates a button press.

When I import the module and run it, it works fine but when I try adding another button by simply copying the script, it acts weird. Each time an event occurs, the screen toggles between blitting one of the images, thus it is only blitting one image at a time. Please help...