windows剪贴板
Posted 刘收获
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了windows剪贴板相关的知识,希望对你有一定的参考价值。
0x01 Windows剪贴板
Windows剪贴板是一种比较简单同时也是开销比较小的IPC(InterProcess Communication,进程间通讯)机制。Windows系统支持剪贴板IPC的基本机制是由系统预留的一块全局共享内存,用来暂存在各进程间进行交换的数据:提供数据的进程创建一个全局内存块,并将要传送的数据移到或复制到该内存块;接受数据的进程(也可以是提供数据的进程本身)获取此内存块的句柄,并完成对该内存块数据的读取。
为使剪贴板的这种IPC机制更加完善和便于使用,需要解决好如下三个问题:提供数据的进程在结束时Windows系统将删除其创建的全局内存块,而接受数据的进程则希望在其退出后剪贴板中的数据仍然存在,可以继续为其他进程所获取;能方便地管理和传送剪贴板数据句柄;能方便设置和确定剪贴板数据格式。为完善上述功能,Windows提供了存在于USER32.dll中的一组API函数、消息和预定义数据格式等,并通过对这些函数、消息的使用来管理在进程间进行的剪贴板数据交换。
Windows系统为剪贴板提供了一组API函数和多种消息,基本可以满足编程的需要。而且Windows还为剪贴板预定义了多种数据格式。通过这些预定义的格式,可以使接收方正确再现数据提供方放置于剪贴板中的数据内容。
0x02 多数据项和延迟提交技术
要把数据放入剪贴板,在打开剪贴板后一定要调用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等剪贴板延迟提交消息的处理。
1.当另一个进程调用GetClipboardData()函数时,系统将会向延迟提交数据的剪贴板拥有者进程发送WM_RENDERFORMAT消息。剪贴板拥有者进程在此消息的响应函数中应使用相应的格式和实际的数据句柄来调用SetClipboardData()函数,但不必再调用OpenClipboard()和EmptyClipboard()去打开和清空剪贴板了。在设置完数据有也无须调用CloseClipboard()关闭剪贴板。
2.如果其他进程打开了剪贴板并且调用EmptyClipboard()函数去清空剪贴板的内容,接管剪贴板的拥有权时,系统将向延迟提交的剪贴板拥有者进程发送WM_DESTROYCLIPBOARD消息,以通知该进程对剪贴板拥有权的丧失。而失去剪贴板拥有权的进程在收到该消息后则不会再向剪贴板提交数据。另外,在延迟提交进程在提交完所有要提交的数据后也会收到此消息。
3.如果延迟提交剪贴板拥有者进程将要终止,系统将会为其发送一条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); // 将内存中的数据放置到剪贴板 |
0x03 API
剪贴板的打开 – OpenClipboard
要想把数据放置到剪贴板中,则必须先打开剪贴板,而这是通过 OpenClipboard 成员函数实现:
BOOL OpenClipboard(
HWND hWndNewOwner ); //指向一个与之关联的窗口句柄,即代表是这个窗口打开剪贴,如果这个参数设置为 NULL 的话,则以当前的任务或者说是进程来打开剪贴板。
如果打开剪贴板成功,则该函数返回非 0 值,如果其他程序已经打开了剪贴板,那么当前这个程序就无法再打开剪贴板了,所以会致使打开剪贴板失败,从而该函数返回 0 值。
剪贴板的清空 - EmptyClipboard
这个函数将清空剪贴板,并释放剪贴板中数据的句柄,然后将剪贴板的所有权分配给当前打开剪贴板的窗口,这样做之后当前打开这个剪贴板的程序就拥有了剪贴板的所有权,因此这个程序就可以往剪贴板上放置数据了。
BOOL EmptyClipboard(void);
剪贴板的关闭 - CloseClipboard
如果某个进程打开了剪贴板,则在这个进程没有调用 CloseClipboard 函数关闭剪贴板句柄之前,其他进程都是无法打开剪贴板的,所以我们每次使用完剪贴板之后都应该关闭剪贴板。注意,这里的关闭剪贴板并不代表当前打开剪贴板的这个程序失去了对剪贴板的所有权,只有在别的程序调用了 EmptyClipboard 函数之后,当前的这个程序才会失去对剪贴板的所有权,
而那个调用 EmptyClipboard 函数的程序才能拥有剪贴板。
BOOL CloseClipboard(void);
数据发送到剪贴板 - SetClipboardData
可以通过 SetClipboardData 函数来实现往剪贴板中放置数据,这个函数以指定的剪贴板格式向剪贴板中放置数据。
HANDLE SetClipboardData(
UINT uFormat, //要放到剪贴板上的数据的格式,常见的有 CF_BITMAP ,CF_TEXT ,CF_DIB 等等(其他格式可以参考 MSDN)。
HANDLE hMem ); //指定格式的数据的句柄,该参数可以是 NULL ,如果该参数为 NULL 则表明直到有程序对剪贴板中的数据进行请求时,该程序(也就是拥有剪贴板所有权的进程)才会将数据复制到剪贴板中,也就是提供指定剪贴板格式的数据,
剪贴板中数据格式判断 – IsClipboardFormatAvaliable
BOOL IsClipboardFormatAvailable( UINT format );
该函数用来判断剪贴板上的数据格式是否为 format 指定的格式。
剪贴板中数据接收 - GetClipboardData
HANDLE GetClipboardData( UINT uFormat );
该函数根据 uFormat 指定的格式,返回一个以指定格式存在于剪贴板中的剪贴板对象的句柄。
0x04 源代码
自己用MFC实现的一个剪贴板程序,如图:
源代码:
1 // ClipBoardDlg.cpp : 实现文件 2 // 3 4 #include "stdafx.h" 5 #include "ClipBoard.h" 6 #include "ClipBoardDlg.h" 7 #include "afxdialogex.h" 8 9 #ifdef _DEBUG 10 #define new DEBUG_NEW 11 #endif 12 13 14 // 用于应用程序“关于”菜单项的 CAboutDlg 对话框 15 VOID ShowErrorMessage(DWORD ErrorCode); 16 class CAboutDlg : public CDialogEx 17 { 18 public: 19 CAboutDlg(); 20 21 // 对话框数据 22 #ifdef AFX_DESIGN_TIME 23 enum { IDD = IDD_ABOUTBOX }; 24 #endif 25 26 protected: 27 virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持 28 29 // 实现 30 protected: 31 DECLARE_MESSAGE_MAP() 32 }; 33 34 CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX) 35 { 36 } 37 38 void CAboutDlg::DoDataExchange(CDataExchange* pDX) 39 { 40 CDialogEx::DoDataExchange(pDX); 41 } 42 43 BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx) 44 END_MESSAGE_MAP() 45 46 47 // CClipBoardDlg 对话框 48 49 50 51 CClipBoardDlg::CClipBoardDlg(CWnd* pParent /*=NULL*/) 52 : CDialogEx(IDD_CLIPBOARD_DIALOG, pParent) 53 { 54 m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); 55 } 56 57 void CClipBoardDlg::DoDataExchange(CDataExchange* pDX) 58 { 59 CDialogEx::DoDataExchange(pDX); 60 DDX_Control(pDX, IDC_MESSAGE_LIST, m_CListBox_Message); 61 } 62 63 BEGIN_MESSAGE_MAP(CClipBoardDlg, CDialogEx) 64 ON_WM_SYSCOMMAND() 65 ON_WM_PAINT() 66 ON_WM_QUERYDRAGICON() 67 ON_BN_CLICKED(IDC_TEXT, &CClipBoardDlg::OnBnClickedText) 68 ON_WM_DESTROYCLIPBOARD() 69 ON_BN_CLICKED(IDC_BITMAP, &CClipBoardDlg::OnBnClickedBitmap) 70 ON_BN_CLICKED(IDC_SELF_DEFINE, &CClipBoardDlg::OnBnClickedSelfDefine) 71 ON_BN_CLICKED(IDC_BUTTON4, &CClipBoardDlg::OnBnClickedButton4) 72 ON_WM_DESTROYCLIPBOARD() 73 END_MESSAGE_MAP() 74 75 76 // CClipBoardDlg 消息处理程序 77 78 BOOL CClipBoardDlg::OnInitDialog() 79 { 80 CDialogEx::OnInitDialog(); 81 82 // 将“关于...”菜单项添加到系统菜单中。 83 84 // IDM_ABOUTBOX 必须在系统命令范围内。 85 ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); 86 ASSERT(IDM_ABOUTBOX < 0xF000); 87 88 CMenu* pSysMenu = GetSystemMenu(FALSE); 89 if (pSysMenu != NULL) 90 { 91 BOOL bNameValid; 92 CString strAboutMenu; 93 bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX); 94 ASSERT(bNameValid); 95 if (!strAboutMenu.IsEmpty()) 96 { 97 pSysMenu->AppendMenu(MF_SEPARATOR); 98 pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); 99 } 100 } 101 102 // 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动 103 // 执行此操作 104 SetIcon(m_hIcon, TRUE); // 设置大图标 105 SetIcon(m_hIcon, FALSE); // 设置小图标 106 107 // TODO: 在此添加额外的初始化代码 108 this->m_DeskopPixelX = ::GetSystemMetrics(SM_CXSCREEN); //以像素为单位计算的屏幕尺寸 109 this->m_DeskopPixelY = ::GetSystemMetrics(SM_CYSCREEN); 110 111 this->m_NewFormat = RegisterClipboardFormat(L"TEST_FORMAT"); //注册一个新的剪贴板格式,如果注册格式已经存在,将返回已经存在格式的值而不是注册新的格式。这样做的好处是可以在多个应用程序中使用同一种格式来复制和粘贴数据。 112 return TRUE; // 除非将焦点设置到控件,否则返回 TRUE 113 } 114 115 void CClipBoardDlg::OnSysCommand(UINT nID, LPARAM lParam) 116 { 117 if ((nID & 0xFFF0) == IDM_ABOUTBOX) 118 { 119 CAboutDlg dlgAbout; 120 dlgAbout.DoModal(); 121 } 122 else 123 { 124 CDialogEx::OnSysCommand(nID, lParam); 125 } 126 } 127 128 // 如果向对话框添加最小化按钮,则需要下面的代码 129 // 来绘制该图标。 对于使用文档/视图模型的 MFC 应用程序, 130 // 这将由框架自动完成。 131 132 void CClipBoardDlg::OnPaint() 133 { 134 if (IsIconic()) 135 { 136 CPaintDC dc(this); // 用于绘制的设备上下文 137 138 SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0); 139 140 // 使图标在工作区矩形中居中 141 int cxIcon = GetSystemMetrics(SM_CXICON); 142 int cyIcon = GetSystemMetrics(SM_CYICON); 143 CRect rect; 144 GetClientRect(&rect); 145 int x = (rect.Width() - cxIcon + 1) / 2; 146 int y = (rect.Height() - cyIcon + 1) / 2; 147 148 // 绘制图标 149 dc.DrawIcon(x, y, m_hIcon); 150 } 151 else 152 { 153 CDialogEx::OnPaint(); 154 } 155 } 156 157 //当用户拖动最小化窗口时系统调用此函数取得光标 158 //显示。 159 HCURSOR CClipBoardDlg::OnQueryDragIcon() 160 { 161 return static_cast<HCURSOR>(m_hIcon); 162 } 163 164 165 166 void CClipBoardDlg::OnBnClickedText() 167 { 168 // TODO: 在此添加控件通知处理程序代码 169 // 打开剪贴板 170 ::OpenClipboard(this->GetSafeHwnd()); 171 //OpenClipboard(); 172 // 首先清空剪贴板 173 ::EmptyClipboard(); 174 // 设置剪贴板文本数据 175 this->WriteTextIntoClipboard(); 176 // 关闭剪贴板 177 ::CloseClipboard(); 178 } 179 180 VOID CClipBoardDlg::WriteTextIntoClipboard() 181 { 182 // 初始化文本信息 183 const char BufferData[] = "刘大大你可别是妖怪吧!"; 184 DWORD BufferLength = strlen(BufferData); 185 HANDLE MemoryHandle = GlobalAlloc(GHND, BufferLength + 1); 186 if (MemoryHandle) 187 { 188 LPBYTE VirtualAddress = (LPBYTE)GlobalLock(MemoryHandle); 189 memcpy(VirtualAddress, BufferData, BufferLength); 190 //VirtualAddress[BufferLength] = 0; //调试看内存发现编译器已经做了优化了,这步没必要 191 GlobalUnlock(MemoryHandle); 192 // 设置剪贴板文本数据 193 if (::SetClipboardData(CF_TEXT, MemoryHandle) == NULL) 194 { 195 ShowErrorMessage(GetLastError()); 196 } 197 } 198 else 199 this->MessageBox(L"WriteTextIntoClipboard() Error"); 200 } 201 202 203 void CClipBoardDlg::OnBnClickedBitmap() 204 { 205 // TODO: 在此添加控件通知处理程序代码 206 // 打开剪贴板 207 ::OpenClipboard(this->GetSafeHwnd()); 208 // 首先清空剪贴板 209 ::EmptyClipboard(); 210 // 设置剪贴板位图数据 211 this->WriteBitmapIntoClipboard(); 212 // 关闭剪贴板 213 ::CloseClipboard(); 214 } 215 216 // 写入位图数据 217 VOID CClipBoardDlg::WriteBitmapIntoClipboard() 218 { 219 // 取得当前屏幕位图 220 CDC DeviceContext, MemoryDeviceContext; 221 CBitmap ScreenBitmap; 222 DeviceContext.CreateDC(L"DISPLAY", NULL, NULL, NULL); 223 MemoryDeviceContext.CreateCompatibleDC(&DeviceContext); 224 ScreenBitmap.CreateCompatibleBitmap(&DeviceContext, m_DeskopPixelX, m_DeskopPixelY); 225 MemoryDeviceContext.SelectObject(&ScreenBitmap); 226 MemoryDeviceContext.BitBlt(0, 0, m_DeskopPixelX, m_DeskopPixelY, &DeviceContext, 0, 0, SRCCOPY); 227 // 设置剪贴板文本数据 228 if (::SetClipboardData(CF_BITMAP, ScreenBitmap.GetSafeHandle()) == NULL) 229 { 230 ShowErrorMessage(GetLastError()); 231 } 232 ScreenBitmap.DeleteObject(); 233 MemoryDeviceContext.DeleteDC(); 234 } 235 236 237 void CClipBoardDlg::OnBnClickedSelfDefine() 238 { 239 // TODO: 在此添加控件通知处理程序代码 240 // TODO: 在此添加控件通知处理程序代码 241 ::OpenClipboard(this->GetSafeHwnd()); 242 // 首先清空剪贴板 243 ::EmptyClipboard(); 244 // 设置剪贴板位图数据 245 this->WriteSelfDefineDataIntoClipboard(); 246 // 关闭剪贴板 247 ::CloseClipboard(); 248 } 249 250 // 写入自定义数据 251 VOID CClipBoardDlg::WriteSelfDefineDataIntoClipboard() 252 { 253 PERSON_INFORMATION PersonInfo = { L"张飞", 23 }; 254 255 // 初始化自定义格式 256 DWORD PersonInfoLength = sizeof(PERSON_INFORMATION); 257 HANDLE MemoryHandle = GlobalAlloc(GHND, PersonInfoLength + 1); 258 if (MemoryHandle) 259 { 260 LPBYTE VirtualAddress = (LPBYTE)GlobalLock(MemoryHandle); 261 memcpy(VirtualAddress, &PersonInfo, PersonInfoLength); 262 VirtualAddress[PersonInfoLength] = 0; 263 GlobalUnlock(MemoryHandle); 264 ::SetClipboardData(this->m_NewFormat, MemoryHandle); 265 } 266 } 267 268 void CClipBoardDlg::OnBnClickedButton4() 269 { 270 // TODO: 在此添加控件通知处理程序代码 271 ::OpenClipboard(this->GetSafeHwnd()); 272 //OpenClipboard(); 273 // 首先清空剪贴板 274 ::EmptyClipboard(); 275 // 写入3种数据 276 this->WriteTextIntoClipboard(); 277 this->WriteBitmapIntoClipboard(); 278 this->WriteSelfDefineDataIntoClipboard(); 279 // 关闭剪贴板 280 ::CloseClipboard(); 281 } 282 283 284 VOID ShowErrorMessage(DWORD ErrorCode) 285 { 286 CHAR Message[128] = { 0 }; 287 sprintf(Message, "(ErrorCode=%p) (ErrorCode=%d)", ErrorCode, ErrorCode); 288 MessageBoxA(0, Message, 0, 0); 289 } 290 291 void CClipBoardDlg::OnDestroyClipboard() 292 { 293 CDialogEx::OnDestroyClipboard(); 294 295 // TODO: 在此处添加消息处理程序代码 296 this->m_CListBox_Message.AddString(L"WM_DESTORYCLIPBOARD"); 297 this->m_CListBox_Message.SetCurSel(this->m_CListBox_Message.GetCount() - 1); 298 }
1 // ClipBoardClientDlg.cpp : 实现文件 2 // 3 4 #include "stdafx.h" 5 #include "ClipBoardClient.h" 6 #include "ClipBoardClientDlg.h" 7 #include "afxdialogex.h" 8 9 #ifdef _DEBUG 10 #define new DEBUG_NEW 11 #endif 12 13 14 // 用于应用程序“关于”菜单项的 CAboutDlg 对话框 15 16 class CAboutDlg : public CDialogEx 17 { 18 public: 19 CAboutDlg(); 20 21 // 对话框数据 22 #ifdef AFX_DESIGN_TIME 23 enum { IDD = IDD_ABOUTBOX }; 24 #endif 25 26 protected: 27 virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持 28 29 // 实现 30 protected: 31 DECLARE_MESSAGE_MAP() 32 }; 33 34 CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX) 35 { 36 } 37 38 void CAboutDlg::DoDataExchange(CDataExchange* pDX) 39 { 40 CDialogEx::DoDataExchange(pDX); 41 } 42 43 BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx) 44 END_MESSAGE_MAP() 45 46 47 // CClipBoardClientDlg 对话框 48 49 50 51 CClipBoardClientDlg::CClipBoardClientDlg(CWnd* pParent /*=NULL*/) 52 : CDialogEx(IDD_CLIPBOARDCLIENT_DIALOG, pParent) 53 { 54 m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); 55 } 56 57 void CClipBoardClientDlg::DoDataExchange(CDataExchange* pDX) 58 { 59 CDialogEx::DoDataExchange(pDX); 60 DDX_Control(pDX, IDC_LIST1, m_CListBox_Message); 61 } 62 63 BEGIN_MESSAGE_MAP(CClipBoardClientDlg, CDialogEx) 64 O以上是关于windows剪贴板的主要内容,如果未能解决你的问题,请参考以下文章
Windows 上 Python 3.4 中的 Tkinter 不会在退出时将内部剪贴板数据发布到 Windows 剪贴板