spaghetticode
spaghetticode

Reputation: 23

C++ updating windows window from async thread

So I just started with C++ and wanted to create a window with a button that starts an asynchronous thread for a counter that counts from 5 to 0, representing a long time consuming task. The number should've been shown on the Window and get updated every second while the counter is counting. For that the child thread has to communicate in any way with the Message Loop of the main window thread. I tried to do this by:

But in both cases, the window does not get updatet. So I'm suspecting an error by either sending the window handle from the main thread to the child thread or sending the UpdateWindow message from the child thread to the main thread or both or I'm completely off track and everythig is wrong.

Maybe my way of thinking is also wrong and i should do that on another way, still, i don t know how i even should start.

#include "stdafx.h"
#include "Testproject.h"
#include <iostream>
#include <string>
#include <thread>

#define MAX_LOADSTRING 100

// Global variables:
HINSTANCE hInst;                                // Aktuelle Instanz
WCHAR szTitle[MAX_LOADSTRING];                  // Titelleistentext
WCHAR szWindowClass[MAX_LOADSTRING];            
HWND Button1;
int i = 0;

My Counter:

void counterr(HWND hWnd)
{
    i = 5;
    while(i>0)
    {

    i -= 1;
    //UpdateWindow(hWnd);
    PostMessage(hWnd, WM_PRINT, NULL, NULL);
    Sleep(1000);

    }
}

standard window and message loop things from VisualStudio2017

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CREATE:
    {
        Button1 =   CreateWindow(L"Button",L"Counter",WS_VISIBLE|WS_CHILD|WS_BORDER,0,40,100,20,hWnd,(HMENU) 1,nullptr,nullptr);

    break;
}
case WM_COMMAND:
    {
        int wmId = LOWORD(wParam);
        // Menüauswahl bearbeiten:
        switch (wmId)
        {
        case IDM_ABOUT:
            DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
            break;
        case IDM_EXIT:
            DestroyWindow(hWnd);
            break;
        case 1:
        {
            std::thread t1(counterr, hWnd);
            t1.detach();
            break;
        }
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
    }
    break;
case WM_PRINT:
case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        //TODO: Zeichencode, der hdc verwendet, hier einfügen...
        RECT rc;
        RECT rc2 = { 0, 0, 0, 0 };
        int spacer = 3;
        GetClientRect(hWnd, &rc);

        SelectObject(hdc, GetStockObject(DEFAULT_GUI_FONT));
        SetBkMode(hdc, TRANSPARENT);
        SetTextColor(hdc, RGB(0, 0, 0));



        std::wstring strOut = std::to_wstring(i); // or wstring if you have unicode set
        DrawText(hdc, strOut.c_str(), strOut.length(), &rc, DT_SINGLELINE);
        DrawText(hdc, strOut.c_str(), strOut.length(), &rc2, DT_CALCRECT);
        rc.left = rc.left + rc2.right + spacer;
        std::wstring strOut2 = L"heya";
        DrawText(hdc, strOut2.c_str(), strOut2.length(), &rc, DT_TOP | DT_LEFT | DT_SINGLELINE);


        EndPaint(hWnd, &ps);
    }
    break;
case WM_DESTROY:
    PostQuitMessage(0);
    break;
default:
    return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

standard stuff again and end of Code

Upvotes: 2

Views: 1777

Answers (1)

zett42
zett42

Reputation: 27766

The usual way to do this, is to either call SendMessage() or PostMessage() with a custom message ID to notify the UI about some changes made by the thread.

Updating the UI directly from the thread is bad practice, because the thread should only do "work" and not be concerned about how the results of this work will be presented by the UI.

You already were on the right track by using PostMessage. But instead of using WM_PRINT you should define a custom message ID like this:

const UINT WM_APP_MY_THREAD_UPDATE = WM_APP + 0;

Messages in the range WM_APP through 0xBFFF are reserved for private use by the application, so you don't have to worry that some Windows component already uses your message ID.

Your thread function then calls:

PostMessage(hWnd, WM_APP_MY_THREAD_UPDATE, 0, 0);

In your WndProc replace the case WM_PRINT: by:

case WM_APP_MY_THREAD_UPDATE:
    // Tell Windows that the window content is no longer valid and 
    // it should update it as soon as possible.
    // If you want to improve performance a little bit, pass a rectangle
    // to InvalidateRect() that defines where the number is painted.    
    InvalidateRect( hWnd, nullptr, TRUE );
    break;

There is another issue with your code:

Your counterr thread function updates the global variable i without taking synchronization into account. The GUI thread who outputs the variable in WM_PAINT may not "see" that the variable has been changed by the other thread and still output the old value. For instance, it may have stored the variable in a register and still uses the register value instead of rereading the actual value from memory. Matters become worse when threads run on multiple CPU cores, where each thread has its own cache. It may work all the time on your own machine but always or sometimes fail on users machines!

Synchronization is a very complex topic so I suggest looking up "C++ thread synchronization" using your favorite search engine and be prepared for some lengthy reading. ;-)

A simple solution for your code would be to add a local variable i to the thread function and only operate on this local variable from within the thread (a good idea anyway). When you post the WM_APP_MY_THREAD_UPDATE message, you would pass the local i as the argument for the WPARAM or LPARAM of the message.

void counterr(HWND hWnd)
{
    int i = 5;  // <-- create local variable i instead of accessing global
                //     to avoid thread synchronization issues
    while(i>0)
    {
       i -= 1;

       // Pass local variable with the message
       PostMessage(hWnd, WM_APP_MY_THREAD_UPDATE, static_cast<WPARAM>( i ), 0);
       Sleep(1000);
    }
}

To avoid confusion i would add a prefix to the global i:

int g_i = 0;

Then in the case branch for WM_APP_MY_THREAD_UPDATE you would update g_i from the WPARAM parameter:

case WM_APP_MY_THREAD_UPDATE:
    g_i = static_cast<int>( wParam );
    InvalidateRect( hWnd, nullptr, TRUE );
    break;

Of course you would also use g_i during WM_PAINT:

case WM_PAINT:
    // other code here....
    std::wstring strOut = std::to_wstring(g_i);
    // other code here....
    break;

Upvotes: 3

Related Questions