更新记录(仅首版,2026年08月21日 发布)
- 首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22176619)
以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。
Go Komura(2026)。《剪贴板与拖放的工作原理——在业务应用中正确处理 OLE 数据传输》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/windows-clipboard-drag-drop-ole-data-transfer/
- DOI(已登记存档)
- 10.5281/zenodo.22176619
- DOI(上次登记版本)
- 10.5281/zenodo.22176620
“粘贴 Excel 的表格时格式会散架”“把自家应用里复制的内容贴进 Word,结果不是想要的样子”“希望能用拖放导入文件”——业务应用的改造中,这类咨询很常见。
这些都是身边的日常操作,但只要把剪贴板当成“装一份数据的盒子”,就会看错它的机制。实际上,它是把同一内容同时放成多种格式,由粘贴方挑出自己能理解的格式的机制。同一次复制,结果却随粘贴目标而变,原因就在这里。
关掉复制源就贴不上的问题,牵涉到等需要时才生成数据的“延迟渲染”。而 OLE 拖放(D&D)同样是把与 OLE 剪贴板一致的数据表示 IDataObject,通过 COM 的接口交接出去。复制粘贴与 D&D 共用数据格式、只是换了搬运方式,是彼此关联的功能。
本文是面向中小企业的信息系统负责人与 Windows 应用开发者的讲解。先理清格式的机制,再看粘贴方、复制方、监视方的实现,然后进入历史、同步、RDP 的管理以及 OLE D&D 的注意事项。
1. 先说结论
需要把握的主线有以下三条。
- 复制方提供多种格式,粘贴方从中挑选。 文本用
CF_UNICODETEXT,文件路径列表用CF_HDROP,带格式的文本用已注册格式“HTML Format”,这是基本盘。1234 - 要设计的不只是数据的格式,还有校验与存活期。 粘贴进来的数据要当作不可信的外部输入来校验。使用延迟渲染的复制方,要把退出时的定稿处理一并实现。监视用
AddClipboardFormatListener与WM_CLIPBOARDUPDATE,读取冲突则用重试来兜底。5678 - 明确数据会传到哪里,以及传过去之后会怎样。 历史、云同步、RDP 重定向都是管理对象。OLE D&D 需要用
OleInitialize做 STA 初始化,从普通权限向提升后的目标放置会被 UIPI 挡住。另外,Move是一份“源数据会消失”的契约。910111213
如果按目的阅读,请从下表指出的章节进入。
| 困扰与目的 | 首先要确认的 | 阅读章节 |
|---|---|---|
| 粘贴 Excel 表格会散架 | 复制方提供的格式,以及粘贴方挑选的格式 | 第 2~4 章 |
| 希望自家应用复制的内容在其他应用里也能用 | 多格式的提供,以及退出后仍保留的处理 | 第 5 章 |
| 希望检测到复制后自动导入 | 侦听器登记与读取的重试 | 第 6 章 |
| 不希望机密经由历史、同步、RDP 留下 | 应用侧的排除格式与组织的策略 | 6.3 节、第 7 章 |
| 想实现 D&D,或只在提升权限时不工作 | OLE 初始化、权限边界、效果与路径的校验 | 第 8~9 章 |
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 20 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
2. 剪贴板到底是什么——不是“一份数据”,而是“同一内容的多种格式”
剪贴板是在应用之间共享数据的机制。同一桌面上的应用都能使用它,但准确的共享单位是窗口站。不同的用户会话和 RDP 会话,各自拥有独立的剪贴板。RDP 下复制粘贴之所以能通,是因为重定向功能在两者之间搭了桥(第 7 章)。
使用上的大原则是由用户发起。不在用户不知情的地方擅自存取数据,这是官方的设计方针。1
复制方先清空剪贴板,然后把同一内容按表现力从高到低的顺序,放成多种格式。6 把电子表格软件里复制表格的场景在概念上排开,大致如下。
| 优先级 | 格式 | 内容 |
|---|---|---|
| 1 | 应用私有格式 | 连公式与格式都完整的内部表示(供粘贴回同一应用) |
| 2 | HTML Format | 保留表格结构与格式的 HTML 片段 |
| 3 | CSV | 以单元格分隔的文本 |
| 4 | CF_UNICODETEXT | 以制表符分隔的纯文本 |
| 5 | 图像格式 | 表格外观的位图 |
粘贴方从这份清单里取出的,是自己能理解的格式。在 Word 里变成带格式的表格、在记事本里变成制表符分隔的文本,是因为两者挑中的格式不同。
也就是说,“结果随粘贴目标而变”这件事本身并不是缺陷。需要把复制方的提供与粘贴方的选择结合起来看。
flowchart TB
accTitle: 同一次复制在不同粘贴目标上得到不同结果的机制
accDescr: 复制方把同一内容以多种格式放进剪贴板,粘贴方挑出自己能理解的格式,因此 Word 得到带格式的表格,记事本得到制表符分隔的文本
copy["复制方:电子表格软件"] --> cb["剪贴板(同一内容的多种格式)"]
cb --> f1["应用私有格式"]
cb --> f2["HTML Format"]
cb --> f3["CSV"]
cb --> f4["CF_UNICODETEXT"]
f2 -->|"Word 挑它"| word["带格式的表格"]
f4 -->|"记事本挑它"| notepad["制表符分隔的文本"]
反过来说,开头那些“格式散架”“贴进来的是奇怪的东西”的咨询,几乎都能归结为某一方挑格式或提供格式的方式的问题。粘贴方的话题放在第 4 章,复制方的话题放在第 5 章。
3. 标准格式与已注册格式——CF_UNICODETEXT、CF_HDROP、HTML Format
3.1. 标准格式——文本要用 Unicode 这一侧
操作系统预先定义好的格式称为标准格式。业务应用中常出场的是下面这几种。2
| 格式 | 值 | 内容 |
|---|---|---|
| CF_TEXT | 1 | ANSI 文本(依赖代码页) |
| CF_UNICODETEXT | 13 | Unicode 文本。文本的规范形式是这个 |
| CF_HDROP | 15 | 文件路径的列表(HDROP 句柄) |
| CF_DIB | 8 | 设备无关位图 |
| CF_LOCALE | 16 | 与文本关联的区域设置标识符 |
CF_TEXT 与 CF_UNICODETEXT 属于系统会隐式互转的“合成格式”。转换使用与 CF_LOCALE 关联的代码页。2
但 ANSI 侧无法表示的字符会在转换中丢失。处理 Unicode 特有的符号或组合字符时,把转换交给系统就会引出乱码。原则是应用的读写统一用 CF_UNICODETEXT,在 .NET 中统一用 DataFormats.UnicodeText。
flowchart LR
accTitle: CF_UNICODETEXT 与 CF_TEXT 的隐式转换
accDescr: 应用只读写 CF_UNICODETEXT,CF_TEXT 由系统按 CF_LOCALE 的代码页隐式转换合成。ANSI 侧无法表示的字符会在这次转换中丢失
apprw["应用的读写"] --> uni["CF_UNICODETEXT(规范形式)"]
uni <-->|"系统隐式转换(CF_LOCALE 的代码页)"| ansi["CF_TEXT(ANSI,依赖代码页)"]
ansi -.-> loss["无法表示的字符会在转换中丢失(乱码的温床)"]
3.2. CF_HDROP——文件是以“路径列表”传递的
文件资源管理器里的文件复制和 D&D 使用的就是 CF_HDROP。首先要记住的是,传过去的不是文件本体,而是完整路径的列表。
内存块的开头是 DROPFILES 结构体,其后排列着以 NUL 字符分隔的路径字符串。最后还要放一个空字符串,所以末尾是“双 NUL”。头部的 pFiles 表示路径列表的起始偏移量,fWide 表示字符串是否为 Unicode。3
[DROPFILES 头部: pFiles=路径列表的起始偏移量, fWide=1(Unicode)]
C:\data\a.txt(NUL)C:\data\b.txt(NUL)(NUL)
在原生代码中用 DragQueryFile 逐个取出,在 .NET 中可以按 DataFormats.FileDrop 以 string[] 接收。“传过去的只有路径,不是文件本体”这一点,在第 8~9 章的 D&D 中同样起作用。
flowchart TB
accTitle: CF_HDROP 的内存块结构
accDescr: 全局内存的开头是 DROPFILES 结构体,pFiles 指出路径列表的起始偏移量,fWide 表示是否为 Unicode。其后排列以 NUL 分隔的完整路径,最后以空字符串构成双 NUL 终止。传过去的只有路径,不是文件本体
hdr["DROPFILES 结构体(pFiles = 列表起始偏移量 / fWide = 1)"] --> p1["C:\\data\\a.txt + NUL"]
p1 --> p2["C:\\data\\b.txt + NUL"]
p2 --> tail["末尾的空字符串(双 NUL)"]
hdr -.-> note["传过去的只有路径,不是文件本体"]
3.3. 已注册格式——RegisterClipboardFormat 与“HTML Format”
标准格式表达不了的数据,可以自定一个名字,作为已注册格式来共享。把名字传给 RegisterClipboardFormat 就会返回格式 ID。只要用同一个名字注册,别的应用也能取得同一个 ID,因此在应用之间约定好名字,就成了彼此的接点。1
在自家的一组应用之间传递结构化数据时,要起一个像 KomuraSoft.Report.RowData 这样不会冲突的名字。
HTML Format 是“带头部的 UTF-8”
已注册格式的代表,是与 RTF 并列的带格式文本格式“HTML Format”。内容是 UTF-8 文本,但开头带有一段罗列字节偏移量的头部。4
Version:0.9
StartHTML:<HTML 整体的起始字节位置>
EndHTML:<HTML 整体的结束字节位置>
StartFragment:<片段的起始字节位置>
EndFragment:<片段的结束字节位置>
<html><body>
<!--StartFragment--><b>粗体的</b>片段文本<!--EndFragment-->
</body></html>
各偏移量的基准是包含头部自身在内的数据开头。StartHTML / EndHTML 指 HTML 整体,StartFragment / EndFragment 指用户所选片段的起点与终点。单位不是字符数,而是字节数。
生成时按下面的顺序组装。
- 用固定位数(例如 10 位)预留偏移量字段。
- 组装 HTML 正文,并编码为 UTF-8。
- 实测编码后的字节位置,写回头部。
在含日文的 UTF-8 中,字符数与字节数并不一致。这里弄错,粘贴到其他应用时开头或末尾就会缺字。4
flowchart TB
accTitle: HTML Format 的头部与偏移量的关系
accDescr: 头部的 StartHTML 与 EndHTML 指向 HTML 整体,StartFragment 与 EndFragment 指向用户所选片段,都是从数据开头算起的字节位置。UTF-8 中字符数与字节数会错开,因此要用编码后实测的字节位置填写
header["头部(Version / StartHTML / EndHTML / StartFragment / EndFragment)"] --> html["HTML 整体(StartHTML~EndHTML)"]
html --> frag["所选片段(StartFragment~EndFragment)"]
header -.-> byte["各偏移量 = 从数据开头算起的字节位置(UTF-8 编码后实测再写回)"]
除此之外,表格型数据也常用 CSV(.NET 的 DataFormats.CommaSeparatedValue)。与 Excel 互操作时,把 HTML Format(带格式)、CSV(仅值)和 CF_UNICODETEXT(制表符分隔)一并提供,就不挑粘贴目标了。
4. 粘贴方的做法——格式的优先级与校验
4.1. 从富格式开始依次查找
粘贴方要在自己能处理的格式中,从信息量最大的开始找。既可以顺着复制方按表现力高低放好的格式顺序来找,也可以自己指定优先级。6
| Win32 API | 挑选方式 |
|---|---|
EnumClipboardFormats |
按复制方放置的顺序枚举,使用第一个能识别的格式 |
GetPriorityClipboardFormat |
从粘贴方传入的优先级列表中,挑出可用的格式 |
用 .NET 写,分支大致如下。
// 粘贴表格:按富格式→纯格式的顺序查找
var data = Clipboard.GetDataObject();
if (data is null) return;
// 即使声称有该格式,实体也未必是 string。只有连类型都确认到了才走
// 这个分支,不行就退到下一个候选
if (data.GetDataPresent(DataFormats.Html)
&& data.GetData(DataFormats.Html) is string html)
{
// 校验 HTML Format 的头部之后,再作为表格导入
}
else if (data.GetDataPresent(DataFormats.CommaSeparatedValue))
{
// 作为 CSV 导入
}
else if (data.GetDataPresent(DataFormats.UnicodeText))
{
// 作为制表符分隔的文本导入
}
这就是对开头那条“粘贴 Excel 表格会散架”咨询的回答。只读纯文本的应用,拿不到表格的结构。接收到哪一级格式为止,是粘贴方的设计决策。
flowchart TB
accTitle: 从富格式依次向下查找的粘贴分支
accDescr: 有 HTML Format 且实体也是字符串就作为表格导入,没有就退到 CSV,再没有就退到制表符分隔的文本,从表现力高的格式依次向下。候选都没有就不接收
startsel["开始粘贴"] --> h{"有 HTML Format 且实体是 string?"}
h -->|"是"| useh["校验头部后作为表格导入"]
h -->|"否"| c{"有 CSV?"}
c -->|"是"| usec["作为 CSV 导入"]
c -->|"否"| t{"有 UnicodeText?"}
t -->|"是"| uset["作为制表符分隔的文本导入"]
t -->|"否"| giveup["不接收"]
4.2. 粘贴进来的数据就是外部输入
容易被忽略的是,剪贴板里的内容是不知道由哪个应用放进去的、来自外部的数据。微软也在 OLE 剪贴板的文档里警告说“剪贴板的数据不可信。在应用中使用之前要仔细解析”。5
仅凭格式存在,还不能判定可以导入。要连实体的类型一起确认,并通过下面这些校验。
| 校验对象 | 确认内容 |
|---|---|
| HTML Format 的头部 | 偏移量是否指向了范围之外。也有应用会输出损坏的头部 |
| 数值、日期、编码等值 | 能否通过与界面输入相同的校验 |
| 数据的大小 | 几百 MB 的图像、几百万行的文本等,是否超出了接收上限 |
注意:在查大小之前,物化就已经开始了
针对超大数据的对策,要区分能挡住哪个阶段的开销。.NET 的 GetData 在调用时也会触发延迟渲染,文本的话会一直物化成托管字符串。只在取到之后确认大小,挡不住这次物化的开销。
用 GlobalSize 检查 Win32 的 GetClipboardData 返回的 HGLOBAL,可以防住超大数据继续进入转换成托管字符串与解析的环节。不过,对延迟渲染的格式来说,GetClipboardData 本身就会触发渲染。复制源那边物化数据这件事本身,是挡不住的。
为了不让界面卡死,要把获取处理挪到 UI 线程之外,超过上限的数据直接拒收。即便如此,.NET 的 Clipboard 仍然必须在 STA 上。不要用 Task.Run 的线程池(MTA),而要用设为 STA 的专用线程(5.1 节)。
“从外部进来的值,不管走的是哪条路径,都要先校验再使用”这个思路,与不要直接使用 QR 码的读取值里梳理的完全一样。以为“粘贴是用户操作所以安全”,正是出问题的根源。
flowchart LR
accTitle: 先校验再使用粘贴数据的流程
accDescr: 从剪贴板取出的数据要依次通过格式存在确认、实体类型确认、大小上限和内容校验,任何一步不合格就拒收或退到下一个候选格式
present["确认格式是否存在(GetDataPresent)"] --> type["确认实体的类型(is string / string[])"]
type --> size["确认大小上限"]
size --> content["校验内容(头部、路径、值)"]
content --> ok["导入"]
type -.->|"类型不符"| rej["拒收 / 退到下一个候选格式"]
size -.->|"过大"| rej
content -.->|"非法"| rej
5. 复制方的做法——同时提供多种格式与延迟渲染
5.1. 同时放置多种格式
复制方的做法是 4.1 的反面,也就是同时提供富格式与纯格式。用 WinForms/WPF 的 DataObject,几行就能写完。14
// WinForms(System.Windows.Forms)。WPF 用 System.Windows 的 DataObject/Clipboard 同理
var data = new DataObject();
data.SetData(DataFormats.Html, htmlFormatText); // 带头部的 HTML Format 字符串
data.SetData(DataFormats.CommaSeparatedValue, csv); // CSV
data.SetData(DataFormats.UnicodeText, plainText); // 纯文本
Clipboard.SetDataObject(data, copy: true); // copy:true=应用退出后仍保留
这里把线程与退出后的存活期分开来确认。
线程条件:.NET 的 Clipboard 类只能从 STA 线程使用。14 WinForms/WPF 的 UI 线程已经由 [STAThread] 置为 STA,通常没有问题,但从不是 STA 的后台线程无法使用。背景在COM STA/MTA 的基础知识中做了说明。
退出后的存活期:copy: true 是“应用退出之后仍保留”的指定。结合下面的延迟渲染一起理解,就能明白为什么需要这个指定。
5.2. 延迟渲染——“一关就贴不上”的真面目
每次复制都把这么多格式全部生成一遍,等于为用不上的格式也要做一遍处理。避开这一点的机制就是延迟渲染(delayed rendering)。6
复制时给 SetClipboardData 的数据句柄传 NULL,登记的不是数据本体,而是“被要求时再生成”的承诺。当该格式被要求时,WM_RENDERFORMAT 会送到复制源,这时才第一次生成数据。
退出时要把“承诺”变成数据
复制源带着尚未渲染的格式就退出,粘贴方便再也收不到那份数据。这就是“关掉复制源就贴不上”的问题。
复制源有责任响应退出前的 WM_RENDERALLFORMATS,把所有尚未渲染的格式物化。没有定稿的格式,会随复制源的退出一起丢失。6
flowchart TB
accTitle: 延迟渲染的流程与“一关就贴不上”
accDescr: 复制源用 NULL 句柄只放一个承诺,被要求时用 WM_RENDERFORMAT 当场物化。退出时有责任用 WM_RENDERALLFORMATS 把全部格式物化,疏忽了该格式就会丢失
promise["复制源:用 SetClipboardData(format, NULL) 只登记“承诺”"] --> req["粘贴方要求该格式"]
req --> render["WM_RENDERFORMAT → 当场生成数据"]
promise --> quit["复制源准备退出"]
quit -->|"用 WM_RENDERALLFORMATS 物化"| ok["退出后仍可粘贴"]
quit -->|"疏于物化"| lost["该格式丢失(“一关就贴不上”)"]
在 OLE 剪贴板中,用 OleSetClipboard 放置 IDataObject。此时剪贴板持有的,是指向该数据对象的指针。
在退出时调用 OleFlushClipboard,数据就会在剪贴板上物化,退出之后也能粘贴。7 .NET 的 Clipboard.SetDataObject(data, copy: true) 指定的就是这种“退出后仍保留”的行为。
在 Excel 里复制一大片区域后退出时,会被问“剪贴板中有大量信息,是否保留?”,这就是在确认要不要做这次定稿处理(刷新)。自己写的应用也一样,要把延迟渲染与退出时的定稿处理成套设计。
延迟了并不等于界面就不会卡住
延迟渲染是性能优化,但被要求的数据是在消息处理过程中同步生成的。生成一旦耗时,界面就会卡住,这是它的取舍。6
6. 剪贴板监视的做法——侦听器、重试、排除出历史
6.1. 使用 AddClipboardFormatListener
“希望检测到条码扫描枪的值或来自核心系统的复制后自动导入”这类需求,就需要监视剪贴板的变化。做法在历史上有三种,但现在的正确答案只有一个。8
| 做法 | 评价 |
|---|---|
| 用定时器定期读取(轮询) | 浪费多,还会漏检。不要用 |
| SetClipboardViewer(查看器链) | 链中一个应用的缺陷会毁掉整条链。仅为向后兼容而残存 |
| AddClipboardFormatListener | 推荐。已登记的窗口会收到 WM_CLIPBOARDUPDATE |
flowchart LR
accTitle: 剪贴板监视的流程
accDescr: 在创建句柄时用 AddClipboardFormatListener 登记,之后无论哪个应用复制都会收到 WM_CLIPBOARDUPDATE。读取要带重试,销毁句柄时用 RemoveClipboardFormatListener 对称地注销
created["OnHandleCreated:AddClipboardFormatListener"] --> wait["等待"]
anyapp["某个应用执行了复制"] --> notify["收到 WM_CLIPBOARDUPDATE"]
wait --> notify
notify --> readtry["带重试地读取(6.2 节)"]
readtry --> wait
destroyed["OnHandleDestroyed:RemoveClipboardFormatListener"] -.->|"对称地注销"| created
// WinForms 中的最小实现
public partial class MainForm : Form
{
[DllImport("user32.dll", SetLastError = true)]
static extern bool AddClipboardFormatListener(IntPtr hwnd);
[DllImport("user32.dll", SetLastError = true)]
static extern bool RemoveClipboardFormatListener(IntPtr hwnd);
const int WM_CLIPBOARDUPDATE = 0x031D;
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
AddClipboardFormatListener(Handle);
}
protected override void OnHandleDestroyed(EventArgs e)
{
// 配合句柄的销毁与重建,对称地注销
RemoveClipboardFormatListener(Handle);
base.OnHandleDestroyed(e);
}
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_CLIPBOARDUPDATE)
{
// 在这里读取 Clipboard.GetDataObject(),是需要的格式就导入
}
base.WndProc(ref m);
}
}
6.2. 打不开时就重试
能打开剪贴板的同一时刻只有一个窗口。别的进程打开着的时候,OpenClipboard 会失败。6
即便是 WM_CLIPBOARDUPDATE 刚送达的那一刻,复制源或别的监视应用也可能正在操作。要把暂时读不到当作正常现象来处理,加入插入几十毫秒的短暂等待、重试几次的逻辑。
在 .NET 中需要注意的是,能指定重试次数与间隔的重载只有写入侧的 SetDataObject 才有。读取侧的 GetDataObject 等并没有。
| 读取侧 | 冲突时的处理 |
|---|---|
| WinForms | 捕获 ExternalException,等待后重试 |
| WPF | 捕获 COMException,等待后重试 |
读取的等待与重试,要由应用自己实现。
flowchart LR
accTitle: 剪贴板读取重试的流程
accDescr: 能打开剪贴板的同一时刻只有一个窗口,因此变更通知刚到时的读取可能与其他进程冲突而失败。接到异常就等几十毫秒再试,到达上限就这次放弃,等下一次更新再取
upd["WM_CLIPBOARDUPDATE"] --> tryread["尝试读取"]
tryread -->|"成功"| useok["进入导入(第 4 章的校验)"]
tryread -->|"失败(其他窗口正在使用)"| waitretry["等待几十毫秒"]
waitretry -->|"最多重试几次"| tryread
waitretry -->|"到达上限"| giveup2["这次放弃(等下一次更新再取)"]
6.3. 不让它进入历史与同步——处理机密的复制功能要有的顾虑
Windows 有剪贴板历史(Win+V)和跨设备同步(云剪贴板),应用放进去的数据默认就在它们的覆盖范围内。把密码、银行账号这类机密交给复制功能的应用,要一并放上用于排除出历史与同步的已注册格式。1
| 已注册格式 | 抑制的对象 |
|---|---|
ExcludeClipboardContentFromMonitorProcessing |
把这次复制的全部内容同时排除出历史与跨设备同步 |
CanIncludeInClipboardHistory (DWORD 0) |
仅历史 |
CanUploadToCloudClipboard (DWORD 0) |
仅跨设备同步 |
密码管理器复制的密码不会留在 Win+V 里,靠的就是这套机制。只要把名字传给 RegisterClipboardFormat 取得格式 ID,再与普通数据并排设置即可,所以处理机密的业务应用值得把它实现上。
7. 信息系统视角下的剪贴板——历史、云同步、RDP 的控制
应用侧控制复制内容,与组织整体允许到什么范围,要分开来考虑。管理员要盯的有三项:历史、云同步、RDP 重定向。
剪贴板历史会积累最近的复制内容。云剪贴板会在用同一 Microsoft 账户 / Microsoft Entra 账户登录的设备之间同步。10
方便的另一面,是会发生从核心系统复制的个人信息留在历史里、办公 PC 的复制内容同步到私人 PC 上这类残留与越界。
7.1. 控制历史与跨设备同步
组织层面用于控制的策略有下面两条。
| 控制对象 | GPO(计算机配置 > 管理模板 > 系统 > OS 策略) | Policy CSP(Intune) | 默认 |
|---|---|---|---|
| 剪贴板历史 | 允许剪贴板历史记录 | Experience/AllowClipboardHistory | 允许 |
| 跨设备同步 | 允许跨设备同步剪贴板 | Privacy/AllowCrossDeviceClipboard | 允许 |
两者都从 Windows 10 版本 1809 起可用,禁用后设置应用中的相应项会变灰,策略立即生效。910
7.2. 控制 RDP 的会话间传输
RDP(远程桌面)的剪贴板重定向,在本地 PC 与远程会话之间搭桥。默认状态下复制粘贴是通的,因此它也会成为把服务器上的机密带出来的通道。
双向都阻断的配置,是“不允许剪贴板重定向”策略(注册表值 fDisableClip)。11
近年的 Windows Server / Windows 11 还加入了更细粒度的控制策略,例如只把服务器到客户端这个方向限定为文本。是全面禁止,还是按方向和格式分档限制,要在运维与安全之间权衡决定。
flowchart LR
accTitle: 剪贴板内容扩散的通道与控制点
accDescr: 复制的内容默认就在历史与云同步的覆盖范围内,在 RDP 下还会经重定向传到另一个会话。每条通道都能用策略控制,应用侧则能用排除格式把内容挡在历史与同步之外
cb["剪贴板"] --> hist["历史(Win+V)"]
cb --> cloud["云同步 → 另一台设备"]
cb --> rdp["RDP 重定向 → 另一个会话"]
hist -.-> p1["控制:AllowClipboardHistory"]
cloud -.-> p2["控制:AllowCrossDeviceClipboard"]
rdp -.-> p3["控制:fDisableClip"]
cb -.-> p4["应用侧:用 ExcludeClipboardContentFromMonitorProcessing 等排除(6.3 节)"]
8. 拖放的真面目是 COM——IDataObject+IDropSource+IDropTarget
8.1. 与剪贴板相同的数据,不同的搬运方式
OLE 拖放由下面三方协作运转。15
| 角色 | 实现方 | 职责 |
|---|---|---|
| IDataObject | 拖动源 | 被搬运的数据本体。与剪贴板相同的多格式数据对象 |
| IDropSource | 拖动源 | 判断拖动是继续还是中止,以及光标反馈 |
| IDropTarget | 放置目标 | 用 DragEnter/DragOver/DragLeave/Drop 表明能否接收并接收数据 |
操作的流程如下。
- 拖动源调用
DoDragDrop,开始拖动的循环。 - 鼠标进入放置目标窗口后,通知会送到
IDropTarget。 - 放置目标表明能否接收,并在放置时从
IDataObject取出需要的格式。
官方文档也说明,D&D 提供的功能与剪贴板的复制粘贴相同,已经实现了复制粘贴的应用只需少量补充。15 也就是说,第 2~5 章准备的多格式 DataObject,在 D&D 中同样是被搬运的数据。
flowchart LR
accTitle: OLE 拖放的流程
accDescr: 拖动源以 IDataObject 为货物调用 DoDragDrop,拖动的循环随之开始,放置目标的 IDropTarget 在 DragEnter 与 DragOver 中表明能否接收,在 Drop 时从 IDataObject 选出格式取走
src["拖动源:IDataObject + IDropSource"] -->|"DoDragDrop"| loop["拖动的循环"]
loop -->|"鼠标进入窗口"| enter["IDropTarget.DragEnter/DragOver(每次都表明 Effect)"]
enter -->|"松开按键"| drop["IDropTarget.Drop"]
drop --> data["从 IDataObject 选出格式并取出"]
8.2. 必须有 OleInitialize(STA)
放置目标窗口用 RegisterDragDrop 登记。作为前提,请先确认 OLE 的初始化与消息处理这两点。
初始化要用 OleInitialize。只是改用 CoInitialize / CoInitializeEx,RegisterDragDrop 会以 E_OUTOFMEMORY 失败。OleInitialize 会把 COM 初始化为 STA。12
执行登记的线程必须转动消息泵。OLE D&D 是扎根于窗口与消息处理的功能,疏忽这一点会让拖动中的其他应用挂起。12 背景与COM STA/MTA 的基础知识中的线程模型相同。
在 WinForms/WPF 中通过事件接收
在 WinForms/WPF 中,框架替你完成 OLE 初始化与接口实现。开发者只需把能否接收的表明和实际的导入写进事件里。
// WinForms:接收文件的放置
listView1.AllowDrop = true;
listView1.DragEnter += (s, e) =>
{
// 还要确认源侧允许 Copy(有的源只允许 Move/Link)
e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop)
&& (e.AllowedEffect & DragDropEffects.Copy) == DragDropEffects.Copy
? DragDropEffects.Copy // 可接收:按复制接收
: DragDropEffects.None; // 不可接收
};
listView1.DragDrop += (s, e) =>
{
// 拖来的数据同样是外部输入。即使声称是 FileDrop,实体也可能是 null 或
// 别的类型,取值本身也可能失败
object data;
try { data = e.Data.GetData(DataFormats.FileDrop); }
catch (COMException) { return; }
if (data is not string[] paths) return;
foreach (var path in paths)
{
// 校验路径之后再导入(9.3 节)
}
};
WPF 的格局也一样。用元素的 AllowDrop="True" 和 DragOver / Drop 事件来接收,再用 e.Data.GetData(DataFormats.FileDrop) 取出路径数组。
在 DragEnter / DragOver 中每次都表明能否接收(Effect),这是 IDropTarget 的规矩。省掉这一步,光标就会一直停在“禁止”上不变。除了格式在不在,还要像代码示例那样确认拖动源是否允许 Copy。
9. D&D 的坑——提升、Move、路径的校验
9.1. 无法向提升为管理员的应用放置
从文件资源管理器把文件放到“以管理员身份运行”的应用上却毫无反应——这不是实现出错,而是操作系统的规格。原因是 UIPI(User Interface Privilege Isolation)默认阻断从低完整性级别进程发往高完整性级别窗口的消息,普通权限(中完整性)的文件资源管理器发出的放置通知,到不了提升后的应用窗口。13
flowchart LR
accTitle: 向提升后的应用放置被 UIPI 阻断的机制
accDescr: 从中完整性的文件资源管理器发往高完整性提升应用的放置通知,会被 UIPI 默认阻断而送不到。把界面保持在普通权限、把特权处理分离出去,放置就能送到
explorer["文件资源管理器(中完整性)"] -->|"放置通知"| uipi{"UIPI"}
uipi -->|"阻断(默认)"| elevated["提升后的应用(高完整性):无反应"]
uipi -->|"通过"| normal["普通权限的界面:放置能送到"]
normal -.->|"只委托特权处理"| broker["把需要提升的处理分离出去的另一个进程"]
也有用 ChangeWindowMessageFilterEx 单独放行 WM_DROPFILES 等消息的变通办法。13 但这只是针对旧式 WM_DROPFILES 放置通知的对策,并不能解决 OLE D&D 的全部问题。
根本性的方针是不让应用一直以提升权限运行。只把需要提升的处理分离到另一个进程,UI 本体就能保持普通权限继续接收 D&D。分离的设计在Windows 应用的管理员权限与代理进程中有详述。
9.2. DragDropEffects 的含义——Move 是“源会消失”的契约
DragDropEffects 的 Copy / Move / Link 不是光标上的装饰,而是拖动源与放置目标之间的契约。
拖动源用 DoDragDrop 声明它允许的效果集合,放置目标从中选出实际的效果。规约是一旦 Move 成立,拖动源就会删除数据(文件)。
接收方不加思索地返回 Move,就会闹出“一放下去原文件就没了”的问题。业务应用的导入场景,应把接收方明确写出 Copy 作为偏安全的默认。
flowchart LR
accTitle: DragDropEffects 的契约——Move 会让源消失
accDescr: 拖动源用 DoDragDrop 声明允许的效果集合,放置目标选出实际的效果。规约是 Move 成立后拖动源会删除文件,因此以导入为目的的接收方明确写出 Copy 更安全
srcdecl["拖动源:声明允许的效果(Copy | Move | Link)"] --> tgtsel["放置目标:选出实际的效果"]
tgtsel -->|"Copy"| copyok["源文件保留(导入用途的安全选择)"]
tgtsel -->|"Move"| moveact["拖动源删除文件——“源没了”的祸根"]
9.3. 校验被放置的路径
通过 CF_HDROP/FileDrop 传来的只有路径(3.2 节)。导入之前,要和粘贴一样,按外部输入过一遍校验。
- 是文件还是文件夹:把整个文件夹被放进来的情况写进规格(是递归导入,还是拒收)。
- OneDrive 的占位符:如果是路径存在、但文件本体不在本地的“按需文件”,打开的瞬间就会触发下载,离线时会失败。行为与对策请参考OneDrive“按需文件”与业务应用。
- 长路径与特殊路径:超过 MAX_PATH 的路径、网络(UNC)路径、可移动介质上的路径,要先确认后续处理能否支持,再决定接收。
- 数量与总大小:为了不让几千个文件的放置把界面卡死,导入要异步化,并设置上限与进度显示。
10. 小结
- 剪贴板是在同一桌面(窗口站)内共享的一块区域上,把同一内容同时放成多种格式的机制。因为由粘贴目标挑格式,同一次复制的结果也会不同。
- 文本用 CF_UNICODETEXT,文件用 CF_HDROP,带格式的文本用已注册格式 HTML Format(字节偏移量头部 + UTF-8)。
- 粘贴方按富格式→纯格式的顺序查找格式,内容则当作外部输入来校验。复制方同时提供多种格式,若使用延迟渲染,还要把退出时的定稿(WM_RENDERALLFORMATS/OleFlushClipboard)实现到位。
- 监视用 AddClipboardFormatListener+WM_CLIPBOARDUPDATE。OpenClipboard 的冲突用重试来兜底,机密则用 ExcludeClipboardContentFromMonitorProcessing 等排除出历史与同步。
- 信息系统部门可以用 GPO/Intune 控制剪贴板历史、云同步与 RDP 重定向。默认全都是允许,因此在处理机密的环境里请自觉作出判断。
- D&D 的真面目是 COM,IDropSource/IDropTarget 交接的是与剪贴板相同的 IDataObject。RegisterDragDrop 必须先有 OleInitialize(STA)。
- 向提升后的应用放置会被 UIPI 阻断。DragDropEffects 的 Move 是“源会消失”的契约,被放置的路径要校验之后再导入。
对用户来说,复制粘贴与 D&D 就像空气一样的功能。正因如此,一旦出现“贴不上”“散架了”“没了”,体验的恶化就格外明显;反过来,多格式提供与放置支持做得周到的应用,仅凭这一点就能让日常操作顺滑起来。希望本文能成为你考虑改造优先级时的判断材料。
相关文章
- COM / ActiveX / OCX 是什么 - 区别与关系一并梳理
- COM STA/MTA 的基础知识 - 线程模型与避免挂起的思路
- 今日的 Windows 外壳集成——上下文菜单、文件关联、Windows 11 的变化
- C# 操作 Excel 时 EXCEL.EXE 残留的问题——COM 引用释放模式与替换判断
- Windows 应用 UX 设计 - 按使用环境划分的优先级
- OneDrive“按需文件”与业务应用——占位符打破的前提与对策
相关咨询领域
小村软件有限公司承接业务应用的复制粘贴与拖放支持的设计与实现(多格式的提供、Excel 联动、文件放置导入)、“粘贴后散架”“复制的内容没了”这类缺陷的原因调查、利用剪贴板监视实现输入自动化,以及机密信息的历史与同步对策的实现。牵涉 COM 与 OLE 底层的项目,也可以从现象排查开始。
参考链接
-
Microsoft Learn, Clipboard Formats. 关于窗口可以把同一信息放成多种剪贴板格式、用 RegisterClipboardFormat 建立的已注册格式(同名注册返回同一个值,可在应用之间共享)、合成格式,以及用 ExcludeClipboardContentFromMonitorProcessing、CanIncludeInClipboardHistory、CanUploadToCloudClipboard 把内容排除出剪贴板历史/云同步。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Standard Clipboard Formats. 关于 CF_TEXT(ANSI)、CF_UNICODETEXT、CF_HDROP、CF_DIB、CF_LOCALE 等标准格式的定义,以及系统使用与 CF_LOCALE 关联的代码页在 CF_TEXT 与 CF_UNICODETEXT 之间隐式转换。 ↩ ↩2 ↩3
-
Microsoft Learn, Shell Clipboard Formats. 关于 CF_HDROP 由 DROPFILES 结构体与双 NUL 终止的完整路径字符串数组构成、用 DragQueryFile 逐个取出路径,以及 CFSTR_ 系列的外壳格式需要用 RegisterClipboardFormat 注册。 ↩ ↩2
-
Microsoft Learn, HTML Clipboard Format. 关于注册名为“HTML Format”、头部结构带有 Version、StartHTML、EndHTML、StartFragment、EndFragment 等偏移量(以字节为单位)、编码始终是 UTF-8,以及 StartFragment/EndFragment 注释的约定。 ↩ ↩2 ↩3
-
Microsoft Learn, OleGetClipboard function (ole2.h). 关于从剪贴板取得 IDataObject 的方法,以及“剪贴板的数据不可信,在应用中使用之前应仔细解析”的警告。 ↩ ↩2
-
Microsoft Learn, Clipboard Operations. 关于同一时刻只有一个窗口能打开剪贴板、复制时按表现力从高到低依次放置格式、粘贴时用 EnumClipboardFormats/GetPriorityClipboardFormat 挑选格式、给 SetClipboardData 传 NULL 的延迟渲染与 WM_RENDERFORMAT/WM_RENDERALLFORMATS 的责任,以及延迟渲染的取舍。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, OleFlushClipboard function (ole2.h). 关于 OleSetClipboard 之下剪贴板只持有指向数据对象的指针、OleFlushClipboard 把数据在剪贴板上物化从而让应用退出后仍可粘贴,以及不需要在退出后保留时应用 OleSetClipboard(NULL) 清空。 ↩ ↩2
-
Microsoft Learn, Using the clipboard. 关于剪贴板监视三种方式(查看器窗口、序列号、格式侦听器)的比较、新程序应使用 AddClipboardFormatListener 的侦听器、查看器链对维护链条的疏漏很脆弱,以及不应把序列号用于轮询。 ↩ ↩2
-
Microsoft Learn, Policy CSP - Experience. 关于用 Experience/AllowClipboardHistory 策略允许/禁止剪贴板历史、从 Windows 10 版本 1809 起可用、默认为允许,以及在 GPO 中映射到“系统 > OS 策略”之下且变更立即生效。 ↩ ↩2
-
Microsoft Learn, Policy CSP - Privacy. 关于用 Privacy/AllowCrossDeviceClipboard 策略允许/禁止跨设备剪贴板同步、同步在用同一 Microsoft 账户/Microsoft Entra 账户登录的设备之间进行,以及默认为允许。 ↩ ↩2 ↩3
-
Microsoft Learn, Policy CSP - ADMX_TerminalServer. 关于用 TS_CLIENT_CLIPBOARD(“不允许剪贴板重定向”,注册表值 fDisableClip)禁止远程桌面会话中本地与远程之间的剪贴板共享,以及默认允许重定向。 ↩ ↩2
-
Microsoft Learn, RegisterDragDrop function (ole2.h). 关于放置目标窗口与 IDropTarget 的登记方法、用 CoInitialize/CoInitializeEx 初始化 COM 时总会以 E_OUTOFMEMORY 失败而必须用 OleInitialize,以及调用线程不转动消息泵会让拖动源应用挂起。 ↩ ↩2 ↩3
-
Microsoft Learn, ChangeWindowMessageFilterEx function (winuser.h). 关于 UIPI 是默认阻断来自低完整性级别发送方的消息的安全机制,以及可以用窗口级的消息筛选器放行特定消息(MSGFLT_ALLOW)。 ↩ ↩2 ↩3
-
Microsoft Learn, How to add data to the Clipboard (Windows Forms). 关于用 DataObject 与 Clipboard.SetDataObject 同时放置多种格式的数据、为了让其他应用能识别应以多种格式添加,以及 Clipboard 类只能从 STA 线程使用并需要 [STAThread]。 ↩ ↩2
-
Microsoft Learn, Drag and Drop (COM). 关于 OLE 拖放由 IDropSource(拖动源)、IDropTarget(放置目标)、DoDragDrop(OLE 提供的循环)三方运转,提供与剪贴板复制粘贴相同的功能、已实现复制粘贴的应用只需少量补充,以及反馈的种类。 ↩ ↩2
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
委托开发 Windows 应用程序前该梳理的事项
在委托外包开发 Windows 应用程序之前,梳理现有软件改造、设备联动、COM/ActiveX、发布与更新、维护等需要注意的要点。
Windows 打印驱动程序停止提供 ── 业务应用的报表与标签打印如何应对
Microsoft 正在分阶段推进 v3/v4 打印驱动程序的停止提供,从 2026 年 7 月起会优先选择 IPP 类驱动程序。本文梳理 Windows protected print mode 下会消失什么,并用判断表整理业务应用程序的报表、标签打印中依赖点的盘点方法与...
什么是 OLE 对象 —— 嵌入与链接的机制以及业务文档中的陷阱
在 Word 中嵌入 Excel 表格的功能,本质就是 OLE 对象。本文从嵌入与链接的区别、复合文件与结构化存储、In-Place Activation 的机制,一直讲到链接断开、文件膨胀与安全对策,全部立足于实务视角。
“无响应”的真相——Windows 如何判定应用卡死,以及不会卡死的设计
Windows 的“无响应”是操作系统在窗口 5 秒未取出消息时作出判定、并换成幽灵窗口的机制。本文讲解判定的内部动作、卡死的常见原因、把繁重处理移出 UI 线程的设计,以及挂起的调查步骤。
Windows 应用的深色模式与对比度主题支持 ── DWM 的深色标题栏、WinForms/WPF 跟随系统主题、高对比度下的绘制
讲解如何让 WinForms/WPF 应用跟随 Windows 11 的深色模式与对比度主题。梳理 DWM 的深色标题栏、.NET 9/10 的 SetColorMode 与 ThemeMode、主题切换的检测,以及高对比度下用系统颜色绘制的做法。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
ActiveX 迁移
整理保留、包装或替换 COM / ActiveX / OCX 资产的阶段性判断的主题页面。
UI 线程 & 计时器
整理 WPF / WinForms UI 线程、异步流程、Dispatcher 使用、计时器判断的主题页面。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
支持包含常驻处理、设备联动、运行日志与可维护结构的 Windows 桌面应用程序。
既有资产活用 & 迁移支持
在持续活用 COM / ActiveX / OCX 资产、原生代码与 32 位依赖的同时,协助规划阶段性的迁移。
常见问题
汇总了咨询这一主题时常见的问题。
- 为什么把从 Excel 复制的表格粘贴到应用里,格式就会散架?
- 剪贴板上放的不是“一份数据”,而是同一内容同时以多种格式(应用私有格式、HTML Format、CSV、Unicode 文本等)存在,粘贴方会挑出自己能理解的格式取走。格式散架时,典型原因是粘贴方只读纯文本(CF_UNICODETEXT)。如果连表格结构也想接收,就要把粘贴侧实现成优先取出 HTML Format 或 CSV。反过来,如果希望自家应用复制的内容能被其他应用正确粘贴,复制时就要同时提供富格式和纯格式。
- 为什么关掉复制源的应用后就贴不上了?
- 因为复制源使用了延迟渲染(delayed rendering)。处理大数据的应用在复制时不放数据本体,只在剪贴板上登记一个“被要求时再生成”的承诺。在这种状态下,如果复制源没有响应退出时的 WM_RENDERALLFORMATS 把数据定稿就退出,尚未渲染的格式就会丢失。使用 OLE 剪贴板(IDataObject)的应用,只要在退出时调用 OleFlushClipboard 把数据物化,就能让退出后仍然可以粘贴。
- 自己写的应用要怎样监视剪贴板的变化?
- 当前推荐的做法是用 AddClipboardFormatListener 把自己的窗口登记为侦听器,并处理内容每次变化时送达的 WM_CLIPBOARDUPDATE 消息。用定时器定期读取内容的轮询浪费很多,而基于 SetClipboardViewer 的旧查看器链,只要链中一个应用有缺陷就会毁掉整条链,如今仅为向后兼容而保留。另外,读取时的 OpenClipboard 可能与其他进程冲突而失败,因此实现成插入短暂等待的重试会更稳定。
- 有没有办法不让密码等机密信息留在剪贴板历史(Win+V)里?
- 有应用侧和策略侧两种手段。在应用侧,复制时一并放上名为 ExcludeClipboardContentFromMonitorProcessing 的已注册格式,该内容就既不会进入历史,也不会进入跨设备同步。也可以用 CanIncludeInClipboardHistory(仅历史)和 CanUploadToCloudClipboard(仅同步)分别控制。密码管理器用的就是这套机制。如果想在整个组织范围内关掉,可以用组策略或 Intune(Policy CSP)的 AllowClipboardHistory、AllowCrossDeviceClipboard 禁用历史与云同步本身。
- 为什么不能把文件拖放到以管理员身份运行的应用上?
- 因为名为 UIPI(User Interface Privilege Isolation)的安全机制会阻断从低完整性级别进程发往高完整性级别窗口的消息。文件资源管理器以普通权限(中完整性)运行,所以拖放的通知到不了提升后应用的窗口。用 ChangeWindowMessageFilterEx 单独放行 WM_DROPFILES 等消息的变通办法也广为人知,但它只适用于旧式的放置通知。根本的做法是不再把应用设计成一直以提升权限运行,而是只把需要提升的处理分离到另一个进程。