您的位置:首页 > 其它

进程间通信 - 剪贴板实现

2012-10-09 21:02 183 查看
/article/4910386.html

引子

由于在启动一个进程后,操作系统会给这个进程分配4GB的私有地址空间,至于为何有4GB这么大,

那得考虑进程的私有地址空间和实际物理内存地址空间之间的映射以及页交换等等细节问题了,这里不予讨论,

从名字就可以知道,既然操作系统给每一个进程分配的是私有地址空间,

自然,这段地址空间也只有这个进程自己才能访问了,不然还称为私有干吗呢?

既然这段私有地址空间只能由进程本身访问,那也就说明别的进程是不能够随意的访问这个进程的地址空间的,

而本篇博文介绍的是进程间的通信,而上面又说任意两个进程之间是并能够互相访问对方的私有地址空间的,

都不能访问了,那还通信个屁啊?

自然上面的访问对方进程的私有地址空间是行不通了,那应该还有其他办法的!!!

解决方法:

如果我在物理内存中划分出一块内存,这一块内存不为任何的进程所私有,但是任何的进程又都可以访问这块内存,

那么进程A就可以往这块内存中存放数据Data,然后进程B
也是可以访问这块内存的,从而进程B就可以访问到数据Data了,

这样不就实现了进程A进程B之间的通信了!!!

而上面的这种思路就是剪贴板了。

当然解决进程间通信还有好几种思路,这将会在后续博文中介绍,本篇博文暂只介绍利用剪贴板来实现进程间的通信。

剪贴板定义

剪贴板是由操作系统维护的一块内存区域,这块内存区域不属于任何单独的进程,但是每一个进程又都可以访问这块内存区域,

而实质上当在一个进程中复制数据时,就是将数据放到该内存区域中,

而当在另一个进程中粘贴数据时,则是从该块内存区域中取出数据。

剪贴板操作

其实在剪贴板中也就那么几个API在使用,所以在这里的还是本着API介绍为主,

不管三七二十一,先列出常用的API再说(到后面结合Demo的使用即可)。

剪贴板的打开–OpenClipboard

要想把数据放置到剪贴板中,则必须先打开剪贴板,而这是通过OpenClipboard成员函数实现:

BOOLOpenClipboard(HWND
hWndNewOwner);


第一个参数hWndNewOwner指向一个与之关联的窗口句柄,即代表是这个窗口打开剪贴板,

如果这个参数设置为NULL的话,则以当前的任务或者说是进程来打开剪贴板。

如果打开剪贴板成功,则该函数返回非0值,如果其他程序已经打开了剪贴板,

那么当前这个程序就无法再打开剪贴板了,所以会致使打开剪贴板失败,从而该函数返回0值。

其实这也好理解,你想啊,剪贴板总共才那么一块内存区域,你进程A要往里面写数据,你进程B
又要往里面写数据,那不乱套去,

解决这个乱套的办法就是,如果我进程A正在往剪贴板里面写数据(可以理解为进程A
打开剪贴板了),那么进程B就不能往剪贴板里头写数据了,

既然要让进程B不能往剪贴板中写数据了,那我就让进程B打开剪贴板失败不就得了。

所以如果某个程序已经打开了剪贴板,那么其他应用程序将不能修改剪贴板,

直到打开了剪贴板的这个程序调用了CloseClipboard函数,

并且只有在调用了EmptyClipboard函数之后,打开剪贴板的当前窗口才能拥有剪贴板,

注意是必须要在调用了EmptyClipboard函数之后才能拥有剪贴板。

剪贴板的清空-EmptyClipboard

这个函数将清空剪贴板,并释放剪贴板中数据的句柄,然后将剪贴板的所有权分配给当前打开剪贴板的窗口,

因为剪贴板是所有进程都可以访问的,

所以应用程序在使用这个剪贴板时,有可能已经有其他的应用程序把数据放置到了剪贴板上,

因此该进程打开剪贴板之后,就需要调用EmptyClipboard函数来清空剪贴板,

释放剪贴板中存放的数据的句柄,并将剪贴板的所有权分配给当前的进程,

这样做之后当前打开这个剪贴板的程序就拥有了剪贴板的所有权,因此这个程序就可以往剪贴板上放置数据了。

BOOLEmptyClipboard(void);

剪贴板的关闭-CloseClipboard

如果某个进程打开了剪贴板,则在这个进程没有调用CloseClipboard函数关闭剪贴板句柄之前,

其他进程都是无法打开剪贴板的,所以我们每次使用完剪贴板之后都应该关闭剪贴板。

注意,这里的关闭剪贴板并不代表当前打开剪贴板的这个程序失去了对剪贴板的所有权,

只有在别的程序调用了EmptyClipboard函数之后,当前的这个程序才会失去对剪贴板的所有权,

而那个调用EmptyClipboard函数的程序才能拥有剪贴板。

BOOLCloseClipboard(void);

数据发送到剪贴板-SetClipboardData

可以通过SetClipboardData函数来实现往剪贴板中放置数据,这个函数以指定的剪贴板格式向剪贴板中放置数据。

HANDLESetClipboardData(UINTuFormat,HANDLEhMem);

第一个参数uFormat用来指定要放到剪贴板上的数据的格式,

比如常见的有CF_BITMAPCF_TEXTCF_DIB
等等(其他格式可以参考MSDN)。

第二个参数hMem用来指定具有指定格式的数据的句柄,该参数可以是NULL

如果该参数为NULL则表明直到有程序对剪贴板中的数据进行请求时,

该程序(也就是拥有剪贴板所有权的进程)才会将数据复制到剪贴板中,也就是提供指定剪贴板格式的数据,

上面提及的就是延迟提交技术,这个延迟提交技术将会在后面做详细的介绍。

剪贴板中数据格式判断–IsClipboardFormatAvaliable

BOOLIsClipboardFormatAvailable(UINTformat);

该函数用来判断剪贴板上的数据格式是否为format指定的格式。

剪贴板中数据接收-GetClipboardData

HANDLEGetClipboardData(UINTuFormat);

该函数根据uFormat指定的格式,返回一个以指定格式存在于剪贴板中的剪贴板对象的句柄。

全局内存分配–HGLOBAL

剪贴板中的内存从何而来

从上面的介绍中可以知道剪贴板其实就是一块内存,那么这块内存是什么时候分配的呢?

难不成说一开机,操作系统就给剪贴板分配个几M的内存的吧?

这种方式也太逊色了,你想啊,我的程序要往剪贴板中放置的数据,我事先又不晓得数据长度,

所以,一开机操作系统究竟要给剪贴板分配多少内存呢?很明显,太不动态了,不可取。

要想动态的话,那有一种方案,就是当我的程序要往剪贴板中放置数据的时候来确定要分配给剪贴板的内存的大小,

很明显,既然我都知道要往剪贴板中放置那些数据了,自然我也就知道了这些数据的长度,

那么我就可以以这个数据长度来给剪贴板分配内存了,这是很动态的了吧,所以这种方案是可取的,

但关键是,当我们以前在程序中分配内存的时候,都是使用的标准C运行库中的malloc
或者是C++中的new关键字,

(当然分配内存还有很多其他的函数,比如就有内核中的执行体中就有很多分配内存的函数,这里不讨论),

而使用malloc或者new有一个问题,那就是,用这个两个东西来分配的内存空间都是在当前进程的私有地址空间上分配内存,

也就是它们两个东东所分配的内存空间为进程私有地址空间所有,并不为所有进程所共享,

上面提到了,任何进程之间都是不能访问对方的私有地址空间的,你把剪贴板中的内存分配到了你当前进程的私有地址空间上,

而其他进程又不能访问你这个进程的私有地址空间,那怎么能够访问剪贴板呢?

很明显,不能使用mallocnew
关键字来分配内存给剪贴板。

我们应该要使用另外一个特殊一点的函数来分配内存给剪贴板,

这个特殊函数所分配的内存不能够是在进程的私有地址空间上分配,而是要在全局地址空间上分配内存,

这样这个函数所分配的内存才能够被所有的进程所共享,这样,剪贴板中的数据就可以被其他的进程所访问了。

GlobalAlloc函数

GlobalAlloc函数是从堆上分配指定数目的字节,

与其他的内存管理函数相比,全局内存函数的运行速度会稍微慢一些(等下会解释为什么会慢),

但是全局函数支持动态数据交换,同时,其分配的内存也不为任何一个进程所私有,而是由操作系统来管理这块内存,

所以用在给剪贴板分配内存空间是很适合的。

这里有读者可能会问:

为什么我们在自己的应用程序中不使用GlobalAlloc函数来分配内存,而是要使用malloc
或者new来实现?

其实,这个也只用稍微想想就知道了,你想啊,使用malloc或者new分配的内存是在进程的私有地址空间上分配的,

这片私有地址空间都是归这个进程所拥有,所管理的,自然,在以后对这块内存的读写会快很多的,

而全局内存不属于这个进程,你下次要去访问全局内存的时候,还得通过映射转换,这样肯定是运行效率低下一些了,

简单点就可以这样理解,你使用malloc或者new分配的内存和你的进程隔得很近,程序要过去拿数据-得,很近吧,

而是用GlobalAlloc函数分配的内存和你的进程隔得很远,程序要过去拿数据-太远了,耗时。

应用程序在调用了SetClipboardData函数之后,

系统就拥有了hMem参数所标识的数据对象,该应用程序可以读取这个数据对象,

但是在应用程序调用CloseClipboard函数之前,它都是不能释放该对象的句柄的,或者锁定这个句柄,

如果hMem标识一个内存对象,那么这个对象必须是利用GMEM_MOVEABLE标识调用
GlobalAlloc函数为其分配内存的。

HGLOBALWINAPIGlobalAlloc(UINTuFlags,SIZE_TdwBytes);

第一个参数uFlags用来指定分配内存的方式。其取值如下列表所示

(但是在剪贴板的使用中,由于要实现动态数据交换,所以必须得使用GHND或者GMEM_MOVEABLE):



描述

GHND

GMEM_MOVEABLEGMEM_ZEROINIT的组合。

GMEM_FIXED

分配一块固定内存,返回值是一个指针。

GMEM_MOVEABLE

分配一块可移动内存。

GMEM_ZEROINIT

初始化内存的内容为0

GPTR

GMEM_FIXEDGMEM_ZEROINIT的组合。

第二个参数dwBytes用来指定分配的字节数。

GlobalReAlloc函数

HGLOBALWINAPIGlobalReAlloc(HGLOBALhMem,SIZE_TdwBytes,UINTuFlags);

该函数为再分配函数,即在原有的数据对象hMem上,为其扩大内存空间。

第一个参数hMem代表由GlobalAlloc函数返回的数据对象句柄。

第二个参数dwBytes指定需要重新分配的内存的大小。

第三个参数uFlags指定分配的方式(可以参考GlobalAlloc函数)。

GlobalSize函数

SIZE_TWINAPIGlobalSize(HGLOBALhMem);

该函数用来返回内存块的大小。

第一个参数hMem代表由GlobalAlloc函数返回的数据对象句柄。

GlobalLock函数

LPVOIDWINAPIGlobalLock(HGLOBALhMem);

该函数的作用是对全局内存对象加锁,然后返回该对象内存块第一个字节的指针。

第一个参数hMem代表由GlobalAlloc函数返回的数据对象句柄。

GlobalUnLock函数

BOOLWINAPIGlobalUnlock(HGLOBALhMem);

你通过上面的GlobalLock函数可以获得这块全局内存的访问权,

加锁的意思就是你已经在使用这块全局内存了,别的程序就不能再使用这块全局内存了,

而如果你一直不解锁,那也不是个事啊,别的程序将会一直都使用不了这块全局内存,

那还叫全局内存干吗啊?所以这个函数就是用来对全局内存对象解锁。

第一个参数hMem代表由GlobalAlloc函数返回的数据对象句柄。

GlobalFree函数

HGLOBALWINAPIGlobalFree(HGLOBALhMem);

该函数释放全局内存块。

第一个参数hMem代表由GlobalAlloc函数返回的数据对象句柄。

Demo1–ConsoleClipboard(剪贴板常用手法)

整个项目结构很简单:





ConsoleClipboard.h

#ifndefCONSOLE_CLIP_BOARD_H

#defineCONSOLE_CLIP_BOARD_H


#include<Windows.h>

#include<iostream>


usingnamespacestd;


constchar*pStrData="Zachary";


voidSetClipBoardData();


voidGetClipBoardData();



#endif


ConsoleClipboard.cpp


#include"ConsoleClipboard.h"


intmain(intargc,char*argv)

{

SetClipBoardData();

GetClipBoardData();


system("pause");

}


voidSetClipBoardData()

{

//将OpenClipboard函数的参数指定为NULL,表明为当前进程打开剪贴板

if(OpenClipboard(NULL))

{

char*pDataBuf;


//全局内存对象

HGLOBALhGlobalClip;


//给全局内存对象分配全局内存

hGlobalClip=GlobalAlloc(GHND,strlen(pStrData)+1);

//通过给全局内存对象加锁获得对全局内存块的引用

pDataBuf=(char*)GlobalLock(hGlobalClip);

strcpy(pDataBuf,pStrData);

//使用完全局内存块后需要对全局内存块解锁

GlobalUnlock(hGlobalClip);


//清空剪贴板

EmptyClipboard();

//设置剪贴板数据,这里直接将数据放到了剪贴板中,而没有使用延迟提交技术

SetClipboardData(CF_TEXT,hGlobalClip);

//关闭剪贴板

CloseClipboard();


cout<<"设置剪贴板为:"<<pStrData<<endl<<endl;

}

}


voidGetClipBoardData()

{

if(OpenClipboard(NULL))

{

//判断剪贴板中的数据格式是否为CF_TEXT

if(IsClipboardFormatAvailable(CF_TEXT))

{

char*pDataBuf;

HGLOBALhGlobalClip;


//从剪贴板中获取格式为CF_TEXT的数据

hGlobalClip=GetClipboardData(CF_TEXT);

pDataBuf=(char*)GlobalLock(hGlobalClip);

GlobalUnlock(hGlobalClip);


cout<<"从剪贴板中获取到数据:"<<pDataBuf<<endl<<endl;

}

CloseClipboard();

}

}


效果展示:

程序运行效果:





打开记事本进行粘贴操作:





延迟提交技术

什么是延迟提交技术?

当把数据放入剪贴板中时,一般来说要制作一份数据的副本,

也就是要分配全局内存,然后将数据再复制一份,然后再将包含这份副本的内存块句柄传递给剪贴板,

对于小数据量来说,这个没什么,但是对于大数据量的话,就有问题了,

你一使用剪贴板,就往里面复制个什么几百MB的数据,

那这个数据在剪贴板中的数据被其他数据取代之前都是存放在内存中的啊,

这个方法也太龌龊了,你想啊,要是我就复制了一个500MB的数据,然后我一直不再复制其他的东西,

那么这个500MB的数据就会一直驻留在内存中,咦...太可怕了!!!太浪费内存的使用效率了!!!

为了解决上面这个问题,就需要通过使用延迟提交技术来避免内存的浪费,

当使用延迟提交技术时,实际上,直到另一个程序需要数据时,程序才会提供这份数据,

也就是,其实我一开始程序A并不往剪贴板中存放真实的数据,

而只是告诉剪贴板,我往里面放了数据(其实数据还没有放进去),

而后,如果有其他的程序B访问了剪贴板中的数据,也就是执行了“粘贴”操作,

那么此时操作系统就会去检查数据是不是真正的存放在了剪贴板中,

如果剪贴板中存放了数据,那么直接把数据送出去就可以了(这就没有使用延迟提交技术了),

而如果剪贴板中没有数据,那么Windows就会给上次往剪贴板中存放数据(尽管没有存放实际的数据)的程序,

也就是程序A发送消息,

而后,我们的程序A就可以再次调用SetClipboardData来将真实的数据放入到剪贴板中了,这样就是延迟提交技术了。

要实现延迟提交技术,则在程序A中不应该将数据句柄传送给Windows

而是在SetClipboardData调用中使用NULL

然后当另外一个程序B调用GetClipboardData函数时,

Windows就会检查这种格式的数据在剪贴板中的句柄是否为NULL

如果为NULL,则Windows会给程序A发送一个消息,从而请求到数据的实际句柄,

这个数据的实际句柄是程序A在响应消息的处理函数中重新调用SetClipboardData
来提供的。

延迟提交技术中涉及的三个消息:

下面提及的程序A代表剪贴板当前拥有者,也就是程序A负责往剪贴板中写入数据,

而程序B则代表从剪贴板中读取出数据,其没有对剪贴板的所有权。

WM_RENDERFORMAT:

程序B调用GetClipboardData时,Windows
将会给程序A的窗口过程发送这个消息,

其中wParam参数的值是所要求的格式。

在处理这个消息时,程序A就不再需要打开或者清空剪贴板了,

也就是不需要再次调用OpenClipboardEmptyClipboard函数了,

为什么不需要再次调用这两个函数?

这是因为,我们一开始的时候已经调用了这两个函数(如果一开始没有调用的话,窗口根本就不会接受到这个消息),

而此举已经告诉操作系统剪贴板已经归我所有了,而且里面的数据已经被清空了,

剪贴板所有权都归我了,那还去打开个鬼啊,不是浪费嘛?

在处理这个消息时,应该为wParam所指定的格式创建一个全局内存块,

然后再把数据传递到这个全局内存块中,并要正确的格式和数据句柄再一次调用SetClipboardData函数。

也就是需要将数据真实的复制到剪贴板中了。

WM_RENDERALLFORAMTS:

如果程序A在它自己仍然是剪贴板所有者的时候就要终止运行,

并且剪贴板上仍然包含着该程序ASetClipboardData所设置的
NULL
数据句柄(延迟提交技术),

也就是程序A当前还是剪贴板的所有者,但是用户又单击了关闭窗口,

而剪贴板中还没有真实的数据存在(因为使用了延迟提交技术),

即数据还没有被提交给剪贴板,程序A就要死了,则此时程序A的窗口过程将接收到这个消息,

这个消息的一般处理为打开剪贴板,并且清空剪贴板,然后把数据加载到内存中,

并为每种格式调用SetClipboardData,然后再关闭剪贴板即可。

WM_DESTROYCLIPBOARD:

当在程序B中调用EmptyClipboard时,Windows
将会给程序A的窗口过程发送这个消息。

即通知程序A其已不再是剪贴板的拥有者了。

Demo2–MFCClipboard(延迟提交技术的使用)

整个项目结构很简单:





主界面:





添加3个消息处理:





消息映射函数声明:


protected:

HICONm_hIcon;


//生成的消息映射函数

virtualBOOLOnInitDialog();

afx_msgvoidOnPaint();

afx_msgHCURSOROnQueryDragIcon();

DECLARE_MESSAGE_MAP()

public:

afx_msgvoidOnDestroyClipboard();

afx_msgvoidOnRenderAllFormats();

afx_msgvoidOnRenderFormat(UINTnFormat);

afx_msgvoidOnBnClickedBtnWrite();

afx_msgvoidOnBnClickedBtnRead();

CStringm_CStrWrite;

CStringm_CStrRead;

};


消息映射实现:

voidCMFCClipboardDlg::DoDataExchange(CDataExchange*pDX)

{

CDialogEx::DoDataExchange(pDX);

DDX_Text(pDX,IDC_EDIT_WRITE,m_CStrWrite);

DDX_Text(pDX,IDC_EDIT_READ,m_CStrRead);

}


BEGIN_MESSAGE_MAP(CMFCClipboardDlg,CDialogEx)

ON_WM_PAINT()

ON_WM_QUERYDRAGICON()

ON_WM_DESTROYCLIPBOARD()

ON_WM_RENDERALLFORMATS()

ON_WM_RENDERFORMAT()

ON_BN_CLICKED(ID_BTN_WRITE,&CMFCClipboardDlg::OnBnClickedBtnWrite)

ON_BN_CLICKED(ID_BTN_READ,&CMFCClipboardDlg::OnBnClickedBtnRead)

END_MESSAGE_MAP()


消息映射函数实现


//WM_DESTROYCLIPBOARD消息处理函数

voidCMFCClipboardDlg::OnDestroyClipboard()

{

//当有另外的程序调用EmptyClipboard时,

//Windows将向当前窗口过程发送WM_DESTROYCLIPBOARD消息

MessageBox(TEXT("很抱歉,您已失去对剪贴板的拥有权..."),

TEXT("提示"),MB_ICONINFORMATION);


CDialogEx::OnDestroyClipboard();

}



//WM_RENDERALLFORMATS消息处理函数

voidCMFCClipboardDlg::OnRenderAllFormats()

{

//当剪贴板中的数据句柄为当前程序所拥有,而当前程序又将被退出时,

//Windows给该程序窗口发送WM_RENDERALLFORMATS消息


OpenClipboard();

EmptyClipboard();

CloseClipboard();


CDialogEx::OnRenderAllFormats();

}


//WM_RENDERFORMAT消息处理函数

voidCMFCClipboardDlg::OnRenderFormat(UINTnFormat)

{

//当有另外的程序访问剪贴板时

//Windows给该程序窗口过程发送WM_RENDERFORMAT消息

intdataNum;

intdataIndex;

char*pDataBuf;

HGLOBALhGlobalClip;


dataNum=this->m_CStrWrite.GetLength();


hGlobalClip=GlobalAlloc(GHND,dataNum+1);

pDataBuf=(char*)GlobalLock(hGlobalClip);

for(dataIndex=0;dataIndex<dataNum;dataIndex++)

{

pDataBuf[dataIndex]=this->m_CStrWrite.GetAt(dataIndex);

}

GlobalUnlock(hGlobalClip);


//此时需要将有效数据写入到剪贴板中

SetClipboardData(CF_TEXT,hGlobalClip);


CDialogEx::OnRenderFormat(nFormat);

}



voidCMFCClipboardDlg::OnBnClickedBtnWrite()

{

UpdateData();

if(this->m_CStrWrite.GetLength()>0)

{

if(OpenClipboard())

{

EmptyClipboard();

SetClipboardData(CF_TEXT,NULL);

CloseClipboard();

MessageBox(TEXT("恭喜您,设置剪贴板成功..."),

TEXT("提示"),MB_ICONINFORMATION);

}

}

}



voidCMFCClipboardDlg::OnBnClickedBtnRead()

{

if(OpenClipboard())

{

//判断剪贴板中的数据格式是否为CF_TEXT

if(IsClipboardFormatAvailable(CF_TEXT))

{

char*pDataBuf;

HGLOBALhGlobalClip;


//从剪贴板中获取到指定格式的数据

hGlobalClip=GetClipboardData(CF_TEXT);

pDataBuf=(char*)GlobalLock(hGlobalClip);

this->m_CStrRead=pDataBuf;

GlobalUnlock(hGlobalClip);


UpdateData(FALSE);

}

CloseClipboard();

}

}


效果展示:

设置剪贴板中数据:





当前程序读取剪贴板中数据:





记事本程序读取剪贴板中数据:





测试当前进程失去剪贴板所有权:

首先单击当前程序设置好剪贴板中的数据,

然后打开一个记事本文件,在在其中输入一些数据,然后选择这部分数据,按下复制:





结束语

对于剪贴板的使用呢,也就是那么几个API在使用而已,熟悉一下就可以了,

关键是延迟提交技术的使用,同时还有对于全局内存对象的理解还是有点难度的,

不过,我相信我解释的还是比较明白了,大家可以通过我的解释再对照Demo来理解,

这样理解起来容易快速一些。

上面介绍的是通过剪贴板来实现进程之间的通信,其实这还是有问题的,

因为我们的剪贴板是位于本地机器上,所以,利用剪贴板还是无法实现本地进程与远程进程通信,

当然要想实现本地进程和远程进程的通信,那也还是有办法的,这会在后续博文中引出的。

然后的话,今天圣诞节嘛,祝诸位节日快乐,也不是我崇洋媚外,说个节日快乐还是可以的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: