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

如何无第三方库创建Spinner按钮?WM_PAINT计算遇难题

自定义Win32 Spinner按钮绘制问题

我正尝试不使用第三方库创建Spinner按钮,当前Spinner的动画效果怪异,各段线条亮度变化逻辑混乱,整体旋转不流畅自然。我在WM_PAINT消息的计算逻辑上遇到了困难,以下是可编译的最小示例代码:

#include <windows.h>
#include <cmath>
#include <commctrl.h>
#pragma comment(lib, "Comctl32.lib")

#define SPINNER_STEPS 12
#define M_PI 3.14159265358979323846

struct Spinner
{
    int x;
    int y;
    int w;
    int h;
    int spinnerState = 0;
};

LRESULT CALLBACK ButtonProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    Spinner& opt = *(Spinner*)dwRefData;

    if (msg == WM_TIMER)
    {  
        // Update the spinner state
        opt.spinnerState = (opt.spinnerState + 1) % SPINNER_STEPS;
        // Redraw the button
        InvalidateRect(hwnd, NULL, TRUE);
        return 0;
    }
    else if (msg == WM_PAINT)
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);

        int spinnerSize = min(opt.w, opt.h) / 2;
        int spinnerX = opt.w / 2;
        int spinnerY = opt.h / 2;
        for (int i = 0; i < SPINNER_STEPS; i++)
        {
            // Calculate alpha based on the spinner state
            int alpha = (i == opt.spinnerState) ? 255 : 255 * i / SPINNER_STEPS;

            // Modify the color based on the spinner state
            COLORREF color = RGB(255 * alpha / 255, 255 * alpha / 255, 255 * alpha / 255);
            HPEN hPen = CreatePen(PS_SOLID, 2, color);
            SelectObject(hdc, hPen);

            // Calculate the x and y coordinates based on the spinner size
            int x1 = spinnerX + cos(2 * M_PI * i / SPINNER_STEPS) * spinnerSize / 2;
            int y1 = spinnerY + sin(2 * M_PI * i / SPINNER_STEPS) * spinnerSize / 2;
            int x2 = spinnerX + cos(2 * M_PI * (i + 1) / SPINNER_STEPS) * spinnerSize / 2;
            int y2 = spinnerY + sin(2 * M_PI * (i + 1) / SPINNER_STEPS) * spinnerSize / 2;
            MoveToEx(hdc, x1, y1, NULL);
            LineTo(hdc, x2, y2);

            DeleteObject(hPen);
        }

        EndPaint(hwnd, &ps);
        DeleteObject(hdc);
        return 0;
    }

    return DefSubclassProc(hwnd, msg, wParam, lParam);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    if (msg == WM_ERASEBKGND)
    {
        RECT rect;
        GetClientRect(hwnd, &rect);
        HBRUSH brush = CreateSolidBrush(RGB(0, 0, 0));
        FillRect((HDC)wParam, &rect, brush);
        DeleteObject(brush);
        return 1;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int)
{
    WNDCLASSEXW wc;

    wc.cbSize = sizeof(WNDCLASSEX);

    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_DBLCLKS;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra  = 0;
    wc.cbWndExtra  = 0;
    wc.hInstance   = 0;
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = CreateSolidBrush(RGB(0, 0, 0));
    wc.lpszMenuName = NULL;
    wc.lpszClassName = L".";
    RegisterClassEx(&wc);

    int guiWidth = 200;
    int guiHeight = 150;
    RegisterClassExW(&wc);
    HWND hwnd = CreateWindowW(wc.lpszClassName, L".", WS_OVERLAPPEDWINDOW, (GetSystemMetrics(SM_CXSCREEN) - guiWidth) / 2, (GetSystemMetrics(SM_CYSCREEN) - guiHeight) / 2, guiWidth, guiHeight, nullptr, nullptr, hInstance, nullptr);

    Spinner spinner;
    spinner.x = 30;
    spinner.y = 30;
    spinner.w = 128;
    spinner.h = 64;

    HWND button = CreateWindow(L"BUTTON", L"", 
                WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON | BS_NOTIFY,
                spinner.x, spinner.y, spinner.w, spinner.h, 
                hwnd, NULL, NULL, NULL);

    SetWindowSubclass(button, ButtonProc, 0, (DWORD_PTR)&spinner);
    SetTimer(button, 1, 50, NULL);

    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd);

    MSG msg;

    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);        
    }

    return (int) msg.wParam;
}

问题分析与修复方案

原代码核心问题

  1. 亮度渐变逻辑错误:亮度随循环变量i线性变化,而非围绕当前激活的spinnerState形成渐变高亮,导致动画杂乱。
  2. 坐标系计算偏差:直接使用Spinner结构体尺寸计算中心,未考虑按钮客户区实际尺寸(按钮边框会占用窗口空间)。
  3. GDI资源管理不当:BeginPaint返回的HDC无需手动删除;替换画笔后未恢复原画笔,易引发资源泄漏。
  4. 旋转方向不符合常规:默认三角函数计算为逆时针旋转,Spinner通常应为顺时针。

修复后的ButtonProc代码

LRESULT CALLBACK ButtonProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    Spinner& opt = *(Spinner*)dwRefData;

    if (msg == WM_TIMER)
    {  
        opt.spinnerState = (opt.spinnerState + 1) % SPINNER_STEPS;
        InvalidateRect(hwnd, NULL, TRUE);
        return 0;
    }
    else if (msg == WM_PAINT)
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);

        // 获取按钮真实客户区尺寸
        RECT clientRect;
        GetClientRect(hwnd, &clientRect);
        int clientW = clientRect.right - clientRect.left;
        int clientH = clientRect.bottom - clientRect.top;

        int spinnerSize = min(clientW, clientH) * 3 / 4; // 调整占比避免边缘裁切
        int centerX = clientW / 2;
        int centerY = clientH / 2;

        HPEN oldPen = (HPEN)SelectObject(hdc, GetStockObject(NULL_PEN)); // 先选空笔避免干扰

        for (int i = 0; i < SPINNER_STEPS; i++)
        {
            // 计算当前段与激活状态的最短距离,实现渐变高亮
            int distance = abs(i - opt.spinnerState);
            distance = min(distance, SPINNER_STEPS - distance);
            int alpha = 255 - (distance * 255 / SPINNER_STEPS);

            COLORREF color = RGB(alpha, alpha, alpha);
            HPEN hPen = CreatePen(PS_SOLID, 2, color);
            HPEN prevPen = (HPEN)SelectObject(hdc, hPen);

            // 修正为顺时针旋转,适配GDI Y轴向下的坐标系
            double angle1 = 2 * M_PI * i / SPINNER_STEPS;
            double angle2 = 2 * M_PI * (i + 1) / SPINNER_STEPS;
            int x1 = centerX + cos(-angle1) * spinnerSize / 2;
            int y1 = centerY + sin(-angle1) * spinnerSize / 2;
            int x2 = centerX + cos(-angle2) * spinnerSize / 2;
            int y2 = centerY + sin(-angle2) * spinnerSize / 2;

            MoveToEx(hdc, x1, y1, NULL);
            LineTo(hdc, x2, y2);

            SelectObject(hdc, prevPen);
            DeleteObject(hPen);
        }

        SelectObject(hdc, oldPen);
        EndPaint(hwnd, &ps);
        return 0;
    }

    return DefSubclassProc(hwnd, msg, wParam, lParam);
}

修复要点说明

  1. 亮度渐变逻辑:以当前激活状态为中心,计算各段的最短距离,距离越近亮度越高,形成平滑的旋转高亮效果。
  2. 真实客户区计算:通过GetClientRect获取按钮内部可绘制区域,确保Spinner精准居中。
  3. GDI资源安全:保存并恢复原画笔,避免资源泄漏;不再手动删除BeginPaint返回的HDC。
  4. 顺时针旋转:对角度取反并适配GDI坐标系,实现符合常规的顺时针旋转动画。

内容的提问来源于stack exchange,提问作者Jabu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 02:02:03