Files
SimpleRemoter/docs/CustomCursor_Design.md
2026-04-19 22:55:21 +02:00

281 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 自定义光标支持设计方案
**状态:已实现**
## 概述
扩展远程桌面的光标支持,使其能够显示应用程序自定义光标和动画光标,而不仅仅是 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<BYTE> 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 的光标,应正确处理或回退