【记录】Electron 读取不到 WPS 看图复制的 PNG?一次 Windows 剪贴板 CF_HDROP 兼容修复实录
在做 Electron 桌面应用时,我遇到一个很“玄学”的问题:从 WPS 看图工具里复制 PNG 图片,在 Electron 里却读取不到任何图片内容;但复制 JPG 又是正常的。最终定位下来,这不是 Electron 的 bug,也不是 WPS 单纯“不兼容”,而是 Windows 剪贴板多格式机制 + WPS 的“懒拷贝”策略 + Electron 对 CF_HDROP 的映射限制叠加导致的坑。
这篇文章记录我的解决过程。
1. Windows 剪贴板:天然就是“多格式并存”
Windows 剪贴板并不是“只存一种数据”。当一个程序执行复制操作时,它可以同时写入多种格式;粘贴/读取方再按自己的能力挑一种最合适的格式读取。
例如在 Word 里复制一段文字,剪贴板可能同时包含:
CF_UNICODETEXT:纯文本CF_HTML:HTMLCF_RTF:富文本
于是:
- 粘贴到记事本 → 读取
CF_UNICODETEXT - 粘贴到 Word → 读取
CF_RTF
结论:复制端放什么很关键,读取端能读什么也同样关键。
2. 图片复制时常见的剪贴板格式
图片相关的格式大致分两类:直接像素数据 与 文件引用。
| 格式 | 说明 |
|---|---|
CF_BITMAP / CF_DIB / CF_DIBV5 | 位图像素数据,图片内容直接在内存里(截图工具、画图软件常用) |
CF_HDROP | 文件引用(文件路径列表),不是图片像素本体(资源管理器复制文件时常用) |
FileNameW | 单个文件路径(UTF-16LE),部分程序会写入 |
UniformResourceLocatorW | URL 引用 |
关键区别在于:
CF_BITMAP:把像素数据复制到剪贴板(接收方可直接拿到图)CF_HDROP:只告诉你文件在磁盘哪里(接收方需要自己去读文件)
3. CF_HDROP 的数据结构:DROPFILES + 路径表
CF_HDROP 对应 Win32 的 DROPFILES 结构体,整体形态可以理解为:
- 头部:告诉你路径表从哪里开始、字符串是 Unicode 还是 ANSI
- 路径表:从偏移处开始,多个路径用
\0分隔,以双\0结束
结构示意:
┌─────────────────────────────────┐
│ DROPFILES header (20 bytes) │
│ pFiles (4B) — 文件名偏移量 │
│ pt.x (4B) — 拖放坐标 x │
│ pt.y (4B) — 拖放坐标 y │
│ fNC (4B) — 是否非客户区坐标 │
│ fWide (4B) — 1=Unicode, 0=ANSI│
├─────────────────────────────────┤
│ 文件路径列表(从 pFiles 偏移开始) │
│ "C:\foo\a.png\0" │
│ "C:\foo\b.jpg\0" │
│ "\0" ← 双空字符表示结束 │
└─────────────────────────────────┘
因此解析逻辑就是:
- 读
pFiles:拿到路径区起始位置 - 读
fWide:判断编码(UTF-16LE / ANSI) - 切片后解码成字符串,再按
\0split 出多路径
原始代码逻辑可以概括为:
const pFiles = hdropBuf.readUInt32LE(0); // 文件名数据起始偏移
const fWide = hdropBuf.readUInt32LE(16); // 编码:1=UTF-16LE, 0=ASCII
const pathsBuf = hdropBuf.slice(pFiles); // 截取文件名区域
const pathsStr = fWide
? pathsBuf.toString('utf16le') // Unicode 解码
: pathsBuf.toString('ascii'); // ANSI 解码
const hdropFiles = pathsStr.split('\0').filter(Boolean); // 按 \0 分割出路径列表
4. Electron 的映射:availableFormats() 看得到,但读不到
Electron 的 clipboard 模块对 Windows 原生剪贴板格式做了一层映射,例如:
CF_BITMAP / CF_DIB→image/png→clipboard.readImage()✅CF_UNICODETEXT→text/plain→clipboard.readText()✅CF_HTML→text/html→clipboard.readHTML()✅CF_HDROP→text/uri-list→ 没有专用读取 API ❌
坑点在这里:Electron 可能在 availableFormats() 里告诉你有 text/uri-list,但你会发现:
readText()读不到(它读的是CF_UNICODETEXT)readBuffer('text/uri-list')返回空(因为CF_HDROP是 Handle 引用,不是简单的 byte stream)readBuffer('CF_HDROP')也返回空(Electron 内部没有实现对 HDROP 的序列化)
这就是本次问题的根本原因:Electron 告诉你有
text/uri-list,但没提供任何 API 能读出它的内容。
5. WPS 看图:复制 JPG 和 PNG 的行为不一样
这也是这次问题最“迷惑”的地方:同一个工具,同一个复制动作,不同图片格式写入剪贴板的内容不同。
| 场景 | WPS 写入剪贴板内容 | 结果 |
|---|---|---|
| WPS 打开 JPG 后复制 | CF_BITMAP + CF_HDROP | Electron readImage() 能读到 ✅ |
| WPS 打开 PNG 后复制 | 只有 CF_HDROP | Electron 读不到任何图片 ❌ |
我的推测是:WPS 对 PNG 做了某种“懒拷贝/零拷贝”优化——不把像素解码后塞进剪贴板,只放一个文件引用,让接收方自己去磁盘读文件。这样复制更快、占用更小,但会让只会读位图格式的应用“失明”。
6. PowerShell 兜底:为什么它能读 CF_HDROP
既然 Electron 没法直接解析 CF_HDROP,我需要一个“系统级可靠方案”把文件路径拿出来。
PowerShell 提供了:
Get-Clipboard -Format FileDropList
它本质是走 .NET 的 System.Windows.Forms.Clipboard.GetFileDropList(),内部会调用 Win32 的 DragQueryFile() 来正确解析 CF_HDROP Handle,从而拿到完整的文件路径列表。
同时,为了避免中文路径乱码,需要显式设置输出编码:
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
否则 PowerShell 默认输出可能是系统代码页(比如 GBK),Electron 侧解析字符串会出现乱码或解码失败。
7. 最终修复:设计一条“降级链”,覆盖不同复制来源
最终我把方案设计成一条逐级降级链(每一步覆盖一批软件行为),确保在不同应用复制图片时都尽量能成功读取:
-
readImage()
覆盖:截图工具、画图、以及大部分会写位图的看图软件(含 WPS 的 JPG) -
FileNameW
覆盖:资源管理器复制文件场景(单文件路径) -
CF_HDROP/ PowerShell FileDropList 兜底
覆盖:WPS 看图(PNG)这类只写入CF_HDROP的软件 -
HTML 中的
<img src=...>
覆盖:浏览器、富文本编辑器复制图片(可能只提供 HTML 或 data URL) -
readText()里的file://URI
覆盖:少数软件直接在纯文本里塞文件 URI
这个链路的核心思想是:不要假设“复制图片=一定有位图数据”,而是承认剪贴板数据来源多样,并用兜底策略最大化兼容性。
总结
这次问题的本质是三件事叠加:
- Windows 剪贴板允许写入多种格式,且“复制图片”未必包含像素数据
- WPS 在复制 PNG 时只写入
CF_HDROP(文件引用),不写位图 - Electron 对
CF_HDROP的映射存在“可见不可读”的限制
最终通过 PowerShell/.NET 解析 FileDropList 获取路径,再回读文件,实现了对 WPS PNG 复制场景的可靠兼容,并把整个方案抽象为一条可扩展的降级链。
更多推荐

所有评论(0)