feat: WindowCase 支持模拟不同类型的光标

This commit is contained in:
Xu 2026-01-05 22:06:31 +08:00
commit fb4c3b183d
23 changed files with 334 additions and 51 deletions

View file

@ -0,0 +1,244 @@
#include "pch.h"
#include "CursorWindow.h"
static HCURSOR CreateCursorFromBitmaps(HBITMAP hColorBitmap, HBITMAP hMaskBitmap) noexcept {
ICONINFO iconInfo = {
.fIcon = FALSE,
.xHotspot = 0,
.yHotspot = 0,
.hbmMask = hMaskBitmap,
.hbmColor = hColorBitmap
};
return CreateIconIndirect(&iconInfo);
}
static HCURSOR CreateColorCursor() noexcept {
const uint32_t width = (uint32_t)GetSystemMetrics(SM_CXCURSOR);
const uint32_t height = (uint32_t)GetSystemMetrics(SM_CYCURSOR);
// 颜色位图初始化为半透明红色
std::vector<uint32_t> colorBits(width * height, 0x80FF0000);
wil::unique_hbitmap hColorBitmap(CreateBitmap(width, height, 1, 32, colorBits.data()));
// 仍需要遮罩位图,不过既然颜色位图有透明通道,遮罩位图不会被使用,因此无需初始化
wil::unique_hbitmap hMaskBitmap(CreateBitmap(width, height, 1, 1, nullptr));
return CreateCursorFromBitmaps(hColorBitmap.get(), hMaskBitmap.get());
}
static HCURSOR CreateMonochromeCursor() noexcept {
const uint32_t width = (uint32_t)GetSystemMetrics(SM_CXCURSOR);
const uint32_t height = (uint32_t)GetSystemMetrics(SM_CYCURSOR);
// width 必定是 16 的倍数,因此遮罩位图每行都已按 WORD 对齐
assert(width % 16 == 0);
const uint32_t widthByteCount = width / 8;
// 单色光标无颜色位图,遮罩位图高度是光标高度的两倍
const uint32_t bitmapHeight = height * 2;
// 分为四个部分:
// 左上:白色 (AND=0, XOR=1)
// 右上:黑色 (AND=0, XOR=0)
// 左下:反色 (AND=1, XOR=1)
// 右下:透明 (AND=1, XOR=0)
assert(width % 16 == 0);
std::vector<uint8_t> maskBits(widthByteCount * bitmapHeight);
// AND 遮罩下半置 1
for (uint32_t y = height / 2; y < height; ++y) {
for (uint32_t x = 0; x < width; ++x) {
uint32_t byteIdx = y * widthByteCount + x / 8;
uint32_t bitIdx = 7 - (x % 8);
maskBits[byteIdx] |= 1 << bitIdx;
}
}
// XOR 遮罩左半置 1
for (uint32_t y = height; y < bitmapHeight; ++y) {
for (uint32_t x = 0; x < width / 2; ++x) {
uint32_t byteIdx = y * widthByteCount + x / 8;
uint32_t bitIdx = 7 - (x % 8);
maskBits[byteIdx] |= 1 << bitIdx;
}
}
wil::unique_hbitmap hMaskBitmap(CreateBitmap(width, bitmapHeight, 1, 1, maskBits.data()));
return CreateCursorFromBitmaps(NULL, hMaskBitmap.get());
}
static HCURSOR CreateMaskedColorCursor() noexcept {
const uint32_t width = (uint32_t)GetSystemMetrics(SM_CXCURSOR);
const uint32_t height = (uint32_t)GetSystemMetrics(SM_CYCURSOR);
// width 必定是 16 的倍数,因此遮罩位图每行都已按 WORD 对齐
assert(width % 16 == 0);
const uint32_t widthByteCount = width / 8;
// 分为两个部分:
// 左半:红色 (COLOR=0x00FF0000, MASK=0)
// 右半:和红色 XOR (COLOR=0x00FF0000, MASK=1)
// 颜色位图初始化为红色。注意透明通道为 0否则会被识别为彩色光标遮罩位图被忽略
std::vector<uint32_t> colorBits(width * height, 0x00FF0000);
std::vector<uint8_t> maskBits(widthByteCount * height, 0);
// XOR 遮罩右半置 1
for (uint32_t y = 0; y < height; ++y) {
for (uint32_t x = width / 2; x < width; ++x) {
uint32_t byteIdx = y * widthByteCount + x / 8;
uint32_t bitIdx = 7 - (x % 8);
maskBits[byteIdx] |= 1 << bitIdx;
}
}
wil::unique_hbitmap hColorBitmap(CreateBitmap(width, height, 1, 32, colorBits.data()));
wil::unique_hbitmap hMaskBitmap(CreateBitmap(width, height, 1, 1, maskBits.data()));
return CreateCursorFromBitmaps(hColorBitmap.get(), hMaskBitmap.get());
}
bool CursorWindow::Create() noexcept {
static const wchar_t* WINDOW_NAME = L"CursorWindow";
_hCursor = CreateColorCursor();
WNDCLASSEXW wcex = {
.cbSize = sizeof(WNDCLASSEX),
.style = CS_HREDRAW | CS_VREDRAW,
.lpfnWndProc = _WndProc,
.hInstance = wil::GetModuleInstanceHandle(),
.lpszClassName = WINDOW_NAME
};
if (!RegisterClassEx(&wcex)) {
return false;
}
CreateWindow(
WINDOW_NAME,
WINDOW_NAME,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
wil::GetModuleInstanceHandle(),
this
);
if (!Handle()) {
return false;
}
const double dpiScale = _DpiScale();
SetWindowPos(Handle(), NULL, 0, 0,
std::lround(500 * dpiScale), std::lround(400 * dpiScale),
SWP_NOMOVE | SWP_SHOWWINDOW);
return true;
}
LRESULT CursorWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) noexcept {
switch (msg) {
case WM_CREATE:
{
const LRESULT ret = base_type::_MessageHandler(msg, wParam, lParam);
const HMODULE hInst = wil::GetModuleInstanceHandle();
_hwndBtn1 = CreateWindow(L"BUTTON", L"彩色光标",
WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, Handle(), (HMENU)1, hInst, 0);
_hwndBtn2 = CreateWindow(L"BUTTON", L"单色光标",
WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, Handle(), (HMENU)2, hInst, 0);
_hwndBtn3 = CreateWindow(L"BUTTON", L"彩色掩码光标",
WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, Handle(), (HMENU)3, hInst, 0);
_UpdateButtonPos();
SendMessage(_hwndBtn1, WM_SETFONT, (WPARAM)_UIFont(), TRUE);
SendMessage(_hwndBtn2, WM_SETFONT, (WPARAM)_UIFont(), TRUE);
SendMessage(_hwndBtn3, WM_SETFONT, (WPARAM)_UIFont(), TRUE);
return ret;
}
case WM_COMMAND:
{
if (HIWORD(wParam) == BN_CLICKED) {
const WORD btnId = LOWORD(wParam);
if (btnId == 1) {
_hCursor = CreateColorCursor();
} else if (btnId == 2) {
_hCursor = CreateMonochromeCursor();
} else {
_hCursor = CreateMaskedColorCursor();
}
return 0;
}
break;
}
case WM_SIZE:
{
_UpdateButtonPos();
break;
}
case WM_SETCURSOR:
{
if (LOWORD(lParam) == HTCLIENT) {
SetCursor(_hCursor);
return TRUE;
}
break;
}
case WM_ERASEBKGND:
{
// 绘制渐变背景
PAINTSTRUCT ps;
HDC hdc = BeginPaint(Handle(), &ps);
RECT rc;
GetClientRect(Handle(), &rc);
TRIVERTEX vertices[] = {
{
.x = rc.left,
.y = rc.top,
.Red = 0xE000,
.Green = 0x6000,
.Blue = 0xE000
},
{
.x = rc.right,
.y = rc.bottom,
.Red = 0x6000,
.Green = 0xE000,
.Blue = 0x6000
}
};
GRADIENT_RECT rect = { 0, 1 };
GradientFill(hdc, vertices, 2, &rect, 1, GRADIENT_FILL_RECT_V);
EndPaint(Handle(), &ps);
return TRUE;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return base_type::_MessageHandler(msg, wParam, lParam);
}
void CursorWindow::_UpdateButtonPos() noexcept {
RECT clientRect;
GetClientRect(Handle(), &clientRect);
const double dpiScale = _DpiScale();
const SIZE btnSize = { std::lround(160 * dpiScale),std::lround(40 * dpiScale) };
const LONG spacing = std::lround(8 * dpiScale);
const LONG btnLeft = ((clientRect.right - clientRect.left) - btnSize.cx) / 2;
const LONG windowCenterY = (clientRect.bottom - clientRect.top) / 2;
SetWindowPos(_hwndBtn1, NULL, btnLeft, windowCenterY - 3 * btnSize.cy / 2 - spacing,
btnSize.cx, btnSize.cy, SWP_NOACTIVATE);
SetWindowPos(_hwndBtn2, NULL, btnLeft, windowCenterY - btnSize.cy / 2,
btnSize.cx, btnSize.cy, SWP_NOACTIVATE);
SetWindowPos(_hwndBtn3, NULL, btnLeft, windowCenterY + btnSize.cy / 2 + spacing,
btnSize.cx, btnSize.cy, SWP_NOACTIVATE);
}