Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] User Interface
#1
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.

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 error
Error:
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:
Reply
#2
TextBox
You can simply make a white rectangle and input text over top of it. That is by far the simplest approach. However TextBox's have expected features such as a blinking prompt for location, text wrap behind the textbox, border, visible background, text, Return Key integration on command, backspace hold deleting all text, etc. All of this requires handling.

The following example does this all. It is similar to the structure of the Button class as before. The line at the endif __name__ == '__main__': means the code in it block will only be executed if it ran directly, and not imported as a module. This code is the example of running the TextBox class. The only difference in its use compared to the Button class, is it requires an extra update call.
the problem is you are expecting your function to return the value, but you also have the main loop in the function that is expected to return it. Its not going to work in any sensible method and i would suggest to redo it.

Here is an example of a textbox class.
import pygame as pg
import string

class TextBox(object):
    def __init__(self,rect,**kwargs):
        '''
        Optional kwargs and their defaults:
            "id" : None,
            "command" : None,
                function to execute upon enter key
                Callback for command takes 2 args, id and final (the string in the textbox)
            "active" : True,
                textbox active on opening of window
            "color" : pg.Color("white"),
                background color
            "font_color" : pg.Color("black"),
            "outline_color" : pg.Color("black"),
            "outline_width" : 2,
            "active_color" : pg.Color("blue"),
            "font" : pg.font.Font(None, self.rect.height+4),
            "clear_on_enter" : False,
                remove text upon enter
            "inactive_on_enter" : True
            "blink_speed": 500
                prompt blink time in milliseconds
            "delete_speed": 500
                backspace held clear speed in milliseconds
            
        Values:
            self.rect = pg.Rect(rect)
            self.buffer = []
            self.final = None
            self.rendered = None
            self.render_rect = None
            self.render_area = None
            self.blink = True
            self.blink_timer = 0.0
            self.delete_timer = 0.0
            self.accepted = string.ascii_letters+string.digits+string.punctuation+" "
        '''
        self.rect = pg.Rect(rect)
        self.buffer = []
        self.final = None
        self.rendered = None
        self.render_rect = None
        self.render_area = None
        self.blink = True
        self.blink_timer = 0.0
        self.delete_timer = 0.0
        self.accepted = string.ascii_letters+string.digits+string.punctuation+" "
        self.process_kwargs(kwargs)

    def process_kwargs(self,kwargs):
        defaults = {"id" : None,
                    "command" : None,
                    "active" : True,
                    "color" : pg.Color("white"),
                    "font_color" : pg.Color("black"),
                    "outline_color" : pg.Color("black"),
                    "outline_width" : 2,
                    "active_color" : pg.Color("blue"),
                    "font" : pg.font.Font(None, self.rect.height+4),
                    "clear_on_enter" : False,
                    "inactive_on_enter" : True,
                    "blink_speed": 500,
                    "delete_speed": 75}
        for kwarg in kwargs:
            if kwarg in defaults:
                defaults[kwarg] = kwargs[kwarg]
            else:
                raise KeyError("TextBox accepts no keyword {}.".format(kwarg))
        self.__dict__.update(defaults)

    def get_event(self,event, mouse_pos=None):
        ''' Call this on your event loop
        
            for event in pg.event.get():
                TextBox.get_event(event)
        '''
        if event.type == pg.KEYDOWN and self.active:
            if event.key in (pg.K_RETURN,pg.K_KP_ENTER):
                self.execute()
            elif event.key == pg.K_BACKSPACE:
                if self.buffer:
                    self.buffer.pop()
            elif event.unicode in self.accepted:
                self.buffer.append(event.unicode)
        elif event.type == pg.MOUSEBUTTONDOWN and event.button == 1:
            if not mouse_pos:
                mouse_pos = pg.mouse.get_pos()
            self.active = self.rect.collidepoint(mouse_pos)

    def execute(self):
        if self.command:
            self.command(self.id,self.final)
        self.active = not self.inactive_on_enter
        if self.clear_on_enter:
            self.buffer = []
            
    def switch_blink(self):
        if pg.time.get_ticks()-self.blink_timer > self.blink_speed:
            self.blink = not self.blink
            self.blink_timer = pg.time.get_ticks()

    def update(self):
        '''
        Call once on your main game loop
        '''
        new = "".join(self.buffer)
        if new != self.final:
            self.final = new
            self.rendered = self.font.render(self.final, True, self.font_color)
            self.render_rect = self.rendered.get_rect(x=self.rect.x+2,
                                                      centery=self.rect.centery)
            if self.render_rect.width > self.rect.width-6:
                offset = self.render_rect.width-(self.rect.width-6)
                self.render_area = pg.Rect(offset,0,self.rect.width-6,
                                           self.render_rect.height)
            else:
                self.render_area = self.rendered.get_rect(topleft=(0,0))
        self.switch_blink()
        self.handle_held_backspace()
        
    def handle_held_backspace(self):
        if pg.time.get_ticks()-self.delete_timer > self.delete_speed:
            self.delete_timer = pg.time.get_ticks()
            keys = pg.key.get_pressed()
            if keys[pg.K_BACKSPACE]:
                if self.buffer:
                    self.buffer.pop()

    def draw(self,surface):
        '''
        Call once on your main game loop
        '''
        outline_color = self.active_color if self.active else self.outline_color
        outline = self.rect.inflate(self.outline_width*2,self.outline_width*2)
        surface.fill(outline_color,outline)
        surface.fill(self.color,self.rect)
        if self.rendered:
            surface.blit(self.rendered,self.render_rect,self.render_area)
        if self.blink and self.active:
            curse = self.render_area.copy()
            curse.topleft = self.render_rect.topleft
            surface.fill(self.font_color,(curse.right+1,curse.y,2,curse.h))
and here is an example of that class executed to have a username and password input boxes.
pg.init()
screen = pg.display.set_mode((600,400))
done = False

def name_on_enter(id, final):
    print('enter pressed, username is "{}"'.format(final))
    
def pass_on_enter(id, final):
    print('enter pressed, password is "{}"'.format(final))

username_settings = {
    "command" : name_on_enter,
    "inactive_on_enter" : False,
}
password_settings = {
    "command" : pass_on_enter,
    "inactive_on_enter" : False,
}

name_entry = TextBox(rect=(70,100,150,30), **username_settings)
pass_entry = TextBox(rect=(70,200,150,30), **password_settings)

while not done:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True
        name_entry.get_event(event)
        pass_entry.get_event(event)
    name_entry.update()
    pass_entry.update()
    name_entry.draw(screen)
    pass_entry.draw(screen)
    pg.display.update()
It functions and acts like a text box. Each textbox can take focus. Each textbox can retrieve its content. When you hit enter, it executes the function assigned via the command and passes the content of the textbox at that time to that function. So the callback function will have an ID and content as args like:
Quote:
def print_on_enter(id, final):
    print('enter pressed, textbox contains "{}"'.format(final))

If you had more than one textbox requirement i would loop all the textbox objects to obtain their current values, and save them in a dict to be compared to the database. This would be handled by a callback function of a button. TO BE CONTINUED
Recommended Tutorials:
Reply
#3
here is an example of combining the button and textbox to get all textbox object values at once.

import pygame as pg
import string



class TextBox(object):
    def __init__(self,rect,**kwargs):
        '''
        Optional kwargs and their defaults:
            "id" : None,
            "command" : None,
                function to execute upon enter key
                Callback for command takes 2 args, id and final (the string in the textbox)
            "active" : True,
                textbox active on opening of window
            "color" : pg.Color("white"),
                background color
            "font_color" : pg.Color("black"),
            "outline_color" : pg.Color("black"),
            "outline_width" : 2,
            "active_color" : pg.Color("blue"),
            "font" : pg.font.Font(None, self.rect.height+4),
            "clear_on_enter" : False,
                remove text upon enter
            "inactive_on_enter" : True
            "blink_speed": 500
                prompt blink time in milliseconds
            "delete_speed": 500
                backspace held clear speed in milliseconds
            
        Values:
            self.rect = pg.Rect(rect)
            self.buffer = []
            self.final = None
            self.rendered = None
            self.render_rect = None
            self.render_area = None
            self.blink = True
            self.blink_timer = 0.0
            self.delete_timer = 0.0
            self.accepted = string.ascii_letters+string.digits+string.punctuation+" "
        '''
        self.rect = pg.Rect(rect)
        self.buffer = []
        self.final = None
        self.rendered = None
        self.render_rect = None
        self.render_area = None
        self.blink = True
        self.blink_timer = 0.0
        self.delete_timer = 0.0
        self.accepted = string.ascii_letters+string.digits+string.punctuation+" "
        self.process_kwargs(kwargs)

    def process_kwargs(self,kwargs):
        defaults = {"id" : None,
                    "command" : None,
                    "active" : True,
                    "color" : pg.Color("white"),
                    "font_color" : pg.Color("black"),
                    "outline_color" : pg.Color("black"),
                    "outline_width" : 2,
                    "active_color" : pg.Color("blue"),
                    "font" : pg.font.Font(None, self.rect.height+4),
                    "clear_on_enter" : False,
                    "inactive_on_enter" : True,
                    "blink_speed": 500,
                    "delete_speed": 75}
        for kwarg in kwargs:
            if kwarg in defaults:
                defaults[kwarg] = kwargs[kwarg]
            else:
                raise KeyError("TextBox accepts no keyword {}.".format(kwarg))
        self.__dict__.update(defaults)

    def get_event(self,event, mouse_pos=None):
        ''' Call this on your event loop
        
            for event in pg.event.get():
                TextBox.get_event(event)
        '''
        if event.type == pg.KEYDOWN and self.active:
            if event.key in (pg.K_RETURN,pg.K_KP_ENTER):
                self.execute()
            elif event.key == pg.K_BACKSPACE:
                if self.buffer:
                    self.buffer.pop()
            elif event.unicode in self.accepted:
                self.buffer.append(event.unicode)
        elif event.type == pg.MOUSEBUTTONDOWN and event.button == 1:
            if not mouse_pos:
                mouse_pos = pg.mouse.get_pos()
            self.active = self.rect.collidepoint(mouse_pos)

    def execute(self):
        if self.command:
            self.command(self.id,self.final)
        self.active = not self.inactive_on_enter
        if self.clear_on_enter:
            self.buffer = []
            
    def switch_blink(self):
        if pg.time.get_ticks()-self.blink_timer > self.blink_speed:
            self.blink = not self.blink
            self.blink_timer = pg.time.get_ticks()

    def update(self):
        '''
        Call once on your main game loop
        '''
        new = "".join(self.buffer)
        if new != self.final:
            self.final = new
            self.rendered = self.font.render(self.final, True, self.font_color)
            self.render_rect = self.rendered.get_rect(x=self.rect.x+2,
                                                      centery=self.rect.centery)
            if self.render_rect.width > self.rect.width-6:
                offset = self.render_rect.width-(self.rect.width-6)
                self.render_area = pg.Rect(offset,0,self.rect.width-6,
                                           self.render_rect.height)
            else:
                self.render_area = self.rendered.get_rect(topleft=(0,0))
        self.switch_blink()
        self.handle_held_backspace()
        
    def handle_held_backspace(self):
        if pg.time.get_ticks()-self.delete_timer > self.delete_speed:
            self.delete_timer = pg.time.get_ticks()
            keys = pg.key.get_pressed()
            if keys[pg.K_BACKSPACE]:
                if self.buffer:
                    self.buffer.pop()

    def draw(self,surface):
        '''
        Call once on your main game loop
        '''
        outline_color = self.active_color if self.active else self.outline_color
        outline = self.rect.inflate(self.outline_width*2,self.outline_width*2)
        surface.fill(outline_color,outline)
        surface.fill(self.color,self.rect)
        if self.rendered:
            surface.blit(self.rendered,self.render_rect,self.render_area)
        if self.blink and self.active:
            curse = self.render_area.copy()
            curse.topleft = self.render_rect.topleft
            surface.fill(self.font_color,(curse.right+1,curse.y,2,curse.h))
            
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









pg.init()
screen = pg.display.set_mode((600,400))
done = False

def name_on_enter(id, final):
    print('enter pressed, username is "{}"'.format(final))
    
def pass_on_enter(id, final):
    print('enter pressed, password is "{}"'.format(final))

username_settings = {
    "command" : name_on_enter,
    "inactive_on_enter" : False,
}
password_settings = {
    "command" : pass_on_enter,
    "inactive_on_enter" : False,
}
btn_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),
}

name_entry = TextBox(rect=(70,100,150,30), **username_settings)
pass_entry = TextBox(rect=(70,200,150,30), **password_settings)
tbs = [name_entry, pass_entry]

def get_textboxes(username, password):
    print('button pressed, username is "{}"'.format(username))
    print('button pressed, password is "{}"'.format(password))

btn = Button(rect=(70,300,105,25), command=lambda:get_textboxes(name_entry.final, pass_entry.final), text='OK', **btn_settings)

while not done:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True
        for tb in tbs:
            tb.get_event(event)
        btn.get_event(event)
    for tb in tbs:
        tb.update()
        tb.draw(screen)
    btn.draw(screen)
    pg.display.update()

Attached Files

Thumbnail(s)
   
Recommended Tutorials:
Reply


Forum Jump:

User Panel Messages

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