一、核心概念(剪贴板在 Win32 里的本质)

1. 系统全局资源

  • 每个会话有一个系统剪贴板,同一会话的所有进程共享。
  • 任意时刻剪贴板内容只能有一个「拥有者」,但可以同时以多种格式存在(例如同时有 CF_UNICODETEXTCF_TEXT)。
  • 会话隔离:不同用户会话(RDP/终端服务)有各自独立的剪贴板,但某些远程桌面软件可以跨会话同步。

2. 格式(Format)

  • 每一块剪贴板数据都带一个格式 ID(UINT),常量以 CF_ 开头(如 CF_UNICODETEXTCF_BITMAPCF_HDROP 等)。
  • 除了标准格式外,还可以注册自定义格式,ID 由 RegisterClipboardFormat 返回。
  • 格式优先级:应用在粘贴时通常按优先级列表选择最匹配的格式(例如先尝试 CF_UNICODETEXT,再尝试 CF_TEXT)。

3. 数据存储形式:句柄 + 全局内存

  • SetClipboardData / GetClipboardData 传递的是一个 HANDLE
  • 对于大多数格式,这个 HANDLE 实际上是 HGLOBAL
    • 必须用 GlobalAlloc(GMEM_MOVEABLE, size) 分配;
    • 使用 GlobalLock / GlobalUnlock 访问内存;
    • SetClipboardData 成功后,内存所有权转移给系统,应用不要再 GlobalFree / 重分配。
  • 对于一些 GDI 对象(CF_BITMAPHBITMAPCF_ENHMETAFILEHENHMETAFILE 等),直接传 GDI 句柄,不是 HGLOBAL。
  • 重要GMEM_MOVEABLE 标志是必需的,使用 GMEM_FIXED 会导致某些应用无法访问数据。

4. 线程与访问序列

  • 剪贴板是单实例资源,访问是串行的:一个进程打开剪贴板后,别的进程会 OpenClipboard 失败。
  • 典型操作模式:
    OpenClipboard → (可选) EmptyClipboard → Set/GetClipboardData → CloseClipboard
  • 访问超时:如果长时间不调用 CloseClipboard,会阻塞其他进程,建议尽快关闭。
  • 重入问题:同一线程不能嵌套 OpenClipboard(会失败)。

5. 延迟渲染(Delayed Rendering)

  • 可用 SetClipboardData(format, NULL) 声明自己可以提供此格式,但暂时不准备数据。
  • 当其他进程请求该格式时,系统会向拥有者窗口发送 WM_RENDERFORMAT 消息。
  • 当拥有者退出或剪贴板被清空时,系统发送 WM_RENDERALLFORMATS 消息,要求渲染所有声明的格式。
  • 适用场景:数据量大(如高分辨率图片)或转换成本高时,可以延迟渲染节省资源。

6. 剪贴板序列号

  • GetClipboardSequenceNumber() 返回一个递增的序列号,每次剪贴板内容变化时递增。
  • 可用于快速检测剪贴板是否有更新,无需打开剪贴板。

二、核心 API 列表(Win32 剪贴板)

1. 打开 / 关闭 / 清空

BOOL OpenClipboard(HWND hWndNewOwner);        // hWndNewOwner 可为 NULL
BOOL CloseClipboard(void);
BOOL EmptyClipboard(void);                    // 清空并设置当前进程为拥有者
HWND GetClipboardOwner(void);                 // 获取当前拥有者窗口
HWND GetOpenClipboardWindow(void);            // 获取当前打开剪贴板的窗口
DWORD GetClipboardSequenceNumber(void);       // 获取剪贴板序列号(Vista+)

关键点:

  • 必须 OpenClipboard 成功后才能调用 GetClipboardData / SetClipboardData / EnumClipboardFormats 等。
  • EmptyClipboard 会清空现有内容,并把当前进程设为剪贴板拥有者。
  • hWndNewOwner 参数指定接收剪贴板消息的窗口,可以为 NULL(但这样无法处理延迟渲染)。

2. 读写数据

HANDLE GetClipboardData(UINT uFormat);              // 读取数据
HANDLE SetClipboardData(UINT uFormat, HANDLE hMem); // 写入数据
BOOL   IsClipboardFormatAvailable(UINT format);    // 检查格式是否可用
int    CountClipboardFormats(void);                 // 获取格式数量

关键点:

  • GetClipboardData 获取句柄,调用者不能释放,数据归剪贴板所有。
  • SetClipboardData 成功后,系统接管句柄的释放。失败时,你需要自行释放。
  • 在调用 GetClipboardData 前最好先用 IsClipboardFormatAvailable 检查。

3. 格式管理

UINT RegisterClipboardFormatW(LPCWSTR lpszFormat);  // 注册自定义格式
int  GetClipboardFormatNameW(UINT format,           // 获取格式名称
                              LPWSTR lpszFormatName, 
                              int cchMaxCount);
UINT EnumClipboardFormats(UINT format);             // 枚举格式
int  GetPriorityClipboardFormat(UINT *paFormatPriorityList, 
                                 int cFormats);      // 按优先级查找格式

关键点:

  • RegisterClipboardFormat:注册自定义格式,返回一个 ID。只要所有进程用相同的名字调用,就能拿到同一个 ID
  • EnumClipboardFormats:遍历当前剪贴板中存在的所有格式 ID(第一次调用传 0)。
  • GetClipboardFormatName:对于自定义格式,可以获取注册时使用的字符串名称。
  • GetPriorityClipboardFormat:按优先级列表从当前剪贴板中找「最合适」的格式,返回第一个匹配的格式 ID。

4. 通知 / 监听

BOOL AddClipboardFormatListener(HWND hwnd);         // 添加监听器(Vista+)
BOOL RemoveClipboardFormatListener(HWND hwnd);      // 移除监听器
HWND SetClipboardViewer(HWND hWndNewViewer);        // 老式监听链(已过时)
BOOL ChangeClipboardChain(HWND hWndRemove, 
                          HWND hWndNewNext);        // 修改监听链

关键点:

  • 推荐使用 AddClipboardFormatListener(Windows Vista+),当剪贴板内容变化时收到 WM_CLIPBOARDUPDATE 消息。
  • 老式的 SetClipboardViewer 链式模型已过时,需要处理 WM_DRAWCLIPBOARDWM_CHANGECBCHAIN 消息。

5. 延迟渲染相关消息

// 这些是消息,不是函数
WM_RENDERFORMAT         // 请求渲染特定格式
WM_RENDERALLFORMATS     // 请求渲染所有格式
WM_DESTROYCLIPBOARD     // 剪贴板内容被清空

三、标准数据格式列表(按类别)

完整列表可查 MSDN:Clipboard Formats

1. 文本类

常量数据类型含义
CF_TEXT1HGLOBALANSI 文本,\0 结尾,行结束使用 \r\n
CF_OEMTEXT7HGLOBALOEM 字符集文本
CF_UNICODETEXT13HGLOBALUTF-16 LE 宽字符文本(建议优先使用
CF_LOCALE16HGLOBALCF_TEXT 关联的 LCID(本地化信息)
CF_DSPTEXT0x81HGLOBAL由拥有者显示的文本(Owner-display,较少用)

最佳实践:

  • 优先使用 CF_UNICODETEXT 进行读写,向下兼容旧程序时同时提供 CF_TEXT
  • 文本必须以 NUL 字符结尾。
  • Windows 标准换行符为 \r\n,但大多数应用也能接受 \n

2. 图片 / 图形类

常量数据类型含义
CF_BITMAP2HBITMAP设备相关位图(DDB)
CF_DIB8HGLOBAL设备无关位图(BITMAPINFO + 像素数据)
CF_DIBV517HGLOBAL增强型 DIB(BITMAPV5HEADER + 像素数据)
CF_PALETTE9HPALETTE颜色调色板
CF_ENHMETAFILE14HENHMETAFILE增强型图元文件
CF_METAFILEPICT3HGLOBAL旧式图元文件(METAFILEPICT 结构)
CF_DSPBITMAP0x82HBITMAPOwner-display 的位图
CF_DSPENHMETAFILE0x8EHENHMETAFILEOwner-display 的增强图元文件
CF_DSPMETAFILEPICT0x83HGLOBALOwner-display 的旧式图元文件

注意:

  • CF_BITMAP 传递的是 HBITMAP 句柄,不是 HGLOBAL
  • CF_DIB / CF_DIBV5 更适合跨进程传输,因为它们是内存块而不是 GDI 对象。
  • 许多现代应用还会注册自定义图片格式:"PNG""JFIF"(JPEG)、"image/bmp" 等。

3. 文件 / 拖放类

常量数据类型含义
CF_HDROP15HDROP文件列表(DROPFILES 结构 + 路径字符串列表)

用途:

  • 用于「复制文件」到剪贴板(例如在资源管理器中 Ctrl+C)。
  • 也用于拖放操作(Drag & Drop)。

4. 其它常用格式

常量数据类型含义
CF_RIFF11HGLOBALRIFF 音频数据
CF_WAVE12HGLOBALWAVE 波形音频数据
CF_TIFF6HGLOBALTIFF 图片
CF_SYLK4HGLOBALSYLK 表格数据(Symbolic Link)
CF_DIF5HGLOBALDIF 表格数据(Data Interchange Format)
CF_PENDATA10HGLOBAL笔输入数据(已过时)
CF_OWNERDISPLAY0x80N/A拥有者负责显示(需要处理 WM_PAINTCLIPBOARD 等消息)

5. 富文本 / HTML(自定义格式)

虽然不是标准 CF_* 常量,但这些是广泛使用的自定义格式:

格式名称注册方式用途
"Rich Text Format"RegisterClipboardFormat(L"Rich Text Format")RTF 富文本
"HTML Format"RegisterClipboardFormat(L"HTML Format")HTML 片段(带特殊头部)
"XML Spreadsheet"RegisterClipboardFormat(L"XML Spreadsheet")Excel XML 格式

HTML Format 特殊说明:

Version:0.9
StartHTML:000000185
EndHTML:000000411
StartFragment:000000221
EndFragment:000000375
<html>
<body>
<!--StartFragment-->
<p>实际内容</p>
<!--EndFragment-->
</body>
</html>

6. 格式 ID 范围

范围用途
0x0000保留,无效格式
0x00010x00BF标准剪贴板格式(系统预定义的 CF_*
0x00C00x01FF自定义应用格式(RegisterClipboardFormat
0x02000x02FF私有格式
0x03000x03FFGDI 对象格式

四、基本读写流程(通用模式)

1. 读取剪贴板数据的一般流程

bool ReadClipboardExample()
{
    // 1. 先检查格式是否可用(可选,但推荐)
    if (!IsClipboardFormatAvailable(CF_UNICODETEXT))
        return false;
 
    // 2. 打开剪贴板
    if (!OpenClipboard(nullptr))
        return false;
 
    // 3. 获取数据句柄
    HANDLE hData = GetClipboardData(CF_UNICODETEXT);
    if (!hData) {
        CloseClipboard();
        return false;
    }
 
    // 4. 锁定内存并访问
    LPCWSTR pText = static_cast<LPCWSTR>(GlobalLock(hData));
    if (!pText) {
        CloseClipboard();
        return false;
    }
 
    // 5. 复制数据到自己的内存
    std::wstring text(pText);
    
    // 6. 解锁
    GlobalUnlock(hData);
    
    // 7. 关闭剪贴板
    CloseClipboard();
 
    // 8. 使用 text ...
    return true;
}

关键要点:

  • 先用 IsClipboardFormatAvailable 检查格式是否存在(避免不必要的打开操作)。
  • OpenClipboard 成功后才可以 GetClipboardData
  • HGLOBAL:用 GlobalLock/GlobalUnlock
  • 必须将数据复制到自己的内存中,不能长期持有锁或依赖剪贴板数据指针。
  • 读取完必须 CloseClipboard
  • 不要 GlobalFree GetClipboardData 返回的句柄。

2. 写入剪贴板数据的一般流程

bool WriteClipboardExample(const std::wstring& text)
{
    // 1. 打开剪贴板
    if (!OpenClipboard(nullptr))
        return false;
 
    // 2. 清空剪贴板(必须,且会将本进程设为 owner)
    EmptyClipboard();
 
    // 3. 分配全局内存
    const size_t bytes = (text.size() + 1) * sizeof(wchar_t);
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
    if (!hMem) {
        CloseClipboard();
        return false;
    }
 
    // 4. 锁定并写入数据
    void* pMem = GlobalLock(hMem);
    if (!pMem) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
    memcpy(pMem, text.c_str(), bytes);
    GlobalUnlock(hMem);
 
    // 5. 设置剪贴板数据
    if (!SetClipboardData(CF_UNICODETEXT, hMem)) {
        GlobalFree(hMem);  // 失败时需要自己释放
        CloseClipboard();
        return false;
    }
 
    // 6. 成功后不要再使用 / 释放 hMem
    CloseClipboard();
    return true;
}

关键要点:

  • 必须先调用 EmptyClipboard(),否则 SetClipboardData 会失败。
  • 使用 GMEM_MOVEABLE 标志分配内存。
  • SetClipboardData 成功后,内存所有权转移给系统,不要再访问或释放
  • SetClipboardData 失败时,必须自己 GlobalFree

3. 错误处理与重试机制

bool OpenClipboardWithRetry(HWND hwnd, int maxRetries = 5, int delayMs = 50)
{
    for (int i = 0; i < maxRetries; ++i) {
        if (OpenClipboard(hwnd))
            return true;
        
        DWORD err = GetLastError();
        if (err != ERROR_ACCESS_DENIED) {
            // 非访问被拒绝错误,不重试
            return false;
        }
        
        if (i < maxRetries - 1)
            Sleep(delayMs);
    }
    return false;
}

使用场景:

  • 剪贴板被其他进程占用时,OpenClipboard 会失败并返回 ERROR_ACCESS_DENIED
  • 短暂等待后重试可以提高成功率。

五、自定义格式注册与遍历

1. 注册自定义格式

// 全局变量或类成员
UINT g_cfMyFormat = 0;
UINT g_cfMyJSON   = 0;
 
void InitCustomClipboardFormats()
{
    // 使用全局唯一的命名风格(建议:公司.产品.用途)
    g_cfMyFormat = RegisterClipboardFormatW(L"MyCompany.MyApp.CustomData");
    g_cfMyJSON   = RegisterClipboardFormatW(L"MyCompany.MyApp.JSON");
    
    // RegisterClipboardFormat 保证相同字符串返回相同 ID
    // 不同进程只要用相同字符串注册,就能互通
}

命名建议:

  • 使用反向域名风格:"com.example.app.format"
  • 或使用点分层次:"MyCompany.MyProduct.DataType"
  • 避免使用通用名称(如 "Data""Custom" 等),容易冲突。

2. 写入自定义格式

bool SetCustomClipboardData(const std::vector<uint8_t>& buf)
{
    if (!OpenClipboard(nullptr))
        return false;
 
    EmptyClipboard();
 
    // 分配内存
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, buf.size());
    if (!hMem) {
        CloseClipboard();
        return false;
    }
 
    // 写入数据
    void* p = GlobalLock(hMem);
    if (!p) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
    memcpy(p, buf.data(), buf.size());
    GlobalUnlock(hMem);
 
    // 设置剪贴板数据
    if (!SetClipboardData(g_cfMyFormat, hMem)) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
 
    CloseClipboard();
    return true;
}

3. 读取自定义格式

bool GetCustomClipboardData(std::vector<uint8_t>& out)
{
    if (!IsClipboardFormatAvailable(g_cfMyFormat))
        return false;
 
    if (!OpenClipboard(nullptr))
        return false;
 
    HANDLE hData = GetClipboardData(g_cfMyFormat);
    if (!hData) {
        CloseClipboard();
        return false;
    }
 
    void* p = GlobalLock(hData);
    if (!p) {
        CloseClipboard();
        return false;
    }
 
    SIZE_T size = GlobalSize(hData);
    out.resize(size);
    memcpy(out.data(), p, size);
 
    GlobalUnlock(hData);
    CloseClipboard();
    return true;
}

4. 遍历当前剪贴板格式列表

struct ClipboardFormatInfo {
    UINT id;
    std::wstring name;
    bool isStandard;
};
 
std::vector<ClipboardFormatInfo> EnumClipboardFormatsExample()
{
    std::vector<ClipboardFormatInfo> formats;
    
    if (!OpenClipboard(nullptr))
        return formats;
 
    UINT fmt = 0;
    WCHAR name[256];
 
    while ((fmt = EnumClipboardFormats(fmt)) != 0) {
        ClipboardFormatInfo info;
        info.id = fmt;
        
        // 判断是否是标准格式
        if (fmt < 0xC000) {  // 标准格式范围
            info.isStandard = true;
            // 可以用映射表转换为友好名称
            switch (fmt) {
                case CF_TEXT: info.name = L"CF_TEXT"; break;
                case CF_BITMAP: info.name = L"CF_BITMAP"; break;
                case CF_UNICODETEXT: info.name = L"CF_UNICODETEXT"; break;
                case CF_HDROP: info.name = L"CF_HDROP"; break;
                case CF_DIB: info.name = L"CF_DIB"; break;
                // ... 其他标准格式
                default: 
                    info.name = L"CF_" + std::to_wstring(fmt);
                    break;
            }
        } else {
            // 自定义格式,获取注册时的名称
            info.isStandard = false;
            int len = GetClipboardFormatNameW(fmt, name, _countof(name));
            if (len > 0) {
                info.name = name;
            } else {
                info.name = L"Unknown_" + std::to_wstring(fmt);
            }
        }
        
        formats.push_back(info);
    }
 
    // 检查是否正常结束
    if (GetLastError() != ERROR_SUCCESS) {
        // 枚举过程中出错
    }
 
    CloseClipboard();
    return formats;
}

5. 按优先级查找格式

UINT FindBestTextFormat()
{
    // 按优先级列出支持的格式
    UINT formats[] = {
        CF_UNICODETEXT,  // 优先 Unicode
        CF_TEXT,         // 其次 ANSI
        CF_OEMTEXT       // 最后 OEM
    };
    
    if (!OpenClipboard(nullptr))
        return 0;
    
    int idx = GetPriorityClipboardFormat(formats, _countof(formats));
    CloseClipboard();
    
    if (idx == -1)
        return 0;  // 没有匹配的格式
    
    return formats[idx];
}

六、文本类数据的读取与写入(各种格式)

1. 推荐做法:统一使用 CF_UNICODETEXT

bool GetClipboardText(std::wstring& out)
{
    if (!IsClipboardFormatAvailable(CF_UNICODETEXT))
        return false;
 
    if (!OpenClipboard(nullptr))
        return false;
 
    HANDLE hData = GetClipboardData(CF_UNICODETEXT);
    if (!hData) {
        CloseClipboard();
        return false;
    }
 
    LPCWSTR pText = static_cast<LPCWSTR>(GlobalLock(hData));
    if (!pText) {
        CloseClipboard();
        return false;
    }
 
    out.assign(pText);  // NUL 结束的字符串
    GlobalUnlock(hData);
    CloseClipboard();
    return true;
}
 
bool SetClipboardText(const std::wstring& text)
{
    if (!OpenClipboard(nullptr))
        return false;
 
    EmptyClipboard();
 
    const size_t bytes = (text.size() + 1) * sizeof(wchar_t);
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
    if (!hMem) {
        CloseClipboard();
        return false;
    }
 
    void* p = GlobalLock(hMem);
    if (!p) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
    memcpy(p, text.c_str(), bytes);
    GlobalUnlock(hMem);
 
    if (!SetClipboardData(CF_UNICODETEXT, hMem)) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
 
    CloseClipboard();
    return true;
}

注意事项:

  • 文本内容应使用 Windows 风格行结束 \r\n,但很多程序也能接受 \n
  • 必须L'\0' 结尾。
  • 不要在字符串中包含额外的 NUL 字符(除非是多字符串格式)。

2. 同时提供多种文本格式(最大兼容性)

bool SetClipboardTextMultiFormat(const std::wstring& wtext)
{
    if (!OpenClipboard(nullptr))
        return false;
 
    EmptyClipboard();
 
    // 1) CF_UNICODETEXT(现代应用)
    {
        size_t bytes = (wtext.size() + 1) * sizeof(wchar_t);
        HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
        if (!hMem) { 
            CloseClipboard(); 
            return false; 
        }
        void* p = GlobalLock(hMem);
        memcpy(p, wtext.c_str(), bytes);
        GlobalUnlock(hMem);
        
        if (!SetClipboardData(CF_UNICODETEXT, hMem)) {
            GlobalFree(hMem);
            CloseClipboard();
            return false;
        }
    }
 
    // 2) CF_TEXT(兼容旧程序,使用系统 ACP 转码)
    {
        int lenA = WideCharToMultiByte(CP_ACP, 0,
                                       wtext.c_str(), -1,
                                       nullptr, 0, nullptr, nullptr);
        if (lenA > 0) {
            HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, lenA);
            if (hMem) {
                LPSTR p = static_cast<LPSTR>(GlobalLock(hMem));
                WideCharToMultiByte(CP_ACP, 0,
                                    wtext.c_str(), -1,
                                    p, lenA, nullptr, nullptr);
                GlobalUnlock(hMem);
                
                if (!SetClipboardData(CF_TEXT, hMem)) {
                    GlobalFree(hMem);
                }
            }
        }
    }
 
    CloseClipboard();
    return true;
}

3. 智能读取文本(自动选择最佳格式)

bool GetClipboardTextAuto(std::wstring& out)
{
    if (!OpenClipboard(nullptr))
        return false;
 
    bool success = false;
 
    // 优先尝试 Unicode
    if (IsClipboardFormatAvailable(CF_UNICODETEXT)) {
        HANDLE hData = GetClipboardData(CF_UNICODETEXT);
        if (hData) {
            LPCWSTR pText = static_cast<LPCWSTR>(GlobalLock(hData));
            if (pText) {
                out.assign(pText);
                GlobalUnlock(hData);
                success = true;
            }
        }
    }
    // 回退到 ANSI
    else if (IsClipboardFormatAvailable(CF_TEXT)) {
        HANDLE hData = GetClipboardData(CF_TEXT);
        if (hData) {
            LPCSTR pText = static_cast<LPCSTR>(GlobalLock(hData));
            if (pText) {
                // 转换为 Unicode
                int len = MultiByteToWideChar(CP_ACP, 0, pText, -1, nullptr, 0);
                if (len > 0) {
                    out.resize(len - 1);
                    MultiByteToWideChar(CP_ACP, 0, pText, -1, &out[0], len);
                    success = true;
                }
                GlobalUnlock(hData);
            }
        }
    }
 
    CloseClipboard();
    return success;
}

4. 处理富文本格式(RTF / HTML)

// 读取 HTML 格式
bool GetClipboardHTML(std::string& htmlOut)
{
    static UINT cfHTML = RegisterClipboardFormatW(L"HTML Format");
    
    if (!IsClipboardFormatAvailable(cfHTML))
        return false;
 
    if (!OpenClipboard(nullptr))
        return false;
 
    HANDLE hData = GetClipboardData(cfHTML);
    if (!hData) {
        CloseClipboard();
        return false;
    }
 
    const char* pHtml = static_cast<const char*>(GlobalLock(hData));
    if (!pHtml) {
        CloseClipboard();
        return false;
    }
 
    htmlOut.assign(pHtml);
    GlobalUnlock(hData);
    CloseClipboard();
    
    // HTML Format 包含特殊头部,需要解析
    // 格式:Version:xxx\r\nStartHTML:xxx\r\n...
    
    return true;
}
 
// 写入 HTML 格式
bool SetClipboardHTML(const std::string& html)
{
    static UINT cfHTML = RegisterClipboardFormatW(L"HTML Format");
    
    // 构建 HTML Format 头部
    std::string fragment = "<!--StartFragment-->" + html + "<!--EndFragment-->";
    std::string fullHtml = 
        "<html>\r\n"
        "<body>\r\n" +
        fragment +
        "\r\n</body>\r\n"
        "</html>";
    
    // 计算偏移量
    char header[256];
    sprintf_s(header, 
        "Version:0.9\r\n"
        "StartHTML:%010d\r\n"
        "EndHTML:%010d\r\n"
        "StartFragment:%010d\r\n"
        "EndFragment:%010d\r\n",
        0, 0, 0, 0);  // 先占位
    
    int headerLen = strlen(header);
    int startHTML = headerLen;
    int endHTML = startHTML + fullHtml.length();
    int startFrag = startHTML + fullHtml.find("<!--StartFragment-->") + 20;
    int endFrag = startHTML + fullHtml.find("<!--EndFragment-->");
    
    // 重新生成正确的头部
    sprintf_s(header,
        "Version:0.9\r\n"
        "StartHTML:%010d\r\n"
        "EndHTML:%010d\r\n"
        "StartFragment:%010d\r\n"
        "EndFragment:%010d\r\n",
        startHTML, endHTML, startFrag, endFrag);
    
    std::string finalData = std::string(header) + fullHtml;
    
    // 写入剪贴板
    if (!OpenClipboard(nullptr))
        return false;
    
    EmptyClipboard();
    
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, finalData.size() + 1);
    if (!hMem) {
        CloseClipboard();
        return false;
    }
    
    char* p = static_cast<char*>(GlobalLock(hMem));
    memcpy(p, finalData.c_str(), finalData.size() + 1);
    GlobalUnlock(hMem);
    
    if (!SetClipboardData(cfHTML, hMem)) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
    
    CloseClipboard();
    return true;
}

七、图片类数据的读取与写入

图片处理需要理解两个层面:

  1. 剪贴板格式层面CF_BITMAP / CF_DIB / CF_DIBV5
  2. 编码层面:PNG/JPEG/BMP 文件格式(需要 GDI+ / WIC 转换)

1. 读取 CF_BITMAP(设备相关位图)

#include <windows.h>
 
bool GetClipboardBitmap(HBITMAP& outBitmap)
{
    if (!IsClipboardFormatAvailable(CF_BITMAP))
        return false;
 
    if (!OpenClipboard(nullptr))
        return false;
 
    HANDLE hData = GetClipboardData(CF_BITMAP);
    if (!hData) {
        CloseClipboard();
        return false;
    }
 
    // 注意:这是剪贴板拥有的 HBITMAP,必须复制一份
    HBITMAP hBmp = static_cast<HBITMAP>(hData);
    HBITMAP hCopy = (HBITMAP)CopyImage(hBmp, IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
    if (!hCopy) {
        CloseClipboard();
        return false;
    }
 
    outBitmap = hCopy; // 调用者负责 DeleteObject(hCopy)
    CloseClipboard();
    return true;
}

2. 写入 CF_BITMAP

bool SetClipboardBitmap(HBITMAP hBmp)
{
    if (!hBmp)
        return false;
 
    if (!OpenClipboard(nullptr))
        return false;
 
    EmptyClipboard();
 
    // 复制位图(避免原始位图被删除影响剪贴板)
    HBITMAP hCopy = (HBITMAP)CopyImage(hBmp, IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
    if (!hCopy) {
        CloseClipboard();
        return false;
    }
 
    if (!SetClipboardData(CF_BITMAP, hCopy)) {
        DeleteObject(hCopy);
        CloseClipboard();
        return false;
    }
 
    // 成功后 hCopy 由系统管理,不要再 DeleteObject
    CloseClipboard();
    return true;
}

3. 读取 CF_DIB(设备无关位图)

bool GetClipboardDIB(std::vector<uint8_t>& dibData)
{
    if (!IsClipboardFormatAvailable(CF_DIB))
        return false;
 
    if (!OpenClipboard(nullptr))
        return false;
 
    HANDLE hData = GetClipboardData(CF_DIB);
    if (!hData) {
        CloseClipboard();
        return false;
    }
 
    void* p = GlobalLock(hData);
    if (!p) {
        CloseClipboard();
        return false;
    }
 
    SIZE_T size = GlobalSize(hData);
    dibData.resize(size);
    memcpy(dibData.data(), p, size);
 
    GlobalUnlock(hData);
    CloseClipboard();
    return true;
}

DIB 数据结构:

[ BITMAPINFOHEADER or BITMAPV5HEADER ]
[ Color palette (可选,取决于位深度) ]
[ Pixel data (从下往上,每行对齐到 4 字节) ]

4. 写入 CF_DIB

bool SetClipboardDIB(const std::vector<uint8_t>& dibData)
{
    if (dibData.empty())
        return false;
 
    if (!OpenClipboard(nullptr))
        return false;
 
    EmptyClipboard();
 
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, dibData.size());
    if (!hMem) {
        CloseClipboard();
        return false;
    }
 
    void* p = GlobalLock(hMem);
    if (!p) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
    memcpy(p, dibData.data(), dibData.size());
    GlobalUnlock(hMem);
 
    if (!SetClipboardData(CF_DIB, hMem)) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
 
    CloseClipboard();
    return true;
}

5. HBITMAP 与 DIB 相互转换

// HBITMAP 转 DIB
std::vector<uint8_t> HBitmapToDIB(HBITMAP hBmp)
{
    std::vector<uint8_t> result;
    
    BITMAP bm;
    if (!GetObject(hBmp, sizeof(bm), &bm))
        return result;
    
    BITMAPINFOHEADER bi = {};
    bi.biSize = sizeof(BITMAPINFOHEADER);
    bi.biWidth = bm.bmWidth;
    bi.biHeight = bm.bmHeight;
    bi.biPlanes = 1;
    bi.biBitCount = 32;  // 使用 32 位 RGBA
    bi.biCompression = BI_RGB;
    
    HDC hdc = GetDC(nullptr);
    
    // 计算图像大小
    int rowSize = ((bm.bmWidth * 32 + 31) / 32) * 4;
    int imageSize = rowSize * bm.bmHeight;
    
    result.resize(sizeof(BITMAPINFOHEADER) + imageSize);
    memcpy(result.data(), &bi, sizeof(bi));
    
    // 获取像素数据
    if (!GetDIBits(hdc, hBmp, 0, bm.bmHeight,
                   result.data() + sizeof(BITMAPINFOHEADER),
                   (BITMAPINFO*)result.data(),
                   DIB_RGB_COLORS)) {
        result.clear();
    }
    
    ReleaseDC(nullptr, hdc);
    return result;
}
 
// DIB 转 HBITMAP
HBITMAP DIBToHBitmap(const std::vector<uint8_t>& dibData)
{
    if (dibData.size() < sizeof(BITMAPINFOHEADER))
        return nullptr;
    
    BITMAPINFO* pbi = (BITMAPINFO*)dibData.data();
    void* pPixels = (void*)(dibData.data() + pbi->bmiHeader.biSize);
    
    HDC hdc = GetDC(nullptr);
    HBITMAP hBmp = CreateDIBitmap(hdc, &pbi->bmiHeader, CBM_INIT,
                                   pPixels, pbi, DIB_RGB_COLORS);
    ReleaseDC(nullptr, hdc);
    
    return hBmp;
}

6. 同时提供多种图片格式(推荐)

bool SetClipboardImage(HBITMAP hBmp)
{
    if (!hBmp)
        return false;
 
    if (!OpenClipboard(nullptr))
        return false;
 
    EmptyClipboard();
 
    // 1) CF_BITMAP(兼容旧程序)
    {
        HBITMAP hCopy = (HBITMAP)CopyImage(hBmp, IMAGE_BITMAP, 0, 0, 0);
        if (hCopy) {
            if (!SetClipboardData(CF_BITMAP, hCopy)) {
                DeleteObject(hCopy);
            }
        }
    }
 
    // 2) CF_DIB(推荐,设备无关)
    {
        std::vector<uint8_t> dib = HBitmapToDIB(hBmp);
        if (!dib.empty()) {
            HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, dib.size());
            if (hMem) {
                void* p = GlobalLock(hMem);
                memcpy(p, dib.data(), dib.size());
                GlobalUnlock(hMem);
                
                if (!SetClipboardData(CF_DIB, hMem)) {
                    GlobalFree(hMem);
                }
            }
        }
    }
 
    // 3) 可选:PNG 格式(需要 GDI+ / WIC)
    // ...
 
    CloseClipboard();
    return true;
}

7. 使用 GDI+ 处理 PNG 格式

#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
 
// 从剪贴板读取 PNG 格式
bool GetClipboardPNG(std::vector<uint8_t>& pngData)
{
    static UINT cfPNG = RegisterClipboardFormatW(L"PNG");
    
    if (!IsClipboardFormatAvailable(cfPNG))
        return false;
 
    if (!OpenClipboard(nullptr))
        return false;
 
    HANDLE hData = GetClipboardData(cfPNG);
    if (!hData) {
        CloseClipboard();
        return false;
    }
 
    void* p = GlobalLock(hData);
    if (!p) {
        CloseClipboard();
        return false;
    }
 
    SIZE_T size = GlobalSize(hData);
    pngData.resize(size);
    memcpy(pngData.data(), p, size);
 
    GlobalUnlock(hData);
    CloseClipboard();
    return true;
}
 
// 写入 PNG 到剪贴板
bool SetClipboardPNG(const std::vector<uint8_t>& pngData)
{
    static UINT cfPNG = RegisterClipboardFormatW(L"PNG");
    
    if (!OpenClipboard(nullptr))
        return false;
 
    EmptyClipboard();
 
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, pngData.size());
    if (!hMem) {
        CloseClipboard();
        return false;
    }
 
    void* p = GlobalLock(hMem);
    memcpy(p, pngData.data(), pngData.size());
    GlobalUnlock(hMem);
 
    if (!SetClipboardData(cfPNG, hMem)) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
 
    CloseClipboard();
    return true;
}

八、文件列表(CF_HDROP)的读取与写入

CF_HDROP 用于在剪贴板中传递文件列表(例如从资源管理器复制文件)。

#include <shellapi.h> // DragQueryFileW

1. 读取剪贴板中的文件列表

bool GetClipboardFileList(std::vector<std::wstring>& files)
{
    if (!IsClipboardFormatAvailable(CF_HDROP))
        return false;
 
    if (!OpenClipboard(nullptr))
        return false;
 
    HDROP hDrop = (HDROP)GetClipboardData(CF_HDROP);
    if (!hDrop) {
        CloseClipboard();
        return false;
    }
 
    UINT count = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0);
    files.clear();
    files.reserve(count);
 
    for (UINT i = 0; i < count; ++i) {
        UINT len = DragQueryFileW(hDrop, i, nullptr, 0);
        std::wstring path(len + 1, L'\0');
        DragQueryFileW(hDrop, i, &path[0], len + 1);
        path.resize(len);  // 移除额外的 NUL
        files.push_back(path);
    }
 
    CloseClipboard();
    return true;
}

2. 写入文件列表到剪贴板

DROPFILES 结构:

typedef struct _DROPFILES {
    DWORD pFiles;  // 从结构起始到文件名列表的字节偏移
    POINT pt;      // 拖放点(剪贴板中通常设为 {0, 0})
    BOOL  fNC;     // 是否在非工作区(剪贴板中通常为 FALSE)
    BOOL  fWide;   // TRUE 表示 Unicode 文件名
} DROPFILES, *LPDROPFILES;

内存布局:

[ DROPFILES 结构 ]
[ 路径1\0 ]
[ 路径2\0 ]
[ ... ]
[ \0 ]  // 双重 NUL 结尾

完整实现:

bool SetClipboardFileList(const std::vector<std::wstring>& files)
{
    if (files.empty())
        return false;
 
    // 计算所有路径字符串总长度
    size_t charCount = 0;
    for (const auto& f : files) {
        charCount += f.size() + 1; // 每个路径 +1 为结尾 NUL
    }
    charCount += 1; // 额外的终止 NUL
 
    const SIZE_T bytes = sizeof(DROPFILES) + charCount * sizeof(wchar_t);
 
    if (!OpenClipboard(nullptr))
        return false;
 
    EmptyClipboard();
 
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes);
    if (!hMem) {
        CloseClipboard();
        return false;
    }
 
    BYTE* pData = static_cast<BYTE*>(GlobalLock(hMem));
    if (!pData) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
 
    // 填充 DROPFILES 结构
    DROPFILES* pDrop = reinterpret_cast<DROPFILES*>(pData);
    pDrop->pFiles = sizeof(DROPFILES);
    pDrop->pt.x = 0;
    pDrop->pt.y = 0;
    pDrop->fNC = FALSE;
    pDrop->fWide = TRUE;
 
    // 填充文件名列表
    wchar_t* pFiles = reinterpret_cast<wchar_t*>(pData + pDrop->pFiles);
    for (const auto& f : files) {
        wcscpy_s(pFiles, f.size() + 1, f.c_str());
        pFiles += f.size() + 1;
    }
    *pFiles = L'\0'; // 双重 NUL 结尾
 
    GlobalUnlock(hMem);
 
    if (!SetClipboardData(CF_HDROP, hMem)) {
        GlobalFree(hMem);
        CloseClipboard();
        return false;
    }
 
    CloseClipboard();
    return true;
}

3. 更安全的路径处理

bool SetClipboardFileListSafe(const std::vector<std::wstring>& files)
{
    if (files.empty())
        return false;
 
    // 验证并规范化路径
    std::vector<std::wstring> validFiles;
    for (const auto& f : files) {
        // 转换为绝对路径
        wchar_t fullPath[MAX_PATH];
        if (GetFullPathNameW(f.c_str(), MAX_PATH, fullPath, nullptr)) {
            // 检查文件是否存在
            DWORD attr = GetFileAttributesW(fullPath);
            if (attr != INVALID_FILE_ATTRIBUTES) {
                validFiles.push_back(fullPath);
            }
        }
    }
 
    if (validFiles.empty())
        return false;
 
    // 使用验证后的路径列表
    return SetClipboardFileList(validFiles);
}

九、延迟渲染(Delayed Rendering)详解

延迟渲染允许应用声明支持某种格式,但直到实际需要时才生成数据。

1. 延迟渲染的基本流程

设置延迟渲染:

bool SetClipboardDataDeferred(HWND hwndOwner)
{
    if (!OpenClipboard(hwndOwner))  // 必须提供窗口句柄
        return false;
 
    EmptyClipboard();
 
    // 声明支持的格式,但不提供数据(传 NULL)
    SetClipboardData(CF_UNICODETEXT, nullptr);
    SetClipboardData(CF_BITMAP, nullptr);
    
    CloseClipboard();
    return true;
}

处理渲染请求:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg) {
    case WM_RENDERFORMAT:
    {
        // 其他进程请求特定格式的数据
        UINT format = (UINT)wParam;
        
        if (!OpenClipboard(hwnd))
            break;
        
        if (format == CF_UNICODETEXT) {
            // 生成文本数据
            std::wstring text = GenerateText();
            size_t bytes = (text.size() + 1) * sizeof(wchar_t);
            HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
            if (hMem) {
                void* p = GlobalLock(hMem);
                memcpy(p, text.c_str(), bytes);
                GlobalUnlock(hMem);
                SetClipboardData(CF_UNICODETEXT, hMem);
            }
        }
        else if (format == CF_BITMAP) {
            // 生成位图数据
            HBITMAP hBmp = GenerateBitmap();
            if (hBmp) {
                SetClipboardData(CF_BITMAP, hBmp);
            }
        }
        
        CloseClipboard();
        break;
    }
    
    case WM_RENDERALLFORMATS:
    {
        // 应用即将退出或剪贴板被清空,渲染所有格式
        if (!OpenClipboard(hwnd))
            break;
        
        // 渲染所有声明的格式
        RenderAllFormats();
        
        CloseClipboard();
        break;
    }
    
    case WM_DESTROYCLIPBOARD:
    {
        // 剪贴板内容被其他进程清空
        // 可以在这里清理资源
        break;
    }
    }
    
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

2. 完整的延迟渲染示例

class ClipboardManager {
private:
    HWND m_hwnd;
    std::wstring m_cachedText;
    HBITMAP m_cachedBitmap;
    
public:
    ClipboardManager(HWND hwnd) : m_hwnd(hwnd), m_cachedBitmap(nullptr) {}
    
    ~ClipboardManager() {
        if (m_cachedBitmap)
            DeleteObject(m_cachedBitmap);
    }
    
    bool SetDeferredData(const std::wstring& text) {
        m_cachedText = text;
        m_cachedBitmap = nullptr;
        
        if (!OpenClipboard(m_hwnd))
            return false;
        
        EmptyClipboard();
        SetClipboardData(CF_UNICODETEXT, nullptr);
        SetClipboardData(CF_BITMAP, nullptr);
        CloseClipboard();
        
        return true;
    }
    
    void OnRenderFormat(UINT format) {
        if (!OpenClipboard(m_hwnd))
            return;
        
        switch (format) {
        case CF_UNICODETEXT:
            RenderText();
            break;
        case CF_BITMAP:
            RenderBitmap();
            break;
        }
        
        CloseClipboard();
    }
    
    void OnRenderAllFormats() {
        if (!OpenClipboard(m_hwnd))
            return;
        
        RenderText();
        RenderBitmap();
        
        CloseClipboard();
    }
    
private:
    void RenderText() {
        size_t bytes = (m_cachedText.size() + 1) * sizeof(wchar_t);
        HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
        if (hMem) {
            void* p = GlobalLock(hMem);
            memcpy(p, m_cachedText.c_str(), bytes);
            GlobalUnlock(hMem);
            SetClipboardData(CF_UNICODETEXT, hMem);
        }
    }
    
    void RenderBitmap() {
        // 从文本生成位图(示例)
        if (!m_cachedBitmap) {
            m_cachedBitmap = CreateBitmapFromText(m_cachedText);
        }
        
        if (m_cachedBitmap) {
            HBITMAP hCopy = (HBITMAP)CopyImage(m_cachedBitmap, 
                                                IMAGE_BITMAP, 0, 0, 0);
            if (hCopy) {
                SetClipboardData(CF_BITMAP, hCopy);
            }
        }
    }
};

3. 延迟渲染的优势与注意事项

优势:

  • 性能优化:避免预先生成用户可能不需要的格式。
  • 内存节省:大数据量(如高分辨率图片)延迟到实际需要时才生成。
  • 按需转换:昂贵的格式转换(如 RTF → 纯文本)延迟执行。

注意事项:

  • 必须提供窗口句柄OpenClipboard 时必须传入有效的 HWND,否则无法接收渲染消息。
  • 应用退出前渲染:收到 WM_RENDERALLFORMATS 时必须渲染所有格式,否则数据会丢失。
  • 数据持久性:确保在收到渲染请求时,原始数据仍然可用。

十、剪贴板监听与通知

1. 现代方式:Clipboard Format Listener(推荐)

适用版本:Windows Vista 及以上

class ClipboardListener {
private:
    HWND m_hwnd;
    DWORD m_lastSequence;
    
public:
    ClipboardListener(HWND hwnd) : m_hwnd(hwnd) {
        m_lastSequence = GetClipboardSequenceNumber();
        AddClipboardFormatListener(m_hwnd);
    }
    
    ~ClipboardListener() {
        RemoveClipboardFormatListener(m_hwnd);
    }
    
    void OnClipboardUpdate() {
        DWORD newSequence = GetClipboardSequenceNumber();
        if (newSequence == m_lastSequence)
            return;  // 没有实际变化
        
        m_lastSequence = newSequence;
        
        // 处理剪贴板变化
        ProcessClipboardChange();
    }
    
private:
    void ProcessClipboardChange() {
        // 枚举当前剪贴板格式
        if (!OpenClipboard(m_hwnd))
            return;
        
        UINT fmt = 0;
        while ((fmt = EnumClipboardFormats(fmt)) != 0) {
            // 处理每种格式
        }
        
        CloseClipboard();
    }
};
 
// 窗口过程中处理消息
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static ClipboardListener* pListener = nullptr;
    
    switch (msg) {
    case WM_CREATE:
        pListener = new ClipboardListener(hwnd);
        break;
        
    case WM_DESTROY:
        delete pListener;
        pListener = nullptr;
        break;
        
    case WM_CLIPBOARDUPDATE:
        if (pListener)
            pListener->OnClipboardUpdate();
        break;
    }
    
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

2. 传统方式:Clipboard Viewer Chain(已过时)

不推荐使用,仅用于 Windows XP 及更早版本。

class ClipboardViewer {
private:
    HWND m_hwnd;
    HWND m_nextViewer;
    
public:
    ClipboardViewer(HWND hwnd) : m_hwnd(hwnd), m_nextViewer(nullptr) {
        m_nextViewer = SetClipboardViewer(m_hwnd);
    }
    
    ~ClipboardViewer() {
        ChangeClipboardChain(m_hwnd, m_nextViewer);
    }
    
    void OnDrawClipboard() {
        // 剪贴板内容已变化
        ProcessClipboardChange();
        
        // 传递给链中的下一个查看器
        if (m_nextViewer)
            SendMessage(m_nextViewer, WM_DRAWCLIPBOARD, 0, 0);
    }
    
    void OnChangeCBChain(HWND hwndRemove, HWND hwndNext) {
        if (hwndRemove == m_nextViewer) {
            m_nextViewer = hwndNext;
        } else if (m_nextViewer) {
            SendMessage(m_nextViewer, WM_CHANGECBCHAIN, 
                       (WPARAM)hwndRemove, (LPARAM)hwndNext);
        }
    }
};

3. 无窗口监听(轮询方式)

class ClipboardMonitor {
private:
    DWORD m_lastSequence;
    std::thread m_thread;
    std::atomic<bool> m_running;
    
public:
    ClipboardMonitor() : m_lastSequence(0), m_running(false) {}
    
    void Start() {
        m_running = true;
        m_lastSequence = GetClipboardSequenceNumber();
        
        m_thread = std::thread([this]() {
            while (m_running) {
                DWORD currentSequence = GetClipboardSequenceNumber();
                if (currentSequence != m_lastSequence) {
                    m_lastSequence = currentSequence;
                    OnClipboardChanged();
                }
                Sleep(100);  // 轮询间隔
            }
        });
    }
    
    void Stop() {
        m_running = false;
        if (m_thread.joinable())
            m_thread.join();
    }
    
private:
    void OnClipboardChanged() {
        // 处理剪贴板变化
        // 注意:在非 UI 线程中访问剪贴板
    }
};

十一、高级技巧与最佳实践

1. RAII 封装剪贴板操作

class ClipboardLock {
private:
    bool m_opened;
    
public:
    explicit ClipboardLock(HWND hwnd = nullptr, int retries = 5) 
        : m_opened(false) 
    {
        for (int i = 0; i < retries && !m_opened; ++i) {
            if (OpenClipboard(hwnd)) {
                m_opened = true;
            } else if (i < retries - 1) {
                Sleep(50);
            }
        }
    }
    
    ~ClipboardLock() {
        if (m_opened) {
            CloseClipboard();
        }
    }
    
    bool IsOpen() const { return m_opened; }
    
    // 禁止拷贝
    ClipboardLock(const ClipboardLock&) = delete;
    ClipboardLock& operator=(const ClipboardLock&) = delete;
};
 
// 使用示例
bool GetClipboardTextRAII(std::wstring& out)
{
    ClipboardLock lock;
    if (!lock.IsOpen())
        return false;
    
    if (!IsClipboardFormatAvailable(CF_UNICODETEXT))
        return false;
    
    HANDLE hData = GetClipboardData(CF_UNICODETEXT);
    if (!hData)
        return false;
    
    LPCWSTR pText = static_cast<LPCWSTR>(GlobalLock(hData));
    if (!pText)
        return false;
    
    out.assign(pText);
    GlobalUnlock(hData);
    
    return true;
}

2. 线程安全的剪贴板访问

class ThreadSafeClipboard {
private:
    std::mutex m_mutex;
    
public:
    bool GetText(std::wstring& out) {
        std::lock_guard<std::mutex> lock(m_mutex);
        
        ClipboardLock cb;
        if (!cb.IsOpen())
            return false;
        
        if (!IsClipboardFormatAvailable(CF_UNICODETEXT))
            return false;
        
        HANDLE hData = GetClipboardData(CF_UNICODETEXT);
        if (!hData)
            return false;
        
        LPCWSTR pText = static_cast<LPCWSTR>(GlobalLock(hData));
        if (!pText)
            return false;
        
        out.assign(pText);
        GlobalUnlock(hData);
        
        return true;
    }
    
    bool SetText(const std::wstring& text) {
        std::lock_guard<std::mutex> lock(m_mutex);
        
        ClipboardLock cb;
        if (!cb.IsOpen())
            return false;
        
        EmptyClipboard();
        
        size_t bytes = (text.size() + 1) * sizeof(wchar_t);
        HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
        if (!hMem)
            return false;
        
        void* p = GlobalLock(hMem);
        memcpy(p, text.c_str(), bytes);
        GlobalUnlock(hMem);
        
        if (!SetClipboardData(CF_UNICODETEXT, hMem)) {
            GlobalFree(hMem);
            return false;
        }
        
        return true;
    }
};

3. 错误日志与诊断

class ClipboardDebugger {
public:
    static void DumpFormats() {
        ClipboardLock lock;
        if (!lock.IsOpen()) {
            Log("Failed to open clipboard: %d", GetLastError());
            return;
        }
        
        int count = CountClipboardFormats();
        Log("Clipboard contains %d formats:", count);
        
        UINT fmt = 0;
        WCHAR name[256];
        
        while ((fmt = EnumClipboardFormats(fmt)) != 0) {
            std::wstring fmtName;
            
            if (fmt < 0xC000) {
                fmtName = GetStandardFormatName(fmt);
            } else {
                int len = GetClipboardFormatNameW(fmt, name, _countof(name));
                if (len > 0) {
                    fmtName = name;
                } else {
                    fmtName = L"Unknown";
                }
            }
            
            HANDLE hData = GetClipboardData(fmt);
            SIZE_T size = hData ? GlobalSize(hData) : 0;
            
            Log("  %04X: %-30s (%llu bytes)", 
                fmt, fmtName.c_str(), (unsigned long long)size);
        }
    }
    
private:
    static std::wstring GetStandardFormatName(UINT fmt) {
        switch (fmt) {
            case CF_TEXT: return L"CF_TEXT";
            case CF_BITMAP: return L"CF_BITMAP";
            case CF_METAFILEPICT: return L"CF_METAFILEPICT";
            case CF_SYLK: return L"CF_SYLK";
            case CF_DIF: return L"CF_DIF";
            case CF_TIFF: return L"CF_TIFF";
            case CF_OEMTEXT: return L"CF_OEMTEXT";
            case CF_DIB: return L"CF_DIB";
            case CF_PALETTE: return L"CF_PALETTE";
            case CF_PENDATA: return L"CF_PENDATA";
            case CF_RIFF: return L"CF_RIFF";
            case CF_WAVE: return L"CF_WAVE";
            case CF_UNICODETEXT: return L"CF_UNICODETEXT";
            case CF_ENHMETAFILE: return L"CF_ENHMETAFILE";
            case CF_HDROP: return L"CF_HDROP";
            case CF_LOCALE: return L"CF_LOCALE";
            case CF_DIBV5: return L"CF_DIBV5";
            default: return L"CF_UNKNOWN_" + std::to_wstring(fmt);
        }
    }
};

4. 性能优化建议

1) 减少打开/关闭次数:

// 不好的做法
bool HasTextOrBitmap() {
    bool hasText = false, hasBitmap = false;
    
    if (OpenClipboard(nullptr)) {
        hasText = IsClipboardFormatAvailable(CF_UNICODETEXT);
        CloseClipboard();
    }
    
    if (OpenClipboard(nullptr)) {
        hasBitmap = IsClipboardFormatAvailable(CF_BITMAP);
        CloseClipboard();
    }
    
    return hasText || hasBitmap;
}
 
// 好的做法
bool HasTextOrBitmap() {
    ClipboardLock lock;
    if (!lock.IsOpen())
        return false;
    
    return IsClipboardFormatAvailable(CF_UNICODETEXT) ||
           IsClipboardFormatAvailable(CF_BITMAP);
}

2) 使用序列号快速检测变化:

class ClipboardCache {
private:
    DWORD m_lastSequence;
    std::wstring m_cachedText;
    bool m_textValid;
    
public:
    ClipboardCache() : m_lastSequence(0), m_textValid(false) {}
    
    const std::wstring& GetText() {
        DWORD currentSeq = GetClipboardSequenceNumber();
        
        if (currentSeq != m_lastSequence) {
            m_textValid = false;
            m_lastSequence = currentSeq;
        }
        
        if (!m_textValid) {
            ClipboardLock lock;
            if (lock.IsOpen()) {
                HANDLE hData = GetClipboardData(CF_UNICODETEXT);
                if (hData) {
                    LPCWSTR pText = (LPCWSTR)GlobalLock(hData);
                    if (pText) {
                        m_cachedText = pText;
                        m_textValid = true;
                        GlobalUnlock(hData);
                    }
                }
            }
        }
        
        return m_cachedText;
    }
};

5. 安全性考虑

1) 数据验证:

bool SetClipboardTextSafe(const std::wstring& text) {
    // 限制大小(防止恶意数据)
    const size_t MAX_TEXT_SIZE = 10 * 1024 * 1024;  // 10 MB
    if (text.size() > MAX_TEXT_SIZE)
        return false;
    
    // 检查是否包含非法字符
    for (wchar_t c : text) {
        if (c < 0x20 && c != L'\r' && c != L'\n' && c != L'\t')
            return false;  // 控制字符
    }
    
    // 正常设置剪贴板
    return SetClipboardText(text);
}

2) 内存分配失败处理:

bool SetClipboardDataRobust(UINT format, const void* data, size_t size) {
    ClipboardLock lock;
    if (!lock.IsOpen())
        return false;
    
    EmptyClipboard();
    
    HGLOBAL hMem = nullptr;
    
    __try {
        hMem = GlobalAlloc(GMEM_MOVEABLE, size);
        if (!hMem)
            return false;
        
        void* p = GlobalLock(hMem);
        if (!p) {
            GlobalFree(hMem);
            return false;
        }
        
        memcpy(p, data, size);
        GlobalUnlock(hMem);
        
        if (!SetClipboardData(format, hMem)) {
            GlobalFree(hMem);
            return false;
        }
        
        return true;
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        if (hMem)
            GlobalFree(hMem);
        return false;
    }
}

十二、常见问题与解决方案

1. OpenClipboard 失败

原因:

  • 剪贴板被其他进程占用
  • 前一次操作未调用 CloseClipboard
  • 系统资源不足

解决方案:

bool OpenClipboardSafe(HWND hwnd, int maxRetries = 10, int delayMs = 50) {
    for (int i = 0; i < maxRetries; ++i) {
        if (OpenClipboard(hwnd))
            return true;
        
        DWORD err = GetLastError();
        
        // 检查具体错误
        if (err == ERROR_ACCESS_DENIED) {
            // 被其他进程占用,重试
            if (i < maxRetries - 1)
                Sleep(delayMs);
        } else {
            // 其他错误,不重试
            return false;
        }
    }
    
    // 找出是谁占用了剪贴板
    HWND hwndOpen = GetOpenClipboardWindow();
    if (hwndOpen) {
        DWORD pid;
        GetWindowThreadProcessId(hwndOpen, &pid);
        // 记录日志:进程 pid 占用剪贴板
    }
    
    return false;
}

2. 数据丢失或损坏

原因:

  • 过早释放了内存
  • 使用了 GMEM_FIXED 而不是 GMEM_MOVEABLE
  • SetClipboardData 成功后继续使用内存

解决方案:

bool SetClipboardDataCorrect(UINT format, const void* data, size_t size) {
    // 正确:使用 GMEM_MOVEABLE
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size);
    if (!hMem)
        return false;
    
    void* p = GlobalLock(hMem);
    memcpy(p, data, size);
    GlobalUnlock(hMem);
    
    ClipboardLock lock;
    if (!lock.IsOpen()) {
        GlobalFree(hMem);  // 打开失败,自己释放
        return false;
    }
    
    EmptyClipboard();
    
    if (!SetClipboardData(format, hMem)) {
        GlobalFree(hMem);  // 设置失败,自己释放
        return false;
    }
    
    // 成功:不要再访问或释放 hMem
    return true;
}

3. Unicode 文本乱码

原因:

  • 混用 CF_TEXTCF_UNICODETEXT
  • 编码转换错误
  • 缺少 NUL 终止符

解决方案:

bool SetClipboardUnicodeText(const std::wstring& text) {
    // 确保使用 CF_UNICODETEXT
    size_t bytes = (text.size() + 1) * sizeof(wchar_t);  // +1 为 NUL
    
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
    if (!hMem)
        return false;
    
    wchar_t* p = (wchar_t*)GlobalLock(hMem);
    wcscpy_s(p, text.size() + 1, text.c_str());  // 安全复制,包含 NUL
    GlobalUnlock(hMem);
    
    ClipboardLock lock;
    if (!lock.IsOpen()) {
        GlobalFree(hMem);
        return false;
    }
    
    EmptyClipboard();
    
    if (!SetClipboardData(CF_UNICODETEXT, hMem)) {
        GlobalFree(hMem);
        return false;
    }
    
    return true;
}

4. 跨进程位图失败

原因:

  • 使用 DDB(设备相关位图)在不同设备上下文中失效
  • GDI 对象句柄不能跨进程共享

解决方案:

// 使用 DIB 而不是 HBITMAP
bool SetClipboardBitmapSafe(HBITMAP hBmp) {
    // 转换为 DIB
    std::vector<uint8_t> dib = HBitmapToDIB(hBmp);
    if (dib.empty())
        return false;
    
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, dib.size());
    if (!hMem)
        return false;
    
    void* p = GlobalLock(hMem);
    memcpy(p, dib.data(), dib.size());
    GlobalUnlock(hMem);
    
    ClipboardLock lock;
    if (!lock.IsOpen()) {
        GlobalFree(hMem);
        return false;
    }
    
    EmptyClipboard();
    
    if (!SetClipboardData(CF_DIB, hMem)) {
        GlobalFree(hMem);
        return false;
    }
    
    return true;
}

十三、实战示例

1. 完整的剪贴板管理器

class ClipboardManager {
public:
    // 文本操作
    static bool CopyText(const std::wstring& text) {
        ClipboardLock lock;
        if (!lock.IsOpen())
            return false;
        
        EmptyClipboard();
        
        // Unicode
        if (!SetFormat(CF_UNICODETEXT, text.c_str(), 
                       (text.size() + 1) * sizeof(wchar_t)))
            return false;
        
        // ANSI(兼容性)
        std::string ansi = WideToAnsi(text);
        SetFormat(CF_TEXT, ansi.c_str(), ansi.size() + 1);
        
        return true;
    }
    
    static bool PasteText(std::wstring& out) {
        ClipboardLock lock;
        if (!lock.IsOpen())
            return false;
        
        if (IsClipboardFormatAvailable(CF_UNICODETEXT)) {
            return GetFormat(CF_UNICODETEXT, out);
        } else if (IsClipboardFormatAvailable(CF_TEXT)) {
            std::string ansi;
            if (GetFormat(CF_TEXT, ansi)) {
                out = AnsiToWide(ansi);
                return true;
            }
        }
        
        return false;
    }
    
    // 图片操作
    static bool CopyImage(HBITMAP hBmp) {
        ClipboardLock lock;
        if (!lock.IsOpen())
            return false;
        
        EmptyClipboard();
        
        // 设置多种格式
        HBITMAP hCopy = (HBITMAP)CopyImage(hBmp, IMAGE_BITMAP, 0, 0, 0);
        if (hCopy) {
            SetClipboardData(CF_BITMAP, hCopy);
        }
        
        std::vector<uint8_t> dib = HBitmapToDIB(hBmp);
        if (!dib.empty()) {
            SetFormat(CF_DIB, dib.data(), dib.size());
        }
        
        return true;
    }
    
    static bool PasteImage(HBITMAP& out) {
        ClipboardLock lock;
        if (!lock.IsOpen())
            return false;
        
        if (IsClipboardFormatAvailable(CF_BITMAP)) {
            HANDLE hData = GetClipboardData(CF_BITMAP);
            if (hData) {
                out = (HBITMAP)CopyImage((HBITMAP)hData, IMAGE_BITMAP, 0, 0, 0);
                return out != nullptr;
            }
        }
        
        return false;
    }
    
    // 文件操作
    static bool CopyFiles(const std::vector<std::wstring>& files) {
        return SetClipboardFileList(files);
    }
    
    static bool PasteFiles(std::vector<std::wstring>& files) {
        return GetClipboardFileList(files);
    }
    
    // 工具方法
    static bool HasText() {
        return IsClipboardFormatAvailable(CF_UNICODETEXT) ||
               IsClipboardFormatAvailable(CF_TEXT);
    }
    
    static bool HasImage() {
        return IsClipboardFormatAvailable(CF_BITMAP) ||
               IsClipboardFormatAvailable(CF_DIB);
    }
    
    static bool HasFiles() {
        return IsClipboardFormatAvailable(CF_HDROP);
    }
    
    static void Clear() {
        ClipboardLock lock;
        if (lock.IsOpen()) {
            EmptyClipboard();
        }
    }
    
private:
    template<typename T>
    static bool SetFormat(UINT format, const T* data, size_t size) {
        HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size);
        if (!hMem)
            return false;
        
        void* p = GlobalLock(hMem);
        memcpy(p, data, size);
        GlobalUnlock(hMem);
        
        if (!SetClipboardData(format, hMem)) {
            GlobalFree(hMem);
            return false;
        }
        
        return true;
    }
    
    template<>
    static bool GetFormat<std::wstring>(UINT format, std::wstring& out) {
        HANDLE hData = GetClipboardData(format);
        if (!hData)
            return false;
        
        LPCWSTR p = (LPCWSTR)GlobalLock(hData);
        if (!p)
            return false;
        
        out.assign(p);
        GlobalUnlock(hData);
        return true;
    }
    
    template<>
    static bool GetFormat<std::string>(UINT format, std::string& out) {
        HANDLE hData = GetClipboardData(format);
        if (!hData)
            return false;
        
        LPCSTR p = (LPCSTR)GlobalLock(hData);
        if (!p)
            return false;
        
        out.assign(p);
        GlobalUnlock(hData);
        return true;
    }
};

2. 剪贴板历史记录

class ClipboardHistory {
private:
    struct Entry {
        std::wstring text;
        SYSTEMTIME time;
    };
    
    std::vector<Entry> m_history;
    const size_t MAX_HISTORY = 100;
    HWND m_hwnd;
    DWORD m_lastSequence;
    
public:
    ClipboardHistory(HWND hwnd) : m_hwnd(hwnd) {
        m_lastSequence = GetClipboardSequenceNumber();
        AddClipboardFormatListener(m_hwnd);
    }
    
    ~ClipboardHistory() {
        RemoveClipboardFormatListener(m_hwnd);
    }
    
    void OnClipboardUpdate() {
        DWORD newSeq = GetClipboardSequenceNumber();
        if (newSeq == m_lastSequence)
            return;
        
        m_lastSequence = newSeq;
        
        std::wstring text;
        if (ClipboardManager::PasteText(text)) {
            AddEntry(text);
        }
    }
    
    const std::vector<Entry>& GetHistory() const {
        return m_history;
    }
    
    bool RestoreEntry(size_t index) {
        if (index >= m_history.size())
            return false;
        
        return ClipboardManager::CopyText(m_history[index].text);
    }
    
private:
    void AddEntry(const std::wstring& text) {
        // 避免重复
        if (!m_history.empty() && m_history.back().text == text)
            return;
        
        Entry entry;
        entry.text = text;
        GetSystemTime(&entry.time);
        
        m_history.push_back(entry);
        
        // 限制历史记录数量
        if (m_history.size() > MAX_HISTORY) {
            m_history.erase(m_history.begin());
        }
    }
};

十四、与 OLE 剪贴板的关系

Win32 剪贴板是底层 API,OLE 剪贴板(IDataObject)是更高级的封装。

1. OLE 剪贴板的优势

  • 更灵活的格式协商:通过 IDataObject::QueryGetData 查询支持的格式
  • 多流支持:可以提供多种存储介质(TYMED_HGLOBALTYMED_ISTREAM 等)
  • 异步传输:支持大数据的异步传输
  • Shell 集成:更好地与 Windows Shell 集成

2. 基本对应关系

Win32 剪贴板OLE 剪贴板
OpenClipboard / CloseClipboardOleGetClipboard / OleSetClipboard
UINT formatFORMATETC 结构
HGLOBALSTGMEDIUM 结构
GetClipboardDataIDataObject::GetData
SetClipboardDataIDataObject::SetData

3. 简单示例

#include <ole2.h>
 
bool GetClipboardTextOLE(std::wstring& out) {
    IDataObject* pDataObj = nullptr;
    if (FAILED(OleGetClipboard(&pDataObj)))
        return false;
    
    FORMATETC fmt = { CF_UNICODETEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
    STGMEDIUM stg = {};
    
    bool success = false;
    
    if (SUCCEEDED(pDataObj->GetData(&fmt, &stg))) {
        if (stg.tymed == TYMED_HGLOBAL) {
            LPCWSTR p = (LPCWSTR)GlobalLock(stg.hGlobal);
            if (p) {
                out.assign(p);
                GlobalUnlock(stg.hGlobal);
                success = true;
            }
        }
        ReleaseStgMedium(&stg);
    }
    
    pDataObj->Release();
    return success;
}

十五、调试工具推荐

  1. ClipSpy(Windows SDK 自带)

    • 实时监控剪贴板格式
    • 查看剪贴板内容
    • 追踪剪贴板消息
  2. InsideClipboard(Nirsoft)

    • 查看所有格式的详细内容
    • 导出剪贴板数据
    • 历史记录
  3. Ditto(开源)

    • 剪贴板管理器
    • 可以查看内部格式
  4. 自定义调试工具

void DumpClipboard() {
    ClipboardLock lock;
    if (!lock.IsOpen())
        return;
    
    printf("=== Clipboard Dump ===\n");
    printf("Sequence Number: %lu\n", GetClipboardSequenceNumber());
    printf("Owner: 0x%p\n", GetClipboardOwner());
    printf("Formats: %d\n\n", CountClipboardFormats());
    
    ClipboardDebugger::DumpFormats();
}

十六、参考资源

官方文档

相关技术

  • 数据交换:DDE、OLE、COM
  • 拖放IDropSourceIDropTarget
  • Shell 扩展:上下文菜单、数据处理程序

常用格式规范