Creating Buttons in PyGame
I am making a program for an automated timetable and I have managed to create a title but I would like to add a log in and sign up button that goes onto a new page. I would also like to add a return button on these new pages. I was using programmingpixels.com to help but I still cannot do what I would like to do. I am new into using PyGame so I may not have done an efficient code as I could have done and there may be quite a few errors. My title screen was previously working but when I tried to add these buttons it is blank and won't let me exit my screen. Any help would be great. Thank You.
import pygame
import pygame.freetype
from pygame.sprite import Sprite
from pygame.rect import Rect
from enum import Enum
PINK = (250, 100, 100)
WHITE = (255, 255, 255)
BLACK = (0,0,0)
def create_surface_with_text(text, font_size, text_rgb, bg_rgb):
font = pygame.freetype.SysFont("Arial", font_size, bold=True)
surface, _ = font.render(text=text, fgcolor=text_rgb, bgcolor=bg_rgb)
return surface.convert_alpha()
class UIElement(Sprite):
def __init__(self, center_position, text, font_size, bg_rgb, text_rgb, action=None):
self.mouse_over = False
# what happens when the mouse is not over the element
default_image = create_surface_with_text(
text=text, font_size=font_size, text_rgb=text_rgb, bg_rgb=bg_rgb
)
# what happens when the mouse is over the element
highlighted_image = create_surface_with_text(
text=text, font_size=font_size * 1.1, text_rgb=text_rgb, bg_rgb=bg_rgb
)
self.images = [default_image, highlighted_image]
self.rects = [
default_image.get_rect(center=center_position),
highlighted_image.get_rect(center=center_position),
]
self.action = action
super().__init__()
@property
def image(self):
return self.images[1] if self.mouse_over else self.images[0]
@property
def rect(self):
return self.rects[1] if self.mouse_over else self.rects[0]
def update(self, mouse_pos, mouse_up):
if self.rect.collidepoint(mouse_pos):
self.mouse_over = True
else:
self.mouse_over = False
def draw(self, surface):
surface.blit(self.image, self.rect)
def main():
pygame.init()
screen = pygame.display.set_mode((800, 600))
game_state = GameState.LOGIN
while True:
if game_state == GameState.LOGIN:
game_state = log_in(screen)
if game_state == GameState.SIGNUP:
game_state = sign_up(screen)
if game_state == GameState.RETURN:
game_state = title_screen(screen)
if game_state == GameState.QUIT:
pygame.quit()
return
def title_screen(screen):
login_btn = UIElement(
center_position=(400,300),
font_size=30,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Log In",
action=GameState.LOGIN,
)
signup_btn = UIElement(
center_position=(400,200),
font_size=30,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Log In",
action=GameState.LOGIN,
)
uielement = UIElement(
center_position=(400, 100),
font_size=40,
bg_rgb=PINK,
text_rgb=BLACK,
text="Welcome to the Automated Timetable Program",
action=GameState.QUIT,
)
buttons = [login_btn, signup_btn]
while True:
mouse_up = False
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
mouse_up = True
elif event.type == pygame.QUIT:
pygame.quit()
sys.exitIO
screen.fill(PINK)
for button in buttons:
ui_action = button.update(pygame.mouse.get_pos(),mouse_up)
if ui_action is not None:
return ui_action
button.draw(screen)
pygame.display.flip()
def log_in(screen):
return_btn = UIElement(
center_position=(140, 570),
font_size=20,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Return to main menu",
action=GameState.TITLE,
)
while True:
mouse_up = False
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
mouse_up = True
screen.fill(PINK)
ui_action = return_btn.update(pygame.mouse.get_pos(),mouse_up)
if ui_action is not None:
return ui_action
return_btn.draw(screen)
pygame.display.flip()
class GameState(Enum):
LOGIN = -1
SIGNUP = 0
RETURN = 1
QUIT = 2
if __name__ == "__main__":
main()
For starters GameState
is missing TITLE
value.
class GameState(Enum):
# ...
TITLE = 3
Adding this makes the code run.
The log_in()
function does not handle the window being closed. You must handle the pygame.QUIT
event, in every event loop. For example:
def log_in( screen ):
# ...
while True:
mouse_up = False
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
pygame.event.post( pygame.event.Event( pygame.QUIT ) ) # re-send the quit event to the next loop
return GameState.QUIT
elif ( event.type == pygame.MOUSEBUTTONUP and event.button == 1 ):
mouse_up = True # Mouse button 1 weas released
ui_action = return_btn.update( pygame.mouse.get_pos(), mouse_up )
if ui_action is not None:
print( "log_in() - returning action" )
return ui_action
screen.fill(PINK)
return_btn.draw(screen)
pygame.display.flip()
The UIElement.update()
looks like it should return the self.action
when the mouse button is released over the control. However, in the existing code, nothing is ever returned. Probably it needs to be something like this:
class UIElement( Sprite ):
# ...
def update(self, mouse_pos, mouse_up):
""" Track the mouse, setting the self.mouse_over. Also check
if the mouse-button was clicked while over this control
returning the pre-defined self.action, if so. """
result = None # No click => no action
if self.rect.collidepoint(mouse_pos):
self.mouse_over = True
if ( mouse_up ):
result = self.action # Mouse was clicked on element, add action
else:
self.mouse_over = False
return result
After these changes, your script runs OK before dropping into an outer loop when the button is clicked. The outer loop also does not handle exiting properly, but probably it's just the same set of changes over again.
It would be better to only have one user-input processing loop. having these separate event-loops is causing the same problem in multiple locations. Work out a way to have a single event processing function, then adapt your UI code to use it. This will make your code easier to write and debug going forward.
Ref: All Code
import pygame
import pygame.freetype
from pygame.sprite import Sprite
from pygame.rect import Rect
from enum import Enum
PINK = (250, 100, 100)
WHITE = (255, 255, 255)
BLACK = (0,0,0)
def create_surface_with_text(text, font_size, text_rgb, bg_rgb):
font = pygame.freetype.SysFont("Arial", font_size, bold=True)
surface, _ = font.render(text=text, fgcolor=text_rgb, bgcolor=bg_rgb)
return surface.convert_alpha()
class UIElement(Sprite):
def __init__(self, center_position, text, font_size, bg_rgb, text_rgb, action=None):
self.mouse_over = False
# what happens when the mouse is not over the element
default_image = create_surface_with_text(
text=text, font_size=font_size, text_rgb=text_rgb, bg_rgb=bg_rgb
)
# what happens when the mouse is over the element
highlighted_image = create_surface_with_text(
text=text, font_size=font_size * 1.1, text_rgb=text_rgb, bg_rgb=bg_rgb
)
self.images = [default_image, highlighted_image]
self.rects = [
default_image.get_rect(center=center_position),
highlighted_image.get_rect(center=center_position),
]
self.action = action
super().__init__()
@property
def image(self):
return self.images[1] if self.mouse_over else self.images[0]
@property
def rect(self):
return self.rects[1] if self.mouse_over else self.rects[0]
def update(self, mouse_pos, mouse_up):
""" Track the mouse, setting the self.mouse_over. Also check
if the mouse-button was clicked while over this control
returning the pre-defined self.action, if so. """
result = None # No click => no action
if self.rect.collidepoint(mouse_pos):
self.mouse_over = True
if ( mouse_up ):
result = self.action # Mouse was clicked on element, add action
else:
self.mouse_over = False
return result
def draw(self, surface):
surface.blit(self.image, self.rect)
def main():
pygame.init()
screen = pygame.display.set_mode((800, 600))
game_state = GameState.LOGIN
while True:
if game_state == GameState.LOGIN:
game_state = log_in(screen)
if game_state == GameState.SIGNUP:
game_state = sign_up(screen)
if game_state == GameState.RETURN:
game_state = title_screen(screen)
if game_state == GameState.QUIT:
pygame.quit()
return
def title_screen(screen):
login_btn = UIElement(
center_position=(400,300),
font_size=30,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Log In",
action=GameState.LOGIN,
)
signup_btn = UIElement(
center_position=(400,200),
font_size=30,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Log In",
action=GameState.LOGIN,
)
uielement = UIElement(
center_position=(400, 100),
font_size=40,
bg_rgb=PINK,
text_rgb=BLACK,
text="Welcome to the Automated Timetable Program",
action=GameState.QUIT,
)
buttons = [login_btn, signup_btn]
while True:
mouse_up = False
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
mouse_up = True
elif event.type == pygame.QUIT:
pygame.quit()
sys.exitIO
screen.fill(PINK)
for button in buttons:
ui_action = button.update(pygame.mouse.get_pos(),mouse_up)
if ui_action is not None:
return ui_action
button.draw(screen)
pygame.display.flip()
def log_in(screen):
return_btn = UIElement(
center_position=(140, 570),
font_size=20,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Return to main menu",
action=GameState.TITLE,
)
while True:
mouse_up = False
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
pygame.event.post( pygame.event.Event( pygame.QUIT ) ) # re-send the quit event to the next loop
return GameState.QUIT
elif ( event.type == pygame.MOUSEBUTTONUP and event.button == 1 ):
mouse_up = True # Mouse button 1 weas released
ui_action = return_btn.update( pygame.mouse.get_pos(), mouse_up )
if ui_action is not None:
print( "log_in() - returning action" )
return ui_action
screen.fill(PINK)
return_btn.draw(screen)
pygame.display.flip()
class GameState(Enum):
LOGIN = -1
SIGNUP = 0
RETURN = 1
QUIT = 2
TITLE=3
if __name__ == "__main__":
main()