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.