PyGame Buttons

Started by
6 comments, last by Alberth 6 years, 11 months ago

Is there an elegant way to create buttons in PyGame? I want to create the main menu for a game I'm making, but I can't figure out a way to check if the mouse pointer is inside the Rect. Is there some elegant way to do this? A code sample is below

import pygame as pg
import os
from pygame.locals import *

def dispMainMenu(SURF):
"""
displays main menu background and buttons
"""
#loads main menu background image
MainMenu_BG = pg.image.load("images\mMenu\MainMenu_BG.png")

#displays main menu background
SURF.blit(MainMenu_BG, (0, 0))

#assigns main menu buttons
mmContinueButt = pg.image.load(os.path.join("images\mMenu\continue.png"))
mmStartButt = pg.image.load(os.path.join("images\mMenu\start.png"))
mmLoadButt = pg.image.load(os.path.join("images\mMenu\load.png"))
mmOptionsButt = pg.image.load(os.path.join("images\mMenu\options.png"))
mmLeaveButt = pg.image.load(os.path.join("images\mMenu\leave.png"))

#assigns mouse position to rat
rat = pg.mouse.get_pos()


#blits main menu buttons to screen
#assigns them as rects
SURF.blit(mmContinueButt,((190, 260), (281, 86)))
SURF.blit(mmStartButt,((190, 380), (281, 86)))
SURF.blit(mmLoadButt,((190, 500), (281, 86)))
SURF.blit(mmOptionsButt,((190, 620), (281, 86)))
SURF.blit(mmLeaveButt,((190, 740), (281, 86)))

Advertisement

Sorry, I don't have time for a precise example, below is some copy/paste from an application I wrote, as inspiration


# Wrap all button data in a class.
class Button(object):
    def __init__(self, x, y, w, h, text, colour=None):
        if colour is None:
            colour = NORMAL

        self.normal_colour = colour
        self.x = x
        self.y = y
        self.w = w
        self.h = h
        self.font = pygame.font.Font(None, 20)
        self.text = text
        self.shiny = False

    def draw(self, screen):
        if self.shiny:
            bg = SHINY
        else:
            bg = self.normal_colour

        surf = self.font.render(self.text, True, BLACK, bg)
        rect = (self.x, self.y, self.w, self.h)
        xo = self.x + (self.w - surf.get_width()) // 2
        yo = self.y + (self.h - surf.get_height()) // 2
        screen.fill(bg, rect)
        screen.blit(surf, (xo, yo))

    def on_button(self, pos):
        return self.x <= pos[0] and self.x + self.w > pos[0] and \
               self.y <= pos[1] and self.y + self.h > pos[1]

# Make list of buttons in the program
buttons.append(Button(x, y, w, sh, '7d', colour=NORMAL_LARGE))

# Mouse click handler
def update_click(pos):
    """
    Mouse got clicked
    Returns whether a change occurred.
    """
    global disable_select

    selected_found = None
    selected_set = None
    for elm in buttons:
        if elm.on_button(pos):
            # do magic, like toggling elm.shiny to highlight the button etc

Obviously, you can do the same trick but with buttons with images.

Hope this helps!

By the way, the \ is an escape character in strings, so your Windows paths to the images won't work if you pick an unlucky name. Use raw strings like r"path\to\image.png", use double backslashes like "path\\to\\image.png", or use slashes like "path/to/image.png".

Thanks for this. I realized I couldn't do much with pygame until I understood classes. I understand them now (barely) but some things about your post still confuse me.

What does self.shiny represent?

It seems like you're creating new x and y values for blitting the image to the window. Why is that?

code example:

xo = self.x + (self.w - surf.get_width()) // 2
yo = self.y + (self.h - surf.get_height()) // 2

screen.blit(surf, (xo, yo))

Do you declare button with the object parameter because this was originally a child class?

Lastly, is it possible to make the update_click function a method of the button instead? I want to call this class in multiple py files.

I realized I couldn't do much with pygame until I understood classes.
You can work without classes too, by storing all data in tuples or dictionaries, but classes have a lot of nice features (and are much less cumbersome to write and modify), so they're useful to know in general.

Nobody said you must use all their features, it's fine to use a small subset only at first, and slowly expand your knowledge.

What does self.shiny represent?
Good point, it's a change in background colour (bg==background). It's a flag that is set when the button is visited by the mouse pointer iirc, and it gives the button a whiter background to give visual feedback to the user that the mouse is over it. I tried it first without this feedback, but the interface felt a bit dead. In hindsight, it should probably not be a flag of the button itself, I might change that at some point.

It seems like you're creating new x and y values for blitting the image to the window. Why is that?
The image is the text of the button, which I want centered on the button. I don't know the size of the image until I create it with the text-draw method "font.render". That code computes the top-left corner of the text-image at the screen. I agree it's the same image and therefore the same size every single time, so you could compute it once, and store the result, but my application renders the buttons around once a minute (a bit more often when moving the mouse over it), so execution time is a non-issue in my application.

Do you declare button with the object parameter because this was originally a child class?
It's Python2, which requires deriving from object since around 2.3 or so, to get a sanely behaving class. In Python3, they fixed that, and you can just write "class Button:"

Lastly, is it possible to make the update_click function a method of the button instead? I want to call this class in multiple py files.
Not really, as it has a different function.

The "Button" class represents a single button that you can select (by clicking). The "update_click" function gets called when you press the LMB. The "pos" gives the position of the mouse at the moment you clicked. The function then walks over all buttons "for elm in buttons:", and asks each button "is this position you?" with 'if elm.on_button(pos):'. If the button answers yes (True), the function can eg set the 'shiny' flag for the button that said 'yes', and clear 'shiny' at all other buttons, and then redraw the entire display (thus letting the user know the button was selected).

Likely, you want something else than having shiny buttons, that requires programming some other code below the "if".

The update_click function thus handles all buttons at the screen, rather than a single button like the Button class does.

For this reason, the update_click doesn't belong in the Button class.

Calling a function or using a class definition from several files can be done with an import statement. You import the file containing the function or class definition, and then you can access it.

Okay, I'm back. I couldn't find a tutorial on blitting an image as a button so this is what I came up with. Unfortunately, it doesn't work. At least, not the way I planned. It keeps raising an error when I try to use my program.

The error is:

Traceback (most recent call last):
File "N:/Aztlan/aztlan.py", line 8, in <module>
from button import Button as b
File "N:\Aztlan\button.py", line 39
self._index = 0
^
SyntaxError: invalid syntax

I just want to iterate through a list of Button class objects but it's giving me a hard time.

my Button class (in button.py)


import pygame as pg

class Button:
    """
    Creates a button
    """
    _buttList = []
    def __init__(self, x, y, w, h, imgAddr, name):
        """
        x and y are ints and
        indicate where the top right corner of the button will be
        
        w is an int that
        represents how wide the button will be
        
        h is an int that
        represents the height of the button
        
        imgAddr is a string that holds the image address on it
        it is used to create a pygame image object
        
        name is a string indicating button function
        """
        # inits self._image to pygame image object
        self._image     = pg.image.load(imgAddr)

        #pygame x y coordinates
        self._x         = x
        self._y         = y

        #width and height of button
        self._w         = w
        self._h         = h

        self._buttList = []
        self._buttList.append((self._x, self._y, self._w, self._h)
        self._index = 0

    def draw(self, surf):
        """
        draws a button the window
        :param surf: 
        surf is the pygame surface window the button is drawn to
        """
        # draws button to screen
        surf.blit(self._image, ((self._x, self._y), (self._w, self._h)))

    def overButton(self, pos):
        """
        checks where the mouse is
        :param pos: 
        pygame mouse object that indicates where the mouse is
        """
        return self._x <= pos[0] and self._x + self._w > pos[0] and \
        self._y <= pos[1] and self._y + self._h > pos[1]

    def __iter__(self):
        return self

    def __next__(self):
        try:
            result = self._buttList[self._index]
        except IndexError:
            raise StopIteration
        self._index += 1
        return result

    def __getitem__(self, index):
        """
        
        :param index: 
        int that finds proper index
        """
        return Button._buttList[index]

my MainMenu class (in screens.py)


"""
Contains classes to handle the various screens of the game
"""
__author__ = "RidiculousName"
__date__ = "4/22/17"
#imports Button class
from button import Button as b
import pygame as pg
class MainMenu:
    """
    Class that handles the main menu screen
    """
    def __init__(self, surf):
        """
        surf is a pygame.surface object that
        the buttons, images, etc. are blitted to
        
        isClick is a pygame mouse object that
        determines whether the mouse has been clicked
        """
        # where main menu images are located
        self._i = "images/mainMenu/"
        self._surf = surf
        self._surf.fill((0,0,0))    #fill surface with white

        #list of main menu buttons
        # x value, y value, width, height, image path, surface, button type
        self._mmList = [
        b(150, 300, 200, 80, self._i + "mmResume.png",
                         "Resume"),
        b(150, 400, 200, 80, self._i + "mmStart.png",
                        "Start"),
        b(150, 500, 200, 80, self._i + "mmLoad.png",
                       "Load"),
        b(150, 600, 200, 80, self._i + "mmCreate.png",
                         "Create"),
        b(150, 700, 200, 80, self._i + "mmExit.png",
                       "Exit"),
        b(600, 100, 400, 160, self._i + "mmTitle.png",
                        "Title")
        ]

        # blits buttons to screen
        for ei in self._mmList:
            b.draw(ei,surf)


    def butts(self, isClick):
        if isClick[0] == True:
            rat = pg.mouse.get_pos()
            for ei in self._mmList:
                for item in ei:
                    # if button-X + button Width > mouse-X > button-X
                    # if button-Y + button Width > mouse-Y > button-Y
                    print(ei[1])
                    print(type(ei[1]))
                    print(self._mmList)
                    if ei[0] + ei[2] > rat[0] > ei[0] \
                    and ei[1] + ei[3] > rat[1] > ei[1]:
                        if ei[-1] == "Exit":
                            return pg.QUIT

my main (put it all together) file: (in aztlan.py)


"""
Combines classes to run the game
"""
__author__ = "RidiculousName"
__Date__ = "4/22/17"

import pygame as pg
from button import Button as b
from screens import *



def main():
    """ set up the game and run the main game loop """
    pg.init()  # prepare pygame module for use
    surfW = 1600  # window width
    surfH = 900  # window height

    # create window
    surf = pg.display.set_mode((surfW, surfH))

    #set window caption
    pg.display.set_caption("Aztlan")

    while True:
        #opens first screen
        mm = MainMenu(surf)

        mm.butts(pg.mouse.get_pressed())

        ev = pg.event.poll()  # look for any event

        if ev.type == pg.QUIT:  # window close button clicked?
            break



        pg.display.flip()

    pg.quit()


if __name__ == "__main__":
    main()

File "N:\Aztlan\button.py", line 39
    self._index = 0
       ^
SyntaxError: invalid syntax

With syntax errors near the start of the line it usually pays to check the previous line too:

self._buttList.append((self._x, self._y, self._w, self._h)
Seems a missing closing parenthesis to me :)

self._buttList.append((self._x, self._y, self._w, self._h)
Seems a missing closing parenthesis to me :)

Urk, that was a really dumb mistake on my part.

A friend and I got the indexing to work, but there are still problems. I want to work on them a little myself before asking again, but thank you. You've really helped me here.

Thanks for the update. Trying to fix problems on your own works much better for learning, so by all means, experiment away as much as you can!

Unfortunately, it's a way of programming I can't really do anymore. I have coded so much, that with anything I write, I immediately see 3-5 things that I need to handle as well to make things work in the next step, and it's very hard to ignore them.

Good luck with fixing, if you want feedback or have a question you know where to go :)

This topic is closed to new replies.

Advertisement