Posts: 1,145
Threads: 114
Joined: Sep 2019
I've been playing around a bit with pygame and image buttons. When creating a button with the button class, I can't seem to get button text correct. It stacks on top of one another in all buttons. Each time I call the class should it not be a new instance of that class? Therefor should not the text be unique to that class. What am I missing here? Thanks for any incite.
import pygame
pygame.init()
pygame.font.init()
# Setup pygame screen
screen_size = (800, 600)
screen = pygame.display.set_mode(screen_size)
pygame.display.set_caption('Pygame Button')
# Setup some colors
screen_bg = 'ivory2'
# Set framerate
fps = 60
framerate = pygame.time.Clock()
# Load and define button images. Three states normal, hover, and pressed
normal = pygame.image.load('normal.png')
hover = pygame.image.load('hover.png')
pressed = pygame.image.load('pressed.png')
# change cursor on hover
hand = pygame.SYSTEM_CURSOR_HAND
# Create Button class
class Button:
def __init__(self, image, pos, callback, text='Default Text'):
'''
Create a animated button from images
self.callback is for a funtion for the button to do - set to None
'''
self.image = image
self.rect = self.image.get_rect(topleft=pos)
self.text = text
self.callback = callback
self.default()
def default(self):
font = pygame.font.SysFont('verdana', 16)
self.text_surf = font.render(self.text, True, 'cyan')
self.text_rect = self.text_surf.get_rect()
# self.text_rect.center = self.rect.center
self.text_rect.center = (65,20)
self.image.blit(self.text_surf, self.text_rect.center)
btns = []
col = 100
for i in range(3):
btns.append(Button(normal, (col, 200), None, text=f'Button {i}'))
col += 200
# Set a variabe to True and enter loop
running = True
while running:
# Fill screen with background color
screen.fill(screen_bg)
# Start event loop
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Get mouse button from pygame
left, middle, right = pygame.mouse.get_pressed()
# Blit button to screen
for btn in btns:
screen.blit(btn.image, btn.rect)
# Set framerate and update
framerate.tick(fps)
pygame.display.update()
Posts: 5,151
Threads: 396
Joined: Sep 2016
Line 44 you are setting all instances of that class text to a specified location regardless of where that instance actually is located.
Recommended Tutorials:
Posts: 6,800
Threads: 20
Joined: Feb 2020
Aug-28-2022, 09:42 PM
(This post was last modified: Aug-28-2022, 09:42 PM by deanhystad.)
You are blitting the text on the image. This changes the image. All your buttons share the same image.
You could make a new image for each button, or you could have your button draw itself, first blitting the image then blitting the text over the image.
In this example the button blits the text on the button surface. To allow changing the text the button must keep an unmodified copy of the image so it can create a new surface that has both image and text.
import pygame
pygame.init()
pygame.font.init()
screen_size = (800, 600)
screen = pygame.display.set_mode(screen_size)
normal = pygame.image.load("ttt_x.png")
class Button:
instances = []
default_font = pygame.font.SysFont('verdana', 16)
def __init__(self, window, image, fg="black", **kwargs):
super().__init__(**kwargs)
self.window = window
self.image = image
self.font = self.default_font
self.fg = fg
self.text = ""
self.instances.append(self)
self.callback = None
@classmethod
def button_press(cls, point):
for button in cls.instances:
if button.click(point):
return True
return False
@classmethod
def draw_all(cls):
for button in cls.instances:
button.draw()
def connect(self, func):
self.callback = func
def click(self, point):
if self.callback and self.rect.collidepoint(point):
self.callback()
return True
return False
def draw(self):
self.window.blit(self.surface, self.pos)
@property
def pos(self):
return self.rect.x, self.rect.y
@pos.setter
def pos(self, xy):
self.rect.x, self.rect.y = xy
@property
def text(self):
return self._text
@text.setter
def text(self, text):
self._text = text
text = self.font.render(self._text, True, self.fg)
text_rect = text.get_rect()
x = (self.rect.width - text_rect.width) // 2
y = (self.rect.height - text_rect.height) // 2
self.surface = self.image.copy()
self.surface.blit(text, (x, y))
self.rect = self.surface.get_rect()
class CounterButton(Button):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.count = 0
self.increment()
self.connect(self.increment)
def increment(self):
self.count += 1
self.text = f"Button {self.count}"
btns = []
for i in range(3):
button = CounterButton(screen, normal)
button.pos = 100+200*i, 200
# Set a variabe to True and enter loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONUP:
Button.button_press(pygame.mouse.get_pos())
screen.fill("black")
Button.draw_all()
pygame.display.update() In this example the text is drawn on top of the image. Setting the button text does not change the image. We no longer have to keep a "clean" image because the text is blitted on the window, not the image. The image is never modified.
import pygame
pygame.init()
pygame.font.init()
screen_size = (800, 600)
screen = pygame.display.set_mode(screen_size)
normal = pygame.image.load("ttt_x.png")
class Button:
instances = []
default_font = pygame.font.SysFont('verdana', 16)
def __init__(self, window, image, fg="black", **kwargs):
super().__init__(**kwargs)
self.window = window
self.image = image
self.rect = self.image.get_rect()
self.font = self.default_font
self.fg = fg
self.text = ""
self.instances.append(self)
self.callback = None
@classmethod
def button_press(cls, point):
for button in cls.instances:
if button.click(point):
return True
return False
@classmethod
def draw_all(cls):
for button in cls.instances:
button.draw()
def connect(self, func):
self.callback = func
def click(self, point):
if self.callback and self.rect.collidepoint(point):
self.callback()
return True
return False
def draw(self):
self.window.blit(self.image, self.pos)
self.window.blit(self.text_surface, self.text_rect)
@property
def pos(self):
return self.rect.x, self.rect.y
@pos.setter
def pos(self, xy):
self.rect.x, self.rect.y = xy
if self.text_rect:
self.text_rect.center = self.rect.center
@property
def text(self):
return self._text
@text.setter
def text(self, text):
self._text = text
self.text_surface = self.font.render(self._text, True, self.fg)
self.text_rect = self.text_surface.get_rect()
self.text_rect.center = self.rect.center
self.draw()
class CounterButton(Button):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.count = 0
self.increment()
self.connect(self.increment)
def increment(self):
self.count += 1
self.text = f"Button {self.count}"
btns = []
for i in range(3):
button = CounterButton(screen, normal)
button.pos = 100+200*i, 200
# Set a variabe to True and enter loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONUP:
Button.button_press(pygame.mouse.get_pos())
screen.fill("black")
Button.draw_all()
pygame.display.update()
Posts: 544
Threads: 15
Joined: Oct 2016
Aug-29-2022, 05:24 PM
(This post was last modified: Aug-29-2022, 05:25 PM by Windspar.)
I always try to avoid global. I keep refactoring my code to the smallest part. I always go for flexibility.
Example
import pygame
from pygame.sprite import Group, Sprite
from itertools import count
class ButtonGroup:
def __init__(self):
self.buttons = Group()
self.text = Group()
def add(self, *buttons):
for button in buttons:
self.buttons.add(button)
self.text.add(button.text)
def draw(self, surface):
self.buttons.draw(surface)
self.text.draw(surface)
def on_event(self, event):
if event.type == pygame.MOUSEMOTION:
for button in self.buttons:
button.collide(event.pos)
elif event.type == pygame.MOUSEBUTTONDOWN:
for button in self.buttons:
if event.button == 1:
button.mouse_down()
elif event.type == pygame.MOUSEBUTTONUP:
for button in self.buttons:
if event.button == 1:
button.mouse_up()
class Pen:
def __init__(self, font, color):
self.font = font
self.color = color
def write(self, text):
return Text(pen, text)
def render(self, text):
return self.font.render(text, 1, self.color)
class Text(Sprite):
def __init__(self, pen, text):
super().__init__()
self.pen = pen
self.text = text
self.image = pen.render(text)
self.rect = self.image.get_rect()
class ButtonImages:
def __init__(self, normal, press, hover):
self.normal = normal
self.press = press
self.hover = hover
class Callback:
def __init__(self, callback, user_data=None):
self.callback = callback
self.user_data = user_data
def call(self, widget):
self.callback(widget, self)
class Button(Sprite):
def __init__(self, images, text, callback, position, anchor):
super().__init__()
self.callback = callback
self.images = images
self.image = images.normal
self.rect = self.image.get_rect()
setattr(self.rect, anchor, position)
self.text = text
self.text.rect.center = self.rect.center
self.is_hovering = False
self.is_press = False
def collide(self, mpos):
hovering = self.rect.collidepoint(mpos)
if hovering is not self.is_hovering:
self.update_image(hovering)
self.is_hovering = hovering
def mouse_up(self):
if self.is_hovering:
self.is_press = False
self.update_image(self.is_hovering)
self.callback.call(self)
def mouse_down(self):
if self.is_hovering:
self.is_press = True
self.update_image(self.is_hovering)
else:
self.is_press = False
def update_image(self, hovering):
if hovering:
if self.is_press:
self.image = self.images.press
else:
self.image = self.images.hover
else:
self.image = self.images.normal
class QuickWindow:
def __init__(self, caption, size, fps=60, flags=0):
# Basic Pygame Setup
pygame.display.set_caption(caption)
self.surface = pygame.display.set_mode(size, flags)
self.rect = self.surface.get_rect()
self.clock = pygame.time.Clock()
self.running = False
self.delta = 0
self.fps = fps
# Variables
self.create_buttons()
def create_buttons(self):
# Blending colors
hcolor = pygame.Color("navy").lerp("white", 0.15)
pcolor = pygame.Color("navy").lerp("white", 0.2)
# Build surface images
surfaces = []
for color in ["navy", pcolor, hcolor]:
surface = pygame.Surface((120, 30))
surface.fill(color)
surfaces.append(surface)
images = ButtonImages(*surfaces)
# Build pen
font = pygame.font.Font(None, 24)
pen = Pen(font, "white")
numb = count(20, 40)
self.buttons = ButtonGroup()
for n in range(1, 6):
self.buttons.add(
Button(images,
Text(pen, "Button " + str(n)),
Callback(button_click),
(20, next(numb)), "topleft")
)
def on_draw(self):
self.surface.fill("gray30")
self.buttons.draw(self.surface)
def on_event(self, event):
if event.type == pygame.QUIT:
self.running = False
else:
self.buttons.on_event(event)
def main_loop(self):
self.running = True
while self.running:
for event in pygame.event.get():
self.on_event(event)
self.on_draw()
pygame.display.flip()
self.delta = self.clock.tick(self.fps)
def button_click(button, callback):
print(button.text.text)
if __name__ == "__main__":
pygame.init()
window = QuickWindow("Pygame Window", (400, 300))
window.main_loop()
pygame.quit()
menator01 likes this post
99 percent of computer problems exists between chair and keyboard.
Posts: 1,145
Threads: 114
Joined: Sep 2019
Thanks for the examples all. @ Windspar I got the images and mouse cursor change to work with your example.
Now to break down the code and study it.
Thanks again all.
Posts: 544
Threads: 15
Joined: Oct 2016
Anchor in my code. Is using pygame rect position. "topleft", "midleft", "center", so on.
Here a more functional example.
import pygame
from pygame.sprite import Group, Sprite
from itertools import count
class ButtonGroup:
def __init__(self):
self.buttons = Group()
self.text = Group()
def add(self, *buttons):
for button in buttons:
self.buttons.add(button)
self.text.add(button.text)
def draw(self, surface):
self.buttons.draw(surface)
self.text.draw(surface)
def on_event(self, event):
if event.type == pygame.MOUSEMOTION:
for button in self.buttons:
button.collide(event.pos)
elif event.type == pygame.MOUSEBUTTONDOWN:
for button in self.buttons:
if event.button == 1:
button.mouse_down()
elif event.type == pygame.MOUSEBUTTONUP:
for button in self.buttons:
if event.button == 1:
button.mouse_up()
class Pen:
def __init__(self, font, color):
self.font = font
self.color = color
def write(self, text):
return Text(pen, text)
def render(self, text):
return self.font.render(text, 1, self.color)
class Text(Sprite):
def __init__(self, pen, text):
super().__init__()
self.pen = pen
self.text = text
self.image = pen.render(text)
self.rect = self.image.get_rect()
def set_text(self, text, anchor):
pos = getattr(self.rect, anchor)
self.text = text
self.image = self.pen.render(text)
self.rect = self.image.get_rect()
setattr(self.rect, anchor, pos)
class ButtonImages:
def __init__(self, normal, press, hover):
self.normal = normal
self.press = press
self.hover = hover
class Callback:
def __init__(self, callback, user_data=None):
self.callback = callback
self.user_data = user_data
def call(self, widget):
self.callback(widget, self)
class Button(Sprite):
def __init__(self, images, text, callback, position, anchor):
super().__init__()
self.callback = callback
self.images = images
self.image = images.normal
self.rect = self.image.get_rect()
setattr(self.rect, anchor, position)
self.text = text
self.text.rect.center = self.rect.center
self.is_hovering = False
self.is_press = False
def collide(self, mpos):
hovering = self.rect.collidepoint(mpos)
if hovering is not self.is_hovering:
self.update_image(hovering)
self.is_hovering = hovering
def mouse_up(self):
if self.is_hovering:
self.is_press = False
self.update_image(self.is_hovering)
self.callback.call(self)
def mouse_down(self):
if self.is_hovering:
self.is_press = True
self.update_image(self.is_hovering)
else:
self.is_press = False
def update_image(self, hovering):
if hovering:
if self.is_press:
self.image = self.images.press
else:
self.image = self.images.hover
else:
self.image = self.images.normal
class ButtonManager:
def __init__(self, window, color, mix_color, size):
self.window = window
self.color_buttons = ButtonGroup()
self.create(color, mix_color, size)
# Shortcuts.
# Link button on_event and draw methods from ButtonGroup.
self.draw = self.color_buttons.draw
self.on_event = self.color_buttons.on_event
def create(self, color, mix_color, size):
# Build Pen
font = pygame.font.Font(None, 24)
pen = Pen(font, "white")
# Creating Images
hcolor = pygame.Color(color).lerp(mix_color, 0.15)
pcolor = pygame.Color(color).lerp(mix_color, 0.2)
surfaces = []
for color in [color, pcolor, hcolor]:
surface = pygame.Surface(size)
surface.fill(color)
surfaces.append(surface)
images = ButtonImages(*surfaces)
# Data for buttons
colors = ("blue", "darkred", "darkgreen", "dodgerblue", "orange", "gray30")
num = count(20, 40)
# Create and add Button to group.
for color in colors:
y = next(num)
self.color_buttons.add(
Button(images,
Text(pen, color),
Callback(self.push_color_button, color),
(20, y), "topleft")
)
def push_color_button(self, button, callback):
self.window.background = callback.user_data
class QuickWindow:
def __init__(self, caption, size, fps=60, flags=0):
# Basic Pygame Setup
pygame.display.set_caption(caption)
self.surface = pygame.display.set_mode(size, flags)
self.rect = self.surface.get_rect()
self.clock = pygame.time.Clock()
self.running = False
self.delta = 0
self.fps = fps
# Variables
self.background = 'gray30'
self.buttons = ButtonManager(self, "navy", "white", (140, 30))
def on_draw(self):
self.surface.fill(self.background)
self.buttons.draw(self.surface)
def on_event(self, event):
if event.type == pygame.QUIT:
self.running = False
else:
self.buttons.on_event(event)
def main_loop(self):
self.running = True
while self.running:
for event in pygame.event.get():
self.on_event(event)
self.on_draw()
pygame.display.flip()
self.delta = self.clock.tick(self.fps)
if __name__ == "__main__":
pygame.init()
window = QuickWindow("Pygame Window", (400, 300))
window.main_loop()
pygame.quit()
99 percent of computer problems exists between chair and keyboard.
|