There are several things that should be changed:
The drawing and button code shouldn't be in the event loop but in the outer while loop. You're calling the button
function every time an event occurs (for example if the mouse moves).
The button
function is doing too much. It creates and blits text surfaces, draws rects, checks for collisions and calls the click
method.
You shouldn't use pygame.mouse.get_pressed()
and instead handle the MOUSEBUTTONDOWN
events in the event loop. mouse.get_pressed
just checks if a mouse button is held and not if a single click occurred.
I'll just show you an easy solution without a function and with a rect as the button here. I handle the collision and update the number in the event loop. If you want to create several buttons, I'd suggest to rewrite it in an object-oriented manner (I can show you an example if you want).
import pygame
pygame.init()
width, height = (200,300)
screen = pygame.display.set_mode((width, height))
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (30, 30, 30)
FONT = pygame.font.Font("freesansbold.ttf", 50)
def loop():
clock = pygame.time.Clock()
number = 0
# The button is just a rect.
button = pygame.Rect(0, 100, 200, 200)
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
# This block is executed once for each MOUSEBUTTONDOWN event.
elif event.type == pygame.MOUSEBUTTONDOWN:
# 1 is the left mouse button, 2 is middle, 3 is right.
if event.button == 1:
# `event.pos` is the mouse position.
if button.collidepoint(event.pos):
# Increment the number.
number += 1
screen.fill(WHITE)
pygame.draw.rect(screen, GRAY, button)
text_surf = FONT.render(str(number), True, BLACK)
# You can pass the center directly to the `get_rect` method.
text_rect = text_surf.get_rect(center=(width/2, 30))
screen.blit(text_surf, text_rect)
pygame.display.update()
clock.tick(30)
loop()
pygame.quit()
Addendum: I recommend using a object-oriented solution with a Button
class that is a subclass of pygame.sprite.Sprite
and can be added to sprite groups. You can pass your own images to the Button
class or use the default images. You also have to pass a callback function or method to each button instance which will get called in the handle_event
method to update the specific attributes of the game class (here I have a method that increments a counter and another one to quit the game).
import pygame as pg
pg.init()
screen = pg.display.set_mode((800, 600))
FONT = pg.font.SysFont('Comic Sans MS', 32)
# Default button images/pygame.Surfaces.
IMAGE_NORMAL = pg.Surface((100, 32))
IMAGE_NORMAL.fill(pg.Color('dodgerblue1'))
IMAGE_HOVER = pg.Surface((100, 32))
IMAGE_HOVER.fill(pg.Color('lightskyblue'))
IMAGE_DOWN = pg.Surface((100, 32))
IMAGE_DOWN.fill(pg.Color('aquamarine1'))
# Button is a sprite subclass, that means it can be added to a sprite group.
# You can draw and update all sprites in a group by
# calling `group.update()` and `group.draw(screen)`.
class Button(pg.sprite.Sprite):
def __init__(self, x, y, width, height, callback,
font=FONT, text='', text_color=(0, 0, 0),
image_normal=IMAGE_NORMAL, image_hover=IMAGE_HOVER,
image_down=IMAGE_DOWN):
super().__init__()
# Scale the images to the desired size (doesn't modify the originals).
self.image_normal = pg.transform.scale(image_normal, (width, height))
self.image_hover = pg.transform.scale(image_hover, (width, height))
self.image_down = pg.transform.scale(image_down, (width, height))
self.image = self.image_normal # The currently active image.
self.rect = self.image.get_rect(topleft=(x, y))
# To center the text rect.
image_center = self.image.get_rect().center
text_surf = font.render(text, True, text_color)
text_rect = text_surf.get_rect(center=image_center)
# Blit the text onto the images.
for image in (self.image_normal, self.image_hover, self.image_down):
image.blit(text_surf, text_rect)
# This function will be called when the button gets pressed.
self.callback = callback
self.button_down = False
def handle_event(self, event):
if event.type == pg.MOUSEBUTTONDOWN:
if self.rect.collidepoint(event.pos):
self.image = self.image_down
self.button_down = True
elif event.type == pg.MOUSEBUTTONUP:
# If the rect collides with the mouse pos.
if self.rect.collidepoint(event.pos) and self.button_down:
self.callback() # Call the function.
self.image = self.image_hover
self.button_down = False
elif event.type == pg.MOUSEMOTION:
collided = self.rect.collidepoint(event.pos)
if collided and not self.button_down:
self.image = self.image_hover
elif not collided:
self.image = self.image_normal
class Game:
def __init__(self, screen):
self.done = False
self.clock = pg.time.Clock()
self.screen = screen
# Contains all sprites. Also put the button sprites into a
# separate group in your own game.
self.all_sprites = pg.sprite.Group()
self.number = 0
# Create the button instances. You can pass your own images here.
self.start_button = Button(
320, 70, 170, 65, self.increment_number,
FONT, 'Increment', (255, 255, 255),
IMAGE_NORMAL, IMAGE_HOVER, IMAGE_DOWN)
# If you don't pass images, the default images will be used.
self.quit_button = Button(
320, 240, 170, 65, self.quit_game,
FONT, 'Quit', (255, 255, 255))
# Add the button sprites to the sprite group.
self.all_sprites.add(self.start_button, self.quit_button)
def quit_game(self):
"""Callback method to quit the game."""
self.done = True
def increment_number(self):
"""Callback method to increment the number."""
self.number += 1
print(self.number)
def run(self):
while not self.done:
self.dt = self.clock.tick(30) / 1000
self.handle_events()
self.run_logic()
self.draw()
def handle_events(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self.done = True
for button in self.all_sprites:
button.handle_event(event)
def run_logic(self):
self.all_sprites.update(self.dt)
def draw(self):
self.screen.fill((30, 30, 30))
self.all_sprites.draw(self.screen)
pg.display.flip()
if __name__ == '__main__':
pg.init()
Game(screen).run()
pg.quit()
Addendum 2: A intermediate solution with buttons as dictionaries. It would also be possible to use lists, but dictionaries are more readable.
import pygame
pygame.init()
WHITE = (255, 255, 255)
ACTIVE_COLOR = pygame.Color('dodgerblue1')
INACTIVE_COLOR = pygame.Color('dodgerblue4')
FONT = pygame.font.Font(None, 50)
def draw_button(button, screen):
"""Draw the button rect and the text surface."""
pygame.draw.rect(screen, button['color'], button['rect'])
screen.blit(button['text'], button['text rect'])
def create_button(x, y, w, h, text, callback):
"""A button is a dictionary that contains the relevant data.
Consists of a rect, text surface and text rect, color and a
callback function.
"""
# The button is a dictionary consisting of the rect, text,
# text rect, color and the callback function.
text_surf = FONT.render(text, True, WHITE)
button_rect = pygame.Rect(x, y, w, h)
text_rect = text_surf.get_rect(center=button_rect.center)
button = {
'rect': button_rect,
'text': text_surf,
'text rect': text_rect,
'color': INACTIVE_COLOR,
'callback': callback,
}
return button
def main():
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
done = False
number = 0
def increment_number(): # A callback function for the button.
"""Increment the `number` in the enclosing scope."""
nonlocal number
number += 1
print(number)
def quit_game(): # A callback function for the button.
nonlocal done
done = True
button1 = create_button(100, 100, 250, 80, 'Click me!', increment_number)
button2 = create_button(100, 200, 250, 80, 'Me too!', quit_game)
# A list that contains all buttons.
button_list = [button1, button2]
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
# This block is executed once for each MOUSEBUTTONDOWN event.
elif event.type == pygame.MOUSEBUTTONDOWN:
# 1 is the left mouse button, 2 is middle, 3 is right.
if event.button == 1:
for button in button_list:
# `event.pos` is the mouse position.
if button['rect'].collidepoint(event.pos):
# Increment the number by calling the callback
# function in the button list.
button['callback']()
elif event.type == pygame.MOUSEMOTION:
# When the mouse gets moved, change the color of the
# buttons if they collide with the mouse.
for button in button_list:
if button['rect'].collidepoint(event.pos):
button['color'] = ACTIVE_COLOR
else:
button['color'] = INACTIVE_COLOR
screen.fill(WHITE)
for button in button_list:
draw_button(button, screen)
pygame.display.update()
clock.tick(30)
main()
pygame.quit()