Search code examples
c++winapiwindowfullscreen

Win32: Add black borders to fullscreen window


I am trying to preserve content aspect ratio in fullscreen mode in Windows. I'd like to hide the rest of the desktop behind black borders if the display aspect ratio differs from the content aspect ratio. Is it possible to create fullscreen window with centered content and black borders with Win32 api?

In OS X this can be achieved quite easily with the following code:

CGSize ar;
ar.width = 800;
ar.height = 600;
[self.window setContentAspectRatio:ar];
[self.window center];
[self.window toggleFullScreen:nil];

If I run the above code in 16:9 display, my app goes to fullscreen mode, the content is centered (since it is 4:3) and I have black borders on both sides of the screen.

I have tried to implement the same functionality in Windows but I begin to wonder if it is even possible. My current fullscreen code maintains the aspect ratio and centers the content, but shows the desktop on the both sides of the window if the fullscreenWidth and fullscreenHeight are not equal to displayWidth and displayHeight:

bool enterFullscreen(int fullscreenWidth, int fullscreenHeight)
{
    DEVMODE fullscreenSettings;
    bool isChangeSuccessful;

    int displayWidth = GetDeviceCaps(m_hDC, HORZRES);
    int displayHeight = GetDeviceCaps(m_hDC, VERTRES);

    int colourBits = GetDeviceCaps(m_hDC, BITSPIXEL);
    int refreshRate = GetDeviceCaps(m_hDC, VREFRESH);

    EnumDisplaySettings(NULL, 0, &fullscreenSettings);
    fullscreenSettings.dmPelsWidth = fullscreenWidth;
    fullscreenSettings.dmPelsHeight = fullscreenHeight;
    fullscreenSettings.dmBitsPerPel = colourBits;
    fullscreenSettings.dmDisplayFrequency = refreshRate;
    fullscreenSettings.dmFields = DM_PELSWIDTH |
        DM_PELSHEIGHT |
        DM_BITSPERPEL |
        DM_DISPLAYFREQUENCY;

    SetWindowLongPtr(m_hWnd, GWL_EXSTYLE, WS_EX_APPWINDOW | WS_EX_TOPMOST);
    SetWindowLongPtr(m_hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
    SetWindowPos(m_hWnd, HWND_TOPMOST, 0, 0, displayWidth, displayHeight, SWP_SHOWWINDOW);
    isChangeSuccessful = ChangeDisplaySettings(&fullscreenSettings, CDS_FULLSCREEN) == DISP_CHANGE_SUCCESSFUL;
    ShowWindow(m_hWnd, SW_MAXIMIZE);

    RECT rcWindow;
    GetWindowRect(m_hWnd, &rcWindow);

    // calculate content position
    POINT ptDiff;
    ptDiff.x = ((rcWindow.right - rcWindow.left) - fullscreenWidth) / 2;
    ptDiff.y = ((rcWindow.bottom - rcWindow.top) - fullscreenHeight) / 2;

    AdjustWindowRectEx(&rcWindow, GetWindowLong(m_hWnd, GWL_STYLE), FALSE, GetWindowLong(m_hWnd, GWL_EXSTYLE));
    SetWindowPos(m_hWnd, 0, ptDiff.x, ptDiff.y, displayWidth, displayHeight, NULL);

    return isChangeSuccessful;
}

Solution

  • The easiest way to accomplish what you are looking for is to create a child window (C) to render your content, leaving any excess space to the parent (P).

    P should be created using a black brush for its background. Specify (HBRUSH)GetStockObject(BLACK_BRUSH) for the hbrBackground member of the WNDCLASS structure when registering the window class (RegisterClass). To prevent flicker while erasing the background, P should have the WS_CLIPCHILDREN Window Style.

    Whenever P changes its size, a WM_SIZE message is sent to P's window procedure. The handler can then adjust C's position and size to maintain the aspect ratio.

    To create a borderless child window C, use the WS_CHILD | WS_VISIBLE window styles in the call to CreateWindow. If you want to handle mouse input in the parent P instead, add the WS_DISABLED window style.


    Sample code (error checking elided for brevity):

    #define STRICT 1
    #define WIN32_LEAN_AND_MEAN
    #include <windows.h>
    
    // Globals
    HWND g_hWndContent = NULL;
    
    // Forward declarations
    LRESULT CALLBACK WndProcMain( HWND, UINT, WPARAM, LPARAM );
    LRESULT CALLBACK WndProcContent( HWND, UINT, WPARAM, LPARAM );
    
    int APIENTRY wWinMain( HINSTANCE hInstance,
                           HINSTANCE /*hPrevInstance*/,
                           LPWSTR /*lpCmdLine*/,
                           int nCmdShow ) {
    

    Both main and content window classes need to be registered. The registration is almost identical, with the exception of the background brush. The content window uses a white brush so that it's visible without any additional code:

        // Register main window class
        const wchar_t classNameMain[] = L"MainWindow";
        WNDCLASSEXW wcexMain = { sizeof( wcexMain ) };
        wcexMain.style = CS_HREDRAW | CS_VREDRAW;
        wcexMain.lpfnWndProc = WndProcMain;
        wcexMain.hCursor = ::LoadCursorW( NULL, IDC_ARROW );
        wcexMain.hbrBackground = (HBRUSH)::GetStockObject( BLACK_BRUSH );
        wcexMain.lpszClassName = classNameMain;
        ::RegisterClassExW( &wcexMain );
    
        // Register content window class
        const wchar_t classNameContent[] = L"ContentWindow";
        WNDCLASSEXW wcexContent = { sizeof( wcexContent ) };
        wcexContent.style = CS_HREDRAW | CS_VREDRAW;
        wcexContent.lpfnWndProc = WndProcContent;
        wcexContent.hCursor = ::LoadCursorW( NULL, IDC_ARROW );
        wcexContent.hbrBackground = (HBRUSH)::GetStockObject( WHITE_BRUSH );
        wcexContent.lpszClassName = classNameContent;
        ::RegisterClassExW( &wcexContent );
    

    With the window classes registered we can move on and create an instance of each. Note that the content window is initially zero-sized. The actual size is calculated in the parent's WM_SIZE handler further down.

        // Create main window
        HWND hWndMain = ::CreateWindowW( classNameMain,
                                         L"Constant AR",
                                         WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
                                         CW_USEDEFAULT, CW_USEDEFAULT, 800, 800,
                                         NULL,
                                         NULL,
                                         hInstance,
                                         NULL );
        // Create content window
        g_hWndContent = ::CreateWindowW( classNameContent,
                                         NULL,
                                         WS_CHILD | WS_VISIBLE,
                                         0, 0, 0, 0,
                                         hWndMain,
                                         NULL,
                                         hInstance,
                                         NULL );
    

    The remainder is boilerplate Windows application code:

        // Show application
        ::ShowWindow( hWndMain, nCmdShow );
        ::UpdateWindow( hWndMain );
    
        // Main message loop
        MSG msg = { 0 };
        while ( ::GetMessageW( &msg, NULL, 0, 0 ) > 0 )
        {
            ::TranslateMessage( &msg );
            ::DispatchMessageW( &msg );
        }
    
        return (int)msg.wParam;
    }
    

    The behavior for a window class is implemented inside it's Window Procedure:

    LRESULT CALLBACK WndProcMain( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ) {
    
        switch ( message ) {
        case WM_CLOSE:
            ::DestroyWindow( hWnd );
            return 0;
    
        case WM_DESTROY:
            ::PostQuitMessage( 0 );
            return 0;
    
        default:
            break;
    

    In addition to standard message handling, the main window's window procedure resizes the content to fit whenever the main window's size changes:

        case WM_SIZE: {
            const SIZE ar = { 800, 600 };
            // Query new client area size
            int clientWidth = LOWORD( lParam );
            int clientHeight = HIWORD( lParam );
            // Calculate new content size
            int contentWidth = ::MulDiv( clientHeight, ar.cx, ar.cy );
            int contentHeight = ::MulDiv( clientWidth, ar.cy, ar.cx );
    
            // Adjust dimensions to fit inside client area
            if ( contentWidth > clientWidth ) {
                contentWidth = clientWidth;
                contentHeight = ::MulDiv( contentWidth, ar.cy, ar.cx );
            } else {
                contentHeight = clientHeight;
                contentWidth = ::MulDiv( contentHeight, ar.cx, ar.cy );
            }
    
            // Calculate offsets to center content
            int offsetX = ( clientWidth - contentWidth ) / 2;
            int offsetY = ( clientHeight - contentHeight ) / 2;
    
            // Adjust content window position
            ::SetWindowPos( g_hWndContent,
                            NULL,
                            offsetX, offsetY,
                            contentWidth, contentHeight,
                            SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER );
    
            return 0;
        }
        }
    
        return ::DefWindowProcW( hWnd, message, wParam, lParam );
    }
    

    The content window's window procedure doesn't implement any custom behavior, and simply forwards all messages to the default implementation:

    LRESULT CALLBACK WndProcContent( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ) {
        return ::DefWindowProcW( hWnd, message, wParam, lParam );
    }