Windows剪贴板
Windows剪贴板是一种比较简单同时也是开销比较小的IPC(InterProcess Communication,进程间通讯)机制。Windows系统支持剪贴板IPC的基本机制是由系统预留的一块全局共享内存,用来暂存在各进程间进行交换的数据:提供数据的进程创建一个全局内存块,并将要传送的数据移到或复制到该内存块;接受数据的进程(也可以是提供数据的进程本身)获取此内存块的句柄,并完成对该内存块数据的读取。
为使剪贴板的这种IPC机制更加完善和便于使用,需要解决好如下三个问题:提供数据的进程在结束时Windows系统将删除其创建的全局内存块,而接受数据的进程则希望在其退出后剪贴板中的数据仍然存在,可以继续为其他进程所获取;能方便地管理和传送剪贴板数据句柄;能方便设置和确定剪贴板数据格式。为完善上述功能,Windows提供了存在于USER32.dll中的一组API函数、消息和预定义数据格式等,并通过对这些函数、消息的使用来管理在进程间进行的剪贴板数据交换。
Windows系统为剪贴板提供了一组API函数和多种消息,基本可以满足编程的需要。而且Windows还为剪贴板预定义了多种数据格式。通过这些预定义的格式,可以使接收方正确再现数据提供方放置于剪贴板中的数据内容。
文本剪贴板和位图剪贴板的使用
这两种剪贴板是比较常用的。其中,文本剪贴板是包含具有格式CF_TEXT的字符串的剪贴板,是最经常使用的剪贴板之一。在文本剪贴板中传递的数据是不带任何格式信息的ASCII字符。若要将文本传送到剪贴板,可以先分配一个可移动全局内存块,然后将要复制的文本内容写入到此内存区域。最后调用剪贴板函数将数据放置到剪贴板:
DWORD dwLength = 100; // 要复制的字串长度 HANDLE hGlobalMemory = GlobalAlloc(GHND, dwLength + 1); // 分配内存 LPBYTE lpGlobalMemory = ( LPBYTE )GlobalLock(hGlobalMemory); // 锁定内存 for ( int i = 0; i < dwLength; i++) // 将"*"复制到全局内存块 *lpGlobalMemory++ = '*' ; GlobalUnlock(hGlobalMemory); // 锁定内存块解锁 HWND hWnd = GetSafeHwnd(); // 获取安全窗口句柄 ::OpenClipboard(hWnd); // 打开剪贴板 ::EmptyClipboard(); // 清空剪贴板 ::SetClipboardData(CF_TEXT, hGlobalMemory); // 将内存中的数据放置到剪贴板 ::CloseClipboard(); // 关闭剪贴板 |
这里以OpenClipboard()打开剪贴板,并在调用了EmptyClipboard()后使hWnd指向的窗口成为剪贴板的拥有者,一直持续到CloseClipboard()函数的调用。在此期间,剪贴板为拥有者所独占,其他进程将无法对剪贴板内容进行修改。
从剪贴板获取文本的过程与之类似,首先打开剪贴板并获取剪贴板的数据句柄,如果数据存在就拷贝其数据到程序变量。由于GetClipboardData()获取的数据句柄是属于剪贴板的,因此用户程序必须在调用CloseClipboard()函数之前使用它:
HWND hWnd = GetSafeHwnd(); // 获取安全窗口句柄 ::OpenClipboard(hWnd); // 打开剪贴板 HANDLE hClipMemory = ::GetClipboardData(CF_TEXT); // 获取剪贴板数据句柄 DWORD dwLength = GlobalSize(hClipMemory); // 返回指定内存区域的当前大小 LPBYTE lpClipMemory = ( LPBYTE )GlobalLock(hClipMemory); // 锁定内存 m_sMessage = CString(lpClipMemory); // 保存得到的文本数据 GlobalUnlock(hClipMemory); // 内存解锁 ::CloseClipboard(); // 关闭剪贴板 |
大多数应用程序对图形数据采取的是位图的剪贴板数据格式。位图剪贴板的使用与文本剪贴板的使用是类似的,只是数据格式要指明为CF_BITMAP,而且在使用SetClipboardData()或GetClipboardData()函数时交给剪贴板或从剪贴板返回的是设备相关位图句柄。下面这段示例代码将把存在于剪贴板中的位图数据显示到程序的客户区:
HWND hWnd = GetSafeHwnd(); // 获取安全窗口句柄 ::OpenClipboard(hWnd); // 打开剪贴板 HANDLE hBitmap = ::GetClipboardData(CF_BITMAP); // 获取剪贴板数据句柄 HDC hDC = ::GetDC(hWnd); // 获取设备环境句柄 HDC hdcMem = CreateCompatibleDC(hDC); // 创建与设备相关的内存环境 SelectObject(hdcMem, hBitmap); // 选择对象 SetMapMode(hdcMem, GetMapMode(hDC)); // 设置映射模式 BITMAP bm; // 得到位图对象 GetObject(hBitmap, sizeof (BITMAP), &bm); BitBlt(hDC, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); //位图复制 ::ReleaseDC(hWnd, hDC); // 释放设备环境句柄 DeleteDC(hdcMem); // 删除内存环境 ::CloseClipboard(); // 关闭剪贴板 |
多数据项和延迟提交技术
要把数据放入剪贴板,在打开剪贴板后一定要调用EmptyClipboard()函数清除当前剪贴板中的内容,而不可以在原有数据项基础上追加新的数据项。但是,可以在EmptyClipboard()和CloseClipboard()调用之间多次调用SetClipboardData()函数来放置多个不同格式的数据项。例如:
OpenClipboard(hWnd); EmptyClipboardData(); SetClipboardData(CF_TEXT, hGMemText); SetClipboardData(CF_BITMAP, hBitmap); CloseClipboard(); |
这时如果用CF_TEXT或CF_BITMAP等格式标记去调用IsClipboardFormatAvailable()都将返回TRUE,表明这几种格式的数据同时存在于剪贴板中。以不同的格式标记去调用GetClipboardData()函数可以得到相应的数据句柄。
对于多数据项的剪贴板数据,还可以用CountClipboardFormats()和EnumClipboardFormats()函数得到当前剪贴板中存在的数据格式数目和具体的数据格式。EnumClipboardFormats()的函数原型为:
UINT EnumClipboardFormats( UINT format); |
参数format指定了剪贴板的数据格式。如果成功执行将返回format指定的格式的下一个数据格式值,如果format为最后的数据格式值,那么将返回0。由此不难写出处理剪贴板中所有格式数据项的程序段代码:
UINT format = 0; // 从第一种格式值开始枚举 OpenClipboard(hWnd); while (format = EnumClipboardFormats(format)) { …… // 对相关格式数据的处理 } CloseClipboard(); |
在数据提供进程创建了剪贴板数据后,一直到有其他进程获取剪贴板数据前,这些数据都要占据内存空间。如在剪贴板放置的数据量过大,就会浪费内存空间,降低对资源的利用率。为避免这种浪费,可以采取延迟提交(Delayed rendering)技术,即由数据提供进程先创建一个指定数据格式的空(NULL)剪贴板数据块,直到有其他进程需要数据或自身进程要终止运行时才真正提交数据。
延迟提交的实现并不复杂,只需剪贴板拥有者进程在调用SetClipboardData()将数据句柄参数设置为NULL即可。延迟提交的拥有者进程需要做的主要工作是对WM_RENDERFORMAT、WM_DESTORYCLIPBOARD和WM_RENDERALLFORMATS等剪贴板延迟提交消息的处理。
当另一个进程调用GetClipboardData()函数时,系统将会向延迟提交数据的剪贴板拥有者进程发送WM_RENDERFORMAT消息。剪贴板拥有者进程在此消息的响应函数中应使用相应的格式和实际的数据句柄来调用SetClipboardData()函数,但不必再调用OpenClipboard()和EmptyClipboard()去打开和清空剪贴板了。在设置完数据有也无须调用CloseClipboard()关闭剪贴板。如果其他进程打开了剪贴板并且调用EmptyClipboard()函数去清空剪贴板的内容,接管剪贴板的拥有权时,系统将向延迟提交的剪贴板拥有者进程发送WM_DESTROYCLIPBOARD消息,以通知该进程对剪贴板拥有权的丧失。而失去剪贴板拥有权的进程在收到该消息后则不会再向剪贴板提交数据。另外,在延迟提交进程在提交完所有要提交的数据后也会收到此消息。如果延迟提交剪贴板拥有者进程将要终止,系统将会为其发送一条WM_RENDERALLFORMATS消息,通知其打开并清除剪贴板内容。在调用SetClipboardData()设置各数据句柄后关闭剪贴板。
下面这段代码将完成对数据的延迟提交,WM_RENDERFORMAT消息响应函数OnRenderFormat()并不会立即执行,当有进程调用GetClipboardData()函数从剪贴板读取数据时才会发出该消息。在消息处理函数中完成对数据的提交:
进行延迟提交:
HWND hWnd = GetSafeHwnd(); // 获取安全窗口句柄 ::OpenClipboard(hWnd); // 打开剪贴板 ::EmptyClipboard(); // 清空剪贴板 ::SetClipboardData(CF_TEXT, NULL); // 进行剪贴板数据的延迟提交 ::CloseClipboard(); // 关闭剪贴板 |
在WM_RENDERFORMAT消息的响应函数中:
DWORD dwLength = 100; // 要复制的字串长度 HANDLE hGlobalMemory = GlobalAlloc(GHND, dwLength + 1); // 分配内存块 LPBYTE lpGlobalMemory = ( LPBYTE )GlobalLock(hGlobalMemory); // 锁定内存块 for ( int i = 0; i < dwLength; i++) // 将"*"复制到全局内存块 *lpGlobalMemory++ = '*' ; GlobalUnlock(hGlobalMemory); // 锁定内存块解锁 ::SetClipboardData(CF_TEXT, hGlobalMemory); // 将内存中的数据放置到剪贴板 |
DSP和自定义数据格式的使用
Windows系统预定义了三个带“DSP”前缀的数据格式:CF_DSPTEXT、CF_DSPBITMAP和CF_DSPMETAFILEPICT。这是一些伪标准格式,用于表示在程序中定义的私有剪贴板数据格式。对于不同的程序,这些格式的规定是不同的,因此这些格式只针对某一具体程序的不同实例才有意义。
为使用DSP数据格式,必须确保进程本身与剪贴板拥有者进程同属一个程序。可以调用GetClipboardOwner()函数来获取剪贴板拥有者窗口句柄,并调用GetClassName()来获取窗口类名:
HWND hClipOwner = GetClipboardOwner(); GetClassName(hClipOwner, &ClassName, 255); |
如果剪贴板拥有者窗口类名同本进程的窗口类名一致,就可以使用带有DSP前缀的剪贴板数据格式了。
除了使用Windows预定义的剪贴板数据格式外,也可以在程序中使用自定义的数据格式。对于自定义的数据格式lpszFormat,可以调用RegisterClipboardFormat()函数来登记,并获取其返回的格式标识值:
UINT format = RegisterClipboardFormat(lpszFormat); |
对此返回的格式标识值的使用与系统预定义的格式标识是一样的。可以通过GetClipboardFormatName()函数来获取自定义格式的ASCII名。