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
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.
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 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() - 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.
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().