GetClipboardFiles

Jan Holan       08.10.2014       I/O operace       13047 zobrazení

Asi víte, že pomoci Windows schránky (Clipboard) lze přenášet z jedné aplikace do jiné i soubory. Pokud bychom toto chtěli používat v naší aplikaci, tak zjistíme, že podpora v .NET na to ale není úplně ideální. Koukneme jak soubory z Windows schránky v naší aplikaci získat.

Pokud zkopírujeme soubor, nebo více souborů do schránky, uloží se ve více formátech. Pokud si například vypíšeme obsažené formáty ve schránce pomoci:

System.Windows.Clipboard.GetDataObject().GetFormats()

tak dostaneme pro zkopírovaný soubor tento seznam formátů:

Shell IDList Array
DataObjectAttributes
DataObjectAttributesRequiringElevation
Shell Object Offsets
Preferred DropEffect
AsyncFlag
FileDrop
FileName
FileContents
FileNameW
FileGroupDescriptorW

Formát FileDrop

Jedním z dostupných formátu je formát FileDrop. Ten můžeme v .NETu jednoduše zpracovat pomoci metody GetFileDropList třídy Clipboard (třída je jak v namespace System.Windows tak v System.Windows.Forms).

Kód může vypadat nějak takto:

if (Clipboard.ContainsFileDropList())
{
    var fileDropList = Clipboard.GetFileDropList();
    if (fileDropList != null)
    {
        foreach (string item in fileDropList)
        {
            var fi = new System.IO.FileInfo(item);
            if (fi.Exists && (fi.Attributes & FileAttributes.Directory) == 0)    //Only files
            {
                using (var stream = fi.OpenRead())
                using (var fs = new FileStream(fi.Name, FileMode.Create))
                {
                    byte[] buffer = new byte[4096];
                    int bytesRead = 0;
                    while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        fs.Write(buffer, 0, bytesRead);
                    }
                }
            }
        }
    }
}

Všimněte si ale, že metoda z clipboardu z tohoto FileDrop formátu (CF_HDROP) vrátí pouze seznam souborů – string s lokální cestou na soubory. Zbytek, tj. načtení obsahu souboru pak již provádíme přímo pomoci System.IO operací s daným souborem.

Toto řešení tedy principiálně funguje pouze na zkopírované souborů, které jsou dostupné z filesystému (například zkopírování z Windows průzkumníku). To ovšem může být dosti nedostačující, protože existuje mnoho situací, kdy soubor není přes filesystém dostupný, nebo na něm vůbec není (například pokud zkopírujeme soubor přílohy z emailu v Outlook aplikaci).

Formát FileGroupDescriptorW a FileContents

Pokud zkopírujeme soubor(y) například z Windows průzkumníka, tak ho ale můžeme přes Windows clipboard přenést například na jiný počítač pomoci Remote Desktop (vzdálená plocha) připojení (neuvažuji staré Windows XP/2003, kde toto nefunguje). K tomu aby bylo možné soubor(y) takto přenést, tak ve schránce nesmí být pouze odkaz (cesta) na soubor, ale musí schránka obsahovat celý obsah (Content) zkopírovaných souborů.

K tomu využijeme uložené formáty FileGroupDescriptorW (CFSTR_FILEDESCRIPTORW) - Wide File Descriptor (Unicode) a FileContents (CFSTR_FILECONTENTS). První obsahuje seznam souborů a jejich vlastnosti (jméno, atributy, délku apod.), druhý pak už vlastní obsahy souborů.

Pozn.: Stejné formáty se používají nejen ve Windows schránce, ale například i při přenosu obsahu souborů mezi aplikacemi při Drag & Drop operaci (například přetažení souboru přílohy z Windows průzkumníku do Outlook email klienta).

Bohužel pro práci s těmito formáty není .NET podpora, a tak jejich zpracování musíme provádět sami pomoci volání Windows Win32 API a COM funkcí. Stručně popíši použitý postup.

Načtení seznamu souborů:

  • Načteme objekt typu DataObject: dataObject = (DataObject)Clipboard.GetDataObject() (tím ale .NET podpora končí)
  • Zkontrolujeme zda clipboard obsahuje požadovaný formát: dataObject.GetDataPresent("FileGroupDescriptorW")
  • Načteme jeho obsah: dataObject.GetData("FileGroupDescriptorW") as Stream
  • Ze streamu načteme seznam souborů a převedeme na hodnoty struktury FILEDESCRIPTOR: List<FILEDESCRIPTOR>

Načtení obsahu souboru:

  • Využijeme COM interface IDataObject a pomoci struktury FORMATETC načteme obsah formátu FileContents (podle indexu souboru):
var formatetc = new FORMATETC
{
    cfFormat = (short)DataFormats.GetDataFormat(NativeMethods.CFSTR_FILECONTENTS).Id,
    dwAspect = DVASPECT.DVASPECT_CONTENT,
    lindex = fileIndex,
    ptd = new IntPtr(0),
    tymed = TYMED.TYMED_ISTREAM
};

STGMEDIUM medium;
((System.Runtime.InteropServices.ComTypes.IDataObject)dataObject).GetData(ref formatetc, out medium);
  • Ze získané struktury STGMEDIUM načteme formát TYMED_ISTREAM – jedná se o COM stream dostupný přes COM interface IStream:
if (medium.tymed == TYMED.TYMED_ISTREAM)
{
    var mediumStream = (IStream)Marshal.GetTypedObjectForIUnknown(medium.unionmember, typeof(IStream));
    var streaWrapper = new ComStreamWrapper(mediumStream, FileAccess.Read, ComRelease.None);
    ...
}

Třída ClipboardUtils

Pro načtení obsahu souboru(ů) z Windows Clipboard jsem napsal metody ContainsClipboardFiles a GetClipboardFiles a umístil je do třídy ClipboardUtils.

Ty podporují načtení souborů jak z lokálního přístupu z FileDrop, tak načtení obsahu z FileGroupDescriptorW / FileContents. Informace o zkopírovaných souborech v clipboardu je vrácená v pomocné třídě ClipboardFileInfo, jedná se o obdobu známe třídy System.IO.FileInfo. Ta potom umožňuje načíst vlastní obsah souboru.

Celé načtení souborů s použitím třídy ClipboardUtils je pak hodně podobné, jako kdybychom prováděli načtení pomoci původní metody Clipboard.GetFileDropList.

foreach (ClipboardFileInfo item in ClipboardUtils.GetClipboardFiles())
{
    using (var stream = item.OpenRead())
    using (var fs = new FileStream(item.Name, FileMode.Create))
    {
        try
        {
            byte[] buffer = new byte[4096];
            int bytesRead = 0;
            while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
            {
                fs.Write(buffer, 0, bytesRead);
            }
        }
        catch (System.Runtime.InteropServices.COMException)
        {
            //Transfer error - ignore file
        }
    }
}

Třída ClipboardUtils:

/// <summary>
/// Helper class with Windows Clipboard functions
/// </summary>
internal static class ClipboardUtils
{
    #region NativeMethods class
    private static class NativeMethods
    {
        public const string CFSTR_FILEDESCRIPTORW = "FileGroupDescriptorW";

        [Flags]
        public enum FD : uint
        {
            FD_ACCESSTIME = 0x10,
            FD_ATTRIBUTES = 4,
            FD_CLSID = 1,
            FD_CREATETIME = 8,
            FD_FILESIZE = 0x40,
            FD_LINKUI = 0x8000,
            FD_SIZEPOINT = 2,
            FD_WRITESTIME = 0x20
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct FILEDESCRIPTOR    //Unicode FILEDESCRIPTORW
        {
            public FD 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;
        }
    }
    #endregion

    #region action methods
    public static bool ContainsClipboardFiles()
    {
        if (Clipboard.ContainsFileDropList())
        {
            return true;
        }

        var dataObject = (DataObject)Clipboard.GetDataObject();
        return dataObject.GetDataPresent(NativeMethods.CFSTR_FILEDESCRIPTORW);    //Wide File Descriptor
    }

    public static List<ClipboardFileInfo> GetClipboardFiles()
    {
        var list = new List<ClipboardFileInfo>();

        if (Clipboard.ContainsFileDropList())
        {
            var fileDropList = Clipboard.GetFileDropList();
            if (fileDropList != null && fileDropList.Count != 0)
            {
                foreach (string item in fileDropList)
                {
                    var fi = new System.IO.FileInfo(item);
                    if (fi.Exists && (fi.Attributes & FileAttributes.Directory) == 0)    //Only files
                    {
                        list.Add(new ClipboardFileInfo(fi));
                    }
                }
            }
        }
        else
        {
            var dataObject = (DataObject)Clipboard.GetDataObject();
            List<NativeMethods.FILEDESCRIPTOR> descriptors = GetFileDescriptors(dataObject);
            int fileIndex = -1;
            foreach (var descriptor in descriptors)
            {
                fileIndex++;

                if ((descriptor.dwFlags & NativeMethods.FD.FD_ATTRIBUTES) != 0 &&
                    ((FileAttributes)descriptor.dwFileAttributes & FileAttributes.Directory) == 0)    //Only files
                {
                    if (descriptors.Any(o => descriptor.cFileName.StartsWith(o.cFileName + "\\")))    //File in folder
                    {
                        continue;
                    }

                    long length = ((descriptor.nFileSizeHigh << 0x20) | (descriptor.nFileSizeLow & ((long) 0xffffffffL)));
                    list.Add(new ClipboardFileInfo(dataObject, fileIndex, descriptor.cFileName, (FileAttributes)descriptor.dwFileAttributes, length));
                }
            }
        }

        return list;
    }
    #endregion

    #region private member functions
    private static List<NativeMethods.FILEDESCRIPTOR> GetFileDescriptors(DataObject dataObject)
    {
        var list = new List<NativeMethods.FILEDESCRIPTOR>();
        if (dataObject.GetDataPresent(NativeMethods.CFSTR_FILEDESCRIPTORW))    //Wide File Descriptor
        {
            var obj2 = dataObject as System.Runtime.InteropServices.ComTypes.IDataObject;
            if (obj2 != null)
            {
                var input = dataObject.GetData(NativeMethods.CFSTR_FILEDESCRIPTORW) as Stream;
                if (input != null)
                {
                    int count = new BinaryReader(input).ReadInt32();
                    for (int i = 0; i < count; i++)
                    {
                        var descriptor = (NativeMethods.FILEDESCRIPTOR)ReadStructureFromStream(input, typeof(NativeMethods.FILEDESCRIPTOR));
                        list.Add(descriptor);
                    }
                }
            }
        }

        return list;
    }

    private static object ReadStructureFromStream(Stream source, Type structureType)
    {
        byte[] buffer = new byte[Marshal.SizeOf(structureType)];
        int readed = source.Read(buffer, 0, buffer.Length);
        if (readed == buffer.Length)
        {
            GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
            try
            {
                IntPtr ptr = handle.AddrOfPinnedObject();
                return Marshal.PtrToStructure(ptr, structureType);
            }
            finally
            {
                handle.Free();
            }
        }
        if (readed != 0)
        {
            throw new ArgumentException("source is too small to hold entire structure");
        }

        return null;
    }
    #endregion
}

Třída ClipboardFileInfo:

internal class ClipboardFileInfo
{
    #region NativeMethods class
    private static class NativeMethods
    {
        public const string CFSTR_FILECONTENTS = "FileContents";

        [DllImport("ole32.dll")]
        public static extern void ReleaseStgMedium([In] ref STGMEDIUM pmedium);
    }
    #endregion

    #region member varible and default property initialization
    private DataObject DataObject;
    private int FileIndex;

    public string FullName { get; private set; }
    public FileAttributes Attributes { get; private set; }
    public long Length { get; private set; }
    #endregion

    #region constructors and destructors
    public ClipboardFileInfo(System.IO.FileInfo fileInfo)
    {
        this.FullName = fileInfo.FullName;
        this.Attributes = fileInfo.Attributes;
        this.Length = fileInfo.Length;
    }

    public ClipboardFileInfo(DataObject dataObject, int fileIndex, string fullName, FileAttributes attributes, long length)
    {
        this.DataObject = dataObject;
        this.FileIndex = fileIndex;
        this.FullName = fullName;
        this.Attributes = attributes;
        this.Length = length;
    }
    #endregion

    #region action methods
    public Stream OpenRead()
    {
        if (this.DataObject == null) //Data from System.IO.FileInfo
        {
            return new FileStream(this.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, false);
        }

        //Read file from clipboard FileContents
        return ReadFromDataObject(this.DataObject, this.FileIndex);
    }
    #endregion

    #region property getters/setters
    public string Name
    {
        get { return System.IO.Path.GetFileName(this.FullName); }
    }

    public string Extension
    {
        get
        {
            int length = this.FullName.Length;
            int startIndex = length;

            while (--startIndex >= 0)
            {
                char ch = this.FullName[startIndex];
                if (ch == '.')
                {
                    return this.FullName.Substring(startIndex, length - startIndex);
                }
                if (ch == Path.DirectorySeparatorChar || ch == Path.AltDirectorySeparatorChar || ch == Path.VolumeSeparatorChar)
                {
                    break;
                }
            }

            return string.Empty;
        }
    }
    #endregion

    #region private member functions
    private static Stream ReadFromDataObject(DataObject dataObject, int fileIndex)
    {
        var formatetc = new FORMATETC
        {
            cfFormat = (short)DataFormats.GetDataFormat(NativeMethods.CFSTR_FILECONTENTS).Id,
            dwAspect = DVASPECT.DVASPECT_CONTENT,
            lindex = fileIndex,
            ptd = new IntPtr(0),
            tymed = TYMED.TYMED_ISTREAM
        };

        STGMEDIUM medium;
        System.Runtime.InteropServices.ComTypes.IDataObject obj2 = dataObject;
        obj2.GetData(ref formatetc, out medium);

        try
        {
            if (medium.tymed == TYMED.TYMED_ISTREAM)
            {
                var mediumStream = (IStream)Marshal.GetTypedObjectForIUnknown(medium.unionmember, typeof(IStream));
                Marshal.Release(medium.unionmember);

                var streaWrapper = new ComStreamWrapper(mediumStream, FileAccess.Read, ComRelease.None);

                streaWrapper.Closed += delegate(object sender, EventArgs e)
                {
                    NativeMethods.ReleaseStgMedium(ref medium);
                    Marshal.FinalReleaseComObject(mediumStream);
                };

                return streaWrapper;
            }

            throw new NotSupportedException(string.Format("Unsupported STGMEDIUM.tymed ({0})", medium.tymed));
        }
        catch
        {
            NativeMethods.ReleaseStgMedium(ref medium);
            throw;
        }
    }
    #endregion
}

Celý soubor ClipboardUtils.cs je ke stažení zde.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback