一、核心概念(剪贴板在 Win32 里的本质)
1. 系统全局资源
- 每个会话有一个系统剪贴板,同一会话的所有进程共享。
- 任意时刻剪贴板内容只能有一个「拥有者」,但可以同时以多种格式存在(例如同时有
CF_UNICODETEXT和CF_TEXT)。 - 会话隔离:不同用户会话(RDP/终端服务)有各自独立的剪贴板,但某些远程桌面软件可以跨会话同步。
2. 格式(Format)
- 每一块剪贴板数据都带一个格式 ID(
UINT),常量以CF_开头(如CF_UNICODETEXT、CF_BITMAP、CF_HDROP等)。 - 除了标准格式外,还可以注册自定义格式,ID 由
RegisterClipboardFormat返回。 - 格式优先级:应用在粘贴时通常按优先级列表选择最匹配的格式(例如先尝试
CF_UNICODETEXT,再尝试CF_TEXT)。
3. 数据存储形式:句柄 + 全局内存
SetClipboardData/GetClipboardData传递的是一个HANDLE。- 对于大多数格式,这个
HANDLE实际上是HGLOBAL:- 必须用
GlobalAlloc(GMEM_MOVEABLE, size)分配; - 使用
GlobalLock/GlobalUnlock访问内存; SetClipboardData成功后,内存所有权转移给系统,应用不要再GlobalFree/ 重分配。
- 必须用
- 对于一些 GDI 对象(
CF_BITMAP的HBITMAP,CF_ENHMETAFILE的HENHMETAFILE等),直接传 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_DRAWCLIPBOARD和WM_CHANGECBCHAIN消息。
5. 延迟渲染相关消息
// 这些是消息,不是函数
WM_RENDERFORMAT // 请求渲染特定格式
WM_RENDERALLFORMATS // 请求渲染所有格式
WM_DESTROYCLIPBOARD // 剪贴板内容被清空三、标准数据格式列表(按类别)
完整列表可查 MSDN:Clipboard Formats
1. 文本类
| 常量 | 值 | 数据类型 | 含义 |
|---|---|---|---|
CF_TEXT | 1 | HGLOBAL | ANSI 文本,\0 结尾,行结束使用 \r\n |
CF_OEMTEXT | 7 | HGLOBAL | OEM 字符集文本 |
CF_UNICODETEXT | 13 | HGLOBAL | UTF-16 LE 宽字符文本(建议优先使用) |
CF_LOCALE | 16 | HGLOBAL | 和 CF_TEXT 关联的 LCID(本地化信息) |
CF_DSPTEXT | 0x81 | HGLOBAL | 由拥有者显示的文本(Owner-display,较少用) |
最佳实践:
- 优先使用
CF_UNICODETEXT进行读写,向下兼容旧程序时同时提供CF_TEXT。 - 文本必须以 NUL 字符结尾。
- Windows 标准换行符为
\r\n,但大多数应用也能接受\n。
2. 图片 / 图形类
| 常量 | 值 | 数据类型 | 含义 |
|---|---|---|---|
CF_BITMAP | 2 | HBITMAP | 设备相关位图(DDB) |
CF_DIB | 8 | HGLOBAL | 设备无关位图(BITMAPINFO + 像素数据) |
CF_DIBV5 | 17 | HGLOBAL | 增强型 DIB(BITMAPV5HEADER + 像素数据) |
CF_PALETTE | 9 | HPALETTE | 颜色调色板 |
CF_ENHMETAFILE | 14 | HENHMETAFILE | 增强型图元文件 |
CF_METAFILEPICT | 3 | HGLOBAL | 旧式图元文件(METAFILEPICT 结构) |
CF_DSPBITMAP | 0x82 | HBITMAP | Owner-display 的位图 |
CF_DSPENHMETAFILE | 0x8E | HENHMETAFILE | Owner-display 的增强图元文件 |
CF_DSPMETAFILEPICT | 0x83 | HGLOBAL | Owner-display 的旧式图元文件 |
注意:
CF_BITMAP传递的是HBITMAP句柄,不是HGLOBAL。CF_DIB/CF_DIBV5更适合跨进程传输,因为它们是内存块而不是 GDI 对象。- 许多现代应用还会注册自定义图片格式:
"PNG"、"JFIF"(JPEG)、"image/bmp"等。
3. 文件 / 拖放类
| 常量 | 值 | 数据类型 | 含义 |
|---|---|---|---|
CF_HDROP | 15 | HDROP | 文件列表(DROPFILES 结构 + 路径字符串列表) |
用途:
- 用于「复制文件」到剪贴板(例如在资源管理器中 Ctrl+C)。
- 也用于拖放操作(Drag & Drop)。
4. 其它常用格式
| 常量 | 值 | 数据类型 | 含义 |
|---|---|---|---|
CF_RIFF | 11 | HGLOBAL | RIFF 音频数据 |
CF_WAVE | 12 | HGLOBAL | WAVE 波形音频数据 |
CF_TIFF | 6 | HGLOBAL | TIFF 图片 |
CF_SYLK | 4 | HGLOBAL | SYLK 表格数据(Symbolic Link) |
CF_DIF | 5 | HGLOBAL | DIF 表格数据(Data Interchange Format) |
CF_PENDATA | 10 | HGLOBAL | 笔输入数据(已过时) |
CF_OWNERDISPLAY | 0x80 | N/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 | 保留,无效格式 |
0x0001–0x00BF | 标准剪贴板格式(系统预定义的 CF_*) |
0x00C0–0x01FF | 自定义应用格式(RegisterClipboardFormat) |
0x0200–0x02FF | 私有格式 |
0x0300–0x03FF | GDI 对象格式 |
四、基本读写流程(通用模式)
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。 - 不要
GlobalFreeGetClipboardData返回的句柄。
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;
}七、图片类数据的读取与写入
图片处理需要理解两个层面:
- 剪贴板格式层面:
CF_BITMAP/CF_DIB/CF_DIBV5 - 编码层面: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> // DragQueryFileW1. 读取剪贴板中的文件列表
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_TEXT和CF_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_HGLOBAL、TYMED_ISTREAM等) - 异步传输:支持大数据的异步传输
- Shell 集成:更好地与 Windows Shell 集成
2. 基本对应关系
| Win32 剪贴板 | OLE 剪贴板 |
|---|---|
OpenClipboard / CloseClipboard | OleGetClipboard / OleSetClipboard |
UINT format | FORMATETC 结构 |
HGLOBAL | STGMEDIUM 结构 |
GetClipboardData | IDataObject::GetData |
SetClipboardData | IDataObject::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;
}十五、调试工具推荐
-
ClipSpy(Windows SDK 自带)
- 实时监控剪贴板格式
- 查看剪贴板内容
- 追踪剪贴板消息
-
InsideClipboard(Nirsoft)
- 查看所有格式的详细内容
- 导出剪贴板数据
- 历史记录
-
Ditto(开源)
- 剪贴板管理器
- 可以查看内部格式
-
自定义调试工具
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();
}十六、参考资源
官方文档
- Clipboard - Win32 apps | Microsoft Learn
- Clipboard Formats - Win32 apps | Microsoft Learn
- Using the Clipboard - Win32 apps | Microsoft Learn
相关技术
- 数据交换:DDE、OLE、COM
- 拖放:
IDropSource、IDropTarget - Shell 扩展:上下文菜单、数据处理程序