Python Forum
[PyGame] Text stacking on top of one another
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] Text stacking on top of one another
#1
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()
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags


Reply
#2
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:
Reply
#3
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()
Reply
#4
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.
Reply
#5
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.
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags


Reply
#6
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.
Reply


Forum Jump:

User Panel Messages

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