WinAPI实现按钮Hover时重绘异常问题求助
WinAPI子类化按钮悬停状态不同步绘制问题
我通过WinAPI创建按钮并子类化,希望所有事件(点击、悬停等)都在按钮子类过程中处理,而非父窗口的MainWndProc。需求是鼠标悬停时改变按钮颜色,用isHover变量控制WM_PAINT的颜色选择,但发现WM_MOUSEHOVER和WM_MOUSELEAVE中已经正确修改了isHover的值,可WM_PAINT里读取的isHover却没有同步,控制台输出能确认这个问题。
原代码
#define UNICODE #include <windows.h> #include <CommCtrl.h> #include <windowsx.h> #include <iostream> LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); LRESULT CALLBACK ButtonWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { const wchar_t g_szClassName[] = L"myWindowClass"; WNDCLASSEX wc = {0}; HWND hwnd; MSG Msg; wc.cbSize = sizeof(WNDCLASSEX); wc.style = 0; wc.lpfnWndProc = MainWndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = CreateSolidBrush(RGB(30, 30, 30)); wc.lpszMenuName = NULL; wc.lpszClassName = g_szClassName; wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); if (!RegisterClassEx(&wc)) { MessageBox(NULL, L"فشل تسجيل فئة النافذة", L"خطأ", MB_OK); return 1; } hwnd = CreateWindowEx(0, L"MyWindowClass", L"نافذة جديدة", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 500, 350, NULL, NULL, wc.hInstance, NULL); if (hwnd == NULL) { MessageBox(NULL, L"فشل إنشاء النافذة", L"خطأ", MB_OK); return 1; } HWND hButton1 = CreateWindowEx(0, L"BUTTON", L"button1", WS_VISIBLE | WS_CHILD | BS_FLAT, 200, 200, 100, 40, hwnd, (HMENU)101, NULL, NULL); if (hButton1 == NULL) { MessageBox(NULL, L"فشل إنشاء الزر", L"خطأ", MB_OK); return 1; } SetWindowSubclass(hButton1, &ButtonWndProc, 1, 0); ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); while (Msg.message != WM_QUIT) { if (GetMessage(&Msg, NULL, 0, 0)) { TranslateMessage(&Msg); DispatchMessage(&Msg); } } return 0; } LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); break; case WM_CLOSE: DestroyWindow(hwnd); break; default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0; } LRESULT CALLBACK ButtonWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) { bool isTracking; bool isHover; switch (uMsg) { case WM_NCDESTROY: RemoveWindowSubclass(hWnd, &ButtonWndProc, uIdSubclass); break; case WM_MOUSEMOVE: std::cout << "WM_MOUSEMOVE RECEIVED " << std::endl; std::cout << "hovered = " << isHover << std::endl; if (!isTracking) { TRACKMOUSEEVENT tme = {sizeof(TRACKMOUSEEVENT)}; tme.dwFlags = TME_LEAVE | TME_HOVER; tme.hwndTrack = hWnd; tme.dwHoverTime = 10; isTracking = TrackMouseEvent(&tme); } return 0 ; case WM_MOUSELEAVE: std::cout << "(" << GET_X_LPARAM(lParam) << ", " << GET_Y_LPARAM(lParam) << ")" << std::endl; isTracking = FALSE; isHover = FALSE; std::cout << "WM_MOUSELEAVE RECEIVED " << std::endl; std::cout << "hovered = " << isHover << std::endl; InvalidateRect(hWnd, NULL, true); UpdateWindow(hWnd); return 0; case WM_MOUSEHOVER: std::cout << "(" << GET_X_LPARAM(lParam) << ", " << GET_Y_LPARAM(lParam) << ")" << std::endl; isTracking = TRUE; isHover = TRUE; std::cout << "WM_MOUSEHOVER RECEIVED " << std::endl; std::cout << "hovered = " << isHover << std::endl; InvalidateRect(hWnd, NULL, true); UpdateWindow(hWnd); return 0; case WM_PAINT: PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); HBRUSH brush; std::cout << "WM_PAINT RECEIVED" << std::endl; std::cout << "paint hovered = " << isHover << std::endl; if (isHover) { brush = CreateSolidBrush(RGB(255, 0, 0)); } else { brush = CreateSolidBrush(RGB(0, 255, 0)); } FillRect(hdc, &ps.rcPaint, brush); DeleteObject(brush); EndPaint(hWnd, &ps); break; } return DefSubclassProc(hWnd, uMsg, wParam, lParam); }
问题根源
你在ButtonWndProc里定义的isTracking和isHover是局部变量,每次窗口过程被调用时都会重新初始化,之前修改的值根本没被保存,导致WM_PAINT读取的始终是未初始化的垃圾值。
修复方案
需要把这两个状态变量改成窗口私有数据,推荐以下两种方法:
方法1:子类化传递结构体指针(多按钮安全)
- 定义存储状态的结构体:
typedef struct { bool isTracking; bool isHover; } ButtonState;
- 在WinMain中创建按钮后,分配内存并传递给子类:
ButtonState* pState = new ButtonState{false, false}; SetWindowSubclass(hButton1, &ButtonWndProc, 1, (DWORD_PTR)pState);
- 在子类过程中通过
dwRefData获取状态指针:
LRESULT CALLBACK ButtonWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) { ButtonState* pState = reinterpret_cast<ButtonState*>(dwRefData); // 替换所有isTracking为pState->isTracking,isHover为pState->isHover
- 在窗口销毁时释放内存:
case WM_NCDESTROY: delete reinterpret_cast<ButtonState*>(dwRefData); RemoveWindowSubclass(hWnd, &ButtonWndProc, uIdSubclass); break;
方法2:静态局部变量(仅单按钮测试用)
如果只创建单个按钮,可以把变量改成静态局部变量,避免每次调用重新初始化:
LRESULT CALLBACK ButtonWndProc(...) { static bool isTracking = false; static bool isHover = false; // 后续逻辑不变
注意:这种方法在多个按钮时会导致状态共享,严重错误,仅适合单按钮测试。
额外优化
- 处理
WM_ERASEBKGND避免背景闪烁:
case WM_ERASEBKGND: return TRUE;
- 添加按钮文本绘制(当前代码仅填充背景):
在WM_PAINT的FillRect后添加:
wchar_t szText[256]; GetWindowText(hWnd, szText, 256); SetTextColor(hdc, RGB(255,255,255)); SetBkMode(hdc, TRANSPARENT); DrawText(hdc, szText, -1, &ps.rcPaint, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
内容的提问来源于stack exchange,提问作者stepic Game
相关产品推荐
相关产品推荐

