C++ 从异步线程更新 windows window
C++ updating windows window from async thread
所以我刚开始使用 C++,想创建一个 window 带有一个按钮,该按钮为一个从 5 计数到 0 的计数器启动一个异步线程,代表一个耗时很长的任务。该数字应该显示在 Window 上,并且在计数器计数时每秒更新一次。为此,子线程必须以任何方式与主 window 线程的消息循环进行通信。
我试图通过以下方式做到这一点:
- 发送更新Window,使用window主window句柄
- 发送带有 window 主句柄的 PostMessage window
但是在这两种情况下,window 都没有更新。所以我怀疑是通过将 window 句柄从主线程发送到子线程或将 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);
}
}
标准 window 和消息循环来自 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 是不好的做法,因为线程应该只做 "work" 而不必关心 [=72 将如何呈现这项工作的结果=].
您已经使用 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线程可能不会"see"该变量已被其他线程更改,仍然输出旧值。例如,它可能已将变量存储在寄存器中并且仍然使用寄存器值而不是从内存中重新读取实际值。当线程 运行 在多个 CPU 核心上时,情况会变得更糟,其中每个线程都有自己的缓存。
它可能在您自己的机器上一直有效,但在用户机器上总是或有时会失败!
同步是一个非常复杂的主题,因此我建议使用您最喜欢的搜索引擎查找 "C++ thread synchronization" 并准备好进行一些冗长的阅读。 ;-)
您的代码的一个简单解决方案是向线程函数添加一个局部变量 i
,并且仅在线程内对该局部变量进行操作(无论如何是个好主意)。当您 post 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;
所以我刚开始使用 C++,想创建一个 window 带有一个按钮,该按钮为一个从 5 计数到 0 的计数器启动一个异步线程,代表一个耗时很长的任务。该数字应该显示在 Window 上,并且在计数器计数时每秒更新一次。为此,子线程必须以任何方式与主 window 线程的消息循环进行通信。 我试图通过以下方式做到这一点:
- 发送更新Window,使用window主window句柄
- 发送带有 window 主句柄的 PostMessage window
但是在这两种情况下,window 都没有更新。所以我怀疑是通过将 window 句柄从主线程发送到子线程或将 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);
}
}
标准 window 和消息循环来自 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 是不好的做法,因为线程应该只做 "work" 而不必关心 [=72 将如何呈现这项工作的结果=].
您已经使用 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线程可能不会"see"该变量已被其他线程更改,仍然输出旧值。例如,它可能已将变量存储在寄存器中并且仍然使用寄存器值而不是从内存中重新读取实际值。当线程 运行 在多个 CPU 核心上时,情况会变得更糟,其中每个线程都有自己的缓存。
它可能在您自己的机器上一直有效,但在用户机器上总是或有时会失败!
同步是一个非常复杂的主题,因此我建议使用您最喜欢的搜索引擎查找 "C++ thread synchronization" 并准备好进行一些冗长的阅读。 ;-)
您的代码的一个简单解决方案是向线程函数添加一个局部变量 i
,并且仅在线程内对该局部变量进行操作(无论如何是个好主意)。当您 post 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;