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()
301_Moved_Permanently
29.4k3 gold badges48 silver badges98 bronze badges
asked Aug 16, 2016 at 3:09
\$\endgroup\$
0

1 Answer 1

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.

answered Aug 21, 2016 at 6:45
\$\endgroup\$
1
  • \$\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\$ Commented Aug 21, 2016 at 13:06

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.