Search code examples
c++canimationwinapirendering

WinApi drawing in every frame


I want to draw a sine wave on every frame. In every frame, after drawing, the angle will be increased and I want to have a smooth wave moving effect, but I don't know how to achieve drawing in every frame.

This is my code:

#include <windows.h>
#include <cmath>
#include <iostream>

LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

HWND hwnd;
float t = 0.0f;

HANDLE tickThreadHandle;

DWORD WINAPI tickThreadProc(HANDLE handle)
{
    Sleep(50);
    ShowWindow(hwnd, SW_SHOW);

    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);

    RECT r;
    GetWindowRect(hwnd, &r);

    HPEN hPen = CreatePen(PS_SOLID, 1, RGB(255, 255, 255));
    HPEN hOldPen = static_cast<HPEN>(SelectObject(hdc, hPen));

    int delay = 1000 / 50;

    while (true)
    {
        for (int x = 0; x<r.right; x += 5, t += 0.3f)
        {
            LineTo(hdc, x, sin(t) * 50 + 200);
        }

        t += 0.1f;
        Sleep(delay);
    }

    SelectObject(hdc, hOldPen);
    DeleteObject(hPen);
    EndPaint(hwnd, &ps);
}

int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nCmdShow)
{
    LPCTSTR lpszClassName = L"WNDCLASS";

    WNDCLASSEX wcex;
    ZeroMemory(&wcex, sizeof(wcex));

    wcex.cbSize = sizeof(wcex);
    wcex.hInstance = hInstance;
    wcex.lpszClassName = lpszClassName;
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.style = CS_DBLCLKS;
    wcex.lpfnWndProc = WndProc;
    wcex.hbrBackground = CreateSolidBrush(RGB(0, 0, 150));

    if (!RegisterClassEx(&wcex))
    {
        return -1;
    }

    hwnd = CreateWindowEx(
        NULL,
        lpszClassName,
        L"WINDOW",
        WS_OVERLAPPEDWINDOW,
        0,
        0,
        400,
        400,
        HWND_DESKTOP,
        NULL,
        hInstance,
        NULL
    );

    if (hwnd == NULL)
    {
        return -2;
    }

    //ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);
    //InvalidateRect(hwnd, NULL, TRUE);

    MSG msg;
    //SendMessage(hwnd, WM_PAINT, NULL, NULL);

    while (GetMessage(&msg, hwnd, 0, 0) > 0)
    {
        /*RedrawWindow(hwnd, &windowRect, NULL, RDW_INTERNALPAINT);*/
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_CREATE:
        {
            tickThreadHandle = CreateThread(NULL, NULL, &tickThreadProc, NULL, NULL, NULL);
        }
        case WM_DESTROY:
        {
            PostQuitMessage(NULL);
        }
        //case WM_PAINT:
        //{

        //}
        default:
        {
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
        }
    }

    return 0;
}

I tried many strategies, in my last attempt I was trying to do another thread, and draw from there, but my window does not even show. I was trying to send to my window messages WM_PAINT too, or using RedrawWindow function but that gave me nothing. I know that I could use it in wrong way, please correct me and give me a hint what I could do.


Solution

  • SetTimer can be used (with a normal message loop) to paint the window in intervals as short as 16 milliseconds. Windows will send WM_TIMER message at each interval where you can invalidate the window.

    LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
        switch (uMsg)
        {
        case WM_CREATE:
            SetTimer(hwnd, 1, 20, NULL); 
            break;
    
        case WM_TIMER:
            InvalidateRect(hwnd, NULL, FALSE);
            break;
    
        case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);
            render(hwnd, hdc);
            EndPaint(hwnd, &ps);
            break;
        }
        ...
    }
    

    The render function can be modified to use the HDC from paint function.

    void render(HWND hwnd, HDC hdc)
    {
        ...
        HBRUSH hbrush = CreateSolidBrush(RGB(0, 0, 150));
        HBRUSH oldbrush = (HBRUSH)SelectObject(hdc, hbrush);
        FillRect(hdc, &cr, hbrush); 
        ...
        SelectObject(hdc, oldbrush);
        DeleteObject(hbrush);
        //Sleep <== remove the Sleep function
    }
    

    Alternatively you can use the so called "gaming loop" with high resolution timers like std::chrono to invalidate the window at shorter intervals. WM_PAINT message can be handled as before.

    Direct3D or OpenGL games may use a similar message loop and call a render() function, but in this case we can just call InvalidateRect to paint the window.

    #include <chrono>
    ...
    
    MSG msg = { 0 };
    while(TRUE)
    {
        if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            if (msg.message == WM_QUIT) //<=== **** EDITED **** 
                break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            typedef std::chrono::high_resolution_clock hiresclock;
            static auto timer = hiresclock::now();
            auto milisec = (hiresclock::now() - timer).count() / 1000000;
            if(milisec > 20)
            {
                timer = hiresclock::now();
                //... draw
            }
        }
    }