Init: Migrate SimpleRemoter (Since v1.3.1) to Gitea
This commit is contained in:
280
docs/CustomCursor_Design.md
Normal file
280
docs/CustomCursor_Design.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# 自定义光标支持设计方案
|
||||
|
||||
**状态:已实现** ✓
|
||||
|
||||
## 概述
|
||||
|
||||
扩展远程桌面的光标支持,使其能够显示应用程序自定义光标和动画光标,而不仅仅是 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 的光标,应正确处理或回退
|
||||
Reference in New Issue
Block a user