What a drag: Dragging a virtual file (HGLOBAL edition) - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20080318-00/?p=23083
Raymond Chen 2008年03月18日
拖拽虚拟文件(HGLOBAL 版本)
现在我们已经对简单的数据对象有所了解,让我们来做点稍微复杂但极其有用的事情:拖拽一个虚拟文件。实现这一功能的方法有很多,但我将从最简单的方法开始,即虚拟文件以内存块的形式表示。
记住,这个系列的副标题是“这是你能做的最少”。你可以(甚至应该)做很多可选的事情,但我将从绝对最小化的部分开始。
对我们一直在研究的拖拽/放置程序进行以下更改。首先,更改数据类型的枚举:
enum {
DATA_FILEGROUPDESCRIPTOR,
DATA_FILECONTENTS,
DATA_NUM,
DATA_INVALID = -1,
};
拖拽虚拟文件的核心剪贴板格式是 FILEGROUPDESCRIPTOR
,它描述了正在拖拽的文件数量以及它们的各种信息。对于文件组描述符中的每个文件,你必须提供相关的文件内容,由 CFSTR_FILECONTENTS
剪贴板格式表示。
CTinyDataObject::CTinyDataObject() : m_cRef(1)
{
SetFORMATETC(&m_rgfe[DATA_FILEGROUPDESCRIPTOR],
RegisterClipboardFormat(CFSTR_FILEDESCRIPTOR));
SetFORMATETC(&m_rgfe[DATA_FILECONTENTS],
RegisterClipboardFormat(CFSTR_FILECONTENTS),
TYMED_HGLOBAL, /* lindex */ 0);
}
初始化文件组描述符条目和之前看到的差不多。注意,结构体称为 FILEGROUPDESCRIPTOR
,但剪贴板格式是 CFSTR_FILEDESCRIPTOR
而不是“group”。这可能最初是一个印刷错误,但现在我们只能接受它。
文件内容条目有一个转折:lindex
是零,而不是 -1
。文件内容剪贴板格式使用 lindex
作为从零开始的索引,选择调用者所谈论的是哪个虚拟文件。由于我们只有一个虚拟文件,它的索引是零。
和以前一样,所有真正的工作都在数据对象的核心,即 IDataObject::GetData
方法中。
HRESULT CTinyDataObject::GetData(FORMATETC *pfe, STGMEDIUM *pmed)
{
ZeroMemory(pmed, sizeof(*pmed));
switch (GetDataIndex(pfe)) {
case DATA_FILEGROUPDESCRIPTOR:
{
FILEGROUPDESCRIPTOR fgd;
ZeroMemory(&fgd, sizeof(fgd));
fgd.cItems = 1;
StringCchCopy(fgd.fgd[0].cFileName,
ARRAYSIZE(fgd.fgd[0].cFileName),
TEXT("Dummy"));
pmed->tymed = TYMED_HGLOBAL;
return CreateHGlobalFromBlob(&fgd, sizeof(fgd),
GMEM_MOVEABLE, &pmed->hGlobal);
}
case DATA_FILECONTENTS:
pmed->tymed = TYMED_HGLOBAL;
return CreateHGlobalFromBlob("Dummy", 5,
GMEM_MOVEABLE, &pmed->hGlobal);
}
return DV_E_FORMATETC;
}
当调用者请求文件组描述符时,我们填写一个 FILEGROUPDESCRIPTOR
结构体,在填写之前清零我们不关心的字节,以避免信息泄露漏洞。正如我指出的,我们从做绝对最小的必要事情开始,这在虚拟文件传输的情况下仅仅包括指定有多少虚拟文件以及它们的名称。
当调用者请求文件零(我们唯一拥有的)的内容时,我们生成一个包含“Dummy”这个词的五字节内存块。
运行这个程序,并将不可见的对象拖出客户端区域并将其放置到桌面上。哇,你的虚拟文件已经被复制到桌面上,并变成了一个真实的文件。(你甚至可以将它拖放到Outlook邮件撰写窗口中,它将作为附件出现!)
这里还有一些问题,但我们已经至少完成了拖拽由内存块表示的虚拟文件的绝对最小必要事项。让我们看看其中一些可选特性,其中一些对您和最终用户都有重大影响。
首先,您可能已经注意到创建的 Dummy 文件在末尾可能有一些垃圾字节。我说“可能”,因为这些垃圾字节的存在取决于堆管理器的感受。如果您只提供 HGLOBAL
,则内存块大小的唯一指示是 GlobalSize
函数的输出。但 GlobalSize
函数返回的大小不需要等于传递给 GlobalAlloc
的大小;唯一的保证是它至少和请求的大小一样大。它可能更大,这是由于内部堆管理,例如将所有分配舍入到最近的16字节的倍数。如果进行了这样的舍入,那么创建的 Dummy 文件将包含那些额外的垃圾字节。
为了避免这个问题,在 FILEGROUPDESCRIPTOR
中设置 FD_FILESIZE
标志,并在 nFileSizeLow
和 nFileSizeHigh
成员中指定确切的文件大小:
在 FILEGROUPDESCRIPTOR
中指定文件大小也有利于最终用户,因为它为文件复制进度条提供了它应该接收多少字节的信息。没有它,进度条不知道那个虚拟文件中有多少字节。它最终在请求文件内容时找到,但它是从每个文件复制时逐个学习的。进度对话框没有机会预先收集这些信息,以提供有意义的进度反馈。
另一个可选细节,您可能希望利用的是,在 FILEGROUPDESCRIPTOR
中指定文件属性和修改时间。例如,您可能希望在复制时使文件隐藏,或者您可能希望自定义最后修改时间。
我们来做一些事情。我们将在文件组描述符中指定文件大小以避免垃圾并改善进度反馈,并将最后修改时间设置为特定日期。
case DATA_FILEGROUPDESCRIPTOR:
{
FILEGROUPDESCRIPTOR fgd;
ZeroMemory(&fgd, sizeof(fgd));
fgd.cItems = 1;
fgd.fgd[0].dwFlags = FD_FILESIZE | FD_WRITESTIME;
fgd.fgd[0].nFileSizeLow = 5;
fgd.fgd[0].ftLastWriteTime.dwLowDateTime = 0x256d4000;
fgd.fgd[0].ftLastWriteTime.dwHighDateTime = 0x01bf53eb;
StringCchCopy(fgd.fgd[0].cFileName,
ARRAYSIZE(fgd.fgd[0].cFileName),
TEXT("Dummy"));
pmed->tymed = TYMED_HGLOBAL;
return CreateHGlobalFromBlob(&fgd, sizeof(fgd),
GMEM_MOVEABLE, &pmed->hGlobal);
}
现在,当您放置文件时,它在末尾将没有任何垃圾字节,时间戳将是2000年1月1日午夜UTC。(由于文件太小,您不会注意到进度条有任何改进。)
尽管我们没有做很多,但这对许多人来说可能已经足够了,尤其是那些只想允许用户从他们的程序中拖拽一个对象并将其放入资源管理器窗口以创建相应文件的人,只要 HGLOBAL
是文件内容的方便格式。这对于小文件是合适的,但随着文件变大,您必须一次性生成整个文件的事实可能变得昂贵。下次我们将看看一个替代方案。