Page 1 of 1

Buttons and Sliders in Pygame

#1 DK3250  Icon User is offline

  • Pythonian
  • member icon

Reputation: 287
  • View blog
  • Posts: 908
  • Joined: 27-December 13

Posted 19 February 2017 - 02:24 PM

***
To the review-team – please delete before approval.
This tutorial is my last planned one – I know the frequency have been very high lately.
I hope you don't feel too 'spamed'.
***


Buttons and Sliders in Pygame


This tutorial is made with Python 3.4 and Pygame 1.9

Interaction with a graphical program is often by buttons or sliders; clicked on, or moved by, the mouse.
In this tutorial I'll show and explain two small programs covering a general button object and a general slider object.

The next two sections are by and large copied from one of my other tutorials, you can skip them if you want.

Handling of Graphics in Pygame

Before turning to the game code, I want to introduce how graphics is handled in Pygame.
A graphic element normally has two parts: A 'Surface' and a 'Rect'.
Throughout this tutorial I'll use the names 'Surface' and 'Rect' when talking about the abstract objects. In real code any valid name can be assigned to the ‘Surface' and 'Rect' variables.

The Surface handles all about size and appearance including colorization, transparency and many specialized task (not relevant to this tutorial).
The Rect handles all about position and collisions (and also many other tasks). The Rect has a number of position handlers like: Rect.center, Rect.midbottom, Rect.topright.
From a Surface, a Rect can be defined using
Rect = Surface.get_rect()
Rect.center = (my_x, my_y)
or:
Rect = Surface.get_rect(center=(my_x, my_y))


Movement of the sprites are handled by assigning to the Rect handlers:

Rect.centerx += 10  # moves the sprite 10 pixels to the right


After such movement, all Rect handlers are automatically updated.
When blitting one surface onto another, the blit() function is used:

screen.blit(Surface, Rect)  # blits 'Surface' onto 'screen' according to the 'Rect' position


Collisions

One of many things you can do with Rect is to check for overlap (collision) with another Rect or a list of other Rects.

To check for collision between two single Rects, you use the colliderect() method:

if rect_1.colliderect(rect_2):
    # collision
else:
    # no collision


The statement ”rect_1.colliderect(rect_2)” returns either True or False.
To check for collision between a single Rect and a group of Rects, you use the collidelist() method:

result = Rect.collidelist(Rect_list)


The variable result will now have the value -1 if no collisions happened or a value indicating which Rect from the list that collided with the single one.

To check for collision between a Rect and a single point (mouse position), the collidepoint() method is used:

if Rect.collidepoint(position):
    # collision
else:
    # no collision


The collidepoint() method will return True/False depending of whether position is overlapping the Rect or not.
For buttons and sliders, this is the most important type of collision detection.

Buttons

It is often required to have a button in graphic programs. The button will often indicate its function by a small text in the button surface. When the button is pressed, an associated action takes place.

The code below generates two buttons; on activation they will produce a simple print in the console. The button's action, however, can be changed to whatever you like.

import pygame, sys
pygame.init()

WHITE = (255, 255, 255)
GREY = (200, 200, 200)
BLACK = (0, 0, 0)


class Button():
    def __init__(self, txt, location, action, bg=WHITE, fg=BLACK, size=(80, 30), font_name="Segoe Print", font_size=16):
        self.color = bg  # the static (normal) color
        self.bg = bg  # actual background color, can change on mouseover
        self.fg = fg  # text color
        self.size = size

        self.font = pygame.font.SysFont(font_name, font_size)
        self.txt = txt
        self.txt_surf = self.font.render(self.txt, 1, self.fg)
        self.txt_rect = self.txt_surf.get_rect(center=[s//2 for s in self.size])

        self.surface = pygame.surface.Surface(size)
        self.rect = self.surface.get_rect(center=location)

        self.call_back_ = action

    def draw(self):
        self.mouseover()

        self.surface.fill(self.bg)
        self.surface.blit(self.txt_surf, self.txt_rect)
        screen.blit(self.surface, self.rect)

    def mouseover(self):
        self.bg = self.color
        pos = pygame.mouse.get_pos()
        if self.rect.collidepoint(pos):
            self.bg = GREY  # mouseover color

    def call_back(self):
        self.call_back_()


def my_great_function():
    print("Great! " * 5)


def my_fantastic_function():
    print("Fantastic! " * 5)


def mousebuttondown():
    pos = pygame.mouse.get_pos()
    for button in buttons:
        if button.rect.collidepoint(pos):
            button.call_back()


screen = pygame.display.set_mode((120, 100))
RED = (255, 0, 0)
BLUE = (0, 0, 255)

button_01 = Button("Great!", (60, 30), my_great_function)
button_02 = Button("Fantastic!", (60, 70), my_fantastic_function, bg=(50, 200, 20))
buttons = [button_01, button_02]

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mousebuttondown()

    for button in buttons:
        button.draw()

    pygame.display.flip()
    pygame.time.wait(40)


The Button object is generated with three mandatory arguments: txt, location and action.
- txt, is a string written to the button surface, the string can be empty.
- location, is the position of the button on the screen
- action, is the function called upon button press
To simplify the Button call, a number of arguments has default values:
- bg, is the background color
- fg, is the foreground color
- size, is the button size
- font_name, is the font used for button text
- font_size, is …, well, the font size

The Button initialization code has four small groups:
color and size
handling of text
the surface and rect of the button
the action of the button

The draw() method calls the mouseover(), then updates the button surface with color and text and finally blit to screen.
The mouseover() method changes the button background temporarily if the mouse is 'hoovering' over the button. The mouse-over color is here fixed, but you can make it a variable if you wish so.

In the general call_back() method, the individual call_back_() function is called (notice the extra underscore in the name).

In the code follows two silly functions; they demonstrate the button actions, and must be modified to serve your needs.

The mousebuttondown() function is only to keep the game loop as clean as possible; it checks if a button is hit on mouse click and activates the relevant button action.

I keep the Button instances in a list; it makes the game loop simpler.

I hope this explains how buttons can be made using Pygame.
You can modify this code; maybe you want even the foreground color to change on mouse-over, or you want different mouse-over color for different buttons – feel free to shape this little code as you like.

Sliders


Sliders are themselves a bit more complicated than Buttons and furthermore my demo program is in its own right somewhat complicated. I will explain the Sliders in detail; the Gradient class is covered by this snippet: http://www.dreaminco...le-value-input/
The mathematics in the wave() function, however, will not be explained as it is out of scope. If you want it explained, feel free to ask.

The full demo code is

import pygame, math, sys
pygame.init()

X = 900  # screen width
Y = 600  # screen height

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 50, 50)
YELLOW = (255, 255, 0)
GREEN = (0, 255, 50)
BLUE = (50, 50, 255)
GREY = (200, 200, 200)
ORANGE = (200, 100, 50)
CYAN = (0, 255, 255)
MAGENTA = (255, 0, 255)
TRANS = (1, 1, 1)

flow = False  # controls type of color flow


class Gradient():
    def __init__(self, palette, maximum):
        self.COLORS = palette
        self.N = len(self.COLORS)
        self.SECTION = maximum // (self.N - 1)

    def gradient(self, x):
        """
        Returns a smooth color profile with only a single input value.
        The color scheme is determinated by the list 'self.COLORS'
        """
        i = x // self.SECTION
        fraction = (x % self.SECTION) / self.SECTION
        c1 = self.COLORS[i % self.N]
        c2 = self.COLORS[(i+1) % self.N]
        col = [0, 0, 0]
        for k in range(3):
            col[k] = (c2[k] - c1[k]) * fraction + c1[k]
        return col


def wave(num):
    """
    The basic calculating and drawing function.
    The internal function is 'cosine' >> (x, y) values.

    The function uses slider values to variate the output.
    Slider values are defined by <slider name>.val
    """
    for x in range(0, X+10, int(jmp.val)):

        # Calculations #
        ang_1 = (x + num) * math.pi * freq.val / 180
        ang_2 = ang_1 - phase.val
        cos_1 = math.cos(ang_1)
        cos_2 = math.cos(ang_2)

        y_1 = int(cos_1 * size.val) + 250
        y_2 = int(cos_2 * size.val) + 250

        radius_1 = int(pen.val + math.sin(ang_1 + focus.val) * pen.val / 2)
        radius_2 = int(pen.val + math.sin(ang_2 + focus.val) * pen.val / 2)

        # Drawing #
        if radius_1 > radius_2:  # draw the smaller circle before the larger one
            pygame.draw.circle(screen, xcolor(int(x + X//2) + num * flow), (x, y_2), radius_2, 0)
            pygame.draw.circle(screen, xcolor(x + num * flow), (x, y_1), radius_1, 0)
        else:
            pygame.draw.circle(screen, xcolor(x + num * flow), (x, y_1), radius_1, 0)
            pygame.draw.circle(screen, xcolor(int(x + X//2) + num * flow), (x, y_2), radius_2, 0)


class Slider():
    def __init__(self, name, val, maxi, mini, pos):
        self.val = val  # start value
        self.maxi = maxi  # maximum at slider position right
        self.mini = mini  # minimum at slider position left
        self.xpos = pos  # x-location on screen
        self.ypos = 550
        self.surf = pygame.surface.Surface((100, 50))
        self.hit = False  # the hit attribute indicates slider movement due to mouse interaction

        self.txt_surf = font.render(name, 1, BLACK)
        self.txt_rect = self.txt_surf.get_rect(center=(50, 15))

        # Static graphics - slider background #
        self.surf.fill((100, 100, 100))
        pygame.draw.rect(self.surf, GREY, [0, 0, 100, 50], 3)
        pygame.draw.rect(self.surf, ORANGE, [10, 10, 80, 10], 0)
        pygame.draw.rect(self.surf, WHITE, [10, 30, 80, 5], 0)

        self.surf.blit(self.txt_surf, self.txt_rect)  # this surface never changes

        # dynamic graphics - button surface #
        self.button_surf = pygame.surface.Surface((20, 20))
        self.button_surf.fill(TRANS)
        self.button_surf.set_colorkey(TRANS)
        pygame.draw.circle(self.button_surf, BLACK, (10, 10), 6, 0)
        pygame.draw.circle(self.button_surf, ORANGE, (10, 10), 4, 0)

    def draw(self):
        """ Combination of static and dynamic graphics in a copy of
    the basic slide surface
    """
        # static
        surf = self.surf.copy()

        # dynamic
        pos = (10+int((self.val-self.mini)/(self.maxi-self.mini)*80), 33)
        self.button_rect = self.button_surf.get_rect(center=pos)
        surf.blit(self.button_surf, self.button_rect)
        self.button_rect.move_ip(self.xpos, self.ypos)  # move of button box to correct screen position

        # screen
        screen.blit(surf, (self.xpos, self.ypos))

    def move(self):
        """
    The dynamic part; reacts to movement of the slider button.
    """
        self.val = (pygame.mouse.get_pos()[0] - self.xpos - 10) / 80 * (self.maxi - self.mini) + self.mini
        if self.val < self.mini:
            self.val = self.mini
        if self.val > self.maxi:
            self.val = self.maxi


font = pygame.font.SysFont("Verdana", 12)
screen = pygame.display.set_mode((X, Y))
clock = pygame.time.Clock()

COLORS = [MAGENTA, RED, YELLOW, GREEN, CYAN, BLUE]
xcolor = Gradient(COLORS, X).gradient

pen = Slider("Pen", 10, 15, 1, 25)
freq = Slider("Freq", 1, 3, 0.2, 150)
jmp = Slider("Jump", 10, 20, 1, 275)
size = Slider("Size", 200, 200, 20, 400)
focus = Slider("Focus", 0, 6, 0, 525)
phase = Slider("Phase", 3.14, 6, 0.3, 650)
speed = Slider("Speed", 50, 150, 10, 775)
slides = [pen, freq, jmp, size, focus, phase, speed]

num = 0

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        elif event.type == pygame.MOUSEBUTTONDOWN:
            pos = pygame.mouse.get_pos()
            for s in slides:
                if s.button_rect.collidepoint(pos):
                    s.hit = True
        elif event.type == pygame.MOUSEBUTTONUP:
            for s in slides:
                s.hit = False

    # Move slides
    for s in slides:
        if s.hit:
            s.move()

    # Update screen
    screen.fill(BLACK)
    num += 2
    wave(num)

    for s in slides:
        s.draw()

    pygame.display.flip()
    clock.tick(speed.val)


Please run the program and test the slider function.

The Slider object is generated with five arguments:
name, a string identifying the slider to the user
- val, the initial slider value
- maxi, the maximum slider value
- mini, the minimum slider value
- pos, the x-position of the slider (in this code y-position is fixed)

The Slider has two parts, a static part constituting all the graphics and text; and a dynamic part which is only the small slider button that can be manipulated by the mouse. Here I'll focus only on the five lines of code used for the dynamic part:

self.button = pygame.surface.Surface((20, 20))
self.button.fill(TRANS)
self.button.set_colorkey(TRANS)
pygame.draw.circle(self.button, BLACK, (10, 10), 6, 0)
pygame.draw.circle(self.button, ORANGE, (10, 10), 4, 0)


First a small surface is made; this surface is filled with a special color (I often use (1, 1, 1)) which is then declared transparent by use of the set_colorkey() method. This is to ensure that only the circular button is visible on the general slide background. The two last lines just draw the circular graphic on the button surface.

Now we are ready to blit the button onto the static slider background and the finished graphic to the screen. We don't want to ruin the static slider background, so we start making a copy:

def draw(self):
    """ Combination of static and dynamic graphics in a copy of
the basic slide surface
"""
    # static
    surf = self.surf.copy()

    # dynamic
    pos = (10+int((self.val-self.mini)/(self.maxi-self.mini)*80), 33)
    self.button_rect = self.button_surf.get_rect(center=pos)
    surf.blit(self.button_surf, self.button_rect)
    self.button_rect.move_ip(self.xpos, self.ypos)  # move of button box to correct screen position

    # screen
    screen.blit(surf, (self.xpos, self.ypos))


ok, first we make a copy of the slider surface.

Then the button rect is calculated using the actual slider value:
pos = (10+int((self.val-self.mini)/(self.maxi-self.mini)*80)
The fraction (self.val-self.mini)/(self.maxi-self.mini) calculates how much of the full slider window is taken up by the current value. The factor of 80 is the slide distance (in pixels) from minimum to maximum and the 10 is to center the slider window in the slider graphic.

We blit the button to the surface and we move the button rect in-place relative to the slider position. Without this movement the button graphic will appear on the slider, but the rect will be given relative to the screen (near the top left corner).

These six lines is really the 'secret' of the slider function.

The move() method is called from the game loop when a mouse click is detected on a slider button.
Here the slider value is updated according to the mouse position but limited by the mini and maxi values.

The last part of the code is instantiation of the 7 sliders and a list enabling handling them in for-loops.
The game-loop itself is standard and will not be commented here.

Comment:
The variable 'flow' in line 19 is hard-coded but can be set to True; this will change the color-flow pattern such that the color positions are non-static. I skipped explaining it, as it only relates to the wave function().

Is This A Good Question/Topic? 0
  • +

Page 1 of 1