5

I made buttons in pygame for click events, but there is a problem. When I click the mouse button and move the mouse between the button boundaries, the click event is repeating itself. I just want a single click until I release the mouse button. How can I do that?

import pygame,time
pygame.init()
x,y = (200,300)
pencere = pygame.display.set_mode((x,y))
pygame.display.set_caption("Click")

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

class Counter:
    count = 0
    def click(self):
        self.count += 1

number = Counter()
def text_objects(text, font, color):
    textSurface = font.render(text, True, color)
    return textSurface, textSurface.get_rect()

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

    smallText = pygame.font.Font("freesansbold.ttf",20)
    textSurf, textRect = text_objects(msg, smallText, white)
    textRect.center = ( (x+(w/2)), (y+(h/2)) )
    pencere.blit(textSurf, textRect)

    if x+w > mouse[0] > x and y+h > mouse[1] > y:
        pygame.draw.rect(pencere, ic,(x,y,w,h))
        if click[0] == 1 != None:
            action()
        smallText = pygame.font.Font("freesansbold.ttf",20)
        textSurf, textRect = text_objects(msg, smallText, white)
        textRect.center = ( (x+(w/2)), (y+(h/2)) )
        pencere.blit(textSurf, textRect)
def loop():
    cikis = False
    while not cikis:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                cikis = True
                pygame.quit()
                quit()
            pencere.fill(white)
            smallText = pygame.font.Font("freesansbold.ttf",50)
            textSurf, textRect = text_objects(str(number.count), smallText, black)
            textRect.center = ((x/2)), (30)
            pencere.blit(textSurf, textRect)
            button("Click",0,100,200,200,black,black2,number.click)
            pygame.display.update()
loop()
pygame.quit()
quit()
skrx
  • 19,980
  • 5
  • 34
  • 48
Arda Altun
  • 137
  • 1
  • 2
  • 9
  • Can you provide the code you are using? – serge1peshcoff Dec 04 '17 at 18:45
  • you have to add to button variable ie. `clicked = True` to remeber it and check it before you execute function assigned to button. And clear it when you release mouse button. – furas Dec 04 '17 at 18:45
  • BTW: do you use `event.type == MOUSEBUTTONDOWN` or `pygame.mouse.get_pressed()` ? With `MOUSEBUTTONDOWN` you shouldn't have this problem because event is created only when mouse button changes state for `not-pressed` to `pressed` but it is not created when mouse button is kept pressed. `get_pressed()` returns `True` when mouse button is kept pressed. – furas Dec 04 '17 at 18:52
  • BTW: examples: [button-click-cycle-color](https://github.com/furas/python-examples/tree/master/pygame/button-click-cycle-color), [button-hover](https://github.com/furas/python-examples/tree/master/pygame/button-hover), – furas Dec 04 '17 at 19:24
  • i added codes. as you can see, im using pygame.mouse.get_pressed() - so what should i do now? what should i change in my codes? – Arda Altun Dec 04 '17 at 20:58
  • I'll take a look at it. Could you maybe provide a very simple runnable example? – skrx Dec 04 '17 at 21:25
  • if you use `get_pressed()` then you have to compare previous value with current value - when it changes from `False` to `True` then you have to save it ie. `clicked = True` and use this `clicked` to skip function. BTW: when `get_pressed()` change value from `True` to `False` then you have to set `clicked = False`. – furas Dec 04 '17 at 21:42
  • BTW: use `pygame.Rect()` to keep position and size of button and then you can use `rect.centerx` instead of `(x+(w/2))`, and you can use `rect.collidepoint(mouse_x, mouse_y)` to check if mouse is in button area. – furas Dec 04 '17 at 21:45
  • BTW: instead of `if click[0] == 1 != None:` you can do `if click[0] == 1:` or even more readale `if click[0] == True`:` (or shorter `if click[0]:`) – furas Dec 04 '17 at 21:46
  • it will be problem to change your code because you have all in one function and it can't keep values between executions - you will have to keep `clicked` outside function or you will have to create class Button to keep `clicked`. – furas Dec 04 '17 at 21:54
  • i added a simple click code. if you click with left mouse button and drag the cursor you can see the number is continuing to count. that True and False solution didnt worked or i couldnt make it i dont know. but you can see the codes, you can solve my problem now. I want a single click until the user release the mouse button. – Arda Altun Dec 05 '17 at 17:10

1 Answers1

8

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()
skrx
  • 19,980
  • 5
  • 34
  • 48
  • Thank you so much!! And yes, i will make several buttons so i need help about your OOP example. – Arda Altun Dec 06 '17 at 12:40
  • 2
    OOP example added. If you haven't worked with pygame sprites and groups yet, take a look at [this tutorial](http://programarcadegames.com/index.php?chapter=introduction_to_sprites&lang=en#section_13). – skrx Dec 07 '17 at 01:16
  • Thank you so much skrx, but there is a problem about me, my main language is not English and i dont know classes too much. Still im learning,so i cant understand those too much. But i will try to figure it out. Btw, is there any chance to take help from you anytime about my codes? – Arda Altun Dec 07 '17 at 21:47
  • Better just post a new question here or on another forum like reddit.com/r/learnpython (if the question doesn't fit to SO), then more people will be able to see it and help you. Also, chapter 12 of the linked tutorial is about classes, so you should read that first if you don't know OOP. – skrx Dec 07 '17 at 23:02
  • Thanks for all your help, i'm appreciated!! When i havea problem i will share it in commands.Btw, i was looking your OOP Button project and i noticed something weird. Your button colors is not defined with RGB or there is any picture to make button colors dodgerblue1,lightskyblue and aquamarine1. Are these colors self defined in python with that special keywords or you just made a trick ? – Arda Altun Dec 08 '17 at 16:15
  • 1
    Pygame has some built-in colors which you can see in the `pygame.color.THECOLORS` dictionary. You can pass one of these names to `pygame.Color` and will get a color object, e.g.: `pygame.Color('dodgerblue1')` has the values `(30, 144, 255, 255)`. – skrx Dec 08 '17 at 21:15
  • Wow... Im trying to make buttons like yours for my project. If i have a problem, i will share it. Thank you a lot, again!! – Arda Altun Dec 08 '17 at 21:39