3
\$\begingroup\$

I've been making games using the Pygame module recently. I've noticed that Pygame has no builtin GUI elements. To save time for me (and hopefully other people), I have created some very simple GUI elements: buttons and labels.

The button module was the first to be created, and is probably the most thoroughly tested. The button module has a button class. The class has main methods to create and update the button. the __init__ method, render_button method, and the update_button method.

I've included doc strings on all methods, so I should not have to explain the code too much:

import pygame as pg
pg.init()


class Button:
    def __init__(self, surface, x, y, width, height):
        """
           Creates a Button that can be used in any pygame project.

           Arguments:
           surface -- a pygame.display.set_mode() object.
           x -- the x axis position of the button's top left corner
           y -- the y axis position of the button's top left corner
           width -- the width of the button
           height -- the height og the button

           the syntax of creating a instance of the class:
           btn_obj = Button(surface, x, y, width, height)

           To display the button and update the button, acording the the mouse
           position, two methods must be called:

           btn_obj.render_button() -- this method is used to render the button.
           Call the function after Updating the pygame screen in your
           Pygame window event loop.

           btn_obj.update_button() -- this method is used to change the buttons
           sytle and to run a specifed function, depending on the mouse position.
           Call this function in your Pygame window event loop.
        """
        # getting surface/window/display reference
        self.surface = surface

        # setting button dimensions
        self.x = x
        self.y = y
        self.width = width
        self.height = height

        # button rectangle and button outline
        self.button_obj = pg.Rect(x, y, width, height)
        self.button_inline = (self.button_obj[0] - 1, self.button_obj[1] - 1,
                              self.button_obj[2], self.button_obj[3] + 1)
        self.button_outline = (self.button_obj[0] + 1, self.button_obj[1] + 1,
                               self.button_obj[2] + 1, self.button_obj[3] + 1)

        # variables to check if the button is is non-active, active, or pressed
        self.normal = True
        self.active = False
        self.run_function = False
        self.clicked = False
        # colors for normal button and active button
        self.button_color = (210, 210, 210)
        self.active_color = (230, 230, 230)
        self.pressed_color = (200, 200, 200)

        # function for button
        self.function = None

        # setting default font parameters
        self.fs = 18
        self.fc = (0, 0, 0)
        self.font_name = pg.font.match_font('arial')
        self.caption = ""

        # setting parameters for button outline and inline color
        self.olc = (0, 0, 0)
        self.ilc = (255, 255, 255)

    def decorate(self, caption="", button_color=(210, 210, 210),
                 active_color=(230, 230, 230), pressed_color=(200, 200, 200),
                 font_size=16, font_color=(0, 0, 0),
                 font_name=pg.font.match_font('arial'),
                 function=None):
        '''
           Gives the user the abiltiy to decorate their buttons.
           Takes the keyword values given, and changes the
           default values accordingly.
           Keyword arguments:

           caption -- the text(if any) to display on the button.
           button_color -- the main color of the button.
           actvie_color -- the color of the button when being hovered over.
           pressed_color -- the color of the button when being pressed.
           font_size -- the size of the button text.
           font_color -- the color of the button text.
           font_name -- the type/name of font used for displaying button text.
           function -- the function to run when the buttons is pressed
        '''

        self.button_color = self.button_color
        self.active_color = self.active_color
        self.pressed_color = self.pressed_color

        self.function = function

        self.fs = font_size
        self.fc = font_color
        self.font_name = font_name
        self.caption = caption


    def _set_button_state_pressed(self):
        """
            Sets the Button 'click' effect when pressed,
            by offsetting the outline and inline lines of the button
        """
        self.button_outline = (self.button_obj[0] - 1, self.button_obj[1] - 1,
                               self.button_obj[2] + 2, self.button_obj[3] + 1)

        self.button_inline = (self.button_obj[0] + 1, self.button_obj[1] + 1,
                              self.button_obj[2], self.button_obj[3])

    def _set_button_state_normal(self):
        """
            Sets the Button state to normal when not being clicked
            by resetting the outline and inline lines of the button

        """
        self.button_inline = (self.button_obj[0] - 1, self.button_obj[1] - 1,
                              self.button_obj[2] + 1, self.button_obj[3] + 1)

        self.button_outline = (self.button_obj[0] - 1, self.button_obj[1] - 1,
                               self.button_obj[2] + 3, self.button_obj[3] + 3)

    def _draw_button_text(self):
        """
            Creates the Button text to draw on the button.
            Changes the button text position text, when the
            button is being clicked
        """
        if self.clicked:
            self.font = pg.font.Font(self.font_name, self.fs)
            self.font_surf = self.font.render(self.caption, True, self.fc)
            w, h = self.font.size(self.caption)
            self.font_pos = (self.x + self.width / 2 - w / 2 + 1, self.y + self.height / 2 - h / 2 + 1)  # I'm adding +1
            # to the text position when pressed to move the text with the button
            self.surface.blit(self.font_surf, self.font_pos)
        else:
            self.font = pg.font.Font(self.font_name, self.fs)
            self.font_surf = self.font.render(self.caption, True, self.fc)
            w, h = self.font.size(self.caption)
            self.font_pos = (self.x + self.width / 2 - w / 2 - 1, self.y + self.height / 2 - h / 2 - 1)
            self.surface.blit(self.font_surf, self.font_pos)

    def render_button(self):
        """
            Renders the button to the screen while checking for
            each button state flag(self.normal, self.active, self.pressed)
        """
        if self.clicked:
            self._set_button_state_pressed()
            pg.draw.rect(self.surface, self.olc, self.button_outline)
            pg.draw.rect(self.surface, self.ilc, self.button_inline)
            pg.draw.rect(self.surface, self.button_color, self.button_obj)
        else:
            self._set_button_state_normal()
            pg.draw.rect(self.surface, self.olc, self.button_outline)
            pg.draw.rect(self.surface, self.ilc, self.button_inline)
            pg.draw.rect(self.surface, self.button_color, self.button_obj)

        # change button color based on the event(active, pressed, or normal)
        if self.active and not self.clicked:
            pg.draw.rect(self.surface, self.active_color, self.button_obj)
        elif self.clicked and self.active:
            pg.draw.rect(self.surface, self.pressed_color, self.button_obj)
        elif self.normal:
            pg.draw.rect(self.surface, self.button_color, self.button_obj)
        self._draw_button_text()


    def _update(self, event):
        """
            Sets the self.active flag and the self.pressed flag
            to True at the correct times
        """
        x, y = event.pos
        mouse_buttons = pg.mouse.get_pressed()
        self.active = False
        if self.x < x < self.x + self.width:
            if self.y < y < self.y + self.height:
                self.active = True

        if mouse_buttons[0] == 1:
            self.active = False

    def _mouse_down(self):
        """
            sets the flag self.clicked to True when being pressed
        """
        if self.active:
            self.run_function = True
            self.clicked = True

    def _mouse_up(self):
        """
            checks if the user gives a function for self.function, if they do, then it will be ran
            when the button is clicked.

            self.run_function is used to track if the button was pressed, like self.clicked.
            however self.clicked is used to set the drawing position of the button and thus
            has to be set back to false on the MOUSEBUTTONUP event. so a seconds variable(self.run_function)
            is needed to know when to run the function given. When the function is run, self.run_function
            is set to False until the button is cliked agian which will set it back to True.
        """
        self.clicked = False
        if self.function is not None and self.active and self.run_function:
            self.run_function = False
            self.function()
        elif self.function is None and self.active is True and self.run_function:
            self.run_function = False

    def update_button(self, event_object):
        """
            checks for the actual pygame events, and calls the right function accordingly
        """
        if event_object.type == pg.MOUSEBUTTONDOWN and self.active is True:
            self._mouse_down()
        if event_object.type == pg.MOUSEBUTTONUP:
            self._mouse_up()
        if event_object.type == pg.MOUSEMOTION:
            self._update(event_object)

    def is_clicked(self):
        """
            Checks if the button was pressed for the user.
            and returns True if it was. Use the method in
            a if-statement, to test if the buttons is\was
            pressed:

            if btn_obj.is_clicked:
                .....
        """
        if self.clicked is True:
            return True
        else:
            return False

I the only thing I'm debating about in my code above, is whether I should have a separate method (decorate) for styling the button, or if I should let the user do that in the __init__ method.

The label module is much simpler, and has a lot less code. The label module has a class called Label. As with the button class, the label class has an __init__, render, and update methods:

import pygame as pg
pg.init()


class Label:
    def __init__(self, surface, x, y, w, h,
                 color=(230, 230, 230),
                 label_highlight=(False, (255, 255, 255)), text="",
                 text_pos=(0, 0), font_size=18,
                 font_type=pg.font.match_font('arial'),
                 font_color=(0, 0, 0)):
        '''
        A multipurpose label object for pygame games.

        Arguments:
        surface -- your pygame.display.set_mode() object.
        x -- the x axis position of the labels's top left corner
        y -- the y axis position of the labels's top left corner
        width -- the width of the button
        height -- the height of the button

        Keyword arguments:
        color -- main color of the label(default: white)
        label_highlight -- takes a boolean and a color. If you entered
        True, the label will b highlighted the color you
        specify(default: False (255, 255, 255))
        text -- text to display on label(default: "")
        text_pos -- the x and y coordinates of the text
        realative to your label(default: (0, 0))
        font_size -- size of label font(default: 18)
        font_type -- type of font to use for label font(default: arial)
        font_color -- color of font displayed on label(default: (0, 0, 0))

        the syntax of creating a instance of the class:
        lbl_obj = Label(surface, x, y, width, height)

        To display the label and update the label, acording the the mouse
        position, two methods must be called:

        lbl_obj.render_label() -- this method is used to render the label.
        Call the function after Updating the pygame screen in your
        Pygame window event loop.

        lbl_obj.update_label() -- this method is used to change the labels
        color depending on the mouse position.
        Call this function in your Pygame window event loop.
        '''

        self.surface = surface
        self.x = x
        self.y = y
        self.w = w
        self.h = h
        self.tp = text_pos
        self.fs = font_size
        self.ft = font_type
        self.fc = font_color
        self.hovering = False
        ######--------------------#######
        self.label = pg.Surface((self.w, self.h))
        self.label_color = color
        self.label_pos = (self.x, self.y)
        self.label.fill(self.label_color)
        self.label_highlight = label_highlight[0]
        self.label_highlight_color = label_highlight[1]
        self.text = text

    def _render_text(self):
        ''' Used to render the label text'''
        self.font = pg.font.Font(self.ft, self.fs)
        w, h = self.font.size(self.text)
        self.font_surf = self.font.render(self.text, True, self.fc)
        self.label.blit(self.font_surf, (self.tp[0], self.tp[1]))


    def render_label(self):
        ''' Used to render the label'''
        if self.hovering:
            self.label.fill(self.label_highlight_color)
            self._render_text()
            self.surface.blit(self.label, self.label_pos)
        elif not self.hovering:
            self.label.fill(self.label_color)
            self._render_text()
            self.surface.blit(self.label, self.label_pos)

    def _update(self, event_obj):
        ''' This function is only called if the user sets
            label_hightlight equal to True and gives a color.
        '''
        x, y = event_obj.pos
        self.hovering = False
        if x > self.x and x < self.x + self.w:
            if y > self.y and y < self.y + self.h:
                self.hovering = True


    def update_label(self, event_obj):
        ''' Calls the acutal Pygame events to update the label'''
        if event_obj.type == pg.MOUSEMOTION:
             if self.label_highlight:
                 self._update(event_obj)
        print(self.hovering)

The main critiques I'd like to know are:

  • Should I make a whole new method to decorate my GUI elements?
  • Am I providing enough info with the doc strings?
  • Is the module easy to use and incorporate?

An example using each module is below. Just make sure the button and label modules are in he same directory. Then just copy 'n paste:

import pygame as pg
import button
import label
pg.init()


WIDTH = 800
HEIGHT = 600
display = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption("Test of GUI elemnts")
clock = pg.time.Clock()

btn = button.Button(display, 300, 300, 100, 20)
lbl = label.Label(display, 300, 270, 100, 20, text="Btn 1")
btn.decorate(caption="btn1")

running = True
while running:
    clock.tick(60)
    for e in pg.event.get():
        if e.type == pg.QUIT:
            running = False
            pg.quit()
            quit()
        btn.update_button(e)
        lbl.update_label(e)

    display.fill((200, 200, 200))
    btn.render_button()
    lbl.render_label()
    pg.display.update()
\$\endgroup\$
2
\$\begingroup\$

You realize that your .decorate method will, when called, reset any parameter that isn't set? So if I call:

button.decorate(caption='mycaption')
button.decorate(font_size=32)

then the second call will reset the caption to "" ?

Also, there are other pygame GUI libraries so you might check there before reinventing the wheel.

\$\endgroup\$
  • \$\begingroup\$ That's the point. Your only supposed to call the decorate method once. That is why highly I'm considering making the user set any decorative parameters when they initialize the button. That way, there forced not to reset there previous parameters. Also, I know that there are other libraries that create buttons is Pygame. I wanted to make my own. For practice and for practicality. \$\endgroup\$ – Christian Dean Aug 21 '16 at 13:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.