May-13-2022, 07:06 PM
Added in a clock display that makes it easier to see that the thread runs the countdown without blocking the pytgame event loop.
import pygame import threading import time pygame.init() FOREGROUND_COLOR = "White" BACKGROUND_COLOR = "Black" DEFAULT_FONT = pygame.font.SysFont(None, 36) CLOCK_FONT = pygame.font.SysFont(None, 20) screen = pygame.display.set_mode([390, 300]) class Countdown: """A countdown timer that calls a function when it counts down to zero""" def __init__(self, display): self.display = display self.callback = None self.counting = False def doit(self, start_count): """Does the countdown. Runs in its own thread so it doesn't block.""" self.counting = True for count in range(start_count, 0, -1): if not self.counting: break self.display.set_text(f"Countdown: {count}") time.sleep(1) else: self.counting = False if self.callback is not None: self.callback() def connect(self, func): """Specify the function called when the timer counts down to zero""" self.callback = func def start(self, start_count): """Start the countdown""" if not self.counting: threading.Thread(target=self.doit, args=(start_count,)).start() def stop(self): """Stop the countdown""" self.counting = False self.display.set_text("") class Widget(pygame.surface.Surface): """Base class for pygame widgets""" def __init__(self, parent, size, foreground=None, background=None): super().__init__(size) self.parent = parent self.foreground = FOREGROUND_COLOR if foreground is None else foreground self.background = BACKGROUND_COLOR if background is None else background self.rect = self.get_rect() def draw(self): """Draw (blit) self on parent surface.""" self.parent.blit(self, (self.rect.x, self.rect.y)) return self def erase(self): """Draw (blit) background color on parent surface""" pygame.draw.rect(self.parent, BACKGROUND_COLOR, self.rect) return self def at(self, x, y): """Set upper left corner at x, y""" self.rect.x = x self.rect.y = y return self def center_at(self, x, y): """Set center at x, y""" self.rect.centerx = x self.rect.centery = y return self @property def x(self): return self.rect.x @x.setter def x(self, new_x): self.rect.x = new_x @property def y(self): return self.rect.y @y.setter def y(self, new_y): self.rect.y = new_y @property def width(self): return self.rect.width @width.setter def width(self, new_width): self.rect.width = new_width @property def height(self): return self.rect.height @height.setter def height(self, new_height): self.rect.height = new_height class Label(Widget): """A tkinter Label like thing for pygame""" def __init__(self, parent, text, width=None, font=None, foreground=None, background=None): width = width or len(text) self.font = DEFAULT_FONT if font is None else font size = self.font.render("W"*width, True, BACKGROUND_COLOR).get_size() super().__init__(parent, size, foreground, background) self.set_text(text) def set_text(self, text): self.text = text text_img = self.font.render(self.text, True, self.foreground) y = (self.rect.height - text_img.get_height()) // 2 self.fill(self.background) self.blit(text_img, (0, y)) self.draw() return self class CircleButton(Widget): """A tkinter Button like think for pygame""" buttons = [] @classmethod def clicked(cls, x, y): """Call this method to find what button was pressed""" for button in cls.buttons: if button.click(x, y): return True return False def __init__(self, parent, text, radius, foreground=None, text_color=None, font=None): super().__init__(parent, (radius*2, radius*2), foreground) self.font = DEFAULT_FONT if font is None else font self.text_color = self.background if text_color is None else text_color self.callback = None self.buttons.append(self) self.set_text(text) def set_text(self, text): """Set my label text. Label is centered in button""" self.text = text text_img = self.font.render(self.text, True, self.text_color) x = (self.rect.width - text_img.get_width()) // 2 y = (self.rect.height - text_img.get_height()) // 2 self.fill(self.background) pygame.draw.ellipse(self, self.foreground, self.get_rect()) self.blit(text_img, (x, y)) return self def connect(self, func): """Set function to call when clicked""" self.callback = func return self def click(self, x, y): """Check if I was clicked. Execute callback method if clicked""" clicked = self.rect.collidepoint(x, y) if clicked and self.callback: self.callback(self) return clicked # Give the buttons something to do def button_pressed(button): display.set_text(display.text + button.text) clock_display = Label(screen, "", width=8, font=CLOCK_FONT).at(300, 20) display = Label(screen, "", width=10, foreground="black", background="white") display.erase().at(20, 20).draw() # Create a countdown timer that goes BOOM!! when it counts all the way down. countdown = Countdown(display) countdown.connect(lambda: display.set_text("BOOM!!")) # Make some buttons for number in range(9): x = 20 + (number % 3) * 80 y = 220 - (number // 3) * 80 CircleButton(screen, str(number+1), 30, "Blue", "White") \ .connect(button_pressed).at(x, y).draw() CircleButton(screen, "Start", 50, "Green") \ .connect(lambda x: countdown.start(10)).at(270, 60).draw() CircleButton(screen, "Stop", 50, "Red") \ .connect(lambda x: countdown.stop()).at(270, 180).draw() running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False break if event.type == pygame.MOUSEBUTTONUP: CircleButton.clicked(*pygame.mouse.get_pos()) clock_display.set_text(time.strftime("%H:%M:%S", time.localtime())) pygame.display.flip() pygame.display.update() pygame.quit()