Задержка рендеринга нескольких файлов в буфере обмена

Работая в .NET 8 в Windows (net8.0-windows), я хотел бы поместить несколько файлов в буфер обмена, но предоставлять фактические данные только после вставки пользователем.

Это кажется простым: я создаю подкласс DataObject и переопределяю функцию GetData, обрабатывая два соответствующих формата («FileGroupDescriptorW» и «FileContents»):

private class MyDataObject : DataObject
{
    public override object GetData(string format, bool autoConvert)
    {
        if (string.Compare(format, CFSTR_FILEDESCRIPTORW, StringComparison.OrdinalIgnoreCase) == 0)
        {
            MemoryStream ms = // Create a FileGroupDescriptorW, get the bytes and create a memory stream
            base.SetData(CFSTR_FILEDESCRIPTORW, ms);
        }
        else if (string.Compare(format, CFSTR_FILECONTENTS, StringComparison.OrdinalIgnoreCase) == 0)
        {      
            // pseudo-code, I have a stream implementation that will provide the file bytes
            base.SetData(CFSTR_FILECONTENTS, new MyVirtualFileStream());
        }

    return base.GetData(format, autoConvert);
}

И я просто помещаю свой объект данных в буфер обмена:

MyDataObject appDataObject = new();
appDataObject.SetData(CFSTR_FILEDESCRIPTORW, null);
appDataObject.SetData(CFSTR_FILECONTENTS, null);
System.Windows.Forms.Clipboard.SetDataObject(appDataObject);

Все идет нормально; и это отлично работает, если у меня есть один файл. Я вставляю в проводник Windows, и мой файл вставляется успешно.

Но что, если у меня несколько файлов? т. е. FileGroupDescriptorW имеет счетчик больше 1. Невозможно получить индекс, запрошенный в GetData или любой из его перегрузок.

В COM-эквиваленте IDataObjectFORMATETC есть свойство lindex, которое предоставляет эту информацию, но похоже, что в .NET нет ничего эквивалентного. Действительно ли это невозможно в управляемом коде?

Я ищу четкий пример того, как это реализовать.

Может ли кто-нибудь предложить путь вперед, как этого добиться?

Посмотрите, сможете ли вы получить пользу от этой статьи. В частности, как обрабатывать несколько файлов. Операции перетаскивания и копирования/вставки обрабатываются одинаково.

dr.null 30.08.2024 06:52

Спасибо за ссылку @dr.null, посмотрю!

TheNextman 30.08.2024 12:20
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
75
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Объекты данных, предоставляемые .NET (WPF и Winforms, и, кстати, это не одно и то же, что немного глупо...) действительно предназначены для .NET, включая объекты .NET и т. д. они не предназначены для буфера обмена общего назначения или операции копирования-вставки.

Вот пример кода C#, который позволяет добавлять потоковые дескрипторы файлов по требованию в собственный собственный объект IDataObject, который сам может быть добавлен в буфер обмена (или во что-то еще). Он поддерживает вставку из Проводника, из приложений Office (Outlook, Word) и т. д.:

using System;
using System.Collections.Generic;
using System.Drawing; // just used for SIZE and POINT struct that could be rewritten if Winforms is not desired
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Windows.Forms; // just used for MessageBox in caller's code

namespace OnDemandDataObject;

internal class Program
{
    [STAThread] // calling thread must be STA
    static void Main()
    {
        using var da = new DataObject();
        da.SetOnDemandFiles([
            FileDescriptor.FromFile(@"d:\temp\first.pdf"),
            FileDescriptor.FromFile(@"d:\temp\second.txt")
            // etc.
            ]);
        da.SetToClipboard();
        // runs message pump in this console app sample
        MessageBox.Show("Press ok to remove files from the clipboard"); 
    }
}

public class FileDescriptor()
{
    // this is what's called on-demand when a stream is asked for reading (on paste action)
    public virtual Func<Stream>? GetStream { get; set; } 
    public virtual FILEDESCRIPTOR Descriptor { get; set; }

    // utility to create a descriptor from a file
    // but could be adapted for network streams, etc.
    public static FileDescriptor FromFile(string filePath, Func<Stream>? getStream = null) { ArgumentNullException.ThrowIfNull(filePath); return FromFile(new FileInfo(filePath), getStream); }
    public static FileDescriptor FromFile(FileInfo info, Func<Stream>? getStream = null)
    {
        ArgumentNullException.ThrowIfNull(info);
        var fd = new FileDescriptor();
        fd.GetStream ??= () => new FileStream(info.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        fd.Descriptor = FILEDESCRIPTOR.FromFileInfo(info);
        return fd;
    }
}

public class DataObject : DataObject.IDataObject, IDisposable
{
    private readonly List<(FORMATETC, STGMEDIUM, bool)> _data = [];
    private bool _disposedValue;

    // can't use Winforms Clipboard.SetData with a custom IDataObject
    public virtual void SetToClipboard()
    {
        OleInitialize(0);
        OleSetClipboard(this);
    }

    public virtual void SetOnDemandFiles(IReadOnlyList<FileDescriptor> files)
    {
        ArgumentNullException.ThrowIfNull(files);
        if (files.Count == 0)
            return;

        if (files.Any(d => d.GetStream == null))
            throw new ArgumentException(null, nameof(files));

        // build CFSTR_FILEDESCRIPTORW
        var elementSize = Marshal.SizeOf<FILEDESCRIPTOR>();
        var size = Marshal.SizeOf<int>() + elementSize * files.Count;
        var fileDescriptorsPtr = Marshal.AllocHGlobal(size);
        var current = fileDescriptorsPtr;
        Marshal.WriteInt32(current, files.Count);
        current += Marshal.SizeOf<int>();
        foreach (var file in files)
        {
            Marshal.StructureToPtr(file.Descriptor, current, false);
            current += elementSize;
        }

        try
        {
            // set CFSTR_FILEDESCRIPTORW
            var fmt = new FORMATETC { dwAspect = DVASPECT.DVASPECT_CONTENT, cfFormat = (short)RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW), lindex = -1, tymed = TYMED.TYMED_HGLOBAL };
            var medium = new STGMEDIUM { tymed = fmt.tymed, unionmember = fileDescriptorsPtr };
            ((IDataObject)this).SetData(ref fmt, ref medium, true);

            // set all CFSTR_FILECONTENTS
            var format = RegisterClipboardFormat(CFSTR_FILECONTENTS);
            fmt = new FORMATETC { dwAspect = DVASPECT.DVASPECT_CONTENT, cfFormat = (short)format, tymed = TYMED.TYMED_ISTREAM };
            medium = new STGMEDIUM { tymed = fmt.tymed };
            for (var i = 0; i < files.Count; i++)
            {
                fmt.lindex = i;
                var stream = new ReadStream(files[i]);
                var unk = Marshal.GetComInterfaceForObject(stream, typeof(IStream));
                try
                {
                    medium.unionmember = unk;
                    medium.pUnkForRelease = stream;
                    ((IDataObject)this).SetData(ref fmt, ref medium, true);
                }
                finally
                {
                    Marshal.Release(unk);
                }
            }
        }
        catch // free only on error
        {
            Marshal.FreeHGlobal(fileDescriptorsPtr);
            throw;
        }
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                foreach (var data in _data.Where(d => d.Item3))
                {
                    var medium = data.Item2;
                    ReleaseStgMedium(ref medium);
                }
                _data.Clear();
            }
            _disposedValue = true;
        }
    }

    ~DataObject() { Dispose(disposing: false); }
    public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); }

    int IDataObject.GetData(ref FORMATETC pformatetcIn, out STGMEDIUM pmedium)
    {
        foreach (var data in _data)
        {
            if (data.Item1.cfFormat == pformatetcIn.cfFormat && data.Item1.lindex == pformatetcIn.lindex)
            {
                var medium = data.Item2;
                return CopyStgMediumOut(ref medium, out pmedium);
            }
        }

        pmedium = new();
        return DV_E_FORMATETC;
    }

    int IDataObject.GetDataHere(ref FORMATETC pformatetcIn, ref STGMEDIUM pmedium)
    {
        foreach (var data in _data)
        {
            if (data.Item1.cfFormat == pformatetcIn.cfFormat && data.Item1.lindex == pformatetcIn.lindex)
            {
                var medium = data.Item2;
                medium.pUnkForRelease = 0;
                return CopyStgMediumRef(ref medium, ref pmedium);
            }
        }
        return DV_E_FORMATETC;
    }

    int IDataObject.QueryGetData(ref FORMATETC pformatetc)
    {
        foreach (var data in _data)
        {
            if (data.Item1.cfFormat == pformatetcIn.cfFormat && data.Item1.lindex == pformatetcIn.lindex)
                return 0;
        }
        return DV_E_FORMATETC;
    }

    int IDataObject.SetData(ref FORMATETC pformatetc, ref STGMEDIUM pmedium, bool fRelease)
    {
        foreach (var data in _data.ToArray())
        {
            if (data.Item1.cfFormat == pformatetc.cfFormat && data.Item1.lindex == pformatetc.lindex)
            {
                _data.Remove(data);
            }
        }
        _data.Add((pformatetc, pmedium, fRelease));
        return 0;
    }

    int IDataObject.GetCanonicalFormatEtc(ref FORMATETC pformatectIn, out FORMATETC pformatetcOut) => throw new NotImplementedException();
    int IDataObject.EnumFormatEtc(DATADIR dwDirection, out IEnumFORMATETC ppenumFormatEtc) { ppenumFormatEtc = new EnumFORMATETC(this, dwDirection); return 0; }
    int IDataObject.DAdvise(ref FORMATETC pformatetc, uint advf, IAdviseSink pAdvSink, out uint dwConnection) => throw new NotImplementedException();
    int IDataObject.DUnadvise(uint dwConnection) => throw new NotImplementedException();
    int IDataObject.EnumDAdvise(out IEnumSTATDATA ppenumAdvise) => throw new NotImplementedException();

    private sealed class EnumFORMATETC(DataObject dataObject, DATADIR direction) : IEnumFORMATETC
    {
        public int Index { get; set; }
        public int Next(int celt, FORMATETC[] rgelt, int[] pceltFetched)
        {
            if (pceltFetched != null) { pceltFetched[0] = 0; }
            if (Index >= dataObject._data.Count)
                return 1;

            var fetched = 0;
            while (fetched < celt && fetched < dataObject._data.Count)
            {
                rgelt[fetched] = dataObject._data[Index].Item1;
                Index++;
                fetched++;
            }

            if (pceltFetched != null) { pceltFetched[0] = fetched; }
            return fetched == celt ? 0 : 1;
        }

        public int Reset() { Index = 0; return 0; }
        public void Clone(out IEnumFORMATETC newEnum) => newEnum = new EnumFORMATETC(dataObject, direction);
        public int Skip(int celt) => throw new NotImplementedException();
    }

    private sealed class ReadStream : IStream
    {
        private readonly FileDescriptor _descriptor;
        private readonly Lazy<Stream> _stream;

        public ReadStream(FileDescriptor descriptor)
        {
            _descriptor = descriptor;
            _stream = new Lazy<Stream>(() => _descriptor.GetStream!() ?? throw new InvalidOperationException());
        }

        private Stream Stream => _stream.Value;

        // Explorer calls here
        void IStream.Read(byte[] pv, int cb, nint pcbRead)
        {
            var read = Stream.Read(pv, 0, cb);
            if (pcbRead != 0) { Marshal.WriteInt32(pcbRead, read); }
        }

        void IStream.Seek(long dlibMove, int dwOrigin, nint plibNewPosition)
        {
            var newPos = Stream.Seek(dlibMove, (SeekOrigin)dwOrigin);
            if (plibNewPosition != 0) { Marshal.WriteInt64(plibNewPosition, newPos); }
        }

        public void Stat(out STATSTG pstatstg, int grfStatFlag)
        {
            const int STGTY_STREAM = 2;
            const int STGM_READWRITE = 2;
            const int STGM_WRITE = 1;
            var stream = Stream;
            pstatstg = new STATSTG { type = STGTY_STREAM, cbSize = stream.Length, };

            const int STATFLAG_NONAME = 1;
            if ((grfStatFlag & STATFLAG_NONAME) == 0) pstatstg.pwcsName = _descriptor.Descriptor.cFileName;
            pstatstg.atime = ToFileTime(_descriptor.Descriptor.ftLastAccessTime);
            pstatstg.ctime = ToFileTime(_descriptor.Descriptor.ftCreationTime);
            pstatstg.mtime = ToFileTime(_descriptor.Descriptor.ftLastWriteTime);
            pstatstg.clsid = _descriptor.Descriptor.clsid;

            if (stream.CanRead && stream.CanWrite)
            {
                pstatstg.grfMode |= STGM_READWRITE;
                return;
            }

            if (stream.CanWrite) { pstatstg.grfMode |= STGM_WRITE; }
        }

        // Office (outlook, word, etc.) calls here
        public void CopyTo(IStream pstm, long cb, nint pcbRead, nint pcbWritten)
        {
            ArgumentNullException.ThrowIfNull(pstm);
            var count = 0L;
            var bytes = new byte[0x14000]; // 81920 under loh
            do
            {
                var max = (int)Math.Min(cb - count, bytes.Length);
                var read = Stream.Read(bytes, 0, max);
                if (read == 0)
                    break;

                pstm.Write(bytes, read, 0);
                count += read;
                if (count == cb)
                    break;
            }
            while (true);

            if (pcbRead != 0) Marshal.WriteInt64(pcbRead, count);
            if (pcbWritten != 0) Marshal.WriteInt64(pcbWritten, count);

            pstm.Commit(0); // STGC_DEFAULT
            Marshal.FinalReleaseComObject(pstm); // we must do this otherwise Office doesn't like it
        }

        public void Commit(int grfCommitFlags) => Stream.Flush();
        void IStream.Write(byte[] pv, int cb, nint pcbWritten) => throw new NotImplementedException();
        void IStream.SetSize(long libNewSize) => throw new NotImplementedException();
        void IStream.Revert() => throw new NotImplementedException();
        void IStream.LockRegion(long libOffset, long cb, int dwLockType) => throw new NotImplementedException();
        void IStream.UnlockRegion(long libOffset, long cb, int dwLockType) => throw new NotImplementedException();
        void IStream.Clone(out IStream ppstm) => throw new NotImplementedException();

        private static FILETIME ToFileTime(long fileTime) => new() { dwLowDateTime = (int)(fileTime & uint.MaxValue), dwHighDateTime = (int)(fileTime >> 32) };
    }

    private const int DV_E_FORMATETC = unchecked((int)0x80040064);
    private const string CFSTR_FILEDESCRIPTORW = "FileGroupDescriptorW";
    private const string CFSTR_FILECONTENTS = "FileContents";

    [DllImport("user32", CharSet = CharSet.Unicode)]
    private static extern int RegisterClipboardFormat(string format);

    [DllImport("ole32")]
    private static extern int OleSetClipboard(IDataObject pDataObj);

    [DllImport("ole32")]
    private static extern int OleInitialize(nint pvReserved);

    [DllImport("ole32")]
    private static extern void ReleaseStgMedium(ref STGMEDIUM medium);

    [DllImport("urlmon", EntryPoint = "CopyStgMedium")]
    private static extern int CopyStgMediumOut(ref STGMEDIUM pcstgmedSrc, out STGMEDIUM pstgmedDest);

    [DllImport("urlmon", EntryPoint = "CopyStgMedium")]
    private static extern int CopyStgMediumRef(ref STGMEDIUM pcstgmedSrc, ref STGMEDIUM pstgmedDest);

    [ComImport, Guid("0000010E-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IDataObject // redefined so we don't need to throw
    {
        [PreserveSig]
        int GetData(ref FORMATETC pformatetcIn, out STGMEDIUM pmedium);
        [PreserveSig]
        int GetDataHere(ref FORMATETC pformatetcIn, ref STGMEDIUM pmedium);
        [PreserveSig]
        int QueryGetData(ref FORMATETC pformatetc);
        [PreserveSig]
        int GetCanonicalFormatEtc(ref FORMATETC pformatectIn, out FORMATETC pformatetcOut);
        [PreserveSig]
        int SetData(ref FORMATETC pformatetc, ref STGMEDIUM pmedium, bool fRelease);
        [PreserveSig]
        int EnumFormatEtc(DATADIR dwDirection, out IEnumFORMATETC ppenumFormatEtc);
        [PreserveSig]
        int DAdvise(ref FORMATETC pformatetc, uint advf, IAdviseSink pAdvSink, out uint dwConnection);
        [PreserveSig]
        int DUnadvise(uint dwConnection);
        [PreserveSig]
        int EnumDAdvise(out IEnumSTATDATA ppenumAdvise);
    }
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct FILEDESCRIPTOR
{
    public FD dwFlags;
    public Guid clsid;
    public Size sizel;
    public Point pointl;
    public FileAttributes dwFileAttributes;
    public long ftCreationTime;
    public long ftLastAccessTime;
    public long ftLastWriteTime;
    public uint nFileSizeHigh;
    public uint nFileSizeLow;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string cFileName;
    public override readonly string ToString() => cFileName;

    public static FILEDESCRIPTOR FromFileInfo(FileInfo info)
    {
        ArgumentNullException.ThrowIfNull(info);
        var fd = new FILEDESCRIPTOR { dwFlags = FD.FD_UNICODE, cFileName = info.Name };
        if (info.Exists)
        {
            fd.dwFlags |= FD.FD_FILESIZE | FD.FD_ATTRIBUTES;
            fd.nFileSizeLow = (uint)(info.Length & uint.MaxValue);
            fd.nFileSizeHigh = (uint)(info.Length >> 32);
            fd.dwFileAttributes = info.Attributes;
            if (IsValidFileTime(info.CreationTimeUtc))
            {
                fd.ftCreationTime = info.CreationTimeUtc.ToFileTimeUtc();
                fd.dwFlags |= FD.FD_CREATETIME;
            }
            if (IsValidFileTime(info.LastAccessTimeUtc))
            {
                fd.ftLastAccessTime = info.LastAccessTimeUtc.ToFileTimeUtc();
                fd.dwFlags |= FD.FD_ACCESSTIME;
            }
            if (IsValidFileTime(info.LastWriteTimeUtc))
            {
                fd.ftLastWriteTime = info.LastWriteTimeUtc.ToFileTimeUtc();
                fd.dwFlags |= FD.FD_WRITESTIME;
            }
        }

        const long fileTimeOffset = 504911232000000000; // daysTo1601 * ticksPerDay;
        static long ToFileTime(DateTime dt) => (dt.Kind != DateTimeKind.Utc ? dt.ToUniversalTime().Ticks : dt.Ticks) - fileTimeOffset;
        static bool IsValidFileTime(DateTime dt) => ToFileTime(dt) >= 0;
        return fd;
    }
}

[Flags]
public enum FD
{
    FD_CLSID = 0x00000001,
    FD_SIZEPOINT = 0x00000002,
    FD_ATTRIBUTES = 0x00000004,
    FD_CREATETIME = 0x00000008,
    FD_ACCESSTIME = 0x00000010,
    FD_WRITESTIME = 0x00000020,
    FD_FILESIZE = 0x00000040,
    FD_PROGRESSUI = 0x00004000,
    FD_LINKUI = 0x00008000,
    FD_UNICODE = unchecked((int)0x80000000),
}

Мне пришлось адаптировать это к моему конкретному случаю, но после некоторых первоначальных тестов все работает отлично. Спасибо за отличный и подробный ответ.

TheNextman 03.09.2024 21:33

Другие вопросы по теме