在 C# 中将虚拟文件传输到 Windows Explorer
使用 C# 以及 CFSTR_FILECONTENTS 和 CFSTR_FILEDESCRIPTOR 格式将虚拟文件传输到 Windows 资源管理器的示例。
引言
我工作的公司销售一款实现自有文件服务器和运行时环境的产品。代码库很老,两年前启动了一个项目,使用 C# 和 .NET 2.0 创建新的接口。功能列表中的一个要求是能够在专有文件服务器环境和 Windows 资源管理器之间复制/剪切/拖放文件。通过使用 CF_HDROP
格式,将文件导入专有环境很容易。然而,尝试提取文件却遇到了问题。由于文件不存在于 Windows 环境中,CF_HDROP
格式不可用。此外,我们希望使用延迟渲染,以便除非绝对需要,否则不会从专有环境中提取文件,以减少开销。我找到的最好用于实现我想要的功能的格式是 CFSTR_FILEDESCRIPTOR
和 CFSTR_FILECONTENTS
格式。对互联网的广泛搜索没有发现任何关于在 C# 中实现此功能的示例,甚至有一些评论说这在托管语言中是不可能的。经过数周的研究,我终于找到了我在本文中介绍的代码。
Using the Code
此代码实现了一个名为 DataObjectEx
的类,该类派生自 System.Windows.Forms.DataObject
和 System.Runtime.InteropServices.ComTypes.IDataObject
。由于虚拟文件可以有多种独特的渲染方式,因此此代码展示了程序员在拥有虚拟文件数据后如何处理这些数据。我在代码的各个位置都留下了注释,说明需要将虚拟文件数据提供给该类。
对于此项目,需要引用以下 命名空间
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Security.Permissions;
需要定义的第一个类是 NativeMethods
类,它将包含 DataObjectEx
类使用的各种常量和本机方法。
public class NativeMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern IntPtr GlobalAlloc(int uFlags, int dwBytes);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern IntPtr GlobalFree(HandleRef handle);
// Clipboard formats used for cut/copy/drag operations
public const string CFSTR_PREFERREDDROPEFFECT = "Preferred DropEffect";
public const string CFSTR_PERFORMEDDROPEFFECT = "Performed DropEffect";
public const string CFSTR_FILEDESCRIPTORW = "FileGroupDescriptorW";
public const string CFSTR_FILECONTENTS = "FileContents";
// File Descriptor Flags
public const Int32 FD_CLSID = 0x00000001;
public const Int32 FD_SIZEPOINT = 0x00000002;
public const Int32 FD_ATTRIBUTES = 0x00000004;
public const Int32 FD_CREATETIME = 0x00000008;
public const Int32 FD_ACCESSTIME = 0x00000010;
public const Int32 FD_WRITESTIME = 0x00000020;
public const Int32 FD_FILESIZE = 0x00000040;
public const Int32 FD_PROGRESSUI = 0x00004000;
public const Int32 FD_LINKUI = 0x00008000;
// Global Memory Flags
public const Int32 GMEM_MOVEABLE = 0x0002;
public const Int32 GMEM_ZEROINIT = 0x0040;
public const Int32 GHND = (GMEM_MOVEABLE | GMEM_ZEROINIT);
public const Int32 GMEM_DDESHARE = 0x2000;
// IDataObject constants
public const Int32 DV_E_TYMED = unchecked((Int32)0x80040069);
}
在类就绪后,我们需要定义 命名空间
和 DataObjectEx
类,以及将要使用的各种结构和类变量。
namespace MyData.Extensions
{
public class DataObjectEx :
DataObject, System.Runtime.InteropServices.ComTypes.IDataObject
{
private static readonly TYMED[] ALLOWED_TYMEDS =
new TYMED[] {
TYMED.TYMED_ENHMF,
TYMED.TYMED_GDI,
TYMED.TYMED_HGLOBAL,
TYMED.TYMED_ISTREAM,
TYMED.TYMED_MFPICT};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct FILEDESCRIPTOR
{
public UInt32 dwFlags;
public Guid clsid;
public System.Drawing.Size sizel;
public System.Drawing.Point pointl;
public UInt32 dwFileAttributes;
public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
public UInt32 nFileSizeHigh;
public UInt32 nFileSizeLow;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public String cFileName;
}
public struct SelectedItem
{
public String FileName;
public DateTime WriteTime;
public Int64 FileSize;
}
private SelectedItem[] m_SelectedItems;
private Int32 m_lindex;
public DataObjectEx(SelectedItem[] selectedItems)
{
m_SelectedItems = selectedItems;
}
公共结构 SelectedItem
用于向类传递各种虚拟文件信息,以执行延迟渲染,并提供 CFSTR_FILEDESCRIPTOR
所需的文件名、大小和日期/时间信息。程序员可以根据需要修改此结构,以提供渲染虚拟文件可能需要的任何其他信息。
我们需要实现的下一个方法是 GetData
的重写。此重写允许我们对 CFSTR_FILEDESCRIPTOR
和 CFSTR_FILECONTENTS
格式执行延迟渲染。当 Windows 资源管理器发出 GetData
调用时,将调用此例程,此时将提取虚拟数据。此外,还会捕获 CFSTR_PERFORMEDDROPEFFECT
格式。当拖放/粘贴操作完成后,Windows 资源管理器会调用此格式。如果在传输后需要进行任何清理,应在此处执行。只有在传输成功完成后才请求此格式。如果用户在传输过程中按取消,则不会请求此格式。
public override object GetData(string format, bool autoConvert)
{
if (String.Compare(format, NativeMethods.CFSTR_FILEDESCRIPTORW,
StringComparison.OrdinalIgnoreCase) == 0 && m_SelectedItems != null)
{
base.SetData(NativeMethods.CFSTR_FILEDESCRIPTORW,
GetFileDescriptor(m_SelectedItems));
}
else if (String.Compare(format, NativeMethods.CFSTR_FILECONTENTS,
StringComparison.OrdinalIgnoreCase) == 0)
{
base.SetData(NativeMethods.CFSTR_FILECONTENTS,
GetFileContents(m_SelectedItems, m_lindex));
}
else if (String.Compare(format, NativeMethods.CFSTR_PERFORMEDDROPEFFECT,
StringComparison.OrdinalIgnoreCase) == 0)
{
//TODO: Cleanup routines after paste has been performed
}
return base.GetData(format, autoConvert);
}
下一个方法返回一个包含 FILEGROUPDESCRIPTOR
结构的 MemoryStream
对象。为了避免定义 FILEGROUPDESCRIPTOR
结构的额外复杂性(它只不过是一个无符号整数,包含文件描述符的数量,后跟一个 FILEDESCRIPTOR
结构数组),我只是将描述符计数直接写入内存流,然后在后面添加文件描述符。
正是在此方法中,才需要 SelectedItems
数组中的数据。为了让 Windows 资源管理器正确创建目标文件,它需要知道要创建的文件名,以及可选的大小和写入日期。我使用 SelectedItems
数组将此信息传递给类。
文件描述符结构填充完毕后,有必要将其写入内存流。这涉及到一些封送处理,将结构转换为字节数组,然后可以将其写入内存流。创建所有虚拟文件描述符后,该方法将内存流返回给调用者。
private MemoryStream GetFileDescriptor(SelectedItem[] SelectedItems)
{
MemoryStream FileDescriptorMemoryStream = new MemoryStream();
// Write out the FILEGROUPDESCRIPTOR.cItems value
FileDescriptorMemoryStream.Write
(BitConverter.GetBytes(SelectedItems.Length), 0, sizeof(UInt32));
FILEDESCRIPTOR FileDescriptor = new FILEDESCRIPTOR();
foreach (SelectedItem si in SelectedItems)
{
FileDescriptor.cFileName = si.FileName;
Int64 FileWriteTimeUtc = si.WriteTime.ToFileTimeUtc();
FileDescriptor.ftLastWriteTime.dwHighDateTime =
(Int32)(FileWriteTimeUtc >> 32);
FileDescriptor.ftLastWriteTime.dwLowDateTime =
(Int32)(FileWriteTimeUtc & 0xFFFFFFFF);
FileDescriptor.nFileSizeHigh = (UInt32)(si.FileSize >> 32);
FileDescriptor.nFileSizeLow = (UInt32)(si.FileSize & 0xFFFFFFFF);
FileDescriptor.dwFlags = NativeMethods.FD_WRITESTIME |
NativeMethods.FD_FILESIZE | NativeMethods.FD_PROGRESSUI;
// Marshal the FileDescriptor structure into a
// byte array and write it to the MemoryStream.
Int32 FileDescriptorSize = Marshal.SizeOf(FileDescriptor);
IntPtr FileDescriptorPointer = Marshal.AllocHGlobal(FileDescriptorSize);
Marshal.StructureToPtr(FileDescriptor, FileDescriptorPointer, true);
Byte[] FileDescriptorByteArray = new Byte[FileDescriptorSize];
Marshal.Copy(FileDescriptorPointer,
FileDescriptorByteArray, 0, FileDescriptorSize);
Marshal.FreeHGlobal(FileDescriptorPointer);
FileDescriptorMemoryStream.Write
(FileDescriptorByteArray, 0, FileDescriptorByteArray.Length);
}
return FileDescriptorMemoryStream;
}
下一个方法通过 FORMATETC
字段 lindex
返回一个包含文件内容的内存流,该文件内容由 Windows 资源管理器请求。此方法是实现特定的,因为它需要通过任何必需的手段从虚拟文件源获取虚拟文件数据,以便将其放入字节数组。在此代码中,SelectedItems
数组可以包含从虚拟文件系统中渲染文件的必要信息。
private MemoryStream GetFileContents(SelectedItem[] SelectedItems, Int32 FileNumber)
{
MemoryStream FileContentMemoryStream = null;
if (SelectedItems != null && FileNumber < SelectedItems.Length)
{
FileContentMemoryStream = new MemoryStream();
SelectedItem si = SelectedItems[FileNumber];
// ******************************************************************
// TODO: Get the virtual file contents and place
// the contents in the byte array bBuffer.
// If the contents are zero length then a single byte
// must be supplied to Windows
// Explorer otherwise the transfer will fail.
// If this is part of a multi-file transfer,
// the entire transfer will fail at this point
// if the buffer is zero length.
// ******************************************************************
Byte[] bBuffer;
// Must send at least one byte for a zero length file to prevent stoppages.
if (bBuffer.Length == 0)
bBuffer = new Byte[1];
FileContentMemoryStream.Write(bBuffer, 0, bBuffer.Length);
}
return FileContentMemoryStream;
}
最后两个方法用于获取与 GetData
请求一起使用的 FORMATETC
结构的副本,以便检索 lindex
字段以用于 FileContents
请求。由于没有可用的重写方法,因此有必要通过复制该方法的 .NET Framework 版本来完全替换它。使用 Lutz Roeder 的 .NET Reflector 软件,我从 System.Windows.Forms.dll 反汇编了该方法,并在该类中重新创建了它,并注意提取了我需要的 lindex
字段。GetTymedUseable
是 GetData
方法的一个支持方法,它利用类定义中定义的 私有 静态 ALLOWED_TYMEDS
数组。
[SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
void System.Runtime.InteropServices.ComTypes.IDataObject.GetData
(ref System.Runtime.InteropServices.ComTypes.FORMATETC formatetc,
out System.Runtime.InteropServices.ComTypes.STGMEDIUM medium)
{
if (formatetc.cfFormat == (Int16)DataFormats.GetFormat
(NativeMethods.CFSTR_FILECONTENTS).Id)
m_lindex = formatetc.lindex;
medium = new System.Runtime.InteropServices.ComTypes.STGMEDIUM();
if (GetTymedUseable(formatetc.tymed))
{
if ((formatetc.tymed & TYMED.TYMED_HGLOBAL) != TYMED.TYMED_NULL)
{
medium.tymed = TYMED.TYMED_HGLOBAL;
medium.unionmember = NativeMethods.GlobalAlloc
(NativeMethods.GHND | NativeMethods.GMEM_DDESHARE, 1);
if (medium.unionmember == IntPtr.Zero)
{
throw new OutOfMemoryException();
}
try
{
((System.Runtime.InteropServices.ComTypes.IDataObject)this).
GetDataHere(ref formatetc, ref medium);
return;
}
catch
{
NativeMethods.GlobalFree(new HandleRef((STGMEDIUM)medium,
medium.unionmember));
medium.unionmember = IntPtr.Zero;
throw;
}
}
medium.tymed = formatetc.tymed;
((System.Runtime.InteropServices.ComTypes.IDataObject)this).
GetDataHere(ref formatetc, ref medium);
}
else
{
Marshal.ThrowExceptionForHR(NativeMethods.DV_E_TYMED);
}
}
private static Boolean GetTymedUseable(TYMED tymed)
{
for (Int32 i = 0; i < ALLOWED_TYMEDS.Length; i++)
{
if ((tymed & ALLOWED_TYMEDS[i]) != TYMED.TYMED_NULL)
{
return true;
}
}
return false;
}
最后,以下代码片段显示了一个使用 DataObjectEx
的示例实现,该实现创建三个文件,名称为 _My Virtual File_,文件大小为零,写入日期为 2008 年 1 月 1 日。由于这三个文件具有相同的名称,Windows 资源管理器会提供一个文件编号,这将导致文件命名为 _My Virtual File_、_My Virtual File (1)_ 和 _My Virtual File (2_)。将三个剪贴板格式设置为 null
,如下所示,将启用延迟渲染。
Int32 NumItems = 3;
DataObjectEx.SelectedItem[] SelectedItems =
new DataObjectEx.SelectedItem[NumItems];
for (Int32 ItemCount = 0; ItemCount < SelectedItems.Length; ItemCount++)
{
// TODO: Get virtual file name
SelectedItems[ItemCount].FileName = "My Virtual File";
// TODO: Get virtual file date
SelectedItems[ItemCount].WriteTime = new DateTime(2008, 1, 1);
// TODO: Get virtual file size
SelectedItems[ItemCount].FileSize = 0;
}
DataObjectEx dataObject = new DataObjectEx(SelectedItems);
dataObject.SetData(NativeMethods.CFSTR_FILEDESCRIPTORW, null);
dataObject.SetData(NativeMethods.CFSTR_FILECONTENTS, null);
dataObject.SetData(NativeMethods.CFSTR_PERFORMEDDROPEFFECT, null);
Clipboard.SetDataObject(dataObject);
关注点
这是一个非常有趣的项目。在做这个项目的时候,我甚至不确定是否能够做到我想做的事情。由于没有任何关于使用这些格式的可用示例,这真是一个“自力更生”的场景。我很快就完成了大部分类,但在访问 FORMATETC
的 lindex
字段时遇到了困难。我花了许多头疼的时间尝试寻找覆盖、窥探、子类化等方法来访问该结构。最终发现,完全用自己的代码替换 IDataObject.GetData
例程是唯一的出路。当然,这还涉及到需要复制 .NET Framework 中存在的该方法。幸运的是,有 Reflector 这样的工具可以反汇编 .NET Framework 代码,以便人们可以学习如何做一些有趣的事情!
历史
- V1.0 - 2008 年 1 月 23 日:初始发布
- V1.1 - 2008 年 1 月 28 日:更改了有效
TYMED
的列表以及 FxCop 建议的次要更改