65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C# 中将虚拟文件传输到 Windows Explorer

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (28投票s)

2008年1月23日

CPOL

6分钟阅读

viewsIcon

212077

downloadIcon

2965

使用 C# 以及 CFSTR_FILECONTENTS 和 CFSTR_FILEDESCRIPTOR 格式将虚拟文件传输到 Windows 资源管理器的示例。

引言

我工作的公司销售一款实现自有文件服务器和运行时环境的产品。代码库很老,两年前启动了一个项目,使用 C# 和 .NET 2.0 创建新的接口。功能列表中的一个要求是能够在专有文件服务器环境和 Windows 资源管理器之间复制/剪切/拖放文件。通过使用 CF_HDROP 格式,将文件导入专有环境很容易。然而,尝试提取文件却遇到了问题。由于文件不存在于 Windows 环境中,CF_HDROP 格式不可用。此外,我们希望使用延迟渲染,以便除非绝对需要,否则不会从专有环境中提取文件,以减少开销。我找到的最好用于实现我想要的功能的格式是 CFSTR_FILEDESCRIPTORCFSTR_FILECONTENTS 格式。对互联网的广泛搜索没有发现任何关于在 C# 中实现此功能的示例,甚至有一些评论说这在托管语言中是不可能的。经过数周的研究,我终于找到了我在本文中介绍的代码。

Using the Code

此代码实现了一个名为 DataObjectEx 的类,该类派生自 System.Windows.Forms.DataObjectSystem.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_FILEDESCRIPTORCFSTR_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 字段。GetTymedUseableGetData 方法的一个支持方法,它利用类定义中定义的 私有 静态 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);

关注点

这是一个非常有趣的项目。在做这个项目的时候,我甚至不确定是否能够做到我想做的事情。由于没有任何关于使用这些格式的可用示例,这真是一个“自力更生”的场景。我很快就完成了大部分类,但在访问 FORMATETClindex 字段时遇到了困难。我花了许多头疼的时间尝试寻找覆盖、窥探、子类化等方法来访问该结构。最终发现,完全用自己的代码替换 IDataObject.GetData 例程是唯一的出路。当然,这还涉及到需要复制 .NET Framework 中存在的该方法。幸运的是,有 Reflector 这样的工具可以反汇编 .NET Framework 代码,以便人们可以学习如何做一些有趣的事情!

历史

  • V1.0 - 2008 年 1 月 23 日:初始发布
  • V1.1 - 2008 年 1 月 28 日:更改了有效 TYMED 的列表以及 FxCop 建议的次要更改
© . All rights reserved.