3

I have a fairly typical pygame project with a game loop that processes events, updates game state and data, and then draws the next frame. That's all working.

For various reasons, this is Windows only at the moment. So I thought I would add an "About..." screen to the Windows System Menu (A.K.A. Window Menu / Control Menu).

Near the initialization of my program, I insert the menu (which works):

WM_COMMAND = 0x0111
ABOUT_MENU_ID = 1001
hwnd = pygame.display.get_wm_info()["window"]
menu = ctypes.windll.user32.GetSystemMenu(hwnd, False)
ctypes.windll.user32.AppendMenuW(menu, 0, ABOUT_MENU_ID, "About...")

I understand that pygame does not get system messages, so I need to use pygame.event.pump() to bring them in. My full game loop is:

running = True
while running:
    # Include system events.
    pygame.event.pump()

    if game.state == "playing":
        if pygame.key.get_pressed()[pygame.K_a] or pygame.key.get_pressed()[pygame.K_LEFT]:
            game.players[-1].move([-4, 0])
        elif pygame.key.get_pressed()[pygame.K_d] or pygame.key.get_pressed()[pygame.K_RIGHT]:
            game.players[-1].move([4, 0])

        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    running = False
                elif event.key == pygame.K_SPACE:
                    game.players[-1].fire()
            elif event.type == pygame.QUIT:
                running = False
            elif event.type == WM_COMMAND and event.wparam == ABOUT_MENU_ID:
                handle_about_screen()
        game.handleFrame()
    elif game.state == "splash":
        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    running = False
                if event.key == pygame.K_p:
                    game.__init__()
            elif event.type == pygame.QUIT:
                running = False
            elif event.type == WM_COMMAND and event.wparam == ABOUT_MENU_ID:
                handle_about_screen()
    elif game.state == "about":
        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    running = False
                if event.key == pygame.K_p:
                    if(game.lastState == "splash"):
                        handle_splash_screen()
            elif event.type == pygame.QUIT:
                running = False

    pygame.display.update()
    clock.tick(game.frame_rate)

handle_about_screen() is never called, so if the event message is making it to pygame, it isn't identified correctly.

I also tried a different strategy, involving more directly accessing the System message queue. But you can see the infinite loop in the code below... I haven't been able to find a way to get at my about screen menu item event, without either having an obvious infinite loop, or without throwing out potentially important messages that I'm not handling, or withotu scrambling the order of events as I re-insert the ones I'm not handling:

while True:
    msg = ctypes.windll.user32.PeekMessageW(None, None, 0, 0, 1)  # Check without removing (to avoid deleting other events)
    if not msg:
        break  # No events found

    if msg[0] == WM_COMMAND and msg[1] == ABOUT_MENU_ID:
        handle_about_screen()  # It's ours...
        ctypes.windll.user32.PeekMessageW(None, None, 0, 0, 0)  # Remove the "About" menu message from the queue

    else:
        break  # It's an event we don't handle... But now we're 
               #   infinite looping because we will find this one again.

This seems like it should be simple... Am I missing some obvious API, pygame or python functionality?

Thanks in advance for any help.

1
  • 1
    There's a module pystray that you might find helpful. Commented Feb 6 at 3:45

1 Answer 1

-1

After spending over hours, I came up with this.

You can add custom items to the Windows system menu (top-left menu of the window) and respond to them using ctypes to subclass the window procedure (WndProc).

Working example:

import sys
import pygame
import ctypes
from ctypes import wintypes

# Constants for custom system menu items
IDM_SYS_ABOUT = 1
IDM_SYS_HELP = 2
IDM_SYS_REMOVE = 3

# Load Windows API
user32 = ctypes.WinDLL('user32', use_last_error=True)

# WNDPROC callback type
WNDPROC = ctypes.WINFUNCTYPE(ctypes.c_long, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM)

# Windows message constants
WM_SYSCOMMAND = 0x0112
WM_DESTROY = 0x0002

# Menu flags
MF_STRING = 0x00000000
MF_SEPARATOR = 0x00000800

# Global variable to store the background color
background_color = (255, 255, 255)  # Default white color

# Custom window procedure to handle menu commands
def wnd_proc(hwnd, msg, wparam, lparam):
    global background_color  # Access the global variable
    if msg == WM_SYSCOMMAND:
        if wparam == IDM_SYS_ABOUT:
            ctypes.windll.user32.MessageBoxW(hwnd, "A Poor-Person's Menu Program\n(c) Me, 2025", "SysMenuApp", 0x40)
            return 0
        elif wparam == IDM_SYS_HELP:
            # Change the background color to a new color (e.g., light blue)
            background_color = (173, 216, 230)  # Light blue
            return 0
        elif wparam == IDM_SYS_REMOVE:
            user32.GetSystemMenu(hwnd, True)  # Reset menu to default
            return 0
    elif msg == WM_DESTROY:
        ctypes.windll.user32.PostQuitMessage(0)
        return 0

    # Forward unhandled messages
    return ctypes.windll.user32.DefWindowProcW(
        wintypes.HWND(hwnd),
        wintypes.UINT(msg),
        wintypes.WPARAM(wparam),
        wintypes.LPARAM(lparam)
    )

# Convert Python function to Windows WNDPROC
WndProc = WNDPROC(wnd_proc)

def main():
    pygame.init()
    screen = pygame.display.set_mode((800, 600))
    pygame.display.set_caption("System Menu Additions")

    # Get native window handle
    hwnd = pygame.display.get_wm_info()['window']

    # Subclass the window
    original_wnd_proc = user32.SetWindowLongPtrW(hwnd, -4, WndProc)

    # Modify the system menu
    hMenu = user32.GetSystemMenu(hwnd, False)
    user32.AppendMenuW(hMenu, MF_SEPARATOR, 0, None)
    user32.AppendMenuW(hMenu, MF_STRING, IDM_SYS_ABOUT, "About...")
    user32.AppendMenuW(hMenu, MF_STRING, IDM_SYS_HELP, "Help...")
    user32.AppendMenuW(hMenu, MF_STRING, IDM_SYS_REMOVE, "Remove Additions")

    # Main game loop
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        # Use the global background color
        screen.fill(background_color)
        pygame.display.flip()

    # Restore original WndProc
    user32.SetWindowLongPtrW(hwnd, -4, original_wnd_proc)
    pygame.quit()

if __name__ == "__main__":
    main()

For your convenience help menu interacts with pygame as an example, while about menu works natively

enter image description here enter image description here

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.