Python Forum
[PyGame] Create multiple buttons in a loop?
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] Create multiple buttons in a loop?
#1
Hi,

I'm trying to create a screen which will create buttons with each letter of the alphabet and then, when clicked, show a list of buttons with people with names beginning with that letter (from the database). I got the letters screen to run and show the different letters, but I haven't actually made them buttons yet - will this be possible without me creating a buttons class? What's the simplest way I can do this in?

def letterButton():
    runlettersButton = True

    screen.fill(WHITE)
    title = lettersTitle.render('What letter does your name begin with?', False, BLACK)
    screen.blit(title, [0, 0])
    lettersDict = {'a': [5, 55],
                   'b': [90, 55],
                   'c': [175, 55],
                   'd': [260, 55],
                   'e': [345, 55],
                   'f': [430, 55],
                   'g': [515, 55],
                   'h': [5, 150],
                   'i': [90, 150],
                   'j': [175, 150],
                   'k': [260, 150],
                   'l': [345, 150],
                   'm': [430, 150],
                   'n': [515, 150],
                   'o': [5, 250],
                   'p': [90, 250],
                   'q': [175, 250],
                   'r': [260, 250],
                   's': [345, 250],
                   't': [430, 250],
                   'u': [515, 250],
                   'v': [5, 350],
                   'w': [90, 350],
                   'x': [175, 350],
                   'y': [260, 350],
                   'z': [345, 350]}
    lettersList = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
    letterX = 5
    letterY = 55
    for i in range(0, len(lettersList)):
        currentLetter = lettersList[i]
        if i % 2 == 1:
            colour = YELLOW
        elif i % 2 == 0:
            colour = BLUE
        x = lettersDict[currentLetter][0]
        y = lettersDict[currentLetter][1]
        currentLetter = pygame.draw.rect(screen, colour, [x, y, 80, 95])
        letterSurface = letters.render(lettersList[i], False, BLACK)
        screen.blit(letterSurface, currentLetter)
     
Reply
#2
A button class is ideal. Once you have a button class setup for one button you just have to loop the letters to insert the text to each button, and position each button. If you want them in odd weird positions, your going to have to set a dictionary with each position. Its much easier to increment them the same.

I modified our tutorial button class to include something like this.

This code creates a button and sets the y axis (top) of each button. enumerate allows you to increment a number for each button which can be used as a multiplier for the y axis.

lambda l=letter:print_on_press(l) passes the letter to the callback function so you can do something with each button uniquely. It requires lambda because you are passing something to the callback function. In addition it also requires letter to be passed into the lambda function as a local variable l. If you only did lambda:print_on_press(letter) then z would be the only thing that shows up because that is the last letter in the loop. letter would then be changed to what the next loop letter value is, instead of retaining it for each iteration.

Quote:
    for position, letter in enumerate(string.ascii_lowercase):
        btn_height = 15
        spacer = 5
        top = position*btn_height + spacer
        b = Button(rect=(10,top,105,btn_height), command=lambda l=letter:print_on_press(l), text=letter, **settings)
        btns.append(b)

Here is the full code including the button class
import pygame as pg
 
class Button(object):
    def __init__(self,rect,command,**kwargs):
        self.rect = pg.Rect(rect)
        self.command = command
        self.clicked = False
        self.hovered = False
        self.hover_text = None
        self.clicked_text = None
        self.process_kwargs(kwargs)
        self.render_text()
 
    def process_kwargs(self,kwargs):
        settings = {
            "color"             : pg.Color('red'),
            "text"              : None,
            "font"              : None, #pg.font.Font(None,16),
            "call_on_release"   : True,
            "hover_color"       : None,
            "clicked_color"     : None,
            "font_color"        : pg.Color("white"),
            "hover_font_color"  : None,
            "clicked_font_color": None,
            "click_sound"       : None,
            "hover_sound"       : None,
            'border_color'      : pg.Color('black'),
            'border_hover_color': pg.Color('yellow'),
            'disabled'          : False,
            'disabled_color'     : pg.Color('grey'),
            'radius'            : 3,
        }
        for kwarg in kwargs:
            if kwarg in settings:
                settings[kwarg] = kwargs[kwarg]
            else:
                raise AttributeError("{} has no keyword: {}".format(self.__class__.__name__, kwarg))
        self.__dict__.update(settings)
 
    def render_text(self):
        if self.text:
            if self.hover_font_color:
                color = self.hover_font_color
                self.hover_text = self.font.render(self.text,True,color)
            if self.clicked_font_color:
                color = self.clicked_font_color
                self.clicked_text = self.font.render(self.text,True,color)
            self.text = self.font.render(self.text,True,self.font_color)
 
    def get_event(self,event):
        if event.type == pg.MOUSEBUTTONDOWN and event.button == 1:
            self.on_click(event)
        elif event.type == pg.MOUSEBUTTONUP and event.button == 1:
            self.on_release(event)
 
    def on_click(self,event):
        if self.rect.collidepoint(event.pos):
            self.clicked = True
            if not self.call_on_release:
                self.function()
 
    def on_release(self,event):
        if self.clicked and self.call_on_release:
            #if user is still within button rect upon mouse release
            if self.rect.collidepoint(pg.mouse.get_pos()):
                self.command()
        self.clicked = False
 
    def check_hover(self):
        if self.rect.collidepoint(pg.mouse.get_pos()):
            if not self.hovered:
                self.hovered = True
                if self.hover_sound:
                    self.hover_sound.play()
        else:
            self.hovered = False
 
    def draw(self,surface):
        color = self.color
        text = self.text
        border = self.border_color
        self.check_hover()
        if not self.disabled:
            if self.clicked and self.clicked_color:
                color = self.clicked_color
                if self.clicked_font_color:
                    text = self.clicked_text
            elif self.hovered and self.hover_color:
                color = self.hover_color
                if self.hover_font_color:
                    text = self.hover_text
            if self.hovered and not self.clicked:
                border = self.border_hover_color
        else:
            color = self.disabled_color
         
        #if not self.rounded:
        #    surface.fill(border,self.rect)
        #    surface.fill(color,self.rect.inflate(-4,-4))
        #else:
        if self.radius:
            rad = self.radius
        else:
            rad = 0
        self.round_rect(surface, self.rect , border, rad, 1, color)
        if self.text:
            text_rect = text.get_rect(center=self.rect.center)
            surface.blit(text,text_rect)
             
             
    def round_rect(self, surface, rect, color, rad=20, border=0, inside=(0,0,0,0)):
        rect = pg.Rect(rect)
        zeroed_rect = rect.copy()
        zeroed_rect.topleft = 0,0
        image = pg.Surface(rect.size).convert_alpha()
        image.fill((0,0,0,0))
        self._render_region(image, zeroed_rect, color, rad)
        if border:
            zeroed_rect.inflate_ip(-2*border, -2*border)
            self._render_region(image, zeroed_rect, inside, rad)
        surface.blit(image, rect)
 
 
    def _render_region(self, image, rect, color, rad):
        corners = rect.inflate(-2*rad, -2*rad)
        for attribute in ("topleft", "topright", "bottomleft", "bottomright"):
            pg.draw.circle(image, color, getattr(corners,attribute), rad)
        image.fill(color, rect.inflate(-2*rad,0))
        image.fill(color, rect.inflate(0,-2*rad))
         
    def update(self):
        #for completeness
        pass
         
if __name__ == '__main__':
    #code pertaining to the main program not in the button module
    import string
    pg.init()
    screen = pg.display.set_mode((600,400))
    screen_rect = screen.get_rect()
    done = False
 
    def print_on_press(letter):
        print('{} pressed'.format(letter))
         
    settings = {
        "clicked_font_color" : (0,0,0),
        "hover_font_color"   : (205,195, 100),
        'font'               : pg.font.Font(None,16),
        'font_color'         : (255,255,255),
        'border_color'       : (0,0,0),
    }
    btns = []
    for position, letter in enumerate(string.ascii_lowercase):
        btn_height = 15
        spacer = 5
        top = position*btn_height + spacer
        b = Button(rect=(10,top,105,btn_height), command=lambda l=letter:print_on_press(l), text=letter, **settings)
        btns.append(b)
 
    while not done:
        mouse = pg.mouse.get_pos()
        for event in pg.event.get():
            if event.type == pg.QUIT:
                done = True
            for btn in btns:
                btn.get_event(event)
        for btn in btns:
            btn.draw(screen)
        pg.display.update()
and here is a modified version of the callback function comparing with a database that only has a. You would just need to modify it accordingly to how your database is.
if __name__ == '__main__':
    #code pertaining to the main program not in the button module
    import string
    pg.init()
    screen = pg.display.set_mode((600,400))
    screen_rect = screen.get_rect()
    done = False
    NAMES = {
        'a':['Alex','Andy']
    }
 
    def print_on_press(letter):
        print('{} pressed'.format(letter))
        try:
            print(NAMES[letter])
        except KeyError:
            print('There are no names in the database starting with {}'.format(letter))
         
    settings = {
        "clicked_font_color" : (0,0,0),
        "hover_font_color"   : (205,195, 100),
        'font'               : pg.font.Font(None,16),
        'font_color'         : (255,255,255),
        'border_color'       : (0,0,0),
    }
    btns = []
    for position, letter in enumerate(string.ascii_lowercase):
        btn_height = 15
        spacer = 5
        top = position*btn_height + spacer
        b = Button(rect=(10,top,105,btn_height), command=lambda l=letter:print_on_press(l), text=letter, **settings)
        btns.append(b)
 
    while not done:
        mouse = pg.mouse.get_pos()
        for event in pg.event.get():
            if event.type == pg.QUIT:
                done = True
            for btn in btns:
                btn.get_event(event)
        for btn in btns:
            btn.draw(screen)
        pg.display.update()
Recommended Tutorials:
Reply
#3
@mzmingle
What I see so far. This would be easier done in tkinter, gtk, wxpython, or qt.
pygame is not a widget toolkit. I wish there was a widget toolkit for SDL that pygame could inherit from.
PyGizmo is my 10th attempt. It's a work in progress.

example.
import pygame
import string

class SimpleButton:
    def __init__(self, caption, position, callback, font, color, bg_color, hover_color):
        self.image = font.render(caption, 1, color)
        self.rect = self.image.get_rect()
        self.rect.topleft = position
        self.bg_color = bg_color
        self.hover_color = hover_color
        self.mouse_hovering = False
        self.callback = callback
        self.caption = caption

    def draw(self, surface):
        if self.mouse_hovering:
            surface.fill(self.hover_color, self.rect)
        else:
            surface.fill(self.bg_color, self.rect)

        surface.blit(self.image, self.rect)

    def on_mousemotion(self, event):
        self.mouse_hovering = self.rect.collidepoint(event.pos)

    def on_click(self, event):
        if self.mouse_hovering:
            self.callback(self)

def button_callback(button):
    print(button.caption)

def main():
    pygame.init()
    pygame.display.set_caption('Simple Button Example')
    surface = pygame.display.set_mode((800, 600))
    clock = pygame.time.Clock()

    buttons = []
    button_layout = (pygame.font.Font(None, 24),
                     pygame.Color('white'),
                     pygame.Color('navy'),
                     pygame.Color('dodgerblue'))

    for enum, letter in enumerate(string.ascii_lowercase):
        x = enum % 13
        pos = x * 20 + 100, enum // 13 * 30 + 40
        buttons.append(SimpleButton(letter, pos, button_callback, *button_layout))

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

            elif event.type == pygame.MOUSEMOTION:
                for button in buttons:
                    button.on_mousemotion(event)

            elif event.type == pygame.MOUSEBUTTONUP:
                if event.button == 1:
                    for button in buttons:
                        button.on_click(event)

        surface.fill(pygame.Color('black'))
        for button in buttons:
            button.draw(surface)

        pygame.display.flip()
        clock.tick(20)

    pygame.quit()

main()
99 percent of computer problems exists between chair and keyboard.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020