10 KiB
10 KiB
自定义光标支持设计方案
状态:已实现 ✓
概述
扩展远程桌面的光标支持,使其能够显示应用程序自定义光标和动画光标,而不仅仅是 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 等)
- 自定义光标显示为箭头
- 动画光标不支持
设计方案
方案概述
采用独立命令传输 + 哈希缓存 + 节流机制:
- 新增
CMD_CURSOR_IMAGE命令传输光标位图 - 使用位图哈希避免重复传输
- 节流机制防止动画光标过载
- 光标索引
-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)
// 新增状态变量
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)
// 新增成员变量
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);
}
获取光标位图实现
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;
}
哈希计算
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处理 - 新客户端 + 新服务端:完整自定义光标支持
可调参数
// client/ScreenCapture.h
const DWORD CURSOR_THROTTLE = 50; // 节流间隔 (ms)
// 50ms → 最高 20fps,动画流畅
// 100ms → 最高 10fps,省带宽
const BYTE MAX_CURSOR_SIZE = 64; // 最大光标尺寸
// 超过此尺寸的光标将被缩放或回退到标准光标
测试用例
- 标准光标 - 箭头、手形、I形等,应使用索引 0-15
- 自定义静态光标 - 应用程序自定义光标,只传输一次
- 动画光标 - Windows 忙碌动画,应持续更新(受节流限制)
- 快速切换 - 频繁切换光标,节流应生效
- 大尺寸光标 - 超过 64x64 的光标,应正确处理或回退