8

I'm developing a grid based game in pygame, and want the window to be resizable. I accomplish this with the following init code:

pygame.display.set_mode((740, 440), pygame.RESIZABLE)

As well as the following in my event handler:

elif event.type == pygame.VIDEORESIZE:
     game.screen = pygame.display.set_mode((event.w, event.h),
                                            pygame.RESIZABLE)
     # Code to re-size other important surfaces

The problem I'm having is that it seems a pygame.VIDEORESIZE event is only pushed once the user is done resizing the window, i.e. lets go of the border. screen.get_size() updates similarly. Since the graphics of my game are very simple, I'd really prefer for them to resize as the user drags the window. This is trivial in many other languages, but I can't find any reference for it in pygame - although I can't imagine a feature this basic would be impossible.

How can I update my game as the screen is being resized in pygame?

EDIT: Here is a minimal working example. Running on Windows 10, pygame 1.9.4, the following code will only draw the updated rectangle after the user finishes dragging the window.

import sys
import pygame

pygame.init()

size = 320, 240
black = 0, 0, 0
red = 255, 0, 0

screen = pygame.display.set_mode(size, pygame.RESIZABLE)

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.VIDEORESIZE:
            pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE)

    screen.fill(black)
    pygame.draw.rect(screen, red, (10,10,screen.get_width(),screen.get_height()))
    pygame.display.flip()
Nico A
  • 227
  • 1
  • 14
  • I don't have a solution, but I've run into a similar problem before and ended up switching technologies (in part) because of it. If there's a way to do it, it certainly isn't easy. – TeaFor2 Oct 26 '20 at 19:06
  • I *don't* get this behaviour running PyGame 1.9.4 under Linux Python 3.7.5. I can in-out the window size like an accordion, and my window just updates normally. Please include your OS-details and more code into the question. Maybe there's an issue in the code, rather than PyGame's resizing. – Kingsley Oct 26 '20 at 21:39
  • @Kingsley Rather than trying to condense my code, I'd direct you to this video: https://www.youtube.com/watch?v=edJZOQwrMKw&t=208s You can see how a very simple example fails to resize properly until let go on Windows 10 (which is what I'm on) – Nico A Oct 26 '20 at 23:05
  • @NicoA - *I believe you* that it's happening, but we can't help to fix it with just 3 lines of code. Also what version of PyGame are you using? – Kingsley Oct 26 '20 at 23:13
  • 3
    @Kingsley I've included a minimal working example with a rectangle instead of my grid. I'm also on pygame 1.9.4 and Python 3.7.5. – Nico A Oct 26 '20 at 23:28
  • 1
    Thanks for the example. I see the same behaviour on Win10, Python 3.7.5 and Pygame 2.0.0.dev22 (which uses SDL 2.0.12). Some searching suggests its an SDL issue. T – import random Oct 27 '20 at 02:53

2 Answers2

7

If you run into this kind of problem, it's always worth to google it using SDL instead of pygame, since pygame is a pretty low-level SDL wrapper.

So that's not a problem of pygame itself, but rather how sdl and your window manager interact, e.g. see this SDL bug report.

Nonetheless, if you really need to update the window while resizing, if you're using Windows, you can listen for the actual WM_SIZE event of Windows, redraw your screen, and update the "Windows"-window by calling RedrawWindow.

Here's a simple example:

import pygame
import win32gui
import win32con

def wndProc(oldWndProc, draw_callback, hWnd, message, wParam, lParam):
    if message == win32con.WM_SIZE:
        draw_callback()
        win32gui.RedrawWindow(hWnd, None, None, win32con.RDW_INVALIDATE | win32con.RDW_ERASE)
    return win32gui.CallWindowProc(oldWndProc, hWnd, message, wParam, lParam)

def main():
    pygame.init()

    screen = pygame.display.set_mode((320, 240), pygame.RESIZABLE | pygame.DOUBLEBUF)

    def draw_game():
        screen.fill(pygame.Color('black'))
        pygame.draw.rect(screen, pygame.Color('red'), pygame.Rect(0,0,screen.get_width(),screen.get_height()).inflate(-10, -10))
        pygame.display.flip()
    
    oldWndProc = win32gui.SetWindowLong(win32gui.GetForegroundWindow(), win32con.GWL_WNDPROC, lambda *args: wndProc(oldWndProc, draw_game, *args))

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return
            elif event.type == pygame.VIDEORESIZE:
                pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE| pygame.DOUBLEBUF)
        draw_game()
        
if __name__ == '__main__':
    main()

Default behaviour:

enter image description here

With RedrawWindow:

enter image description here

sloth
  • 99,095
  • 21
  • 171
  • 219
0

ctypes assumes c_int or "pointer" for argument types and c_int for return type if not specified. c_int is a 32-bit value. LONG_PTR and LRESULT are 64-bit values on a 64-bit OS. Good practice is to define .argtypes and .restype for all functions used. Doing that fixed the issue.

import ctypes as ct
from ctypes import wintypes as w

import pygame

# LPARAM is typedef'ed as LONG_PTR in winuser.h, so it can be used
# for LRESULT and LONG_PTR which are missing from wintypes.
LRESULT = LONG_PTR = w.LPARAM
WNDPROC = ct.WINFUNCTYPE(LRESULT, w.HWND, w.UINT, w.WPARAM, w.LPARAM)
WM_SIZE = 0x0005
RDW_INVALIDATE = 0x0001
RDW_ERASE = 0x0004
GWL_WNDPROC = -4

# ctypes.windll.user32 is a cached, shared version of user32.dll.
# Get our own copy and meticulously define argtypes/restype according
# to MSDN documentation of the C prototypes.
user32 = ct.WinDLL('user32')
user32.GetWindowLongPtrA.argtypes = w.HWND, ct.c_int
user32.GetWindowLongPtrA.restype = LONG_PTR
user32.GetForegroundWindow.argtypes = ()
user32.GetForegroundWindow.restype = w.HWND
user32.RedrawWindow.argtypes = w.HWND, w.LPRECT, w.HRGN, w.UINT
user32.RedrawWindow.restype = w.BOOL
user32.CallWindowProcA.argtypes = WNDPROC, w.HWND, w.UINT, w.WPARAM, w.LPARAM
user32.CallWindowProcA.restype = LRESULT
user32.SetWindowLongPtrA.argtypes = w.HWND, ct.c_int, LONG_PTR
user32.SetWindowLongPtrA.restype = LONG_PTR

def main():
    pygame.init()

    screen = pygame.display.set_mode((320, 240), pygame.RESIZABLE | pygame.DOUBLEBUF)

    def draw_game():
        screen.fill(pygame.Color('black'))
        pygame.draw.rect(screen, pygame.Color('red'), pygame.Rect(0,0,screen.get_width(),screen.get_height()).inflate(-10, -10))
        pygame.display.flip()
    
    old_window_proc = user32.GetWindowLongPtrA(user32.GetForegroundWindow(), GWL_WNDPROC)

    def new_window_proc(hwnd, msg, wparam, lparam):
        if msg == WM_SIZE:
            draw_game()
            user32.RedrawWindow(hwnd, None, None, RDW_INVALIDATE | RDW_ERASE)
        # LONG_PTR is the same bit width as WNDPROC, but
        # need cast to use it here.
        return user32.CallWindowProcA(ct.cast(old_window_proc, WNDPROC), hwnd, msg, wparam, lparam)

    new_window_proc_cb = WNDPROC(new_window_proc)

    # Can't cast a WNDPROC (pointer) to a LONG_PTR directly, but can cast to void*.
    # The .value of a c_void_p instance is its integer address.
    user32.SetWindowLongPtrA(user32.GetForegroundWindow(), GWL_WNDPROC, ct.cast(new_window_proc_cb, ct.c_void_p).value)

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return
            elif event.type == pygame.VIDEORESIZE:
                pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE| pygame.DOUBLEBUF)
        draw_game()
        
if __name__ == '__main__':
    main()
Mark Tolonen
  • 166,664
  • 26
  • 169
  • 251