281 lines
10 KiB
Markdown
281 lines
10 KiB
Markdown
# 自定义光标支持设计方案
|
||
|
||
**状态:已实现** ✓
|
||
|
||
## 概述
|
||
|
||
扩展远程桌面的光标支持,使其能够显示应用程序自定义光标和动画光标,而不仅仅是 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 的光标,应正确处理或回退
|