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

10 KiB
Raw Blame History

自定义光标支持设计方案

状态:已实现

概述

扩展远程桌面的光标支持,使其能够显示应用程序自定义光标和动画光标,而不仅仅是 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)

// 新增状态变量
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;    // 最大光标尺寸
// 超过此尺寸的光标将被缩放或回退到标准光标

测试用例

  1. 标准光标 - 箭头、手形、I形等应使用索引 0-15
  2. 自定义静态光标 - 应用程序自定义光标,只传输一次
  3. 动画光标 - Windows 忙碌动画,应持续更新(受节流限制)
  4. 快速切换 - 频繁切换光标,节流应生效
  5. 大尺寸光标 - 超过 64x64 的光标,应正确处理或回退