You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:子类化传递结构体指针(多按钮安全)

  1. 定义存储状态的结构体:
typedef struct {
    bool isTracking;
    bool isHover;
} ButtonState;
  1. 在WinMain中创建按钮后,分配内存并传递给子类:
ButtonState* pState = new ButtonState{false, false};
SetWindowSubclass(hButton1, &ButtonWndProc, 1, (DWORD_PTR)pState);
  1. 在子类过程中通过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
  1. 在窗口销毁时释放内存:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 15:39:51