当我们在电脑按下ctrl+c,剪切板储存了哪些信息?

如题,比如我使用远程桌面,在远程主机复制一个文件,然后粘贴到我的电脑,这个过程中,我按下复制的时候,剪切板保存的是什么信息? 再或者,当我在浏览器中复…
关注者
123
被浏览
84,726

14 个回答

论逼呼的精准推送……

这两天刚在steam上架了一个相关的工具以填补steam上没有此类工具的空白,就刷到这么个问题。(230710补充:噗这回答怎么被日报收录了,这个工具后续没顾得维护接近弃坑状态了,现在好用的竞品不少,大家看情况入吧


回到正题。首先,剪贴板本质上是一片共享内存,只不过跟普通的共享内存不太一样的是,其由操作系统管理各个程序的访问权限,并提供额外的高级功能。

简单解答问题正文中的提问,需要一些比喻,比喻不可避免的会损失一些细节信息,如果对程序实现有兴趣的话,文章最后我会写一些。

以问题中第二问举例剪贴板的交互

再或者,当我在浏览器中复制一段内容,粘贴到word中,word甚至能保存文字图片原来在网页中的html样式。如果我粘贴到onenote中,还会显示原网页地址。

系统提供的剪贴板,类似于一个有很多格子的空储物柜,和一些标签。你在浏览器的网页中按下复制时,浏览器通常会:

  • 打开贴着标签"文本"的柜格,把选中的内容中的文本扔进去。[1]
  • 再找一个空柜格,把选中内容所关联的html代码扔进去,找个空标签,写上"html代码",贴在柜子上。
  • 再找一个空柜格,把选中部分的详细信息,如起始位置、结束位置、来源于哪个网站,这些信息扔进去,找个空标签写上"html详细信息",贴在柜子上。

然后,你在记事本里进行了粘贴,发生了这些事:

  • 记事本扫了一眼整个柜子,发现其中三个有标签有内容,其中包含写着"文本"的。
  • 记事本不管其他格式,径直打开文本的柜格取出内容并把它放到了编辑框里。

或者,你在word里按下了粘贴,发生了这些事:

  • word扫了一眼整个柜子,发现其中三个有标签有内容。
  • 发现了有写着"html代码"的标签,根据预设的规则,优先将其取出。
  • 将取出的内容按网页解析,分析出了布局,在自己的编辑框里布置好了。

而到了onenote,其实跟上面差不多,只是他发现有"html代码"的情况下,还会去看看有没有"html详细信息",如果有,从里面翻出网址并显示

总结来说,就是剪贴板里可以放多种数据,复制时,"来源程序"可以随自己喜好,放多种格式在里面,粘贴时,"目标程序"可以根据自己需求,提取其中一种或数种并处理,然后展示给用户。

另外,这个放柜子的屋子是有反锁的,通常来说屋子里只能有一个人存放或者取出东西,以防混乱。系统剪贴板基本上就是这样一个东西。

再说题目中的第一问

如题,比如我使用远程桌面,在远程主机复制一个文件,然后粘贴到我的电脑,这个过程中,我按下复制的时候,剪切板保存的是什么信息?

绝大部分情况下,都是远程软件的远程端,读取了远程电脑的剪贴板,并且以数据形式发给了本地端,本地端再把数据还原回来,存入剪贴板。也就是说这个过程其实跟系统剪贴板没关系,相当于你复制了东西,粘贴到了qq里发送,另一个人再从qq里复制出来,此时你的电脑和他的电脑剪贴板里的东西就是相同的了,但是实际上剪贴板没做什么特殊的。(当然qq只能发文本,而远程软件把所有类型都处理了)


相关技术细节

windows剪贴板的API流程很长,并且最终的部分是在内核中处理的,以读取剪贴板图片为例的主要流程。OleGetClipboard (IDataObject::GetData)-> GetClipboardData->用户层NtUserGetClipboardData->Wow64层->Shadow SSDT-> 内核层NtUserGetClipboardData-> xxxGetClipboardData-> xxxGetDummyBitmap->xxxDIBtoBMP,然后拿着一个句柄一路回来。

其中用户层前两层都是公开了接口的,即COM(OLE)层的OleGetClipboard系列函数[2][3],和更底层来自User32的GetClipboardData系列函数[4]

基于众所周知令人头大的COM层的前者,提供了比后者更多的功能,比如说给每项剪贴板子类型一些额外的标注,图片缩略图等[5],但也继承了COM层的繁杂。起初我不知道这些额外的信息是存在哪的,疑惑了很久,后来做了逆向发现它只是自己做了个子格式Ole Private Data在其中存储其他每一项的额外数据。

// 上面提到的相关结构
typedef struct tagFORMATETC {
  CLIPFORMAT     cfFormat;
  DVTARGETDEVICE *ptd;
  DWORD          dwAspect;
  LONG           lindex;
  DWORD          tymed;
} FORMATETC, *LPFORMATETC;

typedef enum tagDVASPECT {
  DVASPECT_CONTENT,
  DVASPECT_THUMBNAIL,
  DVASPECT_ICON,
  DVASPECT_DOCPRINT
} DVASPECT;

typedef enum tagTYMED {
  TYMED_HGLOBAL,
  TYMED_FILE,
  TYMED_ISTREAM,
  TYMED_ISTORAGE,
  TYMED_GDI,
  TYMED_MFPICT,
  TYMED_ENHMF,
  TYMED_NULL
} TYMED;

使用起来,读写大致就是构造一个IDataObject,在里面存储数据(期间要构造成吨的类似上面的烦人结构),然后调用OleSetClipboard写入剪贴板,或者调用OleGetClipboard从剪贴板取回一个此格式并从里面读数据。

--

而后者,来自User32的api系列,就是很普通的windows api的样子了,有点繁杂,但不像前者那样恶心,不过用起来也不是很好受,你甚至不知道GetData回来的到底是个什么东西,是个句柄?是个指针?指向什么的指针?只能自己预设一个表,根据不同的类型自己解析,并且还要调用不同的释放资源api。用法有其他答主列出了我就不多说了。

--

不得不吐槽,不知是是历史兼容问题还是设计眼界问题,剪贴板这套系统非常的不优雅,甚至可以说稀烂。比如说跟窗口强关联(是的,你的程序如果没有窗口,很多剪贴板相关的api都不能用)、使用着GlobalAlloc系列古老的函数、来源和目标程序各自使用上面两者的兼容等问题。

也没有一个封装好的库能完美的解决这些,比如说c#自带的库,使用了COM层的接口,但是暴露的数据却跟User32的接口差不多,看不到那些额外数据。而且稳定性很差,随随便便都会冒出一大堆晦涩难懂的COM层异常,比如说随便去Excel里框几格表并复制,然后执行下面的代码,就是读出剪贴板并写回(几次),然后再贴到一个记事本里,多半粘贴出来的内容不对,会抛出异常,甚至你的c#程序还会崩掉,try不到的那种。

        private void button1_Click(object sender, EventArgs e)
        {
            for (int i = 0; i < 3; i++)
            {
                var foo = Clipboard.GetDataObject();
                Clipboard.SetDataObject(foo, true);
            }
        }
        
        //补充一个更容易出问题的,不直接写回去,而是把剪贴板里所有类型读出
        //放到一个新的容器里,再写回去
        private void button2_Click(object sender, EventArgs e)
        {
            var oldDataObject = Clipboard.GetDataObject();
            var newDataObject = new DataObject();

            foreach (var formatName in oldDataObject.GetFormats())
            {
                var data = oldDataObject.GetData(formatName);
                newDataObject.SetData(formatName, data);
            }

            Clipboard.SetDataObject(newDataObject, true);
        }

参考

  1. ^类似文本这种常用的类型,有预贴标签的柜格
  2. ^OleGetClipboard https://docs.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-olegetclipboard
  3. ^IDataObject::GetData https://docs.microsoft.com/zh-cn/windows/win32/api/objidl/nf-objidl-idataobject-getdata
  4. ^User32 Clipboard Functions https://docs.microsoft.com/en-us/windows/win32/dataxchg/clipboard-functions
  5. ^一个作为例子的繁杂的COM结构 https://docs.microsoft.com/zh-cn/windows/win32/api/objidl/ns-objidl-formatetc

因为过去我也想起过这个问题去了解了下,虽然自己也不是 Windows 桌面应用开发者,现在出来误人子弟还是理直气壮,有什么不对的地方请指出。

Windows 下的剪切板是一个 RAM 中的 buffer

这个 buffer 由所有的程序共享,所以所有的程序都可以知道你在剪切板下存放了什么东西,而想要知道都有什么东西可以放在这里,需要先去了解 Windows 所提供的相应 API。

这样想其实他也是一种IPC(进程间通讯)手段呢

Windows 提供了多个功能各异的接口用来操作剪切板,想要写入剪切板时候,从一个完整的流程上来看大概是这样的:

Bool OpenClipboard(HWND hWndNewOwner);    
//指定关联到打开的剪切板的窗口句柄,传入NULL表示关联到当前任务。
//每次只允许一个进程打开并访问,使用后需要关闭,不然其他进程不能继续使用。

Bool EmptyClipboard(void);                
//写入新内容前必须先清空,否则也是不能写入的。

HGLOBAL GlobalAlloc(UINT uFlags, SIZE\_T dwBytes);
//在堆内存申请内存空间,需要传入分配内存的属性和大小
//成功时指向该内存,失败返回 NULL

LPVOID GlobalLock(HGLOBAL hMem);
//锁定刚刚申请到的内存空间,锁定计数器+1,GlobalUnLock计数器-1为负一
//成功返回内存对象起始指针,失败返回NULL

HANDLE SetClipboardData(UINT uFormat, HANDLE hMem);
//设置剪切板,执行成功返回句柄,否则 NULL

BOOL GlobalUnlock(HGLOBAL hMem);
//解锁内存计数器,此时GlobalUnLock计数器+1为零

Bool CloseClipboard(void);
//关闭剪切板,此时其他进程才能使用

想要粘贴到什么地方时候,可以获取剪切板

HANDLE GetClipboardData(UINT uFormat);
//获取剪切板数据

UINT uFormat

注意到,在设置剪切板和获取剪切板的时候,都传入了这个叫做 UINT uFormat 的参数。显而易见,这个参数用来声明存储在剪切板中数据的类型。也就是题主您想知道的,剪切板中存储了些什么数据。

通过查询微软的开发者手册,就可以知道这里可以存放什么啦!
docs.microsoft.com/en-u

那么手册中说的这些东西,在内存中是怎么表现的呢?

一、纯粹的二进制数据

对于文本最简单常用的是 CF_UNICODETEXT,也是我在整理回答时候使用的数据类型(捂脸)
这里面存储的是 Unicode 文字 ,末尾包含一个 carriage return 和 linefeed 字符,以及一个NULL字符(两个0字节)以表示数据结束。是实打实的“把二进制完整数据存储在剪切板里”

至于题主在问题中描述的富文本格式,其实就可以理解为像微软 RTF 啦,HTML 一类的,带着格式和链接等等信息一并复制进去了。然后如果粘贴时候应用不支持富文本就把这些格式数据丢弃。而在这种情况下使用的数据类型我是不太清楚的,如果有更了解其运行原理的大佬希望可以稍微讲一下给我....

二、指针(不局限于内存指针)

这里泛指一切非文件本体的链接,就是平时复制文件时候会用到的数据类型,比方说 CF_SYLK 就是一个 Microsoft 符号连结数据格式的整体内存块,另外在上面复制的二进制数据中,有时候亦可包含图片的链接。