C++从异步线程更新窗口



所以我刚开始使用C++,并希望创建一个带有按钮的窗口,该按钮启动计数器的异步线程,该计数器从5计数到0,表示一项耗时很长的任务。这个数字应该显示在窗口上,并在计数器计数时每秒更新一次。为此,子线程必须以任何方式与主窗口线程的消息循环进行通信。我试图通过以下方式做到这一点:

  • 使用主窗口的窗口句柄发送UpdateWindow
  • 使用主窗口的窗口句柄发送PostMessage

但在这两种情况下,窗口都不会更新。因此,我怀疑是错误,要么将窗口句柄从主线程发送到子线程,要么将UpdateWindow消息从子线程发送到主线程,要么两者兼而有之,要么我完全偏离了轨道,一切都错了。

也许我的思维方式也是错误的,我应该换一种方式去做,但我甚至不知道该如何开始。

#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;

我的柜台:

void counterr(HWND hWnd)
{
i = 5;
while(i>0)
{
i -= 1;
//UpdateWindow(hWnd);
PostMessage(hWnd, WM_PRINT, NULL, NULL);
Sleep(1000);
}
}

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;
}

标准的东西再次和代码结束

通常的方法是使用自定义消息ID调用SendMessage()或PostMessage()来通知UI线程所做的一些更改。

直接从线程更新UI是一种糟糕的做法,因为线程应该只做"工作",而不关心UI将如何显示这项工作的结果。

使用PostMessage,您已经步入正轨。但是,与其使用WM_PRINT,不如定义一个自定义消息ID,如下所示:

const UINT WM_APP_MY_THREAD_UPDATE = WM_APP + 0;

WM_APP到0xBFFF范围内的消息由应用程序保留供私人使用,因此您不必担心某些Windows组件已经使用了您的消息ID。

然后线程函数调用:

PostMessage(hWnd, WM_APP_MY_THREAD_UPDATE, 0, 0);

在您的WndProc中,将case WM_PRINT:替换为:

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;

您的代码还有另一个问题:

counterr线程函数更新全局变量i,而不考虑同步。在WM_PAINT中输出变量的GUI线程可能不会"看到"该变量已被其他线程更改,并且仍然输出旧值。例如,它可能已经将变量存储在寄存器中,并且仍然使用寄存器值,而不是从内存中重新读取实际值。当线程在多个CPU核心上运行时,情况会变得更糟,因为每个线程都有自己的缓存。它可能一直在你自己的机器上工作,但在用户的机器上总是或有时会失败!

同步是一个非常复杂的话题,所以我建议使用你最喜欢的搜索引擎查找"C++线程同步",并为一些冗长的阅读做好准备

对于您的代码,一个简单的解决方案是向线程函数添加一个局部变量i,并且只在线程内对该局部变量进行操作(无论如何都是个好主意)。当您发布WM_APP_MY_THREAD_UPDATE消息时,您将传递本地i作为消息的WPARAM或LPARAM的参数。

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);
}
}

为了避免混淆,我会在全局i:中添加一个前缀

int g_i = 0;

然后,在WM_APP_MY_THREAD_UPDATE的case分支中,您将从WPARAM参数更新g_i:

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

当然,您也会在WM_PAINT:期间使用g_i

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

最新更新