【Windows】《深入浅出Windows API程序设计:核心编程篇》笔记-Chapter5-剪贴板

第5章 剪贴板

许多文档、数据处理程序都提供了剪切、复制和粘贴功能,当用户选择剪切或复制菜单项时,程序会把数据传递到剪贴板,这些数据采用特定的格式,例如文本、位图;当用户选择粘贴菜单项后,程序会检查剪贴板中是否包含本程序可用的数据格式,如果包含,则把数据从剪贴板传递到该程序。

Windows剪贴板是一种比较简单的同时开销比较小的进程间通信方式,使用剪贴板传递数据使开发人员不必过多地考虑数据存储的共享空间,简化通信过程。Windows系统支持剪贴板IPC的基本机制是由系统预留的一块全局共享内存,可被各进程用于暂时存储数据。写入数据的进程首先创建一个全局内存块,并将数据写入该内存块;接收数据的进程通过剪贴板机制获取到该内存块的句柄,并完成对该内存块数据的读取。

5.1 剪贴板常用函数与消息

Windows在User32.dll中为剪贴板提供了一组API函数和几种消息,还包括多种剪贴板数据格式,使进程能够以指定格式读取剪贴板中的数据。为了学习后续章节,本节首先介绍一些剪贴板函数和相关消息,方便读者进行大致了解。

5.1.1 基本剪贴板函数

(1)OpenClipboard函数用于打开剪贴板:

复制代码
BOOL OpenClipboard(_In_opt_ HWND hWndNewOwner);

hWndNewOwner参数指定与剪贴板相关联的窗口句柄。如果另一个程序已经打开了剪贴板但没有关闭,则 OpenClipboard 函数调用会失败。成功调用OpenClipboard函数打开剪贴板后,在调用CloseClipboard函数关闭剪贴板前,其他应用程序无法使用剪贴板。

(2)EmptyClipboard函数用于清空剪贴板中的数据:

复制代码
BOOL EmptyClipboard();

调用EmptyClipboard函数后,OpenClipboard函数的hWndNewOwner参数指定的窗口会成为剪贴板的新所有者。如果调用OpenClipboard函数时把hWndNewOwner参数设置为NULL,则EmptyClipboard函数会将剪贴板所有者设置为NULL,这会导致后续调用SetClipboardData函数设置剪贴板数据失败。

(3)SetClipboardData函数用于把指定格式的数据写入剪贴板中:

复制代码
HANDLE SetClipboardData(
    _In_      UINT   uFormat,   // 要写入剪贴板中数据的格式
    _In_opt_ HANDLE hMem);     // uFormat参数指定格式数据的句柄
  • uFormat参数用于指定要写入剪贴板中数据的格式,一些常见的标准剪贴板数据格式如表5.1所示。

表5.1

常量 含义
CF_TEXT 1 ANSI文本格式
CF_UNICODETEXT 13 Unicode文本格式
CF_BITMAP 2 设备相关位图
CF_DIB 8 设备无关位图
CF_SYLK 4 符号链接(SYLK)格式
CF_WAVE 12 标准波形格式的音频数据
CF_OWNERDISPLAY 0x80 所有者显示格式,HMEM参数必须设置为NULL。剪贴板所有者必须显示和更新剪贴板查看器窗口,并接收WM_ASKCBFORMATNAME、WM_HSCROLLCLIPBOARD、WM_PAINTCLIPBOARD、WM_SIZECLIPBOARD和WM_VSCROLLCLIPBOARD消息

此外,应用程序还可以创建自己的剪贴板数据格式,由应用程序定义的剪贴板数据格式称为"注册剪贴板数据格式"。例如,如果文字处理程序使用标准文本格式将带颜色、格式的文本复制到剪贴板,则颜色、格式信息将会丢失,解决方法是注册一个新的剪贴板数据格式。

  • hMem参数指定具有指定格式数据的句柄。如果该参数设置为NULL,则表示直到程序自身或其他程序对剪贴板中的数据请求访问时,程序才会将指定格式的数据写入剪贴板中,这就是延迟提交技术。

注意,调用SetClipboardData函数后,hMem所指定的内存对象被系统拥有,程序不应该将它释放、锁定或挪作他用。

如果在调用OpenClipboard打开剪贴板时指定了一个为NULL的窗口句柄,则会导致SetClipboardData函数调用失败。如果函数执行成功,则返回值是剪贴板数据的句柄;如果函数执行失败,则返回值为NULL。

(4)GetClipboardData函数用于从剪贴板中获取指定格式的数据:

复制代码
HANDLE GetClipboardData(_In_ UINT uFormat);

如果函数执行成功,则返回值是指定格式的剪贴板数据的句柄;如果函数执行失败,则返回值为NULL。

(5)CloseClipboard函数用于关闭剪贴板:

复制代码
BOOL CloseClipboard();

在完成对剪贴板数据的修改、访问后,应该及时调用CloseClipboard函数关闭剪贴板,这使其他程序能够打开并访问剪贴板。每次成功调用OpenClipboard函数后都应该有一次CloseClipboard函数调用,无论何时只能有一个程序可以打开剪贴板,如果一个程序调用OpenClipboard函数打开剪贴板以后一直没有调用CloseClipboard函数关闭剪贴板,那么这期间其他所有程序都不能使用剪贴板。

(6)RegisterClipboardFormat函数用于注册一个新的剪贴板数据格式:

复制代码
UINT RegisterClipboardFormat(_In_ LPCTSTR lpszFormat);    // lpszFormat指定剪贴板格式名称,  
                                                    // 不区分大小写

如果函数执行成功,则返回值标识已注册的剪贴板数据格式;如果指定名称的格式已经存在,则函数返回已存在的格式标识值。已注册剪贴板数据格式的值在0xC000~0xFFFF之间,函数返回的格式标识值可以作为一个有效的剪贴板数据格式来使用,注册一个新的剪贴板数据格式则允许多个应用程序使用相同的已注册剪贴板数据格式来复制和粘贴数据。

(7)GetClipboardFormatName函数用于获取已注册剪贴板数据格式的名称:

复制代码
int GetClipboardFormatName(
    _In_  UINT    format,              // 已注册的剪贴板数据格式标识值
    _Out_ LPTSTR lpszFormatName,      // 接收剪贴板数据格式名称的缓冲区
    _In_  int     cchMaxCount);        // 缓冲区的长度(以字符为单位)

如果函数执行成功,则返回值是复制到缓冲区中的字符个数;如果函数执行失败,则返回值为0。

(8)CountClipboardFormats函数用于获取剪贴板中具有不同剪贴板数据格式的数量:

复制代码
int CountClipboardFormats();

如果函数执行成功,则返回值为非零;如果函数执行失败,则返回值为0。

(9)EnumClipboardFormats函数用于枚举剪贴板中当前可用的剪贴板数据格式:

复制代码
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等剪贴板延迟提交消息的处理。

注意,在复制文件时,可以复制一个任意大小的文件,这时剪贴板中存放的只是文件的信息,并非整个文件本身;只有在复制非文件格式,例如文本、图片等时,剪贴板中存放的才是源数据本身。

1.WM_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);
}
2.WM_DESTROYCLIPBOARD

在其他地方打开了剪贴板并且调用EmptyClipboard函数清空剪贴板的内容,接管剪贴板的所有权时,系统会向剪贴板所有者进程发送WM_DESTROYCLIPBOARD消息,以通知该进程对剪贴板所有权的丧失。窗口过程处理完该消息后,应该返回0。

3.WM_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
END

ClipboardRead.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
#endif

(2)写入位图的步骤,以下是用户单击"写入位图"按钮时的处理代码:

复制代码
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_DRAWCLIPBOARD:

复制代码
case 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把消息传递给hwnd3,hwnd3传递给hwnd2,hwnd2传递给hwnd1,hwnd1则不会继续往下传递。

hwnd2程序调用以下函数退出:

复制代码
ChangeClipboardChain(hwnd2, hwnd1);

Windows会向hwnd4发送WM_CHANGECBCHAIN消息,相应的wParam等于hwnd2,lParam等于hwnd1。由于hwnd4的下一个查看器是hwnd3,因此hwnd4把这个消息发送给hwnd3,hwnd3注意到wParam等于它的下一个查看器(hwnd2),所以它把下一个查看器设置成等于lParam(hwnd1)并且返回。剪贴板查看器看起来如下:

复制代码
当前剪贴板查看器           hwnd4
hwnd4的下一个査看器        hwnd3
hwnd3的下一个查看器        hwnd1
hwnd1的下一个查看器        NULL

Windows只维护一个标识当前剪贴板查看器的窗口句柄,并且只向那个窗口发送消息,每个剪贴板查看器窗口都必须处理剪贴板消息WM_CHANGECBCHAIN和WM_DRAWCLIPBOARD,调用SendMessage函数将这些消息传递到剪贴板查看器链中的下个窗口;同时每个剪贴板查看器程序都有责任维护好剪贴板查看器链。

5.3.2 剪贴板监视程序ClipboardMonitor

ClipboardMonitor示例程序简单地演示了监测剪贴板中文本与位图内容的变化,对话框中有一个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项目。

相关推荐
志尊宝1 小时前
Vue3 零基础每日笔记(017):事件处理进阶——$event、多事件与事件修饰符全家桶
javascript·vue.js·笔记
阿洛学长1 小时前
Python笔记:dir() 和 help() 完整用法(适配Python二级 + IDLE实操)
服务器·笔记·python
qeen871 小时前
【C++】智能指针介绍
开发语言·c++·笔记·指针
小此方2 小时前
告别云服务器连接困扰——Windows下利用VirtualBox安装配置Ubuntu虚拟机详细教程
服务器·windows·ubuntu
蒸蒸yyyyzwd4 小时前
cpp 选手秋招学习笔记 day33
c++·笔记·求职招聘
Sunshing1514 小时前
Lyapunov方程系统本身稳定性判定与镇定性判定
笔记·学习
透明的玻璃杯15 小时前
热键驱动 Hotkey Features Integration v9.x(14MB)
笔记
上海心泾国际物流有限公司16 小时前
我在几个医疗器械仓库记下的一组温湿度数据
经验分享·笔记·学习·健康医疗·交通物流
LuminousCPP18 小时前
数据结构-排序(三):快速排序进阶|挖坑法、双指针交换与手动栈非递归实现
c语言·数据结构·笔记·算法·排序算法