剪贴簿

Programming.Windows

剪贴簿

壹佰软件开发小组  整理编译  

Microsoft Windows剪贴簿允许把数据从一个程序传送到另一个程序中。它的原理相对而言比较简单,把数据存放到剪贴簿上的程序或从剪贴簿上取出数据的程序都无须太多的负担。Windows 98和Microsoft Windows NT都提供了剪贴簿浏览程序,该程序可以显示剪贴簿的目前内容。

许多处理文件或者其它数据的程序都包含一个「Edit」菜单,其中包括「Cut」、「Copy」和「Paste」选项。当使用者选择「Cut」或者「Copy」时,程序将数据传送给剪贴簿。这个数据使用某种格式,如文字、位图(一种按位排列的矩形数组,其中的位与平面显示的图素相对应)或者metafile(用二进制元数值内容表示的绘图命令集)等。当使用者从菜单中选择「Paste」时,程序检查剪贴簿中包含的数据,看看使用的是否是程序可以接受的一种格式。如果是,那么数据将从剪贴簿传送到程序中。

如果使用者不发出明确的指令,程序就不能把数据送入或移出剪贴簿。例如,在某个程序中执行剪下或复制(或者按Ctrl-X及Ctrl-C)操作的使用者,应该能够假定数据将储存在剪贴簿上,直到下次剪下或复制操作为止。

回忆一下第十第十一章所示的POPPAD程序的修订版中,我们加上了「Edit」菜单,但是在那边这菜单的作用只是发送消息给编辑控件而已。多数情况下,处理剪贴簿并不方便,您必须自己呼叫剪贴簿传输函数。

本章集中讨论将文字传入和移出剪贴簿。在后面的章节里,我将向您展示如何用剪贴簿处理位图(第十四十五十六章)和metafile(第十八章)。

剪贴簿的简单使用

我们由分析把数据传送到剪贴簿(剪下或复制)和存取剪贴簿数据(粘贴)的程序代码开始。

标准剪贴簿数据格式

Windows支持不同的预先定义剪贴簿格式,这些格式在WINUSER.H定义成以CF为前缀的标识符。

首先介绍三种能够储存在剪贴簿上的文字数据型态,以及一个与剪贴簿格式相关的数据型态:

  • CF_TEXT以NULL结尾的ANSI字符集字符串。它在每行末尾包含一个carriage return和linefeed字符,这是最简单的剪贴簿数据格式。传送到剪贴簿的数据存放在整体内存块中,并且是利用内存块句柄进行传送的(我将简短地讨论此项概念)。这个内存块专供剪贴簿使用,建立它的程序不应该继续使用它。
     
  • CF_OEMTEXT含有文字数据(与CF_TEXT类似)的内存块。但是它使用的是OEM字符集。通常Windows程序不必关心这一点;它只有与在窗口中执行MS-DOS程序一起使用剪贴簿时才会使用。
     
  • CF_UNICODETEXT含有Unicode文字的内存块。与CF_TEXT类似,它在每一行的末尾包含一个carriage return和linefeed字符,以及一个NULL字符(两个0字节)以表示数据结束。CF_UNICODETEXT只支援Windows NT。
     
  • CF_LOCALE一个国家地区标识符的句柄。表示剪贴簿文字使用的国别地区设定。
     

下面是两种附加的剪贴簿格式,它们在概念上与CF_TEXT格式相似(也就是说,它们都是文字数据),但是它们不需要以NULL结尾,因为格式已经定义了数据的结尾。现在已经很少使用这些格式了:

  • CF_SYLK包含Microsoft 「符号连结」数据格式的整体内存块。这种格式用在Microsoft的Multiplan、Chart和Excel程序之间交换数据,它是一种ASCII码格式,每一行都用carriage return和linefeed结尾。
     
  • CF_DIF包含数据交换格式(DIF)之数据的整体内存块。这种格式是由Software Arts公司提出的,用于把数据送到VisiCalc电子表格程序中。这也是一种ASCII码格式,每一行都使用carriage return和linefeed结尾。
     

下面三种剪贴簿格式与位图有关。所谓位图就是数据位的矩形数组,其中的数据位与输出设备的图素相对应。  第十四 第十五章将详细讨论位图以及这些位图剪贴簿的格式:

  • CF_BITMAP与设备相关的位图格式。位图是通过位图句柄传送给剪贴簿的。同样,在把这个位图传送给剪贴簿之后,程序不应该再继续使用这个位图。
     
  • CF_DIB定义一个设备无关位图(在第十五章中描述)的内存块。这种内存块是以位图信息结构开始的,后面跟着可用的颜色表和位图数据位。
     
  • CF_PALETTE调色盘句柄。它通常与CF_DIB配合使用,以定义与设备相关的位图所使用的颜色调色盘。
     

在剪贴簿中,还有可能以工业标准的TIFF格式储存的位图数据:

  • CF_TIFF含有标号图像文件格式(TIFF)数据的整体内存块。这种格式由Microsoft、Aldus公司和Hewlett-Packard公司以及一些硬件厂商推荐使用。这一格式可从Hewlett-Packard的网站上获得。
     

下面是两个metafile格式,我将在第十八章详细讨论。一个metafile就是一个以二进制格式储存的画图命令集:

  • CF_METAFILEPICT以旧的metafile格式存放的「图片」。
     
  • CF_ENHMETAFILE增强型metafile(32位Windows支持的)句柄。
     

最后介绍几个混合型的剪贴簿格式:

  • CF_PENDATA与Windows的笔式输入扩充功能联合使用。
     
  • CF_WAVE声音(波形)文件。
     
  • CF_RIFF使用资源交换文件格式(Resource Interchange File Format)的多媒体数据。
     
  • CF_HDROP与拖放服务相关的文件列表。
     

内存配置

程序向剪贴簿传输一些数据的时候,必须配置一个内存块,并且将这块内存交给剪贴簿处理。在本书早期的程序中需要配置内存时,我们只需使用标准C执行时期链接库所支持的malloc函数。但是,由于在Windows中执行的应用程序之间必须要共享剪贴簿所储存的内存块,这时malloc函数就有些不适任这项任务了。

实际上,我们必须把早期Windows所开发的内存配置函数再拿出来使用,那时的操作系统在16位的实际模式内存结构中执行。现在的Windows仍然支持这些函数,您还可以使用它们,但不是必须使用这些函数就是了。

要用Windows API来配置一个内存块,可以呼叫:

hGlobal = GlobalAlloc (uiFlags, dwSize) ;
        

此函数有两个参数:一系列可能的旗标和内存块的字节大小。函数传回一个HGLOBAL型态的句柄,称为「整体内存块句柄」或「整体句柄」。传回值为NULL表示不能配置足够的内存。

虽然GlobalAlloc的两个参数略有不同,但它们都是32位的无正负号整数。如果将第一个参数设定为0,那么您就可以更有效地使用旗标GMEM_FIXED。在这种情况下,GlobalAlloc传回的整体句柄实际是指向所配置内存块的指针。

如果不喜欢将内存块中的每一位都初始化为0,那么您也能够使用旗标GMEM,_ZEROINIT。在Windows表头文件中,简洁的GPTR旗标定义为GMEM_FIXED和GMEM_ZEROINIT旗标的组合:

#define GPTR (GMEM_FIXED | GMEM_ZEROINIT)
        

下面是一个重新配置函数:

hGlobal = GlobalReAlloc (hGlobal, dwSize, uiFlags) ;
        

如果内存块扩大了,您可以用GMEM_ZEROINIT旗标将新的字节设为0。

下面是获得内存块大小的函数:

dwSize = GlobalSize (hGlobal) ;
        

释放内存块的函数:

GlobalFree (hGlobal) ;
        

在早期16位的Windows中,因为Windows不能在物理内存中移动内存块,所以禁止使用GMEM_FIXED旗标。在32位的Windows中,GMEM_FIXED旗标很常见。这是因为它将传回一个虚拟地址,并且操作系统也能够通过改变内存页映像表在物理内存中移动内存块。因此为16位的Windows写程序时,GlobalAlloc推荐使用GMEM_MOVEABLE旗标。在Windows的表头文件中还定义了一个简写标识符,用此标识符可以在可移动的内存之外填0:

#define GHND (GMEM_MOVEABLE | GMEM_ZEROINIT)
        

GMEM_MOVEABLE旗标允许Windows在虚拟内存中移动一个内存块。这不是说将在物理内存中移动内存块,只是应用程序用于读写这块内存的地址可以被变动。

尽管GMEM_MOVEABLE是16位Windows的通则,但是它的作用现在已经少得多了。如果您的应用程序频繁地配置、重新配置以及释放不同大小的内存块,应用程序的虚拟地址空间将会变得支离破碎。可以想象得到,最后虚拟内存地址空间就会被用完。如果这是个可能会发生的问题,那么您将希望内存是可移动的。下面就介绍如何让内存块成为可搬移位置的。

首先定义一个指标(例如,一个int型态的)和一个GLOBALHANDLE型态的变量:

int * p ;
        
            GLOBALHANDLE hGlobal ;
        

然后配置内存。例如:

hGlobal = GlobalAlloc (GHND, 1024) ;
        

与处理其它Windows句柄一样,您不必担心数字的实际意义,只要照著作就好了。需要存取内存块时,可以呼叫:

p = (int *) GlobalLock (hGlobal) ;
        

此函数将句柄转换为指标。在内存块被锁定期间,Windows将固定虚拟内存中的地址,不再移动那块内存。存取结束后呼叫:

GlobalUnlock (hGlobal) ;
        

这将使Windows可以在虚拟内存中移动内存块。要真正确保此程序正常运作(体验早期Windows程序写作者的痛苦经历),您应该在单一个消息处理期间锁定和解锁内存块。

在释放内存时,呼叫GlobalFree应使用句柄而不是指标。如果您现在不能存取句柄,可以使用下面的函数:

hGlobal = GlobalHandle (p) ;
        

在解锁之前,您能够多次锁定一个内存块。Windows保留一个锁定次数,而且在内存块可被自由移动之前,每次锁定都需要相对应的解锁。当Windows在虚拟内存中移动一个内存块时,不需要将字节从一个位置复制到另一个,只需巧妙地处理内存页映像表。通常,让32位Windows为您的程序配置可移动的内存块,其唯一确实的理由只是避免虚拟内存的空间碎裂出现。使用剪贴簿时,也应该使用可移动内存。

为剪贴簿配置内存时,您应该以GMEM_MOVEABLE和GMEM_SHARE旗标呼叫GlobalAlloc函数。GMEM_SHARE旗标使得其它应用程序也可以使用那块内存。

将文字传送到剪贴簿

让我们想象把一个ANSI字符串传送到剪贴簿上,并且我们已经有了指向这个字符串的指针(pString)。现在希望传送这个字符串的iLength字符,这些字符可能以NULL结尾,也可能不以NULL结尾。

首先,通过使用GlobalAlloc来配置一个足以储存字符串的内存块,其中还包括一个终止字符NULL:

hGlobal = GlobalAlloc (GHND | GMEM_SHARE, iLength + 1) ;
        

如果未能配置到内存块,hGlobal的值将为NULL 。如果配置成功,则锁定这块内存,并得到指向它的一个指标:

pGlobal = GlobalLock (hGlobal) ;
        

将字符串复制到内存块中:

for (i = 0 ; i < wLength ; i++)
        
    *pGlobal++ = *pString++ ;
        

由于GlobalAlloc的GHND旗标已使整个内存块在配置期间被清除为零,所以不需要增加结尾的NULL 。以下叙述为内存块解锁:

GlobalUnlock (hGlobal) ;
        

现在就有了表示以NULL结尾的文字所在内存块的内存句柄。为了把它送到剪贴簿中,打开剪贴簿并把它清空:

OpenClipboard (hwnd) ;
        
EmptyClipboard () ;
        

利用CF_TEXT标识符把内存句柄交给剪贴簿,关闭剪贴簿:

SetClipboardData (CF_TEXT, hGlobal) ;
        
CloseClipboard () ;
        

工作告一段落。

下面是关于此过程的一些规则:

  • 在处理同一个消息的过程中呼叫OpenClipboard和CloseClipboard。不需要时,不要打开剪贴簿。
     
  • 不要把锁定的内存句柄交给剪贴簿。
     
  • 当呼叫SetClipboardData后,请不要再继续使用该内存块。它不再属于使用者程序,必须把句柄看成是无效的。如果需要继续存取数据,可以制作数据的副本,或从剪贴簿中读取它(如下节所述)。您也可以在SetClipboardData呼叫和CloseClipboard呼叫之间继续使用内存块,但是不要使用传递给SetClipboardData函数的整体句柄。事实上,此函数也传回一个整体句柄,必需锁定这些代码以存取内存。在呼叫CloseClipboard之前,应先为此句柄解锁。
     

从剪贴簿上取得文字

从剪贴簿上取得文字只比把文字传送到剪贴簿上稍微复杂一些。您必须首先确定剪贴簿是否含有CF_TEXT格式的数据,最简单的方法是呼叫

bAvailable = IsClipboardFormatAvailable (CF_TEXT) ;
        

如果剪贴簿上含有CF_TEXT数据,这个函数将传回TRUE(非零)。我们在第十章的POPPAD2程序中已使用了这个函数,用它来确定「Edit」菜单中「Paste」项是被启用还是被停用的。IsClipboardFormatAvailable是少数几个不需先打开剪贴簿就可以使用的剪贴簿函数之一。但是,如果您之后想再打开剪贴簿以取得这个文字,就应该再做一次检查(使用同样的函数或其它方法),以便确定CF_TEXT数据是否仍然留在剪贴簿中。

为了传送出文字,首先打开剪贴簿:

OpenClipboard (hwnd) ;
        

会得到代表文字的内存块代号:

hGlobal = GetClipboardData (CF_TEXT) ;
        

如果剪贴簿不包含CF_TEXT格式的数据,此句柄就为NULL。这是确定剪贴簿是否含有文字的另一种方法。如果GetClipboardData传回NULL,则关闭剪贴簿,不做其它任何工作。

从GetClipboardData得到的句柄并不属于使用者程序-它属于剪贴簿。仅在GetClipboardData和CloseClipboard呼叫之间这个句柄才有效。您不能释放这个句柄或更改它所引用的数据。如果需要继续存取这些数据,必须制作这个内存块的副本。

这里有一种将数据复制到使用者程序中的方法。首先,配置一块与剪贴簿数据块大小相同的内存块,并配置一个指向该块的指标:

pText = (char *) malloc (GlobalSize (hGlobal)) ;
        

再次呼叫hGlobal ,而hGlobal是从GetClipboardData呼叫传回的整体句柄。现在锁定句柄,获得一个指向剪贴簿块的指标:

pGlobal = GlobalLock (hGlobal) ;
        

现在就可以复制数据了:

strcpy (pText, pGlobal) ;
        

或者,您可以使用一些简单的C程序代码:

while (*pText++ = *pGlobal++) ;
        

在关闭剪贴簿之前先解锁内存块:

GlobalUnlock (hGlobal) ;
        
CloseClipboard () ;
        

现在您有了一个叫做pText的指针,以后程序的使用者就可以用它来复制文字了。

打开和关闭剪贴簿

在任何时候,只有一个程序可以打开剪贴簿。呼叫OpenClipboard的作用是当一个程序使用剪贴簿时,防止剪贴簿的内容发生变化。OpenClipboard传回BOOL值,它说明是否已经成功地打开了剪贴簿。如果另一个应用程序没有关闭剪贴簿,那么它就不能被打开。如果每个程序在响应使用者的命令时都尽快地、遵守规范地打开然后关闭剪贴簿,那么您将永远不会遇到不能打开剪贴簿的问题。

但是,在不遵守规范程序和优先权式多任务环境中,总会发生一些问题。即使在您的程序将某些东西放入剪贴簿和使用者启动一个「Paste」选项期间,您的程序并没有失去输入焦点,但是您也不能假定您放入的东西仍然在那里,一个背景程序有可能已经在这段期间存取过剪贴簿了。

而且,请留意一个与消息框有关的更微妙问题:如果不能配置足够的内存来将内容复制到剪贴簿,那么您可能希望显示一个消息框。但是,如果这个消息框不是系统模态的,那么使用者可以在显示消息框期间切换到另一个应用程序中。您应该使用系统模态的消息框,或者在您显示消息框之前关闭剪贴簿。

如果您在显示一个对话框时将剪贴簿保持为打开状态,那么您还可能遇到其它问题,对话框中的编辑字段会使用剪贴簿进行文字的剪贴。

剪贴簿和Unicode

迄今为止,我只讨论了用剪贴簿处理ANSI文字(每个字符对应一个字节)。我们用CF_TEXT标识符时就是这种格式。您可能对CF_OEMTEXT和CF_UNICODETEXT还不熟悉吧。

我有一些好消息:在处理您所想要的文字格式时,您只需呼叫SetClipboardData和GetClipboardData,Windows将处理剪贴簿中所有的文字转换。例如,在Windows NT中,如果一个程序用SetClipboardData来处理CF_TEXT剪贴簿数据型态,程序也能用CF_OEMTEXT呼叫GetClipboardData。同样地,剪贴簿也能将CF_OEMTEXT数据转换为CF_TEXT。

在Windows NT中,转换发生在CF_UNICODETEXT、CF_TEXT和CF_OEMTEXT之间。程序应该使用对程序本身而言最方便的一种文字格式来呼叫SetClipboardData 。同样地,程序应该用程序需要的文字格式来呼叫GetClipboardData。我们已经知道,本书附上的程序在编写时可以带有或不带UNICODE标识符。如果您的程序也依此编写,那么在定义了UNICODE标识符之后,程序将执行带有CF_UNICODETEXT参数的SetClipboardData以及GetClipboardData呼叫,而不是CF_TEXT。

CLIPTEXT程序,如程序12-1所示,展示了一种可行的方法。

程序12-1  CLIPTEXT
        
CLIPTEXT.C
        
/*-------------------------------------------------------------------------
        
  CLIPTEXT.C --         The Clipboard and Text
        
                                                                (c) Charles Petzold, 1998
        
--------------------------------------------------------------------------*/
        
#include <windows.h>
        
#include "resource.h"
        

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
        
#ifdef UNICODE
        
#define CF_TCHAR CF_UNICODETEXT
        
TCHAR szDefaultText[]              = TEXT ("Default Text - Unicode Version") ;
        
TCHAR szCaption[]                  = TEXT ("Clipboard Text Transfers - Unicode Version") ;
        
#else
        
#define CF_TCHAR CF_TEXT
        
TCHAR szDefaultText[] = TEXT ("Default Text - ANSI Version") ;
        
TCHAR szCaption[]                  = TEXT ("Clipboard Text Transfers - ANSI Version") ;
        

#endif
        
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
        
                          PSTR szCmdLine, int iCmdShow)
        
{
        
           static TCHAR szAppName[] = TEXT ("ClipText") ;
        
           HACCEL                       hAccel ;
        
           HWND                          hwnd ;
        
           MSG                           msg ;
        
           WNDCLASS                      wndclass ;
        
   
        
           wndclass.style                               = CS_HREDRAW | CS_VREDRAW ;
        
           wndclass.lpfnWndProc                         = WndProc ;
        
           wndclass.cbClsExtra                          = 0 ;
        
           wndclass.cbWndExtra                          = 0 ;
        
           wndclass.hInstance                           = hInstance ;
        
           wndclass.hIcon                               = LoadIcon (NULL, IDI_APPLICATION) ;
        
           wndclass.hCursor                             = LoadCursor (NULL, IDC_ARROW) ;
        
           wndclass.hbrBackground              = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
        
           wndclass.lpszMenuName                = szAppName ;
        
           wndclass.lpszClassName               = szAppName ;
        

           if (!RegisterClass (&wndclass))
        
    {
        
                  MessageBox (  NULL, TEXT ("This program requires Windows NT!"),
        
                                                                        szAppName, MB_ICONERROR) ;
        
                  return 0 ;
        
    }
        
   
        
           hwnd = CreateWindow (szAppName, szCaption,
        
                         WS_OVERLAPPEDWINDOW,
        
                         CW_USEDEFAULT, CW_USEDEFAULT,
        
                         CW_USEDEFAULT, CW_USEDEFAULT,
        
                        NULL, NULL, hInstance, NULL) ;
        
   
        
           ShowWindow (hwnd, iCmdShow) ;
        
           UpdateWindow (hwnd) ;
        
    hAccel = LoadAccelerators (hInstance, szAppName) ;
        

           while (GetMessage (&msg, NULL, 0, 0))
        
           {
        
                  if (!TranslateAccelerator (hwnd, hAccel, &msg))
        
         {
        
                                  TranslateMessage (&msg) ;
        
                                  DispatchMessage (&msg) ;
        
                  }
        
  }
        
           return msg.wParam ;
        
}
        

LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
        
{
        
           static PTSTR          pText ;
        
           BOOL                  bEnable ;
        
           HGLOBAL               hGlobal ;
        
           HDC                   hdc ;
        
           PTSTR                 pGlobal ;
        
           PAINTSTRUCT           ps ;
        
           RECT                  rect ;
        
   
        
  switch (message)
        
           {
        
           case   WM_CREATE:
        
                  SendMessage (hwnd, WM_COMMAND, IDM_EDIT_RESET, 0) ;
        
                  return 0 ;
        

   case   WM_INITMENUPOPUP:
        
                  EnableMenuItem        ((HMENU)      wParam,IDM_EDIT_PASTE,
        
                          IsClipboardFormatAvailable    (CF_TCHAR) ? MF_ENABLED    : MF_GRAYED) ;
        

                  bEnable = pText ? MF_ENABLED : MF_GRAYED ;
        

                  EnableMenuItem        ((HMENU)      wParam,IDM_EDIT_CUT,     bEnable) ;
        
                  EnableMenuItem        ((HMENU)      wParam,IDM_EDIT_COPY,    bEnable) ;
        
                  EnableMenuItem        ((HMENU)      wParam,IDM_EDIT_CLEAR,bEnable) ;
        
                  break ;
        
        
        
           case   WM_COMMAND:
        
                  switch (LOWORD (wParam))
        
                  {
        
                  case   IDM_EDIT_PASTE:
        
                                         OpenClipboard (hwnd) ;
        

                                         if     (hGlobal = GetClipboardData (CF_TCHAR))
        
                                         {
        
                                                        pGlobal = GlobalLock (hGlobal) ;
        
                                                 if (pText)
        
                                                {
        
                                                                        free (pText) ;
        
                                                                        pText = NULL ;
        
                                                 }
        
                                                 pText = malloc (GlobalSize (hGlobal)) ;
        
                                                 lstrcpy (pText, pGlobal) ;
        
                                                InvalidateRect (hwnd, NULL, TRUE) ;
        
                                         }
        
                                         CloseClipboard () ;
        
                                         return 0 ;
        

                  case   IDM_EDIT_CUT:
        
                  case   IDM_EDIT_COPY:
        
                                         if (!pText)
        
                                                 return 0 ;
        

                                         hGlobal = GlobalAlloc (GHND | GMEM_SHARE,
        
                           (lstrlen (pText) + 1) * sizeof (TCHAR)) ;
        
                                         pGlobal = GlobalLock (hGlobal) ;
        
                                         lstrcpy (pGlobal, pText) ;
        
                                        GlobalUnlock (hGlobal) ;
        

                                         OpenClipboard (hwnd) ;
        
                                         EmptyClipboard () ;
        
                                         SetClipboardData (CF_TCHAR, hGlobal) ;
        
                                         CloseClipboard () ;
        

                                  if (   LOWORD (wParam) == IDM_EDIT_COPY)
        
                                                 return 0 ;      
        
                        // fall through for IDM_EDIT_CUT
        
                  case   IDM_EDIT_CLEAR:
        
                                         if (pText)
        
                                         {
        
                                                free (pText) ;
        
                                                 pText = NULL ;
        
                                         }
        
                                  InvalidateRect (hwnd, NULL, TRUE) ;
        
                                  return 0 ;
        

                  case   IDM_EDIT_RESET:
        
                                         if (pText)
        
                                         {
        
                                                 free (pText) ;
        
                                                 pText = NULL ;
        
                                         }
        
                          pText = malloc ((lstrlen (szDefaultText) + 1) * sizeof (TCHAR)) ;
        
                                         lstrcpy (pText, szDefaultText) ;
        
                                        InvalidateRect (hwnd, NULL, TRUE) ;
        
                                         return 0 ;
        
                  }
        
                  break ;
        

           case   WM_PAINT:
        
                  hdc = BeginPaint (hwnd, &ps) ;
        

                  GetClientRect (hwnd, &rect) ;
        
        
        
                  if (pText != NULL)
        
                  DrawText      (hdc, pText, -1, &rect, DT_EXPANDTABS | DT_WORDBREAK) ;
        

                  EndPaint (hwnd, &ps) ;
        
                  return 0 ;
        
        
        
           case   WM_DESTROY:
        
                  if (   pText)
        
                                         free (pText) ;
        

                                  PostQuitMessage (0) ;
        
                                 return 0 ;
        
           }
        
    return DefWindowProc (hwnd, message, wParam, lParam) ;
        
}
        
CLIPTEXT.RC (摘录)
        
//Microsoft Developer Studio generated resource script.
        
#include "resource.h"
        
#include "afxres.h"
        
/////////////////////////////////////////////////////////////////////////////
        
// Menu
        
CLIPTEXT MENU DISCARDABLE
        
BEGIN
        
    POPUP "&Edit"
        
   BEGIN
        
            MENUITEM "Cu&t\tCtrl+X",                     IDM_EDIT_CUT
        
       MENUITEM "&Copy\tCtrl+C",                 IDM_EDIT_COPY
        
       MENUITEM "&Paste\tCtrl+V",                IDM_EDIT_PASTE
        
MENUITEM "De&lete\tDel",                  IDM_EDIT_CLEAR
        
       MENUITEM SEPARATOR               
        
       MENUITEM "&Reset",                    IDM_EDIT_RESET
        
   END
        
END
        

/////////////////////////////////////////////////////////////////////////////
        
// Accelerator
        
CLIPTEXT ACCELERATORS DISCARDABLE
        
BEGIN
        
   "C",           IDM_EDIT_COPY,    VIRTKEY, CONTROL, NOINVERT
        
   "V",           IDM_EDIT_PASTE,    VIRTKEY, CONTROL, NOINVERT
        
   VK_DELETE,                    IDM_EDIT_CLEAR,    VIRTKEY, NOINVERT
        
   "X",           IDM_EDIT_CUT,     VIRTKEY, CONTROL, NOINVERT
        
END
        
RESOURCE.H(摘录)
        
// Microsoft Developer Studio generated include file.
        
// Used by ClipText.rc
        
#define IDM_EDIT_CUT           40001
        
#define IDM_EDIT_COPY          40002
        
#define IDM_EDIT_PASTE         40003
        
#define IDM_EDIT_CLEAR         40004
        
#define IDM_EDIT_RESET         40005
        

这是在Windows NT下执行Unicode版和ANSI版程序的概念,而且可以看到,剪贴簿是如何在两种字符集之间转换的。注意CLIPTEXT.C顶部的#ifdef叙述。如果定义了UNICODE标识符,那么CF_TCHAR(我命名的一种常用的剪贴簿格式)就等于CF_UNICODETEXT;否则,它就等于CF_TEXT。程序后面呼叫的IsClipboardFormatAvailable、GetClipboardData和SetClipboardData函数都使用CF_TCHAR来指定数据型态。

在程序的开始部分(以及您从「Edit」菜单中选择「Reset」选项时),静态变量pText包含一个指针,在Unicode版的程序中,指针指向Unicode字符串「Default Text -Unicode version」;在非Unicode版的程序中,指针指向「Default Text - ANSI version」。您可以用「Cut」或「Copy」命令将字符串传递给剪贴簿,用「Cut」或「Delete」命令从程序中删除字符串。「Paste」命令将剪贴簿中的文字内容复制到pText。在WM_PAINT消息处理期间,pText将字符串显示在程序的显示区域。

如果您先在Unicode版的CLIPTEXT中选择了「Copy」命令,然后在非Unicode版中选择「Paste」命令,那么您就能看到文字已经从Unicode转换成了ANSI。类似地,如果您执行相反的操作,那么文字就会从ANSI转换成Unicode。

复杂的剪贴簿用法

我们已经看到,在将数据准备好之后,从剪贴簿传输数据时需要四个呼叫:

OpenClipboard                      (hwnd) ;
        
EmptyClipboard                     () ;
        
SetClipboardData           (iFormat, hGlobal) ;
        
CloseClipboard                     () ;
        

存取这些数据需要三个呼叫

OpenClipboard (hwnd) ;
        
hGlobal = GetClipboardData (iFormat) ;
        
其它行程序
        
CloseClipboard () ;
        

在GetClipboardData和CloseClipboard呼叫之间,可以复制剪贴簿数据或以其它方式来使用它。很多应用程序都需要采用这种方法,但也可以用更复杂的方式来使用剪贴簿。

利用多个数据项

当打开剪贴簿并把数据传送给它时,必须先呼叫EmptyClipboard,通知Windows释放或删除剪贴簿上的内容。不能在现有的剪贴簿内容中附加其它东西。所以,从这种意义上说,剪贴簿每次只能保留一个数据项。

但是,可以在EmptyClipboard和CloseClipboard呼叫之间多次呼叫SetClipboardData,每次都使用不同的剪贴簿格式。例如,如果想在剪贴簿中储存一个很短的文字字符串,可以把这个文字写入metafile,也可以把这个文字写入位图。把位图选进内存设备内容中,并把这个字符串写进位图中。利用这种方法可以使字符串不仅能为从剪贴簿上读取文字的程序所使用,也可以为从剪贴簿上读取位图和metafile的程序所使用。当然,这些程序并不能知道metafile或位图实际上包含了一个字符串。

如果想把一些句柄写到剪贴簿上,对每个句柄均可以呼叫SetClipboardData:

OpenClipboard             (hwnd) ;
        
EmptyClipboard             () ;
        
SetClipboardData   (CF_TEXT, hGlobalText) ;
        
SetClipboardData   (CF_BITMAP, hBitmap) ;
        
SetClipboardData   (CF_METAFILEPICT, hGlobalMFP) ;
        
CloseClipboard             () ;
        

当这三种格式的数据同时位于剪贴簿上时,用CF_TEXT、CF_BITMAP或CF_METAFILEPICT参数呼叫IsClipboardFormatAvailable将传回TRUE。通过下列呼叫程序可以存取这些代码:

hGlobalText = GetClipboardData (CF_TEXT) ;
        

hBitmap = GetClipboardData (CF_BITMAP) ;
        

hGlobalMFP = GetClipboardData (CF_METAFILEPICT) ;
        

下一次程序呼叫EmptyClipboard时,Windows将释放或删除剪贴簿上保留的所有三个句柄。

在将不同的文字格式、不同的位图格式或者不同的metafile格式添加到剪贴簿时,不要使用这种技术。只使用一种文字格式、一种位图格式以及一种metafile格式。就像我所说的那样,Windows将在CF_TEXT、CF_OEMTEXT和CF_UNICODETEXT之间转换,也可以在CF_BITMAP和CF_DIB之间,以及在CF_METAFILEPICT和CF_ENHMETAFILE之间进行转换。

透过首先打开剪贴簿,然后呼叫EnumClipboardFormats,程序可以确定剪贴簿储存的所有格式。开始时设定变量iFormat为0:

iFormat = 0 ;
        
OpenClipboard (hwnd) ;
        

现在从0值开始逐次进行连续的EnumClipboardFormats呼叫。函数将为目前在剪贴簿中的每种格式传回一个正的iFormat值。当函数传回0时,表示完成:

while (iFormat = EnumClipboardFormats (iFormat))
        
{
        
   各个iFormat值的处理方式
        
}
        
CloseClipboard () ;
        

您可以通过下面的呼叫来取得目前在剪贴簿中之不同格式的个数:

iCount = CountClipboardFormats () ;
        

延迟提出

当把数据放入剪贴簿中时,一般来说要制作一份数据的副本,并将包含这份副本的内存块句柄传给剪贴簿。对非常大的数据项来说,这种方法会浪费内存空间。如果使用者不想把数据粘贴到另一个程序里,那么,在被其它内容取代之前,它将一直占据着内存空间。

通过使用一种叫做「延迟提出」的技术可以避免这个问题。实际上,直到另一个程序需要数据,程序才提供这份数据。为此,不将数据句柄传给Windows,而是在SetClipboardData呼叫中使用NULL:

OpenClipboard              (hwnd) ;
        
EmptyClipboard             () ;
        
SetClipboardData   (iFormat, NULL) ;
        
CloseClipboard             () ;
        

可以有多个使用不同iFormat值的SetClipboardData呼叫,对其中某些呼叫可使用NULL值。而对其他一些则使用实际的句柄值。

前面的过程比较简单,以下的过程就要稍微复杂一些了。当另一个程序呼叫GetClipboardData时,Windows将检查那种格式的句柄是否为NULL。如果是,Windows将给「剪贴簿所有者」(您的程序)发送一个消息,要求取得数据的实际句柄,这时您的程序必须提供这个句柄。

更具体地说,「剪贴簿所有者」是将数据放入剪贴簿的最后一个窗口。当一个程序呼叫OpenClipboard时,Windows储存呼叫这个函数时所用的窗口句柄,这个句柄标示打开剪贴簿的窗口。一旦收到一个EmptyClipboard呼叫,Windows就使这个窗口作为新的剪贴簿所有者。

使用延迟提出技术的程序在它的窗口消息处理程序中必须处理三个消息:WM_RENDERFORMAT、WM_RENDERALLFORMATS和WM_DESTROYCLIPBOARD。当另一个程序呼叫GetClipboardData时,Windows给窗口消息处理程序发送一个WM_RENDERFORMAT消息,wParam的值是所要求的格式。在处理WM_RENDERFORMAT消息时,不要打开或清空剪贴簿。为wParam所指定的格式建立一个整体内存块,把数据传给它,并用正确的格式和相应句柄呼叫SetClipboardData。很明显地,为了在处理WM_RENDERFORMAT时正确地构造出此数据,需要在程序中保留这些信息。当另一个程序呼叫EmptyClipboard时,Windows给您的程序发送一个WM_DESTROYCLIPBOARD消息,告诉您不再需要构造剪贴簿数据的信息。您的程序不再是剪贴簿的所有者。

如果程序在它自己仍然是剪贴簿所有者的时候就要终止执行,并且剪贴簿上仍然包含着该程序用SetClipboardData设定的NULL数据句柄,它将收到WM_RENDERALLFORMATS消息。这时,应该打开剪贴簿,清空它,把数据加载内存块中,并为每种格式呼叫SetClipboardData,然后关闭剪贴簿。WM_RENDERALLFORMATS消息是窗口消息处理程序最后收到的消息之一。它后面跟有WM_DESTROYCLIPBOARD消息(由于已经提出了所有数据),然后是正常的WM_DESTROY消息。

如果您的程序只能向剪贴簿传输一种格式的数据(例如文字),那么您可以把WM_RENDERALLFORMATS和WM_RENDERFORMAT处理结合在一起。这些程序代码应该类似下面这样:

case        WM_RENDERALLFORMATS :
        
           OpenClipboard (hwnd) ;
        
           EmptyClipboard () ;
        
                                                         // fall through
        
case        WM_RENDERFORMAT :
        
   // 将文字放入整体内存块
        
           SetClipboardData (CF_TEXT, hGlobal) ;
        
           if (message == WM_RENDERALLFORMATS)
        
                  CloseClipboard () ;
        
           return 0 ;
        

如果您的程序使用好几种剪贴簿格式,那么您可能想为wParam所要求的格式处理WM_ RENDERFORMAT。除非程序在存放构造数据所需的信息时遇到困难,否则不需要处理WM_DESTROYCLIPBOARD消息。

自订数据格式

到目前为止,我们仅处理了Windows定义的标准剪贴簿资料格式。但是,您可能想用剪贴簿来储存「自订数据格式」。许多文书处理程序使用这种技术来储存包含着字体和格式化信息的文字。

初看之下,这个概念似乎是没有意义的。如果剪贴簿的作用是在应用程序之间传送数据,那么,为什么剪贴簿中要含有只有一个应用程序才能理解的数据呢?答案很简单:剪贴簿允许在同一个程序的内部(或者可能在一个程序中的不同执行实体之间)传送数据。很明显地,这些执行实体能理解它们自己的自订数据格式。

有几种使用自订数据格式的方法。最简单的方法用到一种表面上是标准剪贴簿格式(文字、位图或metafile)的数据,可是该数据实际上只对您的程序有意义。这种情况下,在SetClipboardData和GetClipboardData呼叫中可使用下列wFormat值:CF_DSPTEXT、CF_DSPBITMAP、CF_DSPMETAFILEPICT或CF_DSPENHMETAFILE(字母DSP代表「显示器」)。这些格式允许Windows按文字、位图或metafile来浏览或显示资料。但是,另一个使用常规的CF_TEXT、CF_BITMAP、CF_DIB、CF_METAFILEPICT或CF_ENHMETAFILE格式呼叫GetClipboardData的程序将不能取得这个数据。

如果用其中一种格式把数据放入剪贴簿中,则必须使用同样的格式读出数据。但是,如何知道数据是来自程序的另一个执行实体,还是来自使用其中某种数据格式的另一个程序呢?这里有一种方法,可以透过下列呼叫首先获得剪贴簿所有者:

hwndClipOwner = GetClipboardOwner () ;
        

然后可以得到此窗口句柄的窗口类别名称:

TCHAR szClassName [32] ;
        
//其它行程序
        
GetClassName (hwndClipOwner, szClassName, 32) ;
        

如果类别名称与程序名称相同,那么数据是由程序的另一个执行实体传送到剪贴簿中的。

使用自订数据格式的第二种方法涉及到CF_OWNERDISPLAY旗标。SetClipboardData的整体内存句柄是NULL:

SetClipboardData (CF_OWNERDISPLAY, NULL) ;
        

这是某些文书处理程序在Windows的剪贴簿浏览器的显示区域中显示格式化文字时所采用的方法。很明显地,剪贴簿浏览器不知道如何显示这种格式化文字。当一个文书处理程序指定CF_OWNERDISPLAY格式时,它也就承担起在剪贴簿浏览器的显示区域中绘图的责任。

由于整体内存句柄为NULL,所以用CF_OWNERDISPLAY格式(剪贴簿所有者)呼叫SetClipboardData的程序必须处理由Windows发往剪贴簿所有者的延迟提出消息、以及5条附加消息。这5个消息是由剪贴簿浏览器发送到剪贴簿所有者的:

  • WM_ASKCBFORMATNAME剪贴簿浏览器把这个消息发送到剪贴簿所有者,以得到数据格式名称。lParam参数是指向缓冲区的指标,wParam是这个缓冲区能容纳的最大字符数目。剪贴簿所有者必须把剪贴簿数据格式的名字复制到这个缓冲区中。
     
  • WM_SIZECLIPBOARD这个消息通知剪贴簿所有者,剪贴簿浏览器的显示区域大小己发生了变化。wParam参数是剪贴簿浏览器的句柄,lParam是指向包含新尺寸的RECT结构的指针。如果RECT结构中都是0,则剪贴簿浏览器退出或最小化。尽管Windows的剪贴簿浏览器只允许它自己的一个执行实体执行,但其它剪贴簿浏览器也能把这个消息发送给剪贴簿所有者。应付多个剪贴簿浏览器并非不可能(假定wParam标识特定的浏览器),但剪贴簿所有者处理起来也不容易。
     
  • WM_PAINTCLIPBOARD这个消息通知剪贴簿所有者修改剪贴簿浏览器的显示区域。同时,wParam是剪贴簿浏览器窗口的句柄,lParam是指向PAINTSTRUCT结构的整体指针。剪贴簿所有者可以从此结构的hdc栏中得到剪贴簿浏览器设备内容的句柄。
     
  • WM_HSCROLLCLIPBOARD和WM_VSCROLLCLIPBOARD这两个消息通知剪贴簿所有者,使用者已经卷动了剪贴簿浏览器的卷动列。wParam参数是剪贴簿浏览器窗口的句柄,lParam的低字组是卷动请求,并且,如果低字组是SB_THUMBPOSITION,那么lParam的高字组就是滑块位置。
     

处理这些消息比较麻烦,看来并不值得这样做。但是,这种处理对使用者来说是有益的。当从文书处理程序把文字复制到剪贴簿时,使用者在剪贴簿浏览器的显示区域中看见文字还保持着格式时心里会舒坦些。

使用私有剪贴簿数据格式的第三种方法是注册自己的剪贴簿格式名。您向Windows提供格式名,Windows给程序提供一个序号,它可以用作SetClipboardData和GetClipboardData的格式参数。一般来说,采用这种方法的程序也要以一种标准格式把数据复制到剪贴簿。这种方法允许剪贴簿浏览器在它的显示区域中显示数据(没有与CF_OWNERDISPLAY相关的冲突),并且允许其它程序从剪贴簿上复制数据。

例如,假定我们已经编写了一个以位图格式、metafile格式和自己的已注册的剪贴簿格式把数据复制到剪贴簿中的向量绘图程序。剪贴簿浏览器将显示metafile或者位图,其它从剪贴簿上读取位图和metafile的程序将获得这几种格式。但是,当我们的向量绘图程序需要从剪贴簿上读数据时,它会按照自己已注册的格式复制数据,这是因为这种格式可能包含着比位图文件或者metafile更多的信息。

程序透过下面的呼叫来注册一个新的剪贴簿格式:

iFormat = RegisterClipboardFormat (szFormatName) ;
        

iFormat的值介于0xC000和0xFFFF之间。剪贴簿浏览器(或一个通过呼叫EnumClipboardFormats取得目前所有剪贴簿数据格式的程序)可以取得这种数据格式的ASCII名称,这是通过下面呼叫实作的:

GetClipboardFormatName (iFormat, psBuffer, iMaxCount) ;
        

Windows将多达iMaxCount个字符复制到psBuffer中。

使用这种方法把数据复制到剪贴簿中的程序写作者,可能需要公开数据格式名称和实际的数据格式。如果这个程序流行起来,那么其它程序就会以这种格式从剪贴簿中复制数据。

实作剪贴簿浏览器

监视剪贴簿内容变化的程序称为「剪贴簿浏览器」。您可以在Windows中得到一个剪贴簿浏览器,但是您也可以编写自己的剪贴簿浏览器程序。剪贴簿浏览器通过传递到浏览器窗口消息处理程序的消息来监视剪贴簿内容的变化。

剪贴簿浏览器链

任意数量的剪贴簿浏览器应用程序都可以同时在Windows下执行,它们都可以监视剪贴簿内容的变化。但是,从Windows的角度来看,只存在一个剪贴簿浏览器,我们称之为「目前剪贴簿浏览器」。Windows只保留一个识别目前剪贴簿浏览器的窗口句柄,并且当剪贴簿的内容发生变化时只把消息发送到那个窗口中。

剪贴簿浏览器应用程序有必要加入「剪贴簿浏览器链」,以便执行的所有剪贴簿浏览器都可以收到Windows发送给目前剪贴簿浏览器的消息。当一个程序将自己注册为一个剪贴簿浏览器时,它就成为目前的剪贴簿浏览器。Windows把先前的目前浏览器窗口句柄交给这个程序,并且此程序将储存这个句柄。当此程序收到一个剪贴簿浏览器消息时,它把这个消息发送给剪贴簿链中下一个程序的窗口消息处理程序。

剪贴簿浏览器的函数和消息

程序透过呼叫SetClipboardViewer函数可以成为剪贴簿浏览器链的一部分。如果程序的主要作用是作为剪贴簿浏览器,那么这个程序在WM_CREATE消息处理期间可以呼叫这个函数,该函数传回前一个目前剪贴簿浏览器的窗口句柄。程序应该把这个句柄储存在静态变量中:

static HWND hwndNextViewer ;
        
//其它行程序
        
case        WM_CREATE :
        
    //其它行程序
        
           hwndNextViewer = SetClipboardViewer (hwnd) ;
        

如果在Windows的一次执行期间,您的程序成为剪贴簿浏览器的第一个程序,那么hwndNextViewer将为NULL。

不管剪贴簿中的内容怎样变化,Windows都将把WM_DRAWCLIPBOARD消息发送给目前的剪贴簿浏览器(最近注册为剪贴簿浏览器的窗口)。剪贴簿浏览器链中的每个程序都应该用SendMessage把这个消息发送到下一个剪贴簿浏览器。浏览器链中的最后一个程序(第一个将自己注册为剪贴簿浏览器的窗口)所储存的hwndNextViewer为NULL。如果hwndNextViewer为NULL,那么程序只简单地将控件权还给系统而已,而不向其它程序发送任何消息(不要把WM_DRAWCLIPBOARD消息和WM_PAINTCLIPBOARD消息混淆了。WM_PAINTCLIPBOARD是由剪贴簿浏览器发送给使用CF_OWNERDISPLAY剪贴簿数据格式的程序,而WM_ DRAWCLIPBOARD消息是由Windows发往目前剪贴簿浏览器的)。

处理WM_DRAWCLIPBOARD消息的最简单方法是将消息发送给下一个剪贴簿浏览器(除非hwndNextViewer为NULL),并使窗口的显示区域无效:

case        WM_DRAWCLIPBOARD :
        
           if     (      hwndNextViewer)
        
                          SendMessage (hwndNextViewer, message, wParam, lParam) ;
        
           InvalidateRect (hwnd, NULL, TRUE) ;
        
           return 0 ;
        

在处理WM_PAINT消息处理期间,通过使用常规的OpenClipboard、GetClipboardData和CloseClipboard呼叫可以读取剪贴簿的内容。

当某个程序想从剪贴簿浏览器链中删除它自己时,它必须呼叫ChangeClipboardChain。这个函数接收脱离浏览器链的程序之窗口句柄,和下一个剪贴簿浏览器的窗口句柄:

ChangeClipboardChain (hwnd, hwndNextViewer) ;
        

当程序呼叫ChangeClipboardChain时,Windows发送WM_CHANGECBCHAIN消息给目前的剪贴簿浏览器。wParam参数是从链中移除它自己的那个浏览器窗口句柄(ChangeClipboardChain的第一个参数),lParam是从链中移除自己后的下一个剪贴簿浏览器的窗口句柄(ChangeClipboardChain的第二个参数)。

当程序接收到WM_CHANGECBCHAIN消息时,必须检查wParam是否等于已经储存的hwndNextViewer的值。如果是这样,程序必须设定hwndNextViewer为lParam。这项工作保证将来的WM_DRAWCLIPBOARD消息不会发送给从剪贴簿浏览器链中删除了自己的窗口。如果wParam不等于hwndNextViewer ,并且hwndNextViewer不为NULL,则把消息送到下一个剪贴簿浏览器。

case        WM_CHANGECBCHAIN :
        
           if ((HWND) wParam == hwndNextViewer)
        
                  hwndNextViewer = (HWND) lParam ;
        
           else if (hwndNextViewer)
        
                                 SendMessage (hwndNextViewer, message, wParam, lParam) ;
        
           return 0 ;
        

不一定要使用else if叙述,它只用于保证hwndNextViewer为非NULL的值。hwndNextViewer的值为NULL时,执行这段程序代码的程序就是链中最后一个浏览器,而这是不可能的。

当程序快结束时,如果它仍然在剪贴簿浏览器链中,则必须从链中删除它。您可以在处理WM_DESTROY消息时呼叫ChangeClipboardChain来完成这项工作。

case        WM_DESTROY :
        
    ChangeClipboardChain (hwnd, hwndNextViewer) ;
        
           PostQuitMessage (0) ;
        
           return 0 ;
        

Windows还有一个允许程序获得第一个剪贴簿浏览器窗口句柄的函数:

hwndViewer = GetClipboardViewer () ;
        

一般来说不需要这个函数。如果没有目前的剪贴簿浏览器,则传回值为NULL。

下面是一个说明剪贴簿浏览器链如何工作的例子。当Windows刚启动时,目前剪贴簿浏览器是NULL:

剪贴簿浏览器:NULL

一个具有hwnd1窗口句柄的程序呼叫SetClipboardViewer。这个函数传回的NULL成为这个程序中的hwndNextViewer值:

目前剪贴簿浏览器:hwnd1

hwnd1的下一个浏览器:NULL

第二个具有hwnd2窗口句柄的程序呼叫SetClipboardViewer ,并传回hwnd1:

目前的剪贴簿浏览器:hwnd2

hwnd2的下一个浏览器:hwnd1

hwnd1的下一个浏览器:NULL

每三个程序(hwnd3)和第四个程序(hwnd4) 也呼叫SetClipboardViewer ,并且传回hwnd2和hwnd3:

目前的剪贴簿浏览器:hwnd4

hwnd4的下一个浏览器:hwnd3

hwnd3的下一个浏览器:hwnd2

hwnd2的下一个浏览器:hwnd1

hwnd1的下一个浏览器:NULL

当剪贴簿的内容发生变化时,Windows发送一个WM_DRAWCLIPBOARD消息给hwnd4,hwnd4发送消息给hwnd3,hwnd3发送消息给hwnd2,hwnd2发送消息给hwnd1,hwnd1传回。

现在hwnd2决定通过下列呼叫从链中删除自己:

ChangeClipboardChain (hwnd2, hwnd1) ;

Windows将wParam等于hwnd2、lParam等于hwnd1的WM_CHANGECBCHAIN消息发送给hwnd4。由于hwnd4的下一个测览器是hwnd3,所以hwnd4把这个消息传给hwnd3。现在hwnd3注意到wParam等于它的下一个测览器(hwnd2),所以将下一个浏览器设定为lParam (hwnd1)并且传回。这样工作就完成了。现在剪贴簿浏览器链如下:

目前剪贴簿浏览器:hwnd4

hwnd4的下一个浏览器:hwnd3

hwnd3的下一个浏览器:hwnd1

hwnd1的下一个浏览器:NULL

一个简单的剪贴簿浏览器

剪贴簿浏览器不一定要像Windows所提供的那样完善,例如,剪贴簿浏览器可以只显示一种剪贴簿数据格式。程序12-2中所示的CLIPVIEW程序是一种只能显示CF_TEXT格式的剪贴簿浏览器。

程序12-2  CLIPVIEW
        
CLIPVIEW.C
        
/*-------------------------------------------------------------------------
        
  CLIPVIEW.C --Simple Clipboard Viewer
        
                                                       (c) Charles Petzold, 1998
        
--------------------------------------------------------------------------*/
        
#include <windows.h>
        
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
        
int WINAPI WinMain(      HINSTANCE hInstance, HINSTANCE hPrevInstance,
        
                  PSTR szCmdLine, int iCmdShow)
        
{
        
           static TCHAR  szAppName[] = TEXT ("ClipView") ;
        
           HWND                          hwnd ;
        
           MSG                           msg ;
        
           WNDCLASS                      wndclass ;
        
   
        
           wndclass.style                              = CS_HREDRAW | CS_VREDRAW ;
        
           wndclass.lpfnWndProc                         = WndProc ;
        
           wndclass.cbClsExtra                          = 0 ;
        
           wndclass.cbWndExtra                          = 0 ;
        
           wndclass.hInstance                           = hInstance ;
        
           wndclass.hIcon                               = LoadIcon (NULL, IDI_APPLICATION) ;
        
           wndclass.hCursor                             = LoadCursor (NULL, IDC_ARROW) ;
        
           wndclass.hbrBackground              = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
        
           wndclass.lpszMenuName                = NULL ;
        
           wndclass.lpszClassName               = szAppName ;
        
   
        
           if (!RegisterClass (&wndclass))
        
    {
        
                  MessageBox (  NULL, TEXT ("This program requires Windows NT!"),
        
                                  szAppName, MB_ICONERROR) ;
        
                  return 0 ;
        
    }
        
   
        
           hwnd = CreateWindow (szAppName,
        
                                        TEXT ("Simple Clipboard Viewer (Text Only)"),
        
                         WS_OVERLAPPEDWINDOW,
        
                         CW_USEDEFAULT, CW_USEDEFAULT,
        
                         CW_USEDEFAULT, CW_USEDEFAULT,
        
                        NULL, NULL, hInstance, NULL) ;
        
   
        
           ShowWindow (hwnd, iCmdShow) ;
        
           UpdateWindow (hwnd) ;
        
   
        
           while (GetMessage (&msg, NULL, 0, 0))
        
           {
        
                  TranslateMessage (&msg) ;
        
                  DispatchMessage (&msg) ;
        
           }
        
           return msg.wParam ;
        
}
        

LRESULT CALLBACK WndProc   (      HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
        
{
        
           static HWND   hwndNextViewer ;
        
           HGLOBAL                       hGlobal ;
        
           HDC                           hdc ;
        
           PTSTR                         pGlobal ;
        
           PAINTSTRUCT              ps ;
        
           RECT                          rect ;
        
   
        
           switch (message)
        
           {
        
           case   WM_CREATE:
        
                  hwndNextViewer = SetClipboardViewer (hwnd) ;
        
                 return 0 ;
        
        
        
           case   WM_CHANGECBCHAIN:
        
                  if ((HWND) wParam == hwndNextViewer)
        
                                         hwndNextViewer = (HWND) lParam ;
        
        
        
                  else if(hwndNextViewer)
        
                                                 SendMessage (hwndNextViewer, message, wParam, lParam) ;
        
        
        
         return 0 ;
        
           case   WM_DRAWCLIPBOARD:
        
                  if (hwndNextViewer)
        
                                         SendMessage (hwndNextViewer, message, wParam, lParam) ;
        
        
        
                  InvalidateRect (hwnd, NULL, TRUE) ;
        
                  return 0 ;
        
        
        
           case   WM_PAINT:
        
                  hdc = BeginPaint (hwnd, &ps) ;
        
                 GetClientRect (hwnd, &rect) ;
        
                  OpenClipboard (hwnd) ;
        
        
        
#ifdef UNICODE
        
                  hGlobal = GetClipboardData (CF_UNICODETEXT) ;
        
#else
        
                  hGlobal = GetClipboardData (CF_TEXT) ;
        
#endif
        
                  if (hGlobal != NULL)
        
                 {
        
                                  pGlobal = (PTSTR) GlobalLock (hGlobal) ;
        
                                  DrawText (hdc, pGlobal, -1, &rect, DT_EXPANDTABS) ;
        
                                  GlobalUnlock (hGlobal) ;
        
                  }
        
        
        
                  CloseClipboard () ;
        
                  EndPaint (hwnd, &ps) ;
        
                  return 0 ;
        
        
        
           case   WM_DESTROY:
        
                  ChangeClipboardChain (hwnd, hwndNextViewer) ;
        
                  PostQuitMessage (0) ;
        
                  return 0 ;
        
           }
        
           return DefWindowProc (hwnd, message, wParam, lParam) ;
        
}
        

CLIPVIEW依上面所讨论的方法来处理WM_CREATE、WM_CHANGECBCHAIN、WM_DRAWCLIPBOARD和WM_DESTROY消息。WM_PAINT消息处理打开剪贴簿,并用CF_TEXT格式呼叫GetClipboardData。如果函数传回一个整体内存句柄,那么CLIPVIEW将锁定它,并用DrawText在显示区域显示文字。

处理标准格式(如Windows提供的那个剪贴簿一样)以外的数据格式的剪贴簿浏览器还需要完成一些其它工作,比如显示剪贴簿中目前所有数据格式的名称。使用者可以通过呼叫EnumClipboardFormats并使用GetClipboardFormatName得到非标准数据格式名称来完成这项工作。使用CF_OWNERDISPLAY数据格式的剪贴簿浏览器必须把下面四个消息送往剪贴簿数据的拥有者以显示该资料:

WM_PAINTCLIPBOARD

WM_SIZECLIPBOARD

WM_VSCROLLCLIPBOARD

WM_HSCROLLCLIPBOARD

如果您想编写这样的剪贴簿浏览器,那么必须使用GetClipboardOwner获得剪贴簿所有者的窗口句柄,并当您需要修改剪贴簿的显示区域时,将这些消息发送给该窗口。