# 自定义光标支持设计方案 **状态:已实现** ✓ ## 概述 扩展远程桌面的光标支持,使其能够显示应用程序自定义光标和动画光标,而不仅仅是 16 种标准系统光标。 ## 现有实现 ### 数据流 ``` 客户端 (ScreenCapture.h): getCurrentCursorIndex() → 0-15 或 -1 帧数据: [算法(1)] [光标位置(8)] [光标索引(1)] [图像数据...] 服务端 (ScreenSpyDlg.cpp): cursorIndex == -1 → 回退到标准箭头 cursorIndex 0-15 → 使用 m_CursorInfo.getCursorHandle(index) ``` ### 限制 - 仅支持 16 种标准系统光标 (IDC_ARROW, IDC_HAND, IDC_IBEAM 等) - 自定义光标显示为箭头 - 动画光标不支持 ## 设计方案 ### 方案概述 采用**独立命令传输 + 哈希缓存 + 节流机制**: 1. 新增 `CMD_CURSOR_IMAGE` 命令传输光标位图 2. 使用位图哈希避免重复传输 3. 节流机制防止动画光标过载 4. 光标索引 `-2` 表示使用已缓存的自定义光标 ### 光标索引定义 | 索引值 | 含义 | |-------|------| | 0-15 | 标准系统光标(现有) | | -1 (255) | 不支持,显示箭头(现有,向后兼容) | | -2 (254) | 使用缓存的自定义光标(新增) | ### CMD_CURSOR_IMAGE 数据格式 ``` 字节偏移 大小 字段 ──────────────────────────── 0 1 CMD_CURSOR_IMAGE (命令字节) 1 4 hash (位图哈希,用于去重) 5 2 hotspotX (热点X坐标) 7 2 hotspotY (热点Y坐标) 9 1 width (光标宽度,最大255) 10 1 height (光标高度,最大255) 11 N BGRA位图数据 (N = width * height * 4) 典型大小: 32x32光标 = 11 + 4096 = 4107 字节 ``` ### 客户端逻辑 (ScreenCapture.h / ScreenManager.cpp) ```cpp // 新增状态变量 DWORD m_lastCursorHash = 0; // 上次发送的光标哈希 DWORD m_lastCursorSendTime = 0; // 上次发送时间 const DWORD CURSOR_THROTTLE = 50; // 最小发送间隔 (ms) // 每帧处理逻辑 int cursorIndex = getCurrentCursorIndex(); if (cursorIndex == -1) { // 非标准光标,获取位图 CursorBitmapInfo info; if (GetCurrentCursorBitmap(&info)) { DWORD hash = CalculateBitmapHash(info); DWORD now = GetTickCount(); // 哈希变化且超过节流时间 → 发送光标图像 if (hash != m_lastCursorHash && (now - m_lastCursorSendTime) > CURSOR_THROTTLE) { SendCursorImage(info, hash); m_lastCursorHash = hash; m_lastCursorSendTime = now; } cursorIndex = -2; // 使用自定义光标 } } // 帧数据中写入 cursorIndex ``` ### 服务端逻辑 (ScreenSpyDlg.cpp) ```cpp // 新增成员变量 HCURSOR m_hCustomCursor = NULL; // 缓存的自定义光标 DWORD m_customCursorHash = 0; // 当前光标哈希 // 处理 CMD_CURSOR_IMAGE case CMD_CURSOR_IMAGE: { DWORD hash = *(DWORD*)(buffer + 1); if (hash != m_customCursorHash) { WORD hotX = *(WORD*)(buffer + 5); WORD hotY = *(WORD*)(buffer + 7); BYTE width = buffer[9]; BYTE height = buffer[10]; LPBYTE bitmapData = buffer + 11; // 销毁旧光标 if (m_hCustomCursor) { DestroyCursor(m_hCustomCursor); } // 创建新光标 m_hCustomCursor = CreateCursorFromBitmap( hotX, hotY, width, height, bitmapData); m_customCursorHash = hash; } break; } // 处理帧数据中的光标索引 BYTE cursorIndex = buffer[2 + sizeof(POINT)]; HCURSOR cursor; if (cursorIndex == 254) { // -2 cursor = m_hCustomCursor ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW); } else if (cursorIndex == 255) { // -1 cursor = LoadCursor(NULL, IDC_ARROW); } else { cursor = m_CursorInfo.getCursorHandle(cursorIndex); } ``` ### 获取光标位图实现 ```cpp struct CursorBitmapInfo { WORD hotspotX; WORD hotspotY; BYTE width; BYTE height; std::vector bgraData; // width * height * 4 }; bool GetCurrentCursorBitmap(CursorBitmapInfo* info) { CURSORINFO ci = { sizeof(CURSORINFO) }; if (!GetCursorInfo(&ci) || ci.flags != CURSOR_SHOWING) return false; ICONINFO iconInfo; if (!GetIconInfo(ci.hCursor, &iconInfo)) return false; info->hotspotX = (WORD)iconInfo.xHotspot; info->hotspotY = (WORD)iconInfo.yHotspot; BITMAP bm; GetObject(iconInfo.hbmColor ? iconInfo.hbmColor : iconInfo.hbmMask, sizeof(BITMAP), &bm); info->width = (BYTE)min(bm.bmWidth, 255); info->height = (BYTE)min(bm.bmHeight, 255); // 获取 BGRA 位图数据 // ... (使用 GetDIBits) // 清理 DeleteObject(iconInfo.hbmColor); DeleteObject(iconInfo.hbmMask); return true; } ``` ### 哈希计算 ```cpp DWORD CalculateBitmapHash(const CursorBitmapInfo& info) { // 使用 FNV-1a 或简单的累加哈希 DWORD hash = 2166136261; // FNV offset basis const BYTE* data = info.bgraData.data(); size_t len = info.bgraData.size(); for (size_t i = 0; i < len; i += 16) { // 每16字节采样一次,加速 hash ^= data[i]; hash *= 16777619; // FNV prime } return hash; } ``` ## 数据流图 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 客户端 │ ├─────────────────────────────────────────────────────────────────┤ │ getCurrentCursorIndex() │ │ │ │ │ ├─ 0-15 ──→ 帧数据: cursorIndex = 0-15 │ │ │ │ │ └─ -1 ──→ GetCurrentCursorBitmap() │ │ │ │ │ ├─ 计算哈希 │ │ │ │ │ ├─ 哈希变化 && 超过节流? │ │ │ │ │ │ │ ├─ Yes ──→ 发送 CMD_CURSOR_IMAGE │ │ │ │ 更新 lastHash, lastTime │ │ │ │ │ │ │ └─ No ──→ 跳过发送 │ │ │ │ │ └─ 帧数据: cursorIndex = -2 │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 服务端 │ ├─────────────────────────────────────────────────────────────────┤ │ 收到 CMD_CURSOR_IMAGE: │ │ └─ hash != m_customCursorHash? │ │ ├─ Yes ──→ CreateCursorFromBitmap() → m_hCustomCursor │ │ └─ No ──→ 忽略 (已缓存) │ │ │ │ 收到帧数据: │ │ └─ cursorIndex: │ │ ├─ 0-15 ──→ m_CursorInfo.getCursorHandle(index) │ │ ├─ -1 ──→ LoadCursor(IDC_ARROW) (向后兼容) │ │ └─ -2 ──→ m_hCustomCursor │ └─────────────────────────────────────────────────────────────────┘ ``` ## 动画光标支持 本方案自然支持动画光标: | 机制 | 说明 | |-----|------| | **自动检测** | 动画每帧位图哈希不同,自动触发传输 | | **节流保护** | 50ms 节流 ≈ 最高 20fps,防止过载 | | **带宽控制** | 32x32 动画 @ 10fps ≈ 40 KB/s,可接受 | ## 修改文件清单 | 文件 | 修改内容 | |-----|---------| | `common/commands.h` | 新增 `CMD_CURSOR_IMAGE = 75` | | `client/CursorInfo.h` | 新增 `GetCurrentCursorBitmap()`, `CalculateBitmapHash()` | | `client/ScreenCapture.h` | 发送逻辑:检测、节流、发送 CMD_CURSOR_IMAGE | | `client/ScreenManager.cpp` | 处理 CMD_CURSOR_IMAGE(如果服务端发给客户端需要) | | `server/2015Remote/ScreenSpyDlg.h` | 新增 `m_hCustomCursor`, `m_customCursorHash` | | `server/2015Remote/ScreenSpyDlg.cpp` | 处理 CMD_CURSOR_IMAGE,更新光标显示逻辑 | ## 向后兼容性 - 旧客户端:继续发送 `-1`,服务端显示箭头(现有行为) - 新客户端 + 旧服务端:发送 `-2` 和 CMD_CURSOR_IMAGE,旧服务端忽略新命令,`-2` 当作 `-1` 处理 - 新客户端 + 新服务端:完整自定义光标支持 ## 可调参数 ```cpp // client/ScreenCapture.h const DWORD CURSOR_THROTTLE = 50; // 节流间隔 (ms) // 50ms → 最高 20fps,动画流畅 // 100ms → 最高 10fps,省带宽 const BYTE MAX_CURSOR_SIZE = 64; // 最大光标尺寸 // 超过此尺寸的光标将被缩放或回退到标准光标 ``` ## 测试用例 1. **标准光标** - 箭头、手形、I形等,应使用索引 0-15 2. **自定义静态光标** - 应用程序自定义光标,只传输一次 3. **动画光标** - Windows 忙碌动画,应持续更新(受节流限制) 4. **快速切换** - 频繁切换光标,节流应生效 5. **大尺寸光标** - 超过 64x64 的光标,应正确处理或回退