You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 8 Windows平台多文件剪贴板延迟渲染实现难题

实现方案:自定义COM IDataObject实现延迟多文件剪贴板数据

核心思路

Windows剪贴板的多文件传输依赖CFSTR_FILEDESCRIPTORW(文件描述符组)和CFSTR_FILECONTENTS(文件内容)两个格式。当粘贴端请求多文件内容时,会通过FORMATETC.lindex指定要获取的文件索引。.NET封装的DataObject类没有暴露这个索引,所以必须直接实现完整的COM IDataObject接口,直接处理FORMATETC结构体才能解决多文件场景的问题。

实现步骤与代码

1. 定义必要的常量和结构体

对应Windows原生的文件描述符结构体定义:

using System.Runtime.InteropServices;
using System.Drawing;

// 剪贴板格式常量
public static class ClipboardFormats
{
    public const string CFSTR_FILEDESCRIPTORW = "FileGroupDescriptorW";
    public const string CFSTR_FILECONTENTS = "FileContents";
}

// FILEDESCRIPTORW 结构体(匹配Win32定义)
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct FILEDESCRIPTORW
{
    public uint dwFlags;
    public Guid clsid;
    public Size sizel;
    public Point pointl;
    public uint dwFileAttributes;
    public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
    public uint nFileSizeHigh;
    public uint nFileSizeLow;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string cFileName;
}

// FILEGROUPDESCRIPTORW 结构体(匹配Win32定义)
[StructLayout(LayoutKind.Sequential)]
public struct FILEGROUPDESCRIPTORW
{
    public uint cItems;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] // 动态长度,用1占位
    public FILEDESCRIPTORW[] fgd;
}

2. 实现自定义IDataObject类

重点在GetData方法中解析FORMATETC.lindex,返回对应索引的文件内容流:

public class LazyMultiFileDataObject : System.Runtime.InteropServices.ComTypes.IDataObject
{
    private readonly List<(string FileName, Func<Stream> GetContentStream)> _files = new();

    public LazyMultiFileDataObject(List<(string, Func<Stream>)> files)
    {
        _files.AddRange(files);
    }

    public void GetData(ref System.Runtime.InteropServices.ComTypes.FORMATETC formatetc, out System.Runtime.InteropServices.ComTypes.STGMEDIUM medium)
    {
        medium = new System.Runtime.InteropServices.ComTypes.STGMEDIUM();
        try
        {
            // 处理文件描述符组请求
            var fdFormatId = (ushort)NativeMethods.RegisterClipboardFormat(ClipboardFormats.CFSTR_FILEDESCRIPTORW);
            if (formatetc.cfFormat == fdFormatId)
            {
                // 动态分配内存存储文件描述符组
                int totalSize = Marshal.SizeOf<FILEGROUPDESCRIPTORW>() + (_files.Count - 1) * Marshal.SizeOf<FILEDESCRIPTORW>();
                IntPtr fgdPtr = Marshal.AllocCoTaskMem(totalSize);

                var fgd = new FILEGROUPDESCRIPTORW
                {
                    cItems = (uint)_files.Count,
                    fgd = new FILEDESCRIPTORW[_files.Count]
                };

                for (int i = 0; i < _files.Count; i++)
                {
                    fgd.fgd[i] = new FILEDESCRIPTORW
                    {
                        cFileName = _files[i].FileName,
                        dwFileAttributes = (uint)FileAttributes.Normal,
                        ftLastWriteTime = DateTime.Now.ToFileTimeStruct()
                    };
                }

                Marshal.StructureToPtr(fgd, fgdPtr, false);
                medium.tymed = System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL;
                medium.unionmember = fgdPtr;
                medium.pUnkForRelease = null;
                return;
            }

            // 处理文件内容请求,通过lindex获取对应文件索引
            var fcFormatId = (ushort)NativeMethods.RegisterClipboardFormat(ClipboardFormats.CFSTR_FILECONTENTS);
            if (formatetc.cfFormat == fcFormatId && formatetc.lindex >= 0 && formatetc.lindex < _files.Count)
            {
                using var contentStream = _files[(int)formatetc.lindex].GetContentStream();
                var buffer = new byte[contentStream.Length];
                contentStream.Read(buffer, 0, buffer.Length);

                IntPtr hGlobal = Marshal.AllocCoTaskMem(buffer.Length);
                Marshal.Copy(buffer, 0, hGlobal, buffer.Length);

                medium.tymed = System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL;
                medium.unionmember = hGlobal;
                medium.pUnkForRelease = null;
                return;
            }

            throw new NotImplementedException($"不支持的格式: {formatetc.cfFormat}");
        }
        catch
        {
            ReleaseStgMedium(ref medium);
            throw;
        }
    }

    // 辅助方法:释放STGMEDIUM资源
    private void ReleaseStgMedium(ref System.Runtime.InteropServices.ComTypes.STGMEDIUM medium)
    {
        if (medium.unionmember != IntPtr.Zero && medium.tymed == System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL)
        {
            Marshal.FreeCoTaskMem(medium.unionmember);
            medium.unionmember = IntPtr.Zero;
        }
    }

    // 实现IDataObject的其他方法
    public void GetDataHere(ref System.Runtime.InteropServices.ComTypes.FORMATETC formatetc, ref System.Runtime.InteropServices.ComTypes.STGMEDIUM medium)
        => throw new NotImplementedException();

    public bool QueryGetData(ref System.Runtime.InteropServices.ComTypes.FORMATETC formatetc)
    {
        var fdFormat = (uint)NativeMethods.RegisterClipboardFormat(ClipboardFormats.CFSTR_FILEDESCRIPTORW);
        var fcFormat = (uint)NativeMethods.RegisterClipboardFormat(ClipboardFormats.CFSTR_FILECONTENTS);
        return (formatetc.cfFormat == fdFormat && formatetc.tymed == System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL) ||
               (formatetc.cfFormat == fcFormat && formatetc.lindex >= 0 && formatetc.lindex < _files.Count && formatetc.tymed == System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL);
    }

    public void GetCanonicalFormatEtc(ref System.Runtime.InteropServices.ComTypes.FORMATETC formatetcIn, out System.Runtime.InteropServices.ComTypes.FORMATETC formatetcOut)
    {
        formatetcOut = formatetcIn;
        formatetcOut.dwAspect = System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT;
    }

    public void SetData(ref System.Runtime.InteropServices.ComTypes.FORMATETC formatetc, ref System.Runtime.InteropServices.ComTypes.STGMEDIUM medium, bool release)
        => throw new NotImplementedException();

    public System.Runtime.InteropServices.ComTypes.IEnumFORMATETC EnumFormatEtc(System.Runtime.InteropServices.ComTypes.DATADIR dir)
    {
        var formats = new List<System.Runtime.InteropServices.ComTypes.FORMATETC>();
        var fdFormat = (ushort)NativeMethods.RegisterClipboardFormat(ClipboardFormats.CFSTR_FILEDESCRIPTORW);
        formats.Add(new System.Runtime.InteropServices.ComTypes.FORMATETC
        {
            cfFormat = fdFormat,
            tymed = System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL,
            dwAspect = System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT
        });

        var fcFormat = (ushort)NativeMethods.RegisterClipboardFormat(ClipboardFormats.CFSTR_FILECONTENTS);
        for (int i = 0; i < _files.Count; i++)
        {
            formats.Add(new System.Runtime.InteropServices.ComTypes.FORMATETC
            {
                cfFormat = fcFormat,
                lindex = i,
                tymed = System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL,
                dwAspect = System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT
            });
        }
        return new FormatEnum(formats.ToArray());
    }

    public int DAdvise(ref System.Runtime.InteropServices.ComTypes.FORMATETC formatetc, System.Runtime.InteropServices.ComTypes.ADVF advf, System.Runtime.InteropServices.ComTypes.IAdviseSink adviseSink, out int connection)
    {
        connection = 0;
        return unchecked((int)0x80004001); // E_NOTIMPL
    }

    public void DUnadvise(int connection) => throw new NotImplementedException();

    public int EnumDAdvise(out System.Runtime.InteropServices.ComTypes.IEnumSTATDATA ppenumAdvise)
    {
        ppenumAdvise = null;
        return unchecked((int)0x80004001); // E_NOTIMPL
    }
}

// 辅助类:FORMATETC枚举器
public class FormatEnum : System.Runtime.InteropServices.ComTypes.IEnumFORMATETC
{
    private readonly System.Runtime.InteropServices.ComTypes.FORMATETC[] _formats;
    private int _currentIndex = 0;

    public FormatEnum(System.Runtime.InteropServices.ComTypes.FORMATETC[] formats)
    {
        _formats = formats;
    }

    public int Next(int celt, System.Runtime.InteropServices.ComTypes.FORMATETC[] rgelt, int[] pceltFetched)
    {
        int fetched = 0;
        while (_currentIndex < _formats.Length && fetched < celt)
        {
            rgelt[fetched] = _formats[_currentIndex];
            fetched++;
            _currentIndex++;
        }
        pceltFetched?[0] = fetched;
        return fetched == celt ? 0 : unchecked((int)0x80004004); // S_FALSE
    }

    public int Skip(int celt)
    {
        _currentIndex += celt;
        return _currentIndex >= _formats.Length ? unchecked((int)0x80004004) : 0;
    }

    public int Reset()
    {
        _currentIndex = 0;
        return 0;
    }

    public void Clone(out System.Runtime.InteropServices.ComTypes.IEnumFORMATETC ppenum)
    {
        ppenum = new FormatEnum(_formats);
        ((FormatEnum)ppenum)._currentIndex = _currentIndex;
    }
}

// 原生Win32方法封装
internal static class NativeMethods
{
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern uint RegisterClipboardFormat(string lpszFormat);

    [DllImport("ole32.dll")]
    public static extern int OleSetClipboard(System.Runtime.InteropServices.ComTypes.IDataObject pDataObj);

    [DllImport("ole32.dll")]
    public static extern int OleFlushClipboard();
}

// DateTime扩展方法:转换为FILETIME结构体
internal static class DateTimeExtensions
{
    public static System.Runtime.InteropServices.ComTypes.FILETIME ToFileTimeStruct(this DateTime dt)
    {
        long fileTime = dt.ToFileTime();
        return Marshal.PtrToStructure<System.Runtime.InteropServices.ComTypes.FILETIME>(Marshal.AllocCoTaskMem(8));
    }
}

3. 剪贴板操作工具类

使用原生OleSetClipboard设置自定义数据对象:

public static class ClipboardHelper
{
    public static void SetLazyMultiFileData(List<(string FileName, Func<Stream> GetContentStream)> files)
    {
        if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA)
        {
            throw new InvalidOperationException("剪贴板操作必须在STA线程中执行");
        }

        var dataObj = new LazyMultiFileDataObject(files);
        NativeMethods.OleSetClipboard(dataObj);
    }

    public static void ClearClipboard()
    {
        NativeMethods.OleFlushClipboard();
    }
}

4. 使用示例

[STAThread]
static void Main(string[] args)
{
    // 模拟延迟加载的文件列表:文件名 + 内容生成委托
    var files = new List<(string, Func<Stream>)>
    {
        ("文档1.txt", () => new MemoryStream(Encoding.UTF8.GetBytes("这是延迟加载的文档1内容"))),
        ("报表.xlsx", () => File.OpenRead(@"C:\Temp\模板.xlsx")),
        ("日志文件.log", () => new MemoryStream(Encoding.UTF8.GetBytes("这是延迟加载的日志内容")))
    };

    ClipboardHelper.SetLazyMultiFileData(files);
    Console.WriteLine("多文件已放入剪贴板,请尝试粘贴到资源管理器");
    Console.ReadLine();
    ClipboardHelper.ClearClipboard();
}

关键注意事项

  • STA线程要求:所有剪贴板操作必须在STA(单线程单元)线程中执行,否则会出现COM交互错误。
  • 内存管理:所有通过Marshal.AllocCoTaskMem分配的内存必须正确释放,避免内存泄漏。
  • 流兼容性:自定义内容流必须支持完整的读取操作,粘贴端会依赖流内容生成文件。
  • 结构体一致性:FILEDESCRIPTORW和FILEGROUPDESCRIPTORW的定义必须与Windows原生完全匹配,否则粘贴端无法解析文件描述符。

内容的提问来源于stack exchange,提问作者TheNextman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 21:54:58