Search code examples
pythonpygamescreen-size

How to adjust content to changing screen-size?


I made my pygame window resizeable by using pygame.RESIZEABLE in pygame.display.set_mode() but now the content (buttons) I put on the screen earlier haven't shifted with the resizing which is expected but I cannot figure out a way to successfully implement this. I don't get any error messages thankfully. The only problem is that I am not getting the output I want. If the start button is at 100, 100 out of a screen of 600,600 I want it to be at 200,200 out of the resized screen which for this example I'm taking 1200,1200 for the sake of simplicity. I tried using percentages of the total screen size but that completely messed up the positioning.

import pygame

#Initializing pygane
pygame.init()

#Setting Screen Size
SCREENSIZE = (1000,700)
SCREEN = pygame.display.set_mode(SCREENSIZE, pygame.RESIZABLE)


class Button():
def __init__(self, x, y, image, scale):
    ...

def draw(self, surface):
    ...

#create button instances
start_button = Button(100,100,start_img,0.8)
exit_button = Button(250,250,exit_img,0.8)
setting_button = Button(450,75,setting_img, 0.35)

Solution

  • As suggested, it's pretty easy to do this with percentage size and position.

    When the Button is first initialised, there is a given position and size. But there is also a window size and position at startup too. So instead of storing an exact x co-ordinate, instead we also store the relative x co-ordinate as a factor of the window width.

    So say the window is 1000 pixels wide. If your button is positioned in the exact middle, at pixel 500 - the relative position is:

    initial_x (500) / initial_width (1000)  =>  0.5
    

    So we're keeping that relative_x of 0.5. Any time we need to re-position the button, the new x will always be relative_x * window_width for any window size. Bigger or smaller, it still works.

    So say now the window is stretched to 1500 pixels wide. The new x co-ordinate would be computer as:

    relative_x (0.5) * window_width (1500) => 750
    

    Which is obviously still right in the middle. Say the window then shrunk to 300 pixels ~

    relative_x (0.5) * window_width (300) => 150
    

    Obviously you need to extend this further for the y, width and height. But that is just more of the exact same thing.

    Working example:

    animated_screen_shot

    Code:

    import pygame
    
    WINDOW_WIDTH = 600   # initial size only
    WINDOW_HEIGHT= 300
    MAX_FPS      = 60
    
    WHITE = (255,255,255)
    BLACK = (  0,  0,  0)
    RED   = (255,  0,  0)
    
    
    class StretchyButton( pygame.sprite.Sprite ):
        """ A Button Sprite that maintains its relative position and size
            despite size changes to the underlying window.
            (Based on a pyGame Sprite, just to make drawing simpler) """
        def __init__( self, window, x, y, image ):
            super().__init__()
            self.original_image = image.convert_alpha()  # re-scaling the same image gets messed-up
            self.image = image
            self.rect  = self.image.get_rect()
            self.rect.topleft = ( x, y )
            # Calculate the relative size and position, so we can maintain our 
            # position & proportions when the window size changes
            # So "x_percent" is the relative position of the original x co-ord at the original size
            window_rect = window.get_rect()
            image_rect  = image.get_rect()
            # The given (x,y) of the button is relative to the initial window size.
            # Keep the ratios of the position against window size
            self.x_percent      = x / window_rect.width
            self.y_percent      = y / window_rect.height
            self.width_percent  = image_rect.width / window_rect.width
            self.height_percent = image_rect.height / window_rect.height
    
        def reScale( self, window ):
            """ When the window size changes, call this to re-scale the button to match """
            window_rect = window.get_rect()
            # re-calculate the button's position and size to match the new window size
            # we calcualted the percentage (relative) positions at startup, so now
            # they can just be used to re-position & re-size
            self.rect.x      = self.x_percent * window_rect.width
            self.rect.width  = self.width_percent * window_rect.width
            self.rect.y      = self.y_percent * window_rect.height
            self.rect.height = self.height_percent * window_rect.height
            # re-scale the button's image too
            # always re-scale off an original image, otherwise the image degrades
            self.image = pygame.transform.smoothscale( self.original_image, ( self.rect.width, self.rect.height ) )
    
    
    
    ### I don't like the look of text without some surrounding space
    def renderWithBorder( font, text, border=3, foreground=WHITE, background=BLACK ):
        """ Render the given text with the given font, but surrounded by a border of pixels """
        text = font.render( text, True, foreground, background )
        width,height = text.get_rect().size
        bigger_surf = pygame.Surface( ( width + border + border, height + border + border ), pygame.SRCALPHA )
        bigger_surf.fill( background )
        bigger_surf.blit( text, ( border, border ) )
        return bigger_surf
    
    
    ###
    ### MAIN
    ###
    pygame.init()
    window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), pygame.RESIZABLE )
    pygame.display.set_caption( "Stretchy Buttons" )
    
    font = pygame.font.Font( None, 30 )
    
    # Just a re-square image
    red_box = pygame.Surface( ( 64, 64 ), pygame.SRCALPHA )
    red_box.fill( RED )
    
    # create a set of buttons
    butt0 = StretchyButton( window,  10,  10, red_box )
    butt1 = StretchyButton( window, 200, 100, renderWithBorder( font, "The Owl" ) )
    butt2 = StretchyButton( window, 200, 150, renderWithBorder( font, "And The Pussycat" ) )
    butt3 = StretchyButton( window, 200, 200, renderWithBorder( font, "Went to Sea" ) )
    
    # use a sprite group to hold the buttons, because it's quicker
    buttons = pygame.sprite.Group()
    buttons.add( [ butt0, butt1, butt2, butt3 ] )
    
    running = True
    while running:
        clock = pygame.time.Clock()   # used to govern the max MAX_FPS
        # Handle events
        for event in pygame.event.get():
            if ( event.type == pygame.QUIT ):
                running = False
            elif ( event.type == pygame.VIDEORESIZE ):
                # The window size has changed, re-scale our buttons
                #new_width, new_height = event.size
                for butt in buttons:
                    butt.reScale( window )
    
        # paint the window
        window.fill( WHITE )
        buttons.draw( window )
    
        pygame.display.flip()
        clock.tick( MAX_FPS )   # keep the frame-rate sensible, don't waste electricity