Pygame does not come with widget toolkits such as buttons, sliders, input boxes, etc. You have to create them from scratch. Writing a single button for example is easy, but creating a complete UI framework and maintaining it is a massive task. There are a few frameworks that people have created over time (sgc, pgu, GooeyPy, Albow, ocempgui, PygameGUILib, and even more). I think almost all have been abandoned after some point in time, if not all of them. The newer ones dont have much and your not sure if they will be maintained and added to.
So most of the time, pygamers run their own code. If you need a button then create one, if you need a label create one, etc. If you dont need to embed an image, dont bother adding the code to handle it, etc.
Button
A basic button can be done with just a rectangle that gets when the mouse button clicks. Fancy stuff can be added if needed later. We are making this a class because we want to make a button that can be used in a variety of circumstances, not this one time.
The following just creates a red rectangle that identifies a left mouse click and prints it and thats it. But it will give you a basic idea of what we need.
The get_event method identifies the mouse button and the correct button.
The on_click method handles what happens if the mouse was clicked. First it checks if the mouse position was inside the buttons rect, if so executes the command. You could also instead of passing the event through, do pygame.mouse.get_pos()instead of event.pos.
Currently our button does what we want. But it doesnt look good at all. To do this we need to add some things. Buttons can have a LOT of effects. This means we need to change the code up a bit to account for all these things. We are going to add a method called process_kwargs that 1) stores default settings and 2) allows us to change them on the fly for each button created. That is the point in classes after all.
EXAMPLE: When we create our button we are using all the defaults.
The rest of the changes were to draw method for changing the color of the button if the mouse is colliding with the rect (AKA hovering) and drawing the text. The dunder init method adds a couple lines to create the button text. It creates the font object with the proper settings and aligns the text to the center. Here i am assuming you want it in the center. But you could just make a new entry in the settings dictionary for font position if you wanted...or for anything. And your button class customization will expand and expand.
The following is a more fully fledged button class with an example to be runnable
You can extend this beyond one button quite easily. The following uses the previous code snippets button class with a modified main code after
So most of the time, pygamers run their own code. If you need a button then create one, if you need a label create one, etc. If you dont need to embed an image, dont bother adding the code to handle it, etc.
Button
A basic button can be done with just a rectangle that gets when the mouse button clicks. Fancy stuff can be added if needed later. We are making this a class because we want to make a button that can be used in a variety of circumstances, not this one time.
The following just creates a red rectangle that identifies a left mouse click and prints it and thats it. But it will give you a basic idea of what we need.
import pygame as pg class Button: def __init__(self, rect, command): self.rect = pg.Rect(rect) self.image = pg.Surface(self.rect.size).convert() self.image.fill((255,0,0)) self.function = command def get_event(self, event): if event.type == pg.MOUSEBUTTONDOWN and event.button == 1: self.on_click(event) def on_click(self, event): if self.rect.collidepoint(event.pos): self.function() def draw(self, surf): surf.blit(self.image, self.rect) def button_was_pressed(): print('button_was_pressed') screen = pg.display.set_mode((800,600)) done = False btn = Button(rect=(50,50,105,25), command=button_was_pressed) while not done: for event in pg.event.get(): if event.type == pg.QUIT: done = True btn.get_event(event) btn.draw(screen) pg.display.update()In the dunder init method we take the rect coordinates; left and top position length and height, create a surface for that rect, color it via fill, and pass the command to an attribute.
The get_event method identifies the mouse button and the correct button.
The on_click method handles what happens if the mouse was clicked. First it checks if the mouse position was inside the buttons rect, if so executes the command. You could also instead of passing the event through, do pygame.mouse.get_pos()instead of event.pos.
Currently our button does what we want. But it doesnt look good at all. To do this we need to add some things. Buttons can have a LOT of effects. This means we need to change the code up a bit to account for all these things. We are going to add a method called process_kwargs that 1) stores default settings and 2) allows us to change them on the fly for each button created. That is the point in classes after all.
import pygame as pg pg.init() class Button: def __init__(self, rect, command, **kwargs): self.process_kwargs(kwargs) self.rect = pg.Rect(rect) self.image = pg.Surface(self.rect.size).convert() self.function = command self.text = self.font.render(self.text,True,self.font_color) self.text_rect = self.text.get_rect(center=self.rect.center) def process_kwargs(self, kwargs): settings = { 'color' :pg.Color('red'), 'text' :'default', 'font' :pg.font.SysFont('Arial', 16), 'hover_color' :(200,0,0), 'font_color' :pg.Color('white'), } 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 get_event(self): if event.type == pg.MOUSEBUTTONDOWN and event.button == 1: self.on_click() def on_click(self): if self.is_hovering(): self.function() def is_hovering(self): if self.rect.collidepoint(pg.mouse.get_pos()): return True def draw(self, surf): if self.is_hovering(): self.image.fill(self.hover_color) else: self.image.fill(self.color) surf.blit(self.image, self.rect) surf.blit(self.text, self.text_rect) def button_was_pressed(): print('button_was_pressed') screen = pg.display.set_mode((800,600)) done = False btn = Button((50,50,105,25), button_was_pressed) while not done: for event in pg.event.get(): if event.type == pg.QUIT: done = True btn.get_event() btn.draw(screen) pg.display.update()process_kwargs() has a settings dictionary that is flexible and stores the defaults. More importantly it shows what arguments you can give the button to customize it on a per button basis. All of these keys in this dictionary get implemented into the class as attributes of the class via this line
self.__dict__.update(settings)
. After this point there is a self.color and its value is pg.Color('red'). And the same with every other key:entry pair. By having this method you can change any one of these with an argument or pass a dictionary of many to change some or all them. This is very useful as buttons often have different effects from one another. EXAMPLE: When we create our button we are using all the defaults.
btn = Button((50,50,105,25), button_was_pressed)
. However we can easily change the text by adding a single argument now. btn = Button((50,50,105,25), button_was_pressed, text='Press Me')
. This simply changes the text of the button. If i wanted to change many settings, i can keep a dictionary of button customizations somewhere, and pass that in instead.btn_settings = { 'text':'Press Me', 'color':pg.Color('grey'), 'font_color':pg.Color('red'), } btn = Button((50,50,105,25), button_was_pressed, **btn_settings)This will update the settings changed, and keep the defaults of those not changed. If you try to give an argument that is not in the dictionary (or other attribute) then you are going to get a custom AttributeError.
btn = Button((50,50,105,25), button_was_pressed, click_sound='button.mp3'
will output the errorError:Traceback (most recent call last):
File "test3.py", line 53, in <module>
btn = Button((50,50,105,25), button_was_pressed, click_sound='button.mp3')
File "test3.py", line 6, in __init__
self.process_kwargs(kwargs)
File "test3.py", line 25, in process_kwargs
raise AttributeError("{} has no keyword: {}".format(self.__class__.__name__, kwarg))
AttributeError: Button has no keyword: click_sound
And now your slowly on your way to creating your own UI Toolkit. The rest of the changes were to draw method for changing the color of the button if the mouse is colliding with the rect (AKA hovering) and drawing the text. The dunder init method adds a couple lines to create the button text. It creates the font object with the proper settings and aligns the text to the center. Here i am assuming you want it in the center. But you could just make a new entry in the settings dictionary for font position if you wanted...or for anything. And your button class customization will expand and expand.
The following is a more fully fledged button class with an example to be runnable
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__': pg.init() screen = pg.display.set_mode((600,400)) screen_rect = screen.get_rect() done = False def print_on_press(): print('button pressed') 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), } btn = Button(rect=(10,10,105,25), command=print_on_press, text='Press Me', **settings) while not done: mouse = pg.mouse.get_pos() for event in pg.event.get(): if event.type == pg.QUIT: done = True btn.get_event(event) btn.draw(screen) pg.display.update()
You can extend this beyond one button quite easily. The following uses the previous code snippets button class with a modified main code after
__name__ == '__main__':
to create a button for each letter of the alphabet programmatically. You do not need to change the button class at all. That is why you create a button class. 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. 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()
Recommended Tutorials: