本文由danny发表于http://blog.csdn.net/danny_share
前面两篇废话了这么多,本文开始上干货。
本文从剪贴板概念、剪贴板内容监听,普通数据类型,打开剪贴板,读剪贴板,写剪贴板、API汇总、他山之石、通过剪贴板实现进程间通信和总结共10个方面来较为深入地探讨Windows剪贴板的机制和使用。
说明:建议先下载本文配套工程,其中
(1) ClipBoardDemo工程主要演示剪贴板相关API使用
(2) MainProcess工程、ASubProcess工程、BSubProcess工程分别用于演示本文第九部分进程间通信的主进程和两个子进程
下载地址:http://download.csdn.net/detail/danny_share/7703279
一.剪贴板概念
剪贴板是系统级的堆空间,且任何一个应用程序都具备访问权,主要涉及复制(Ctrl+C),剪切(Ctrl+X)、粘贴(Ctrl+V)、清空和监听五个操作。
二.剪贴板内容监听
1.开始监听
【a】SetClipboardViewer将窗口加入监听链
【b】一旦剪贴板内容变化,系统会向监听链中的第一个注册监听的窗口发送WM_DRAWCLIPBOARD消息。为什么说是第一个呢?参见“消息链顺序”
【c】通过实验发现,如果新拷贝的内容和原来一样,系统还是会出发消息,说明普通的复制操作不管内容相不相同,都是直接覆盖原来剪贴板中的数据的
2.处理消息
(1)WM_CHANGECBCHAIN
【a】除自己以外,其他进程加入了监听链,或者退出了监听链,系统就会发出这个消息
【b】这里只说明监听者发生了变化,并不表示剪贴板中内容发生变化。
【c】如果存在下一个监听进程,那么需要将该消息发送给下一个监听者
否则下一个监听者将收不到该消息
【d】对于MFC,可通过重载OnChangeCbChain函数实现
(2)WM_DRAWCLIPBOARD
【a】进程首次运行时或者是剪贴板内容发生变化时,进程都会收到这个消息
【b】如果存在下一个监听进程,那么需要将该消息发送给下一个监听者
否则下一个监听者将收不到该消息
【c】对于MFC,可通过重载OnDrawClipboard函数实现
3.停止监听
【a】使用ChangeClipboardChain()函数停止监听
【b】本质是发送WM_CHANGECBCHAIN消息,且将自己从监听链中去除
【c】其他监听进程将会收到WM_CHANGECBCHAIN消息
4.消息链顺序
【a】刚才一直在说当剪贴板内容变化时,进程将会收到WM_DRAWCLIPBOARD消息,然后该进程处理完以后,需向下个监听者发送该消息,那下个监听者是谁呢?
【b】我们在WM_DRAWCLIPBOARD消息处理函数OnDrawClipboard中closeClipboard后sleepl两秒后弹窗
【c】然后拷贝ClipBoardOne.exe三份,依次启动,我们分别取名A、B和C
【d】当剪贴板内容变化时,进程弹出剪贴板变化的顺序也是A、B、C,说明加入监听的顺序和收到消息的顺序是一致的。
三.数据类型
分类 |
ID |
格式 |
说明 |
标准 |
1 |
CF_BITMAP |
位图句柄 |
2 |
CF_DIB |
|
|
3 |
CF_DIBV5 |
包含BITMAPV5HEADER结构且跟着位图颜色空间和位图数据 |
|
4 |
CF_DIF |
|
|
5 |
CF_DSPBITMAP |
|
|
6 |
CF_DSPENHMETAFILE |
|
|
7 |
CF_DSPMETAFILEPICT |
|
|
8 |
CF_DSPTEXT |
|
|
9 |
CF_ENHMETAFILE |
|
|
10 |
CF_GDIOBJFIRST |
|
|
11 |
CF_GDIOBJLAST |
|
|
12 |
CF_HDROP |
拖放服务,文件拷贝也是这个类型 |
|
13 |
CF_LOCALE |
|
|
14 |
CF_METAFILEPICT |
|
|
15 |
CF_OEMTEXT |
在窗口中执行DOS一起使用剪贴板 |
|
16 |
CF_OWNERDISPLAY |
|
|
17 |
CF_PALETTE |
调色板句柄,通常和CF_DIB配合 |
|
18 |
CF_PENDATA |
|
|
19 |
CF_PRIVATEFIRST |
|
|
20 |
CF_PRIIVATELAST |
|
|
21 |
CF_RIFF |
资源交换文件格式的多媒体数据 |
|
22 |
CF_SYLK |
|
|
23 |
CF_TEXT |
ANSI文本 |
|
24 |
CF_TIFF |
TIFF格式图片数据内存块 |
|
25 |
CF_UNICODETEXT |
Unicode格式字符 |
|
26 |
CF_WAVE |
wave文件 |
|
注册 |
27 |
自定义数值 |
RegisterClipboardFormat自定义格式 |
私有 |
28 |
值范围 CF_PRIVATEFIRST CF_PRIVATELAST |
(1) 不需要向系统注册 (2) WM_DESTROYCLIPBOARD消息释放资源 |
多重 |
29 |
实质没有新定义格式 |
(1) CoutClipboardFormats查询格式总数 (2) 要返回具体格式,EnumClipboardFormats |
转换 |
30 |
(1) 当剪贴板中为格式A,而Windows需格式B,假如A转成B在左侧表格中,则系统默认转换 (2) 例如拷贝BMP文件时,由于BMP文件和系统调色板相关,所以最好格式为CF_DIB或CF_DIBVS,这样系统请求CF_BITMAP时,会自动使用调色板 |
四.打开剪贴板
(1)打开剪贴板之前,我们首先需要查看是否有其他应用正在使用剪贴板
(2)Windows提供了两个和查看拥有者相关的函数,分别是
GetClipboardOwner和GetOpenClipboardWindow
(3)GetClipboardOwner函数一般指上次存放数据的进程
(4)GetOpenClipboardWindow才是真正指当前获得剪贴板操作权的进程
(5)因此OpenClipboard前需调用GetOpenClipboardWindow函数,当返回句柄为空才行。
HWND myHwnd=GetSafeHwnd();//当前窗口进程
CWnd* ownerCHwnd=GetClipboardOwner();//一般指上次使用读写剪贴板的进程
CWnd* wndUserCHwnd=GetOpenClipboardWindow();//这才是真正的当前取得剪贴板使用权的进程
(6)根据前面的分析,进程打开剪贴板使用完毕以后,一定要调用CloseClipboard交出使用权,否则其他进程将无法取得剪贴板使用权
五.读剪贴板
(1)读剪贴板之前,首先还是打开剪贴板,读完以后关闭剪贴板
(2)假如想读取特定格式的数据,则可先用IsClipboardFormatAvailable来判断该格式是否存在。
(3)读文本CF_TEXT
void*tempData=NULL;
if(this->myClip->readData(CBType_Text,&tempData))
{
char* test=(char*)tempData;
CStringinfo="Read Success , the content is:\r\n";
info+=test;
MessageBox(info,MB_OK);
}
else
{
MessageBox("Read Failed",MB_OK);
}
(1)读图片CF_BITMAP
这里复制时不是复制图片文件,而是类似于在浏览器里右击复制图片
这里我复制了百度首页的logo。原理跟读取CF_TEXT一样,先读出一个HBITMAP句柄,再保存成图片即可
(2)读文件CF_HDROP
【a】本质上是得到文件路径
【b】打开剪贴板前需
UINTuDropEffect=RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT);
注意,Windows系统在ShlObj.h文件中
#define CFSTR_PREFERREDDROPEFFECT TEXT("Preferred DropEffect")
【c】通过DragQueryFile函数可遍历剪贴板中文件,先传入0得到一个UNIT指,而后将该值迭代传参即可
(3)枚举剪切板中所有格式
存在两个枚举格式相关函数。分别是
CountClipboardFormats() //此函数是获取格式总数
EnumClipboardFormats() //首次传0,以后用返回值迭代,即可遍历
六.写剪贴板
都比较简单,详见ClipBoardDemo工程
1.写文本CF_TEXT
2.写图片CF_BITMAP
3.写文件CF_HDROP
(1)对于拷贝,标志位是DROPEFFECT_COPY
(2)对于剪切,标志位是DROPEFFECT_MOVE
4.写多种格式
原理一样,只要写数据的时候不清空,多种格式写完以后再关闭剪贴板即可。
七.API汇总
(1)MFC提供了两组本质是一样的API,一组属于全局函数,一组属于窗口的成员函数(全局函数中需传窗口句柄的函数都被MFC封装了一层)。
(2)我们在窗口button里调用this-> OpenClipboard(),然后debug F11进入观察一下
_AFXWIN_INLINE BOOLCWnd::OpenClipboard()
{ASSERT(::IsWindow(m_hWnd)); return::OpenClipboard(m_hWnd); }
所以窗口成员剪贴板函数实质上调用的仍是全局函数,不过在内部多传了个句柄
(3)所以我们自己封装时,也可借此思路,内部多传个句柄封装。
ID |
函数 |
功能 |
1 |
SetClipboardViewer |
监听剪贴板 |
2 |
ChangeClipboardChain |
取消监听剪贴板 |
3 |
OnChangeCbChain |
剪贴板监听对象变化事件 |
4 |
OnDrawClipboard |
剪贴板内容变化事件 |
5 |
AddClipboardFormatListener |
添加某种格式的监听 |
6 |
RemoveClipboardFormatListener |
去除某种格式的监听 |
7 |
GetClipboardSequenceNumber |
获取在监听链中的顺序 |
8 |
EnumClipboardFormats |
枚举格式 |
9 |
CountClipboardFormats |
获取格式数量 |
10 |
GetPriorityClipboardFormat |
获取最符合的格式 |
11 |
GetClipboardFormatName |
获取非预定义的格式名称 |
12 |
IsClipboardFormatAvailable |
判断剪贴板中是否含有目标格式 |
13 |
RegisterClipboardFormat |
注册格式 |
14 |
GetUpdatedClipboardFormats |
获取当前支持的格式 |
15 |
OpenClickborder |
打开剪贴板,取得使用权 |
16 |
CloseClipboard |
关闭剪贴板,释放使用权 |
17 |
GetClipboardViewer |
获取剪贴板监听链的第一个窗口句柄 |
18 |
GetOpenClipboardWindow |
获取当前正在使用剪贴板的窗口句柄 |
19 |
GetClipboardOwner |
一般指上次向剪贴板写数据的对象 |
20 |
EmptyClipboard |
清空剪贴板 |
21 |
SetClipboardData |
写数据,第二个参数NULL表示延迟写入 |
22 |
GetClipboardData |
读数据 |
八.他山之石
1.迅雷
当我们复制文本时,如果剪贴板中包含链接地址,且是迅雷支持的下载格式,则迅雷会弹出下载对话框,这其实就是使用了剪贴板的监听原理
2.QQ
我们在QQ聊天界面里粘贴时,如果之前复制的是文字,则文字内容复制到了输入框中,如果之前复制的是文件,则文件直接发给对方好友了。其原理就是在粘贴的时候判断当前剪贴板中到底是什么内容,然后做出不同的处理
九.通过剪贴板实现进程间通信
(1)需求设计
好,原理说了这么多,现在来实现我们在“Windows进程间通信之目录”中所说的需求,我们下面简化需求,形成下面的模型:
【a】 一个主进程启动两个子进程
【b】 主进程可随时发送不同的命令给不同的子进程
【c】 子进程收到命令以后去做相应的操作,完成后发送响应给主进程
【d】 主进程收到子进程的响应后,再做相应处理
使用剪贴板的思维来实现上述需求:
【a】 三个进程都监听剪贴板
【b】 设计协议格式,使得进程读取剪贴板中数据即可区分相关参数
我们简化,在剪贴板中放置纯文本数据来传输数据,协议格式如下:
(2)实现
贴上主进程关键代码,其他详见工程
voidCMainProcessDlg::analysisClipboard()
{
CBProtocolStructtempData;
m_protocol->getProtocolData(tempData);
if(tempData.receiver=='M')
{
if(tempData.sender='A')
{
if(tempData.command=='0')
{
CStringinfo="A Sub Process send Work response to MainProcess\r\nthe Data is ";
info+=tempData.data;
MessageBox(info,"Info",MB_OK);
}
else
{
if(tempData.command=='1')
{
CStringinfo="A Sub Process send Close response toMain Process\r\nthe Data is ";
info+=tempData.data;
MessageBox(info,"Info",MB_OK);
}
}
}
else
{
if(tempData.sender='B')
{
if(tempData.command=='0')
{
CStringinfo="B Sub Process send Work response to MainProcess\r\nthe Data is ";
info+=tempData.data;
MessageBox(info,"Info",MB_OK);
}
else
{
if(tempData.command=='1')
{
CStringinfo="B Sub Process send Close response toMain Process\r\nthe Data is ";
info+=tempData.data;
MessageBox(info,"Info",MB_OK);
}
}
}
}
}
}
voidCMainProcessDlg::OnDrawClipboard()
{
CDialog::OnDrawClipboard();
if(NULL!=this->nextWindow)
{
::SendMessage(nextWindow,WM_DRAWCLIPBOARD, 0, 0);
}
analysisClipboard();
}
另外贴上子进程A的关键函数
void CASubProcessDlg::analysisClipboard()
{
CBProtocolStruct tempData;
m_protocol->getProtocolData(tempData);
if(tempData.receiver=='A')
{
if(tempData.sender='M')
{
if(tempData.command=='0')
{
CBProtocolStruct responseData;
responseData.sender='A';
responseData.receiver='M';
responseData.command='0';
m_protocol->setProtocolData(responseData);
MessageBox("A receive Work command","Info",MB_OK);
}
else
{
if(tempData.command=='1')
{
CBProtocolStruct responseData;
responseData.sender='A';
responseData.receiver='M';
responseData.command='1';
responseData.data='Z';
m_protocol->setProtocolData(responseData);
MessageBox("A receive Close command","Info",MB_OK);
SendMessage(WM_CLOSE,0,0);
}
}
}
}
}
void CASubProcessDlg::OnDrawClipboard()
{
CDialog::OnDrawClipboard();
if(NULL!=this->nextWindow)
{
::SendMessage(nextWindow, WM_DRAWCLIPBOARD, 0, 0);
}
analysisClipboard();
}
(1) 时序
我们以主进程向A发送MA00为例,来解析整个命令时序
【a】 第2步中A感知变化后第3步是B感知变化,而不是A处理变化的是因为,在OnDrawClipboard中是先SendMessage,然后再处理的。
void CASubProcessDlg::OnDrawClipboard()
{
CDialog::OnDrawClipboard();
if(NULL!=this->nextWindow)
{
::SendMessage(nextWindow,WM_DRAWCLIPBOARD, 0, 0);
}
analysisClipboard();
}
【b】剪贴板内容变化后,感知顺序都是先A再B再M,是因为,我们启动程序的顺序就是A、B和M(参见本文第二部分监听剪贴板内容消息链顺序一节)
十.总结
优点:
(1) 原理简单,上手很快
(2) 支持不同的数据格式
缺点:
(1)后台虽有两个子进程,但由于剪贴板权限只有一个,无法实现两个子进程同时操作;
(2)剪贴板更适合传输数据,虽提供剪贴板监听机制达到传输命令和时序的目的,但需自己额外设计数据格式,比较繁琐,不像Windows多线程拥有众多基础设施。所以适合主子进程间命令交互不频繁、数据交互较多的应用。
Danny
2014年8月1号
于天津河西