Skip to content

Source: Responsive window with continuous updating during resize and move operations via Win32 API and SFML

wooodser edited this page Mar 19, 2023 · 7 revisions

The goal

If you want you SFML window and application logic overall to not 'freeze' when you resize or move its window, this topic is for you!

Code example

Let's get to business. Here is a code example that slightly bigger than a minimal working one to demonstrate how it can be done.

#include "SFML/Graphics.hpp"

#define NOMINMAX
#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <thread>


inline namespace Globals
{
    inline namespace Constants
    {
        static const char* g_MainWndClassName       = "Main window";
        static const char* g_ContentWndClassName    = "Content window (child)";
        static const DWORD g_MainWndStyle           = WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_CLIPCHILDREN;

        static const long g_MinContentWidth         = 300;
        static const long g_MinContentHeight        = 300;
        static const long g_MaxContentWidth         = 800;
        static const long g_MaxContentHeight        = 800;
    }

    static HWND  volatile   g_hMainWnd          = 0;
    static HWND  volatile   g_hContentWnd       = 0;
    static DWORD volatile   g_idMainThread      = 0;
    static DWORD volatile   g_idContentThread   = 0;

    static long g_MainWndMinWidth   = 0;
    static long g_MainWndMinHeight  = 0;
    static long g_MainWndMaxWidth   = 0;
    static long g_MainWndMaxHeight  = 0;

    static long g_ContentWndWidth   = (g_MaxContentWidth + g_MinContentWidth) / 2;
    static long g_ContentWndHeight  = (g_MaxContentHeight + g_MinContentHeight) / 2;
}

inline namespace Windows
{
    bool CreateMainWindow(HINSTANCE);

    void ContentThreadMain(HINSTANCE);

    inline namespace Handlers
    {
        bool ProcessEvents(sf::RenderWindow&);

        LRESULT CALLBACK MainWndEventProcessor(HWND, UINT, WPARAM, LPARAM);
        LRESULT CALLBACK ContentEventProcessor(HWND, UINT, WPARAM, LPARAM);
    }
}


int APIENTRY WinMain(_In_ HINSTANCE hInst, _In_opt_ HINSTANCE, _In_ PSTR, _In_ int)
{
    try
    {
        g_idMainThread = GetCurrentThreadId();

        if (!CreateMainWindow(hInst))
            return 1;

        std::thread contentWndThread(ContentThreadMain, hInst);
        SetThreadPriority(contentWndThread.native_handle(), THREAD_PRIORITY_ABOVE_NORMAL);
        contentWndThread.detach();

        MSG message {};
        message.message = static_cast<UINT>(~WM_QUIT);
        // Loop until a WM_QUIT message is received
        while (message.message != WM_QUIT)
        {
            if (PeekMessageA(&message, nullptr, 0, 0, PM_REMOVE))
            {
                // If a message was waiting in the message queue, process it
                TranslateMessage(&message);
                DispatchMessageA(&message);
            }
        }

        // Destroy the main window (all its child controls will be destroyed)
        DestroyWindow(g_hMainWnd);

        // Don't forget to unregister windows classes
        UnregisterClassA(g_MainWndClassName, hInst);
    }
    catch (const std::exception& ex)
    {
        MessageBoxA(nullptr, ex.what(), "Error", MB_ICONERROR | MB_OK);
        exit(1);
    }
    catch (...) { exit(-1); }

    return EXIT_SUCCESS;
}


namespace Windows
{
    bool CreateMainWindow(HINSTANCE hInst)
    {
        WNDCLASS mainWndClass {};
        mainWndClass.lpszClassName  = g_MainWndClassName;
        mainWndClass.lpfnWndProc    = &MainWndEventProcessor;
        mainWndClass.hInstance      = hInst;
        mainWndClass.hbrBackground  = (HBRUSH)GetStockObject(BLACK_BRUSH);

        if (!RegisterClassA(&mainWndClass))
            return false;

        const auto screenResX = GetSystemMetrics(SM_CXSCREEN);
        const auto screenResY = GetSystemMetrics(SM_CYSCREEN);

        RECT rect = {};
        AdjustWindowRect(&rect, g_MainWndStyle, false);
        g_MainWndMinWidth   = g_MinContentWidth - rect.left + rect.right;
        g_MainWndMinHeight  = g_MinContentHeight - rect.top + rect.bottom;
        g_MainWndMaxWidth   = g_MaxContentWidth - rect.left + rect.right;
        g_MainWndMaxHeight  = g_MaxContentHeight - rect.top + rect.bottom;

        const auto initMainWndWidth  = g_ContentWndWidth - rect.left + rect.right;
        const auto initMainWndHeight = g_ContentWndHeight - rect.top + rect.bottom;

        // Create the main window in the screen center.
        g_hMainWnd = CreateWindowExA(0UL, g_MainWndClassName, g_MainWndClassName,
            g_MainWndStyle, 0, 0, initMainWndWidth, initMainWndHeight,
            nullptr, nullptr, hInst, nullptr);

        MoveWindow(g_hMainWnd, screenResX/2 - initMainWndWidth/2,
            screenResY/2 - initMainWndHeight/2, initMainWndWidth,
            initMainWndHeight, false);
        ShowWindow(g_hMainWnd, SW_SHOW);

        if (!g_hMainWnd)
            return false;

        return g_hMainWnd != 0;
    }

    void ContentThreadMain(HINSTANCE hInst)
    {
        g_idContentThread = GetCurrentThreadId();

        WNDCLASS contentWndClass {};
        contentWndClass.lpszClassName  = g_ContentWndClassName;
        contentWndClass.lpfnWndProc    = &ContentEventProcessor;
        contentWndClass.hbrBackground  = (HBRUSH)GetStockObject(BLACK_BRUSH);

        if (!RegisterClassA(&contentWndClass))
        {
            PostThreadMessageA(g_idMainThread, WM_QUIT, 0, 0);
            return;
        }

        //  Window that owns all client area inside the main window.
        g_hContentWnd = CreateWindowExA(0UL, g_ContentWndClassName, g_ContentWndClassName,
            WS_CHILD | WS_VISIBLE, 0, 0, g_ContentWndWidth, g_ContentWndHeight,
            g_hMainWnd, nullptr, hInst, nullptr);

        // Some adjustments for video context settings
        sf::ContextSettings contextSettings;
        contextSettings.depthBits = 24;
        contextSettings.antialiasingLevel = 4;

        sf::RenderWindow window(g_hContentWnd, contextSettings);
        window.setVerticalSyncEnabled(true);
        window.setActive(true);

        //  Background area visualization (except 10 pixels).
        sf::RectangleShape bgRectShape;
        bgRectShape.setFillColor({80, 80, 80});
        bgRectShape.setPosition(5, 5);

        //  Active game object simulation.
        sf::RectangleShape rectShape;
        rectShape.setSize({200, 200});
        rectShape.setOrigin({100, 100});
        rectShape.setFillColor({100, 180, 100});

        sf::Clock clock;
        sf::Time  timePrev = clock.getElapsedTime();
        while (true)
        {
            if (!ProcessEvents(window))
                break;

            window.clear(sf::Color::Black);

            //  Current time saving and the delta calculation
            const sf::Time& timeNow  = clock.getElapsedTime();
            const sf::Time& timeDiff = timeNow - timePrev;

            //  Updating states for game and its objects
            bgRectShape.setSize({window.getSize().x - 10.f, window.getSize().y - 10.f});

            rectShape.setPosition(window.getSize().x / 2.f, window.getSize().y / 2.f);
            rectShape.rotate(360 * timeDiff.asSeconds() / 5);

            //  Draw everything
            window.draw(bgRectShape);
            window.draw(rectShape);

            //  Update window
            window.display();

            //  Update time
            timePrev = timeNow;
        }

        window.close();
        UnregisterClassA(g_ContentWndClassName, hInst);
        g_idContentThread = 0;
    }

    namespace Handlers
    {
        bool ProcessEvents(sf::RenderWindow& window)
        {
            MSG message {0, static_cast<UINT>(~WM_QUIT)};
            while (PeekMessageA(&message, nullptr, 0, 0, PM_REMOVE))
            {
                if (message.message == WM_QUIT)
                    return false;

                TranslateMessage(&message);
                DispatchMessageA(&message);
            }

            sf::Event event;
            while (window.pollEvent(event))
            {
                switch (event.type)
                {
                case sf::Event::Closed:
                    PostQuitMessage(0);
                    return false;

                case sf::Event::Resized:
                {
                    sf::FloatRect visibleArea(0.f, 0.f, float(event.size.width), float(event.size.height));
                    window.setView(sf::View(visibleArea));
                } break;

                case sf::Event::KeyPressed:
                    switch (event.key.code)
                    {
                    case sf::Keyboard::Escape:
                        PostQuitMessage(0);
                        return false;
                    }
                    break;
                }
            }
            return true;
        }

        LRESULT CALLBACK MainWndEventProcessor(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
        {
            switch (message)
            {
            case WM_SIZING:
            case WM_SIZE:
            {
                RECT rect;
                GetClientRect(g_hMainWnd, &rect);
                g_ContentWndWidth = rect.right - rect.left;
                g_ContentWndHeight = rect.bottom - rect.top;
                MoveWindow(g_hContentWnd, 0, 0, g_ContentWndWidth, g_ContentWndHeight, true);
            } break;

            case WM_GETMINMAXINFO:
            {
                //  Setting up minimal size of the client area
                auto* pMinMxInfo = reinterpret_cast<LPMINMAXINFO>(lParam);
                pMinMxInfo->ptMinTrackSize.x = g_MainWndMinWidth;
                pMinMxInfo->ptMinTrackSize.y = g_MainWndMinHeight;
                pMinMxInfo->ptMaxTrackSize.x = g_MainWndMaxWidth;
                pMinMxInfo->ptMaxTrackSize.y = g_MainWndMaxHeight;
            } break;

            case WM_CHAR:
                //  Send message to 'Content' window
                SendNotifyMessageA(g_hContentWnd, message, wParam, lParam);
                switch (wParam)
                {
                case VK_ESCAPE:
                    PostQuitMessage(0);
                    break;
                } break;

            case WM_KEYDOWN:
            case WM_KEYUP:
            case WM_KEYLAST:
            case WM_MOUSEACTIVATE:
            case WM_MOUSEHOVER:
            case WM_MOUSELEAVE:
            case WM_MOUSELAST:
                //  Send message to 'Content' window
                SendNotifyMessageA(g_hContentWnd, message, wParam, lParam);
                //  Note: mouse messages are not necessary to send,
                //  'Content' window still can see them by its own.
                break;

            case WM_MOVE:
            case WM_MOVING:
                break;

            case WM_CLOSE:
                //  Send message to 'Content' window
                SendNotifyMessageA(g_hContentWnd, message, wParam, lParam);
                PostQuitMessage(0);
                break;

            default:
                return DefWindowProcA(hWnd, message, wParam, lParam);
            }
            return 0;
        }
        LRESULT CALLBACK ContentEventProcessor(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
        {
            switch (message)
            {
            case WM_CHAR:
                switch (wParam)
                {
                case VK_ESCAPE:
                    PostQuitMessage(0);
                }
                break;

            case WM_CLOSE:
                PostQuitMessage(0);
                break;

            default:
                return DefWindowProcA(hWnd, message, wParam, lParam);
            }
            return 0;
        }
    }
}

Details

So how it works? Here, as you can see, one main application window with typical style and one child window (placed inside of the parent). Roughly, child window is an empty area inside the main window. For this the example we will ensure that child's size will always remain exactly same size with parent's client area by calling 'MoveWindow' method with corresponding width and height, recieved from 'GetClientRect'. Every time when main window size changes we immediately change 'Content' window size as well.

Child 'Content' window is passed to sf::RenderWindow object right after creation. Now SFML is responsible for drawing inside of it and can recieve messages from OS before they come to our callback function 'ContentEventProcessor', which is not necessary here because all messages would be processed by SFML earlier. There isn't much interesting here so let's keep going.

For both of the windows we have a thread to process messages incoming from OS: initial thread of 'WinMain', and manually created 'contentWndThread'. Main performs processing a message queue for our main thread and decides which events will be forwarded to content thread for our purposes. Content thread contains default SFML working sequence: it receives and then process previously filtered events, manipulates/updates all objects, draws stuff. The only difference with standard SFML minimal working example is that we firstly need to recieve all messages from Main thread and dispatch them so SFML could work with them in a game loop.

Afterword

Provided solution may not been the best possible, but works well for most simple cases. Have a good day and huge progress with your projects!

Clone this wiki locally