【Windows】《深入浅出Windows API程序设计:核心编程篇》笔记-Chapter5-剪贴板
第5章 剪贴板许多文档、数据处理程序都提供了剪切、复制和粘贴功能当用户选择剪切或复制菜单项时程序会把数据传递到剪贴板这些数据采用特定的格式例如文本、位图当用户选择粘贴菜单项后程序会检查剪贴板中是否包含本程序可用的数据格式如果包含则把数据从剪贴板传递到该程序。Windows剪贴板是一种比较简单的同时开销比较小的进程间通信方式使用剪贴板传递数据使开发人员不必过多地考虑数据存储的共享空间简化通信过程。Windows系统支持剪贴板IPC的基本机制是由系统预留的一块全局共享内存可被各进程用于暂时存储数据。写入数据的进程首先创建一个全局内存块并将数据写入该内存块接收数据的进程通过剪贴板机制获取到该内存块的句柄并完成对该内存块数据的读取。5.1 剪贴板常用函数与消息Windows在User32.dll中为剪贴板提供了一组API函数和几种消息还包括多种剪贴板数据格式使进程能够以指定格式读取剪贴板中的数据。为了学习后续章节本节首先介绍一些剪贴板函数和相关消息方便读者进行大致了解。5.1.1 基本剪贴板函数1OpenClipboard函数用于打开剪贴板BOOL OpenClipboard(_In_opt_ HWND hWndNewOwner);hWndNewOwner参数指定与剪贴板相关联的窗口句柄。如果另一个程序已经打开了剪贴板但没有关闭则 OpenClipboard 函数调用会失败。成功调用OpenClipboard函数打开剪贴板后在调用CloseClipboard函数关闭剪贴板前其他应用程序无法使用剪贴板。2EmptyClipboard函数用于清空剪贴板中的数据BOOL EmptyClipboard();调用EmptyClipboard函数后OpenClipboard函数的hWndNewOwner参数指定的窗口会成为剪贴板的新所有者。如果调用OpenClipboard函数时把hWndNewOwner参数设置为NULL则EmptyClipboard函数会将剪贴板所有者设置为NULL这会导致后续调用SetClipboardData函数设置剪贴板数据失败。3SetClipboardData函数用于把指定格式的数据写入剪贴板中HANDLE SetClipboardData( _In_ UINT uFormat, // 要写入剪贴板中数据的格式 _In_opt_ HANDLE hMem); // uFormat参数指定格式数据的句柄uFormat参数用于指定要写入剪贴板中数据的格式一些常见的标准剪贴板数据格式如表5.1所示。表5.1常量值含义CF_TEXT1ANSI文本格式CF_UNICODETEXT13Unicode文本格式CF_BITMAP2设备相关位图CF_DIB8设备无关位图CF_SYLK4符号链接SYLK格式CF_WAVE12标准波形格式的音频数据CF_OWNERDISPLAY0x80所有者显示格式HMEM参数必须设置为NULL。剪贴板所有者必须显示和更新剪贴板查看器窗口并接收WM_ASKCBFORMATNAME、WM_HSCROLLCLIPBOARD、WM_PAINTCLIPBOARD、WM_SIZECLIPBOARD和WM_VSCROLLCLIPBOARD消息此外应用程序还可以创建自己的剪贴板数据格式由应用程序定义的剪贴板数据格式称为“注册剪贴板数据格式”。例如如果文字处理程序使用标准文本格式将带颜色、格式的文本复制到剪贴板则颜色、格式信息将会丢失解决方法是注册一个新的剪贴板数据格式。hMem参数指定具有指定格式数据的句柄。如果该参数设置为NULL则表示直到程序自身或其他程序对剪贴板中的数据请求访问时程序才会将指定格式的数据写入剪贴板中这就是延迟提交技术。注意调用SetClipboardData函数后hMem所指定的内存对象被系统拥有程序不应该将它释放、锁定或挪作他用。如果在调用OpenClipboard打开剪贴板时指定了一个为NULL的窗口句柄则会导致SetClipboardData函数调用失败。如果函数执行成功则返回值是剪贴板数据的句柄如果函数执行失败则返回值为NULL。4GetClipboardData函数用于从剪贴板中获取指定格式的数据HANDLE GetClipboardData(_In_ UINT uFormat);如果函数执行成功则返回值是指定格式的剪贴板数据的句柄如果函数执行失败则返回值为NULL。5CloseClipboard函数用于关闭剪贴板BOOL CloseClipboard();在完成对剪贴板数据的修改、访问后应该及时调用CloseClipboard函数关闭剪贴板这使其他程序能够打开并访问剪贴板。每次成功调用OpenClipboard函数后都应该有一次CloseClipboard函数调用无论何时只能有一个程序可以打开剪贴板如果一个程序调用OpenClipboard函数打开剪贴板以后一直没有调用CloseClipboard函数关闭剪贴板那么这期间其他所有程序都不能使用剪贴板。6RegisterClipboardFormat函数用于注册一个新的剪贴板数据格式UINT RegisterClipboardFormat(_In_ LPCTSTR lpszFormat); // lpszFormat指定剪贴板格式名称 // 不区分大小写如果函数执行成功则返回值标识已注册的剪贴板数据格式如果指定名称的格式已经存在则函数返回已存在的格式标识值。已注册剪贴板数据格式的值在0xC0000xFFFF之间函数返回的格式标识值可以作为一个有效的剪贴板数据格式来使用注册一个新的剪贴板数据格式则允许多个应用程序使用相同的已注册剪贴板数据格式来复制和粘贴数据。7GetClipboardFormatName函数用于获取已注册剪贴板数据格式的名称int GetClipboardFormatName( _In_ UINT format, // 已注册的剪贴板数据格式标识值 _Out_ LPTSTR lpszFormatName, // 接收剪贴板数据格式名称的缓冲区 _In_ int cchMaxCount); // 缓冲区的长度(以字符为单位)如果函数执行成功则返回值是复制到缓冲区中的字符个数如果函数执行失败则返回值为0。8CountClipboardFormats函数用于获取剪贴板中具有不同剪贴板数据格式的数量int CountClipboardFormats();如果函数执行成功则返回值为非零如果函数执行失败则返回值为0。9EnumClipboardFormats函数用于枚举剪贴板中当前可用的剪贴板数据格式UINT EnumClipboardFormats(_In_ UINT format);在枚举格式前必须先调用OpenClipboard函数打开剪贴板否则枚举函数会执行失败。要枚举剪贴板数据格式需要循环调用EnumClipboardFormats函数第一次调用时format参数设置为0函数返回第一个可用的剪贴板数据格式对于后续调用将format参数设置为上次调用的返回值即可直到函数返回0。枚举剪贴板数据格式的代码通常是如下格式UINT uFormat 0; uFormat EnumClipboardFormats(0); while (uFormat) { // 做一些工作 uFormat EnumClipboardFormats(uFormat); }要把数据写入剪贴板在打开剪贴板后必须调用EmptyClipboard函数清除当前剪贴板中的内容而不可以在原有数据项的基础上追加新的数据项。但是可以在调用EmptyClipboard和CloseClipboard函数之间多次调用SetClipboardData函数来写入多个不同格式的数据项。例如TCHAR szText[] TEXT(老王你好); LPTSTR lpText NULL; HBITMAP hBitmap NULL; lpText (LPTSTR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (_tcslen(szText) 1) * sizeof(TCHAR)); if (lpText) StringCchCopy(lpText, _tcslen(szText) 1, szText); hBitmap LoadBitmap(g_hInstance, MAKEINTRESOURCE(IDB_BITMAP)); OpenClipboard(hwndDlg); EmptyClipboard(); SetClipboardData(CF_UNICODETEXT, lpText); // lpText参数指定为Unicode字符串指针 SetClipboardData(CF_BITMAP, hBitmap); // hBitmap参数指定为位图句柄 CloseClipboard();自身进程或其他进程中可以按如下方式获取对应的数据LPTSTR lpStr; OpenClipboard(hwndDlg); // 获取Unicode字符串指针 lpStr (LPTSTR)GetClipboardData(CF_UNICODETEXT); // 获取位图句柄 hBitmap (HBITMAP)GetClipboardData(CF_BITMAP); CloseClipboard();在获取剪贴板中指定格式的数据前可能需要先确定剪贴板中是否包含该格式的数据这可以通过调用IsClipboardFormatAvailable函数实现BOOL IsClipboardFormatAvailable(_In_ UINT format); // 标准或注册剪贴板格式如果剪贴板中包含指定格式的数据则返回值为非零否则返回值为0。对于包含多数据项的剪贴板数据可以调用CountClipboardFormats和EnumClipboardFormats函数获取当前剪贴板中存在的数据格式的数量和具体的数据格式标识值。调用EmptyClipboard函数后OpenClipboard函数的hWndNewOwner参数指定的窗口会成为剪贴板的新所有者剪贴板所有者用于提供剪贴板数据任何程序都可以打开剪贴板并获取其中的数据不需要成为所有者在再次调用OpenClipboard和EmptyClipboard两个函数前剪贴板所有者不会改变。5.1.2 剪贴板相关的消息在创建剪贴板数据后在其他进程清空剪贴板数据前这些数据会一直占据内存空间。如果在剪贴板中放置的数据量过大就会浪费内存空间降低对资源的利用率。为了避免这种浪费可以采取延迟提交Delayed Rendering由数据提供进程先创建一个指定数据格式的空NULL剪贴板数据块直到自身进程或其他进程需要数据或者自身进程要终止运行时才真正提交数据。延迟提交的实现并不复杂剪贴板所有者进程在调用SetClipboardData函数时将数据句柄参数设置为NULL即可延迟提交的所有者进程需要做的后续工作是对WM_RENDERFORMAT、WM_DESTROYCLIPBOARD和WM_RENDERALLFORMATS等剪贴板延迟提交消息的处理。注意在复制文件时可以复制一个任意大小的文件这时剪贴板中存放的只是文件的信息并非整个文件本身只有在复制非文件格式例如文本、图片等时剪贴板中存放的才是源数据本身。1WM_RENDERFORMAT如果剪贴板所有者采用了延迟提交当有进程对剪贴板中的数据进行请求例如调用GetClipboardData函数时剪贴板所有者的窗口过程会收到WM_RENDERFORMAT消息该消息的wParam参数表示所需的剪贴板数据格式剪贴板所有者可以调用SetClipboardData函数设置指定格式的数据。窗口过程处理完该消息后应该返回0。一个进程可以按如下方式采用延迟提交OpenClipboard(hwnd); EmptyClipboard(); SetClipboardData(CF_UNICODETEXT, NULL); // 创建一个CF_UNICODETEXT数据格式的空剪贴板数据块 CloseClipboard();当自身进程或其他进程调用GetClipboardData函数从剪贴板读取数据时剪贴板所有者可以按如下方式处理WM_RENDERFORMAT消息TCHAR szText[] TEXT(我是延迟提交的字符串数据); LPVOID lpMemory HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (_tcslen(szText) 1) * sizeof(TCHAR)); if (lpMemory) { StringCchCopy((LPTSTR)lpMemory, _tcslen(szText) 1, szText); // 将内存中的数据放置到剪贴板 SetClipboardData(wParam, lpMemory); }2WM_DESTROYCLIPBOARD在其他地方打开了剪贴板并且调用EmptyClipboard函数清空剪贴板的内容接管剪贴板的所有权时系统会向剪贴板所有者进程发送WM_DESTROYCLIPBOARD消息以通知该进程对剪贴板所有权的丧失。窗口过程处理完该消息后应该返回0。3WM_RENDERALLFORMATS如果剪贴板所有者采用了延迟提交当延迟提交所有者进程将要终止时那么系统会发送一条WM_RENDERALLFORMATS消息通知其打开并清除剪贴板数据OpenClipboard、EmptyClipboard然后调用SetClipboardData函数设置所需格式的数据最后关闭剪贴板CloseClipboard。该消息的wParam和lParam参数都没有用到窗口过程处理完该消息后应该返回0。通过剪贴板通信的5种基本情况总结如下。1文本剪贴板。文本剪贴板具有CF_TEXT格式在文本剪贴板中传递的数据是不带任何格式信息的ANSI字符串因为可以把任何数据格式化为ANSI字符串信息所以文本剪贴板可以用于存放任何数据。2位图剪贴板。位图剪贴板具有CF_BITMAP格式调用SetClipboardData函数时需要提供位图句柄而调用GetClipboardData函数时返回的是位图句柄。3自定义格式。除使用预定义的剪贴板数据格式外也可以在程序中使用自定义的数据格式。调用RegisterClipboardFormat函数可以注册自定义数据格式函数返回系统分配的数据格式整型标识值。4延迟提交。延迟提交可以充分利用内存资源数据量较大时尤为重要前面已经解释过。5多项数据。设置剪贴板数据前首先需要调用EmptyClipboard函数清空剪贴板而不能追加数据但是可以在打开剪贴板后连续多次调用SetClipboardData函数为剪贴板设置多项数据。5.2 使用剪贴板进行进程间通信为了演示在不同进程间共享剪贴板数据需要编写两个程序一个负责写入不同格式的剪贴板数据另一个负责读取写入端不同格式的数据。运行效果如原书的图5.1所示。写入端分别演示了剪贴板的5种基本使用方式。写入文本读取文本数据编辑框中用户输入的字符串然后写入剪贴板中。写入位图把当前屏幕截图写入剪贴板中。自定义数据把自定义结构的数据写入剪贴板中。延迟提交延迟提交屏幕截图位图数据。多项数据同时设置文本/位图/自定义3种数据。Clipboard.rc资源脚本文件的主要内容如下IDD_MAIN DIALOGEX 200, 100, 309, 92 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION Clipboard写入端 FONT 8, MS Shell Dlg, 400, 0, 0x1 BEGIN LTEXT 文本数据,IDC_STATIC,7,10,41,8 EDITTEXT IDC_EDIT_TEXT,52,7,176,14,ES_AUTOHSCROLL LISTBOX IDC_LIST_MSG,7,24,221,61,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP PUSHBUTTON 写入文本,IDC_BTN_TEXT,235,7,68,14 PUSHBUTTON 写入位图,IDC_BTN_BITMAP,235,23,68,14 PUSHBUTTON 写入自定义数据,IDC_BTN_CUSTOM,235,39,68,14 PUSHBUTTON 延迟提交,IDC_BTN_DELAY,235,55,68,14 PUSHBUTTON 写入多项数据,IDC_BTN_MULTIPLE,235,71,68,14 ENDClipboardRead.rc资源脚本文件的主要内容如下IDD_MAIN DIALOGEX 200, 100, 387, 167 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION Clipboard读取端 FONT 8, MS Shell Dlg, 400, 0, 0x1 BEGIN LTEXT 文本数据,IDC_STATIC,7,10,41,8 EDITTEXT IDC_EDIT_TEXT,56,7,172,14,ES_AUTOHSCROLL LTEXT 自定义数据,IDC_STATIC,6,25,49,8 EDITTEXT IDC_EDIT_CUSTOM,56,22,172,14,ES_AUTOHSCROLL LTEXT ,IDC_STATIC_BITMAP,7,38,373,106,WS_BORDER PUSHBUTTON 读取文本数据,IDC_BTN_TEXT,7,146,74,14 PUSHBUTTON 读取位图,IDC_BTN_BITMAP,81,146,74,14 PUSHBUTTON 读取自定义数据,IDC_BTN_CUSTOM,155,146,74,14 PUSHBUTTON 读取延迟提交,IDC_BTN_DELAY,229,146,74,14 PUSHBUTTON 读取多项数据,IDC_BTN_MULTIPLE,303,146,74,14 END其中作为图像静态控件使用的IDC_STATIC_BITMAP在这里通过资源编辑器的工具箱中的Static Text添加静态文本控件的默认ID为IDC_STATIC因为在单击读取位图、读取延迟提交和读取多项数据按钮时需要更换图片因此必须自定义一个新的ID。读者也可以通过资源编辑器的工具箱中的Picture Control添加一个图像静态控件但是以这种方式添加的图像静态控件无法在资源编辑器中随意调整大小因此这里使用工具箱中的Static Text添加了一个静态文本控件并设置一个边框WS_BORDER然后在对话框初始化消息WM_INITDIALOG中为静态文本控件设置SS_BITMAP | SS_REALSIZECONTROL样式使之成为图像静态控件。SS_REALSIZECONTROL样式的作用是按照控件的大小缩放显示位图。5.2.1 Clipboard写入端1写入文本数据的步骤。以下是用户单击“写入文本”按钮时的处理代码VOID OnBtnText() //“写入文本”按钮按下 { TCHAR szText[128] { 0 }; LPTSTR lpStr; // 打开剪贴板 OpenClipboard(g_hwnd); // 清空剪贴板 EmptyClipboard(); // 获取文本数据编辑框的文本 GetDlgItemText(g_hwnd, IDC_EDIT_TEXT, szText, _countof(szText)); // 从默认堆中分配内存 lpStr (LPTSTR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (_tcslen(szText) 1) * sizeof(TCHAR)); if (lpStr) { // 复制文本数据到刚刚分配的内存中 StringCchCopy(lpStr, _tcslen(szText) 1, szText); // 将内存中的数据放置到剪贴板 //SetClipboardData(CF_TEXT, lpStr); //SetClipboardData(CF_UNICODETEXT, lpStr); SetClipboardData(CF_TCHAR, lpStr); } // 关闭剪贴板 CloseClipboard(); }我们的程序既可以编译为Unicode版本又可以编译为ANSI版本文本数据格式指定为CF_TEXT或CF_UNICODETEXT均不合适因此程序开头有如下条件编译语句// 定义CF_TCHAR不管程序是Unicode还是ANSI版本剪贴板文本数据格式都会被正确设置 #ifdef UNICODE #define CF_TCHAR CF_UNICODETEXT #else #define CF_TCHAR CF_TEXT #endif2写入位图的步骤以下是用户单击“写入位图”按钮时的处理代码VOID OnBtnBitmap() //“写入位图”按钮按下 { // 打开剪贴板 OpenClipboard(g_hwnd); // 清空剪贴板 EmptyClipboard(); // 设置剪贴板位图数据 HDC hdcDesk, hdcMem; HBITMAP hBmp; int nWidth GetSystemMetrics(SM_CXSCREEN); int nHeight GetSystemMetrics(SM_CYSCREEN); hdcDesk CreateDC(TEXT(DISPLAY), NULL, NULL, NULL); hdcMem CreateCompatibleDC(hdcDesk); hBmp CreateCompatibleBitmap(hdcDesk, nWidth, nHeight); SelectObject(hdcMem, hBmp); BitBlt(hdcMem, 0, 0, nWidth, nHeight, hdcDesk, 0, 0, SRCCOPY); SetClipboardData(CF_BITMAP, hBmp); DeleteObject(hBmp); DeleteDC(hdcMem); DeleteDC(hdcDesk); // 关闭剪贴板 CloseClipboard(); }3写入自定义数据的步骤下面是用户单击“写入自定义数据”按钮时的处理代码VOID OnBtnCustom() //“写入自定义数据”按钮按下 { LPVOID lpMem; // 自定义数据 CUSTOM_DATA customData { TEXT(老王), 39 }; // 打开剪贴板 OpenClipboard(g_hwnd); // 清空剪贴板 EmptyClipboard(); // 分配内存 lpMem HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(CUSTOM_DATA)); if (lpMem) { // 复制自定义数据到刚刚分配的内存中 memcpy_s(lpMem, sizeof(CUSTOM_DATA), customData, sizeof(CUSTOM_DATA)); // 设置剪贴板自定义数据 SetClipboardData(g_uFormat, lpMem); } // 关闭剪贴板 CloseClipboard(); }在程序开头有如下自定义数据结构的定义// 自定义剪贴板数据格式 typedef struct _CUSTOM_DATA { // 假设存储的是一个人的姓名、年龄 TCHAR szName[128]; UINT uAge; }CUSTOM_DATA, * PCUSTOM_DATA; // 在处理WM_INITDIALOG消息时注册一个新的剪贴板数据格式 VOID OnInit(HWND hwndDlg) // WM_INITDIALOG { g_hwnd hwndDlg; g_hwndList GetDlgItem(hwndDlg, IDC_LIST_MSG); // 注册一个自定义剪贴板数据格式 g_uFormat RegisterClipboardFormat(TEXT(RegisterFormat)); }本程序只是对自定义数据类型进行简单的显示许多字处理程序使用这种技术来存储包含有字体和格式信息的文本。前面提到注册一个新的剪贴板数据格式使一个以上的应用程序可以使用相同的已注册剪贴板数据格式复制和粘贴数据例如Word。如何确定数据是来自于自己的程序的另一个实例还是来自于另一个使用这些格式的程序呢方法如下。首先调用以下函数获取剪贴板所有者hwndClipOwner GetClipboardOwner();然后获取该窗口句柄的窗口类的名称TCHAR szClassName[256]; GetClassName(hwndClipOwner, szClassName, 256);如果获取到的类名与自己的程序类名相同那么数据就是被程序的另一个实例放入剪贴板的。4延迟提交的步骤下面是用户单击“延迟提交”按钮时的处理代码VOID OnBtnDelay() //“延迟提交”按钮按下 { // 打开剪贴板 OpenClipboard(g_hwnd); // 清空剪贴板 EmptyClipboard(); // 设置为空的剪贴板数据将数据句柄参数设置为NULL SetClipboardData(CF_BITMAP, NULL); // 关闭剪贴板 CloseClipboard();延迟提交的所有者进程需要做的主要工作是对WM_RENDERFORMAT、WM_DESTROYCLIPBOARD和WM_RENDERALLFORMATS等剪贴板延迟提交消息的处理VOID OnRenderFormat(UINT uFormat) // WM_RENDERFORMAT { SendMessage(g_hwndList, LB_ADDSTRING, 0, (LPARAM)TEXT(WM_RENDERFORMAT)); // 设置剪贴板位图数据 HDC hdcDesk, hdcMem; HBITMAP hBmp; int nWidth GetSystemMetrics(SM_CXSCREEN); int nHeight GetSystemMetrics(SM_CYSCREEN); hdcDesk CreateDC(TEXT(DISPLAY), NULL, NULL, NULL); hdcMem CreateCompatibleDC(hdcDesk); hBmp CreateCompatibleBitmap(hdcDesk, nWidth, nHeight); SelectObject(hdcMem, hBmp); BitBlt(hdcMem, 0, 0, nWidth, nHeight, hdcDesk, 0, 0, SRCCOPY); SetClipboardData(uFormat, hBmp); DeleteObject(hBmp); DeleteDC(hdcMem); DeleteDC(hdcDesk); } VOID OnDestroyClipbord() // WM_DESTROYCLIPBOARD { SendMessage(g_hwndList, LB_ADDSTRING, 0, (LPARAM)TEXT(WM_DESTROYCLIPBOARD)); } VOID OnRenderAllFormats() // WM_RENDERALLFORMATS { SendMessage(g_hwndList, LB_ADDSTRING, 0, (LPARAM)TEXT(WM_RENDERALLFORMATS)); OnBtnBitmap(); }在处理WM_RENDERFORMAT消息时不需要打开并清空剪贴板只需要根据wParam参数给出的剪贴板数据格式调用SetClipboardData函数设置剪贴板数据即可。如果剪贴板所有者采用了延迟提交则当有程序调用EmptyClipboard函数时Windows向剪贴板所有者发送一个WM_DESTROYCLIPBOARD消息该消息指出不再需要用于建立剪贴板的数据程序不再是剪贴板所有者可以释放为支持延迟提交而预留的任何资源。如果程序在自己仍然是剪贴板所有者时终止则剪贴板所有者将收到WM_RENDERALLFORMATS消息此时应该打开剪贴板、清空剪贴板设置剪贴板数据最后关闭剪贴板。WM_RENDERALLFORMATS消息的ALL表示可以延迟提交多项数据格式这里仅仅演示了位图数据的一种格式。5写入多项数据的步骤下面是用户单击“写入多项数据”按钮时的处理代码VOID OnBtnMultiple() //“写入多项数据”按钮按下 { // 打开剪贴板 OpenClipboard(g_hwnd); // 清空剪贴板 EmptyClipboard(); // 文本数据 TCHAR szText[128] { 0 }; LPTSTR lpStr; GetDlgItemText(g_hwnd, IDC_EDIT_TEXT, szText, _countof(szText)); lpStr (LPTSTR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (_tcslen(szText) 1) * sizeof(TCHAR)); if (lpStr) { StringCchCopy (lpStr, _tcslen(szText) 1, szText); SetClipboardData(CF_TCHAR, lpStr); } // 位图数据 HDC hdcDesk, hdcMem; HBITMAP hBmp; int nWidth GetSystemMetrics(SM_CXSCREEN); int nHeight GetSystemMetrics(SM_CYSCREEN); hdcDesk CreateDC(TEXT(DISPLAY), NULL, NULL, NULL); hdcMem CreateCompatibleDC(hdcDesk); hBmp CreateCompatibleBitmap(hdcDesk, nWidth, nHeight); SelectObject(hdcMem, hBmp); BitBlt(hdcMem, 0, 0, nWidth, nHeight, hdcDesk, 0, 0, SRCCOPY); SetClipboardData(CF_BITMAP, hBmp); DeleteObject(hBmp); DeleteDC(hdcMem); DeleteDC(hdcDesk); // 自定义数据 LPVOID lpMem; CUSTOM_DATA customData { TEXT(老王), 39 }; lpMem HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(CUSTOM_DATA)); if (lpMem) { memcpy_s(lpMem, sizeof(CUSTOM_DATA), customData, sizeof(CUSTOM_DATA)); SetClipboardData(g_uFormat, lpMem); } // 关闭剪贴板 CloseClipboard(); }步骤和前面4种基本相同只不过写入多项数据的步骤有多个SetClipboardData函数调用。5.2.2 Clipboard读取端1读取文本数据的步骤下面是用户单击“读取文本数据”按钮时的处理代码VOID OnBtnText() //“读取文本数据”按钮按下 { LPTSTR lpStr; // 剪贴板是否包含CF_TCHAR格式的文本 if (IsClipboardFormatAvailable(CF_TCHAR)) { // 打开剪贴板 OpenClipboard(g_hwnd); // 获取剪贴板中文本格式的数据并复制到szText缓冲区 lpStr (LPTSTR)GetClipboardData(CF_TCHAR); // 显示到编辑框中 SetDlgItemText(g_hwnd, IDC_EDIT_TEXT, lpStr); // 关闭剪贴板 CloseClipboard(); } else { MessageBox(g_hwnd, TEXT(剪贴板没有文本格式的数据), TEXT(Error), MB_OK); } }2读取位图数据的步骤下面是用户单击“读取位图”按钮时的处理代码VOID OnBtnBitmap() //“读取位图”按钮按下 { // 剪贴板是否包含CF_BITMAP格式的数据 if (IsClipboardFormatAvailable(CF_BITMAP)) { // 打开剪贴板 OpenClipboard(g_hwnd); // 获取剪贴板中位图格式的数据 HBITMAP hBmp (HBITMAP)GetClipboardData(CF_BITMAP); // 显示图片 SendDlgItemMessage(g_hwnd, IDC_STATIC_BITMAP, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)hBmp); // 关闭剪贴板 CloseClipboard(); } else { MessageBox(g_hwnd, TEXT(剪贴板没有位图格式的数据), TEXT(Error), MB_OK); } }显示图片的控件最初是一个普通的静态文本控件在WM_INITDIALOG消息中设置为位图样式VOID OnInit(HWND hwndDlg) { g_hwnd hwndDlg; // 注册一个自定义剪贴板格式与写入端使用相同的数据格式名称因此返回相同的格式标识值 g_uFormat RegisterClipboardFormat(TEXT(RegisterFormat)); // 显示图片的静态控件设置SS_BITMAP | SS_REALSIZECONTROL样式 LONG lStyle GetWindowLongPtr(GetDlgItem(g_hwnd, IDC_STATIC_BITMAP), GWL_STYLE); SetWindowLongPtr(GetDlgItem(g_hwnd, IDC_STATIC_BITMAP), GWL_STYLE, lStyle | SS_BITMAP | SS_REALSIZECONTROL); }3读取自定义数据的步骤下面是用户单击“读取自定义数据”按钮时的处理代码VOID OnBtnCustom() //“读取自定义数据”按钮按下 { TCHAR szText[128] { 0 }; PCUSTOM_DATA pCustomData; // 剪贴板是否包含g_uFormat格式的数据 if (IsClipboardFormatAvailable(g_uFormat)) { // 打开剪贴板 OpenClipboard(g_hwnd); // 获取剪贴板中g_uFormat格式的数据 pCustomData (PCUSTOM_DATA)GetClipboardData(g_uFormat); wsprintf(szText, TEXT(%s, %d), pCustomData-szName, pCustomData-uAge); // 显示到编辑框中 SetDlgItemText(g_hwnd, IDC_EDIT_CUSTOM, szText); // 关闭剪贴板 CloseClipboard(); } else { MessageBox(g_hwnd, TEXT(剪贴板没有自定义格式的数据), TEXT(Error), MB_OK); } }4读取延迟提交数据的步骤下面是用户单击“读取延迟提交”按钮时的处理代码VOID OnBtnDelay() //“读取延迟提交”按钮按下 { // 本程序读取延迟提交实际上读取的也是位图所以直接调用OnBtnBitmap OnBtnBitmap(); }5读取多项数据的步骤下面是用户单击“读取多项数据”按钮时的处理代码VOID OnBtnMultiple() //“读取多项数据”按钮按下 { // 清空文本数据编辑框、自定义数据编辑框和图像静态控件的内容 SetDlgItemText(g_hwnd, IDC_EDIT_TEXT, TEXT()); SetDlgItemText(g_hwnd, IDC_EDIT_CUSTOM, TEXT()); SendDlgItemMessage(g_hwnd, IDC_STATIC_BITMAP, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)NULL); // 打开剪贴板 OpenClipboard(g_hwnd); // 枚举剪贴板上当前可用的数据格式 UINT uFormat EnumClipboardFormats(0); while (uFormat) { // 这里只处理3种格式 if (uFormat CF_TCHAR) { LPTSTR lpStr; // 获取剪贴板中文本格式的数据并复制到szText缓冲区 lpStr (LPTSTR)GetClipboardData(CF_TCHAR); // 显示到编辑框中 SetDlgItemText(g_hwnd, IDC_EDIT_TEXT, lpStr); } else if (uFormat CF_BITMAP) { // 获取剪贴板中位图格式的数据 HBITMAP hBmp (HBITMAP)GetClipboardData(CF_BITMAP); // 显示图片 SendDlgItemMessage(g_hwnd, IDC_STATIC_BITMAP, STM_SETIMAGE, IMAGE_ BITMAP, (LPARAM)hBmp); } else if (uFormat g_uFormat) { TCHAR szBuf[128] { 0 }; PCUSTOM_DATA pCustomData; // 获取剪贴板中g_uFormat格式的数据 pCustomData (PCUSTOM_DATA)GetClipboardData(g_uFormat); wsprintf(szBuf, TEXT(%s, %d), pCustomData-szName, pCustomData-uAge); // 显示到编辑框中 SetDlgItemText(g_hwnd, IDC_EDIT_CUSTOM, szBuf); } uFormat EnumClipboardFormats(uFormat); } // 关闭剪贴板 CloseClipboard(); }打开剪贴板枚举剪贴板上当前可用的数据格式并分别处理最后关闭剪贴板。向剪贴板写入数据需要4个函数调用OpenClipboard(hwnd); EmptyClipboard(); SetClipboardData(uFormat, lpMem); CloseClipboard();要获取这些数据则需要3个函数调用OpenClipboard(hwnd); lpMem GetClipboardData(uFormat); // 其他代码 CloseClipboard();获取到数据后我们可以复制一份剪贴板数据在程序中使用也可以在GetClipboardData和CloseClipboard函数调用之间直接使用数据。完整代码参见Chapter5\Clipboard项目。5.3 监视剪贴板内容变化5.3.1 相关函数和消息SetClipboardViewer函数用于将指定窗口添加到剪贴板查看器的链中HWND SetClipboardViewer(HWND hWndNewViewer);如果函数执行成功则hWndNewViewer窗口成为新的当前剪贴板查看器返回值是剪贴板查看器链中的下一个窗口程序应该保存该返回值如果出现错误或剪贴板查看器链中没有其他窗口则返回值为NULL。如果要监视剪贴板的内容变化则需要把程序添加到剪贴板查看器链中因为每当剪贴板的内容发生变化时剪贴板查看器就会收到WM_DRAWCLIPBOARD消息。之所以叫作剪贴板查看器链是因为当前系统中可能不止有一个剪贴板查看器所有的剪贴板查看器组成一个链。如前所述函数调用后hWndNewViewer成为新的当前剪贴板查看器每当剪贴板的内容发生变化时事实上只有当前剪贴板查看器会收到Windows发送的WM_DRAWCLIPBOARD消息如果链中还有其他剪贴板查看器那么当前剪贴板查看器需要负责把WM_DRAWCLIPBOARD消息传递下去以通知它们剪贴板的内容发生了变化这是当前剪贴板查看器的责任也是每一个剪贴板查看器的责任每一个剪贴板查看器都应该这样传递剪贴板内容变化消息。Windows只维护一个标识当前剪贴板查看器的窗口句柄并且只向那个窗口发送WM_DRAWCLIPBOARD消息通知它剪贴板内容已经改变。剪贴板查看器程序通常需要向剪贴板查看器链中的下一个窗口传递剪贴板内容变化消息即WM_DRAWCLIPBOARDcase WM_DRAWCLIPBOARD: if (g_hwndNextViewer) { // 如果剪贴板查看器链中存在下一个窗口g_hwndNextViewer是SetClipboardViewer函数的返回值 SendMessage(g_hwndNextViewer, uMsg, wParam, lParam); }如果一个程序需要监视剪贴板内容的变化则应该在WM_CREATE或WM_INITDIALOG消息中调用SetClipboardViewer函数加入剪贴板查看器链中case WM_INITDIALOG: g_hwndNextViewer SetClipboardViewer(hwndDlg); break;剪贴板查看器程序退出时例如响应WM_DESTROY消息必须通过调用ChangeClipboardChain函数从剪贴板查看器链中删除自身。ChangeClipboardChain函数原型如下BOOL ChangeClipboardChain( _In_ HWND hWndRemove, // 要从链中移除的窗口的句柄 _In_ HWND hWndNewNext); // hWndRemove的下一个窗口句柄也就是调用SetClipboardViewer返回的句柄例如case WM_COMMAND: switch (LOWORD(wParam)) { case IDCANCEL: ChangeClipboardChain(hwndDlg, g_hwndNextViewer); EndDialog(hwndDlg, IDCANCEL); break; } return TRUE;当程序调用ChangeClipboardChain时Windows会向当前剪贴板查看器窗口发送一个 WM_CHANGECBCHAIN消息该消息的wParam参数是要从链中退出的窗口句柄ChangeClipboardChain的第1个参数lParam是将要退出的窗口的下一个剪贴板查看器的窗口句柄ChangeClipboardChain的第2个参数。我们的程序不一定是当前剪贴板查看器有可能在程序加入剪贴板查看器链成为新的当前剪贴板查看器后又运行了本程序的其他实例或其他剪贴板查看器。为了维护好剪贴板查看器链每一个剪贴板查看器在处理WM_CHANGECBCHAIN消息时都应该判断要退出的那个程序wParam是不是自己的下一个剪贴板查看器g_hwndNextViewer。如果是就把自己的下一个剪贴板查看器g_hwndNextViewer的值设为lParam这样整个链才是正确的。如果要退出的那个程序wParam不是自己的下一个剪贴板查看器g_hwndNextViewer就负责把有剪贴板查看器程序要退出这个消息传递给下一个剪贴板查看器。下面是剪贴板查看器处理WM_CHANGECBCHAIN消息的代码case WM_CHANGECBCHAIN: if ((HWND)wParam g_hwndNextViewer) g_hwndNextViewer (HWND)lParam; else if (g_hwndNextViewer) SendMessage(g_hwndNextViewer, uMsg, wParam, lParam); break;举例说明剪贴板查看器链是怎样工作的。当Windows刚开始启动时当前剪贴板查看器为NULL当前剪贴板查看器 NULL窗口句柄为hwnd1的程序调用了SetClipboardViewer函数会返回NULL这个返回值成为调用程序里的g_hwndNextViewer的值当前剪贴板查看器 hwnd1 hwnd1的下一下査看器 NULL窗口句柄为hwnd2的第2个程序调用SetClipboardViewer并且返回hwnd1当前剪贴板查看器 hwnd2 hwnd2的下一个査看器 hwnd1 hwnd1的下一个查看器 NULL第3个程序hwnd3和第4个程序hwnd4也调用了SetClipboardViewer分别返回hwnd2和hwnd3当前剪贴板查看器 hwnd4 hwnd4的下一个査看器 hwnd3 hwnd3的下一个查看器 hwnd2 hwnd2的下一个查看器 hwnd1 hwnd1的下一个查看器 NULL当剪贴板内容发生变化时Windows向hwnd4发送WM_DRAWCLIPBOARD消息hwnd4把消息传递给hwnd3hwnd3传递给hwnd2hwnd2传递给hwnd1hwnd1则不会继续往下传递。hwnd2程序调用以下函数退出ChangeClipboardChain(hwnd2, hwnd1);Windows会向hwnd4发送WM_CHANGECBCHAIN消息相应的wParam等于hwnd2lParam等于hwnd1。由于hwnd4的下一个查看器是hwnd3因此hwnd4把这个消息发送给hwnd3hwnd3注意到wParam等于它的下一个查看器hwnd2所以它把下一个查看器设置成等于lParamhwnd1并且返回。剪贴板查看器看起来如下当前剪贴板查看器 hwnd4 hwnd4的下一个査看器 hwnd3 hwnd3的下一个查看器 hwnd1 hwnd1的下一个查看器 NULLWindows只维护一个标识当前剪贴板查看器的窗口句柄并且只向那个窗口发送消息每个剪贴板查看器窗口都必须处理剪贴板消息WM_CHANGECBCHAIN和WM_DRAWCLIPBOARD调用SendMessage函数将这些消息传递到剪贴板查看器链中的下个窗口同时每个剪贴板查看器程序都有责任维护好剪贴板查看器链。5.3.2 剪贴板监视程序ClipboardMonitorClipboardMonitor示例程序简单地演示了监测剪贴板中文本与位图内容的变化对话框中有一个RichEdit20富文本控件和一个图像静态控件富文本控件用于实时显示剪贴板中的文本数据如果使用QQ截图截取不同的图片则图像静态控件中会随之显示出不同的图片程序运行效果如原书的图5.2所示。ClipboardMonitor.cpp源文件的内容如下#include Windows.h #include Richedit.h #include resource.h #ifdef UNICODE #define CF_TCHAR CF_UNICODETEXT #else #define CF_TCHAR CF_TEXT #endif // 全局变量 HWND g_hwndNextViewer; // 剪贴板查看器链中下一个剪贴板查看器的窗口句柄 // 函数声明 INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // 加载动态链接库Riched20.dll LoadLibrary(TEXT(Riched20.dll)); DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL); return 0; } INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { static HWND hwndEdit; LONG lStyle; LPTSTR lpStr; HBITMAP hBmp; UINT uFormat; TCHAR szSeparator[] TEXT(\n------------------------------------\n); switch (uMsg) { case WM_INITDIALOG: // 显示图片的静态控件设置SS_BITMAP | SS_REALSIZECONTROL样式 lStyle GetWindowLongPtr(GetDlgItem(hwndDlg, IDC_STATIC_BITMAP), GWL_STYLE); SetWindowLongPtr(GetDlgItem(hwndDlg, IDC_STATIC_BITMAP), GWL_STYLE, lStyle | SS_BITMAP | SS_REALSIZECONTROL); // 将当前程序窗口添加到剪贴板查看器链中 g_hwndNextViewer SetClipboardViewer(hwndDlg); hwndEdit GetDlgItem(hwndDlg, IDC_RICHEDIT); return TRUE; case WM_COMMAND: switch (LOWORD(wParam)) { case IDCANCEL: // 从剪贴板查看器链中删除自身 ChangeClipboardChain(hwndDlg, g_hwndNextViewer); EndDialog(hwndDlg, IDCANCEL); break; } return TRUE; case WM_DRAWCLIPBOARD: // 剪贴板的内容发生变化 // 如果剪贴板查看器链中存在下一个窗口 if (g_hwndNextViewer) SendMessage(g_hwndNextViewer, uMsg, wParam, lParam); // 更新显示 OpenClipboard(hwndDlg); uFormat EnumClipboardFormats(0); while (uFormat) { // 这里只处理这2种格式 if (uFormat CF_TCHAR) { lpStr (LPTSTR)GetClipboardData(CF_TCHAR); SendMessage(hwndEdit, EM_SETSEL, -1, -1); SendMessage(hwndEdit, EM_REPLACESEL, TRUE, (LPARAM)lpStr); SendMessage(hwndEdit, EM_SETSEL, -1, -1); SendMessage(hwndEdit, EM_REPLACESEL, TRUE, (LPARAM)szSeparator); } else if (uFormat CF_BITMAP) { hBmp (HBITMAP)GetClipboardData(CF_BITMAP); SendDlgItemMessage(hwndDlg, IDC_STATIC_BITMAP, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)hBmp); } uFormat EnumClipboardFormats(uFormat); } CloseClipboard(); return TRUE; case WM_CHANGECBCHAIN: // 处理WM_CHANGECBCHAIN消息维护好剪贴板查看器链 if ((HWND)wParam g_hwndNextViewer) g_hwndNextViewer (HWND)lParam; else if (g_hwndNextViewer) SendMessage(g_hwndNextViewer, uMsg, wParam, lParam); return TRUE; } return FALSE; }因为用到了一个 RichEdit20 富文本控件所以在 WinMain 函数中调用 DialogBoxParam函数弹出程序对话框前必须调用LoadLibrary函数加载动态链接库Riched20.dll富文本控件的用法与多行编辑控件类似需要的读者可以自行参考MSDN。5.3.3 监视剪贴板的新方法监视剪贴板的更改有3种方式。以前的方式是创建剪贴板查看器窗口当有程序不能正确地维护剪贴板查看器链或者剪贴板查看器链中的某一程序停止响应消息时就会导致剪贴板的监视出现问题这是很难避免的支持剪贴板查看器窗口是为了与早期版本Windows的向后兼容性。Windows 2000增加了查询剪贴板序列号的功能Windows Vista增加了对剪贴板数据格式监听器的支持这两种方式实际上更简单新程序应该使用剪贴板序列号或剪贴板数据格式监听器。1剪贴板序列号每当剪贴板的内容发生变化时剪贴板序列号的32位值会递增程序可以通过调用GetClipboard SequenceNumber函数来获取当前剪贴板序列号通过将返回的值与先前调用返回的值进行比较程序可以确定剪贴板的内容是否已经更改该方法更适合需要等待当前剪贴板内容变化结果的程序但是剪贴板的内容发生变化时程序不会得到通知要在剪贴板内容发生变化时得到通知需要使用剪贴板查看器或剪贴板数据格式监听器。2剪贴板数据格式监听器程序可以通过调用AddClipboardFormatListener函数注册成为剪贴板数据格式监听器当剪贴板的内容发生变化时窗口将会收到WM_CLIPBOARDUPDATE消息BOOL AddClipboardFormatListener(HWND hwnd);WM_CLIPBOARDUPDATE消息很简单wParam和lParam参数均为0程序只需要响应这个消息对剪贴板内容变化做出处理即可。如果程序需要移除剪贴板数据格式监听器可以调用RemoveClipboardFormatListener函数BOOL RemoveClipboardFormatListener(HWND hwnd);本节示例程序还是ClipboardMonitor项目的例子稍微修改一下即可具体源代码参见 Clipboard Monitor_Listener项目。

相关新闻

Zoom 插件查询路由实战:用 zoom-general 编排多技能链(Query Routing Playbook 全解析)

Zoom 插件查询路由实战:用 zoom-general 编排多技能链(Query Routing Playbook 全解析)

Zoom 插件查询路由实战:用 zoom-general 编排多技能链(Query Routing Playbook 全解析) 【免费下载链接】knowledge-work-plugins Open source repository of plugins primarily intended for knowledge workers to use in Claude Cowork 项…

2026/9/13 22:46:56 阅读更多 →
Kilo V2 配置架构重构指南:从遗留 Schema 到 11 组配置评审全景

Kilo V2 配置架构重构指南:从遗留 Schema 到 11 组配置评审全景

Kilo V2 配置架构重构指南:从遗留 Schema 到 11 组配置评审全景 【免费下载链接】kilocode Kilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent. 项目地址: https://gitcode…

2026/9/13 22:46:56 阅读更多 →
Crawlee 如何用 crawlee.json、环境变量或 Configuration 类修改默认配置并确认优先级

Crawlee 如何用 crawlee.json、环境变量或 Configuration 类修改默认配置并确认优先级

Crawlee 如何用 crawlee.json、环境变量或 Configuration 类修改默认配置并确认优先级 【免费下载链接】crawlee Crawlee—A web scraping and browser automation library for Node.js to build reliable crawlers. In JavaScript and TypeScript. Extract data for AI, LLMs,…

2026/9/13 22:46:56 阅读更多 →

最新新闻

AI至少5年内不会完全替代人类程序员

AI至少5年内不会完全替代人类程序员

基于最新公布的报告所呈现的情况来看, 人类程序员于短期内根本不会被AI彻底替代。这份报告进行了预测, 实现完全编程自动化尚需5至6年的时长, 而此预测相较于该社区之前所认定的2027年1月至2028年9月有推迟的状况。到这一时间线被延长的时候, 距离初始研究结果仅仅过去了8个月,…

2026/9/13 23:56:25 阅读更多 →
Python自动化测试入门:pytest环境搭建与第一个测试用例

Python自动化测试入门:pytest环境搭建与第一个测试用例

1. 从零开始:为什么选择作为你的测试起点要是你才刚开始着手接触自动化测试, 或者正从nose等框架朝着更具现代感的工具转变, 那“”这名儿你肯定不会感到陌生, 它基本上已然变成了社区测试用具的默认选项, 然诸多新手在头一步“环境筹备”这儿就会碰到各式各样的小纠…

2026/9/13 23:56:25 阅读更多 →
大公司偏爱RPC的核心原因深度解析

大公司偏爱RPC的核心原因深度解析

大公司偏爱RPC的核心原因深度解析一、先搞懂:RPC到底是什么?RPC全称为远程过程调用(Call), 就单个而言, 其意思是, 对于不同服务器之中的调用情况, 对于其不同服务之间的情况, 能够让这样的调用, 如同针对本地方法进行调用那般轻易…

2026/9/13 23:56:25 阅读更多 →
Zola 静态站点部署全攻略:Docker、GitHub Pages、Netlify、Vercel 与云厂商实战

Zola 静态站点部署全攻略:Docker、GitHub Pages、Netlify、Vercel 与云厂商实战

Zola 静态站点部署全攻略:Docker、GitHub Pages、Netlify、Vercel 与云厂商实战 【免费下载链接】zola A fast static site generator in a single binary with everything built-in. https://www.getzola.org 项目地址: https://gitcode.com/GitHub_Trending/zo/…

2026/9/13 23:56:25 阅读更多 →
Qbot 比特币套利机会检测与自动交易:bitcoin-arbitrage 模块原理解读与实战指南

Qbot 比特币套利机会检测与自动交易:bitcoin-arbitrage 模块原理解读与实战指南

Qbot 比特币套利机会检测与自动交易:bitcoin-arbitrage 模块原理解读与实战指南 【免费下载链接】Qbot [🔥updating ...] AI 自动量化交易机器人(完全本地部署) AI-powered Quantitative Investment Research Platform. 📃 online docs: http…

2026/9/13 23:56:25 阅读更多 →
Iosevka 24.1.4 更新解析:新增字符全览与字形修复背后的源码逻辑

Iosevka 24.1.4 更新解析:新增字符全览与字形修复背后的源码逻辑

Iosevka 24.1.4 更新解析:新增字符全览与字形修复背后的源码逻辑 【免费下载链接】Iosevka Versatile typeface for code, from code. 项目地址: https://gitcode.com/GitHub_Trending/io/Iosevka Iosevka 是一套"由代码生成的代码字体"&#xff0…

2026/9/13 23:55:24 阅读更多 →

日新闻

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验 【免费下载链接】ai The AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and ag…

2026/9/13 0:00:24 阅读更多 →
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化 【免费下载链接】refine A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility. 项目地址: https://gitcode.com/GitH…

2026/9/13 0:00:24 阅读更多 →
Flutter应用改名全指南:从Android到iOS的配置与工具实践

Flutter应用改名全指南:从Android到iOS的配置与工具实践

刚接一个外包项目时,甲方要求把工程里临时用的应用名改成正式产品名。我本来觉得“改名”这种小事,打开配置文件改一行不就完了?结果真动手才发现,Flutter项目里“应用名称”根本不是一处配置,而是一整套散落在 Androi…

2026/9/13 0:00:24 阅读更多 →

周新闻

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验 【免费下载链接】ai The AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and ag…

2026/9/13 0:00:24 阅读更多 →
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化 【免费下载链接】refine A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility. 项目地址: https://gitcode.com/GitH…

2026/9/13 0:00:24 阅读更多 →
Flutter应用改名全指南:从Android到iOS的配置与工具实践

Flutter应用改名全指南:从Android到iOS的配置与工具实践

刚接一个外包项目时,甲方要求把工程里临时用的应用名改成正式产品名。我本来觉得“改名”这种小事,打开配置文件改一行不就完了?结果真动手才发现,Flutter项目里“应用名称”根本不是一处配置,而是一整套散落在 Androi…

2026/9/13 0:00:24 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/13 16:51:11 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/12 18:29:34 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/12 19:02:44 阅读更多 →