[PyGame] Text stacking on top of one another - Printable Version +- Python Forum (https://python-forum.io) +-- Forum: Python Coding (https://python-forum.io/forum-7.html) +--- Forum: Game Development (https://python-forum.io/forum-11.html) +--- Thread: [PyGame] Text stacking on top of one another (/thread-38057.html) |
Text stacking on top of one another - menator01 - Aug-28-2022 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() RE: Text stacking on top of one another - metulburr - Aug-28-2022 Line 44 you are setting all instances of that class text to a specified location regardless of where that instance actually is located. RE: Text stacking on top of one another - deanhystad - Aug-28-2022 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() RE: Text stacking on top of one another - Windspar - Aug-29-2022 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() RE: Text stacking on top of one another - menator01 - Aug-29-2022 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. RE: Text stacking on top of one another - Windspar - Aug-30-2022 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() |