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()
1 Answer 1
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.
-
\$\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\$Chris– Chris2016年08月21日 13:06:46 +00:00Commented Aug 21, 2016 at 13:06