Pygame Button Single Click

Pygame button single click

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()

Clicking buttons in pygame

Define two callback functions which you need to pass as the action argument to the button function.

In the quit_game function, you can call pygame.quit and sys.exit to close the window. The start_game function needs to be defined in the game_intro function, because we have to access the intro variable, with the help of the nonlocal keyword, and set it to False, so that the game_intro function will be terminated and the main loop can start.

def button(msg, x, y, w, h, ic, ac, action=None):
mouse = pygame.mouse.get_pos()
click = pygame.mouse.get_pressed()

if x + w > mouse[0] > x and y + h > mouse[1] > y:
pygame.draw.rect(win, ac, (x, y, w, h))
if click[0] == 1 and action is not None:
action() # Call the callback function.
# etc. ...

def quit_game():
pygame.quit()
sys.exit() # `import sys` at the top of the file.

def game_intro():
intro = True

def start_game():
# Set the intro variable in the enclosing scope to False.
nonlocal intro
intro = False

while intro:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()

win.fill(WHITE)
largeText2 = pygame.font.Font('freesansbold.ttf', 115)
TextSurf, TextRect = text_objects("Red Square", largeText2)
TextRect.center = ((600), (100))
win.blit(TextSurf, TextRect)

# Pass the two callback functions to `button`.
button("Start", 525, 250, 150, 60, YELLOW, BRIGHT_YELLOW, start_game)
button("Quit", 525, 350, 150, 60, YELLOW, BRIGHT_YELLOW, quit_game)

pygame.display.update()
clock.tick(15)

Also, don't call game_intro inside the main while loop.

game_intro()

run = True
while run:

how to get pygame button to register only one click?

A simple and more efficient way to do what you want would be to explicitly check for pygame.MOUSEBUTTONDOWN events and only do the mouse event processing when necessary. You can also streamline the logic in makeButton() by using pygame's Rect class which knows how to do collision detection.

Here's what I mean:

import pygame
import sys

pygame.init()
screen = pygame.display.set_mode((640, 480),0,32)
clock = pygame.time.Clock()

def makeButton(cur, rect):
if rect.collidepoint(cur):
print "button pressed"

square = pygame.Rect((0,0), (32,32))

while True:
screen.fill((255,255,255))
screen.fill((55,155,0), square)
pygame.display.update()
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
quit()
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # left mouse button?
makeButton(event.pos, square)

Python pygame - avoiding accidental continuous clicking

I think your original code is a bit too convoluted to fix it and I'll rather show you some better ways to do what you want. You need a finite-state machine to transition between different states/scenes. You can find a simple example with functions as scenes here.

If the logic in your scenes is mostly the same, you could also try to just swap out the data for the scene, for example the background image. Each state/scene needs to know to which new states it can switch, so I put the data into a dictionary of dictionaries. The nested dicts contain the background image of the scene and the connected left and right scenes. When the user presses a button/rect I check if it was the left or right button and then switch to the corresponding scene (subdict) in the states dictionary.

import pygame

pygame.init()

display_width= 1280
display_height = 720

gameDisplay = pygame.display.set_mode((display_width, display_height))
clock = pygame.time.Clock()

# Use uppercase names for constants that should never be changed.
DARK_GRAY = pygame.Color('gray13')
BACKGROUND1 = pygame.Surface((display_width, display_height))
BACKGROUND1.fill((30, 150, 90))
BACKGROUND2 = pygame.Surface((display_width, display_height))
BACKGROUND2.fill((140, 50, 0))
BACKGROUND3 = pygame.Surface((display_width, display_height))
BACKGROUND3.fill((0, 80, 170))

states = {
'scene1': {'background': BACKGROUND1, 'left_scene': 'scene2', 'right_scene': 'scene3'},
'scene2': {'background': BACKGROUND2, 'left_scene': 'scene1', 'right_scene': 'scene3'},
'scene3': {'background': BACKGROUND3, 'left_scene': 'scene1', 'right_scene': 'scene2'},
}

def game_loop():
# The buttons are just pygame.Rects.
left_button = pygame.Rect(440, 450, 60, 40)
right_button = pygame.Rect(740, 450, 60, 40)
# The current_scene is a dictionary with the relevant data.
current_scene = states['scene1']

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
elif event.type == pygame.MOUSEBUTTONDOWN:
# If the left button is clicked we switch to the 'left_scene'
# in the `current_scene` dictionary.
if left_button.collidepoint(event.pos):
current_scene = states[current_scene['left_scene']]
print(current_scene)
# If the right button is clicked we switch to the 'right_scene'.
elif right_button.collidepoint(event.pos):
current_scene = states[current_scene['right_scene']]
print(current_scene)

# Blit the current background.
gameDisplay.blit(current_scene['background'], (0, 0))
# Always draw the button rects.
pygame.draw.rect(gameDisplay, DARK_GRAY, left_button)
pygame.draw.rect(gameDisplay, DARK_GRAY, right_button)
pygame.display.update()
clock.tick(30) # 30 FPS feels more responsive.

game_loop()
pygame.quit()

How do I make pygame recognize a single mouse click as a single mouse click?

You have to use the MOUSEDOWN event instead of pygame.mouse.get_pressed(). While pygame.mouse.get_pressed() returns the current state of the buttons, the MOUSEBUTTONDOWN and MOUSEBUTTONUP occurs only once a button is pressed.

Pygame button get.pressed()

You can use state variables (True/False) to control which element to display and you can change those varibles using button.

In example I have

    display_text = True
display_button_1 = True
display_button_2 = False

to control when display text and two buttons.

First button change values and it hides text and first button, and it shows second button.

There is one problem - your button function is ugly and it use get_buttons() so when I hold pressed button and I remove one button and put another in the same place then it automatically click second button. You should use event.type == MOUSEBUTTONUP (as suggest @Hilea) to fix this.

import pygame
import sys # every import in separated line

# --- constants ---

display_width = 800
display_height = 600

black = (0,0,0)
white = (255,255,255)

red = (200,0,0)
green = (0,200,0)

bright_red = (255,0,0)
bright_green = (0,255,0)

block_color = (53,115,255)

# --- functions ---

def text_objects(text, font):
textSurface = font.render(text, True, black)
return textSurface, textSurface.get_rect()

def button(msg,x,y,w,h,ic,ac,action=None):
mouse = pygame.mouse.get_pos()
click = pygame.mouse.get_pressed()

if x+w > mouse[0] > x and y+h > mouse[1] > y:
pygame.draw.rect(gameDisplay, ac,(x,y,w,h))
if click[0] == 1 and action != None:
pygame.mixer.music.stop()
action()

else:
pygame.draw.rect(gameDisplay, ic,(x,y,w,h))
smallText = pygame.font.SysFont("comicsansms",20)
textSurf, textRect = text_objects(msg, smallText)
textRect.center = ( (x+(w/2)), (y+(h/2)) )
gameDisplay.blit(textSurf, textRect)

def quitgame():
pygame.quit()
sys.exit()
quit()

def hide_text():
global display_text
global display_button_1
global display_button_2

display_text = False
display_button_1 = False
display_button_2 = True

def game_loop():
global display_text
global display_button_1
global display_button_2

gameExit = False
display_text = True
display_button_1 = True
display_button_2 = False

meie_font = pygame.font.SysFont("Arial", 36)

tekst = "This game will go as far as you choose!"
teksti_pilt = meie_font.render(tekst, False, (50,50,155))

tekst2 = "You are the smith of your destiny"
teksti_pilt = meie_font.render(tekst2, False, (50,50,155))

while not gameExit:

for event in pygame.event.get():
if event.type == pygame.QUIT:
return

gameDisplay.fill(white)

if display_text:
gameDisplay.blit(teksti_pilt, (100, 250))
gameDisplay.blit(teksti_pilt, (100, 400))

if display_button_1:
button("Start playing", 300,500,150,50,green,bright_green, hide_text)

if display_button_2:
button("Exit", 300,100,150,50,green,bright_green, pygame.quit)

pygame.display.update()

# --- main ---

pygame.init()

gameDisplay = pygame.display.set_mode((display_width,display_height))
pygame.display.set_caption('One Day After')

clock = pygame.time.Clock()

pause = False

game_loop()
pygame.quit()


Related Topics



Leave a reply



Submit