如何无第三方库创建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; }
问题分析与修复方案
原代码核心问题
- 亮度渐变逻辑错误:亮度随循环变量
i线性变化,而非围绕当前激活的spinnerState形成渐变高亮,导致动画杂乱。 - 坐标系计算偏差:直接使用Spinner结构体尺寸计算中心,未考虑按钮客户区实际尺寸(按钮边框会占用窗口空间)。
- GDI资源管理不当:BeginPaint返回的HDC无需手动删除;替换画笔后未恢复原画笔,易引发资源泄漏。
- 旋转方向不符合常规:默认三角函数计算为逆时针旋转,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); }
修复要点说明
- 亮度渐变逻辑:以当前激活状态为中心,计算各段的最短距离,距离越近亮度越高,形成平滑的旋转高亮效果。
- 真实客户区计算:通过
GetClientRect获取按钮内部可绘制区域,确保Spinner精准居中。 - GDI资源安全:保存并恢复原画笔,避免资源泄漏;不再手动删除BeginPaint返回的HDC。
- 顺时针旋转:对角度取反并适配GDI坐标系,实现符合常规的顺时针旋转动画。
内容的提问来源于stack exchange,提问作者Jabu
相关产品推荐
相关产品推荐

