Why won't my button change color when i hover

2020-04-10 03:35发布

问题:

I'm new to pygame and have been attempting to create a simple interface with some buttons. I can't get the button to change color when the mouse hovers over it.

I've managed to create the button, but cannot get it to interact with my mouse. The code create an button object with one instance of a green button. It should change the button from green to red when mouse hovers over.

import pygame

pygame.init()

display_width = 1200
display_height = 600

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

StartScreen = pygame.display.set_mode((display_width, display_height))
pygame.display.set_caption('Log In')
clock = pygame.time.Clock()

StartScreen.fill(white)

class Buttons():
    def __init__(self, color, x, y, width, height, text=''):
        self.color = color
        self.x = int(x)
        self.y = int(y)
        self.w = int(width)
        self.h = int(height)
        self.text = text


    def Draw(self, StartScreen, outline=None):
        if outline:
            pygame.draw.rect(StartScreen, outline, (float(self.x-2), float(self.y-2), float(self.w+4), float(self.h+4)), 0)

        pygame.draw.rect(StartScreen, self.color, (self.x, self.y, self.w, self.h), 0)

        if self.text != '':
            font = pygame.font.SysFont('comicsans', 20)
            text = font.render(self.text, 1, black)
            StartScreen.blit(text, (self.x + (self.w/2 - text.get_width()/2), self.y + (self.h/2 - text.get_height()/2)))


    def MouseOver(self, pos):
        if pos[0] > self.x and pos[0] < self.x + self.w:
            if pos[1] > self.y and pos[1] < self.y + self.h:
                return True

        return False

def redrawWindow():
    StartScreen.fill(white)
    GrnBut.Draw(StartScreen, black)

run = True

GrnBut = Buttons(green, 150, 200, 90, 100, 'Press')
while run:
    redrawWindow()
    pygame.display.update()

    for event in pygame.event.get():
        pos = pygame.mouse.get_pos()

        Exit = False
        while not Exit:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    print(event)
                    pygame.quit()
                    quit()

        if event.type == pygame.MOUSEBUTTONDOWN:
            if GrnBut.MouseOver(pos):
                print("Clicked")

        if event.type == pygame.MOUSEMOTION:
            if GrnBut.MouseOver(pos):
                GrnBut.color = red
            else:
                GrnBut.color = green


回答1:

Your main problem is that you have a nested event loop inside your event loop:

while run:         # outer loop
    redrawWindow()
    pygame.display.update()

    for event in pygame.event.get():
        pos = pygame.mouse.get_pos()

        Exit = False
        while not Exit:       # inner loop
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    print(event)
                    pygame.quit()
                    quit()

When execution reaches this inner loop, neither redrawWindow() or GrnBut.MouseOver(pos) is ever called again.

Just get rid of it:

while run:
    redrawWindow()
    pygame.display.update()

    for event in pygame.event.get():
        pos = pygame.mouse.get_pos()

        if event.type == pygame.QUIT:
            print(event)
            pygame.quit()
            quit()

Your code can be improved by using some of pygame's features, such as the Sprite and Rect classes.

Here's an example of how you could create a more "pygamy" version of your Button class that supports multiple, different buttons:

import pygame

pygame.init()

display_width = 1200
display_height = 600

# use python style variable names (lowercase)
screen = pygame.display.set_mode((display_width, display_height))
pygame.display.set_caption('Log In')
clock = pygame.time.Clock()

# load the font only once instead of every frame
font = pygame.font.SysFont('comicsans', 20)

# class name should be singular
class Button(pygame.sprite.Sprite):
    # 1) no need to have 4 parameters for position and size, use pygame.Rect instead
    # 2) let the Button itself handle which color it is
    # 3) give a callback function to the button so it can handle the click itself 
    def __init__(self, color, color_hover, rect, callback, text='', outline=None):
        super().__init__()
        self.text = text
        # a temporary Rect to store the size of the button
        tmp_rect = pygame.Rect(0, 0, *rect.size)

        # create two Surfaces here, one the normal state, and one for the hovering state
        # we create the Surfaces here once, so we can simple blit them and dont have
        # to render the text and outline again every frame
        self.org = self._create_image(color, outline, text, tmp_rect)
        self.hov = self._create_image(color_hover, outline, text, tmp_rect)

        # in Sprites, the image attribute holds the Surface to be displayed...
        self.image = self.org
        # ...and the rect holds the Rect that defines it position
        self.rect = rect
        self.callback = callback

    def _create_image(self, color, outline, text, rect):
        # function to create the actual surface
        # see how we can make use of Rect's virtual attributes like 'size'
        img = pygame.Surface(rect.size)
        if outline:
            # here we can make good use of Rect's functions again
            # first, fill the Surface in the outline color
            # then fill a rectangular area in the actual color
            # 'inflate' is used to 'shrink' the rect
            img.fill(outline)
            img.fill(color, rect.inflate(-4, -4))
        else:
            img.fill(color)

        # render the text once here instead of every frame
        if text != '':
            text_surf = font.render(text, 1, pygame.Color('black'))
            # again, see how easy it is to center stuff using Rect's attributes like 'center'
            text_rect = text_surf.get_rect(center=rect.center)
            img.blit(text_surf, text_rect)
        return img

    def update(self, events):
        # here we handle all the logic of the Button
        pos = pygame.mouse.get_pos()
        hit = self.rect.collidepoint(pos)
        # if the mouse in inside the Rect (again, see how the Rect class
        # does all the calculation for use), use the 'hov' image instead of 'org'
        self.image = self.hov if hit else self.org
        for event in events:
            # the Button checks for events itself.
            # if this Button is clicked, it runs the callback function
            if event.type == pygame.MOUSEBUTTONDOWN and hit:
                self.callback(self)

run = True

# we store all Sprites in a Group, so we can easily
# call the 'update' and 'draw' functions of the Buttons
# in the main loop
sprites = pygame.sprite.Group()
sprites.add(Button(pygame.Color('green'), 
                   pygame.Color('red'), 
                   pygame.Rect(150, 200, 90, 100), 
                   lambda b: print(f"Button '{b.text}' was clicked"),
                   'Press',
                   pygame.Color('black')))

sprites.add(Button(pygame.Color('dodgerblue'), 
                   pygame.Color('lightgreen'), 
                   pygame.Rect(300, 200, 90, 100), 
                   lambda b: print(f"Click me again!"),
                   'Another'))

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

    # update all sprites
    # it now doesn't matter if we have one or 200 Buttons
    sprites.update(events)
    # clear the screen
    screen.fill(pygame.Color('white'))
    # draw all sprites/Buttons
    sprites.draw(screen)
    pygame.display.update()
    # limit framerate to 60 FPS
    clock.tick(60)



回答2:

There are a lot of problems with your code, and would like to suggest a less verbose way to code this using pygame.Rect instead of pygame.Sprite, as it does not require as much understanding of game design and is an inheritance-free approach.

Firstly we create the button class:

import pygame
import sys

class Button:
    def __init__(self, rect, default_colour=(0,255,0), hovered_colour=(255,0,0), text="", font=None): #using default arguments
        self.rect = pygame.Rect(rect)

        self.default_colour = default_colour
        self.hovered_colour = hovered_colour

        self.font = font if font else pygame.font.Font(None, 20) #initialise/import font
        self.text = self.font.render(text, True, (0,0,0)) #render text

    def draw(self, surf, mouse_coords):
        if self.hover(mouse_coords):
            pygame.draw.rect(surf, self.hovered_colour, self.rect, 0)
        else:
            pygame.draw.rect(surf, self.default_colour, self.rect, 0)

        surf.blit(self.text, self.text.get_rect(center=self.rect.center)) #rect has a centre attribute

    def hover(self, mouse):
        mouse_rect = pygame.Rect(mouse, [1,1]) #using inbuilt collision function
        return mouse_rect.colliderect(self.rect) #returns a boolean, no need to do this: if mouse_rect.colliderect(self.rect): return True, else: return False 

then we write the main program loop (continues on from previous block of code)

pygame.init()
screen = pygame.display.set_mode([500,500])

def response1(): #callback function when button 1 is pressed
    print("Button 1 pressed")

def response2(): #callback function when button 2 is pressed
    print("Button 2 pressed")

def main(buttons):
    while True: #setting a variable to True is unnecessary as you can just use "break" to exit the loop.
    #this is only not the case in rare cases

        screen.fill((255,255,255)) #unneccessary to put in another function

        for event in pygame.event.get(): #only 1 event loop required
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

            elif event.type == pygame.MOUSEBUTTONDOWN:
                for button in buttons: 
                    if button["button"].hover(pygame.mouse.get_pos()):
                        button["func"]() #calls function if clicked on

        for button in buttons: #draws all buttons in array, can be optimised to not occur when user clicks
            button["button"].draw(screen, pygame.mouse.get_pos())

        pygame.display.flip() #update the surface at the end of the loop instead of the beginning
    #only use pygame.display.update(rect=None) to update a specific portion of the display, otherwise stick to flip()

if __name__ == "__main__": #if file is not a module
    button1 = Button([130, 200, 90, 100], text="Press")
    button2 = Button([280, 200, 90, 100], text="Me", default_colour=(255,255,0))

    buttons = [ #array of dicts to store buttons and callback functions, for multiple buttons
        {
         "button": button1,
         "func": response1
        },
        {
         "button": button2,
         "func": response2
        }
    ]

    main(buttons)