diff --git a/Windows.Shell/ShellDataObject.cs b/Windows.Shell/ShellDataObject.cs
index 8e87a07b..d61caa30 100644
--- a/Windows.Shell/ShellDataObject.cs
+++ b/Windows.Shell/ShellDataObject.cs
@@ -1,80 +1,412 @@
using System;
using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
-using System.Text;
using System.Windows.Forms;
using Vanara.Extensions;
+using Vanara.InteropServices;
+using Vanara.PInvoke;
using static Vanara.PInvoke.Ole32;
using static Vanara.PInvoke.Shell32;
using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject;
namespace Vanara.Windows.Shell
{
- // TODO
- internal static class DataObjectExtensions
+ /// Shell extended .
+ // TODO: Finish adding ShellClipboardFormat handling, tests and release
+ class ShellDataObject : DataObject
{
- public static IReadOnlyList GetFileNameMap(this DataObject dobj)
+ /// Initializes a new instance of the class.
+ public ShellDataObject() : base()
{
- var l = new List();
- if (dobj.GetDataPresent(ShellDataFormat.FileNameMap.Id()))
- {
- if (dobj.GetData(ShellDataFormat.FileNameMap.Id(), true) is string[] data)
- l.AddRange(data);
- }
- return (IReadOnlyList)l;
}
- public static DROPEFFECT GetPreferredDropEffect(this DataObject dobj)
+ /// Initializes a new instance of the class and adds the specified object to it.
+ /// The data to store.
+ public ShellDataObject(object data) : base(data)
{
- dobj.GetData(typeof(uint));
- var eff = DROPEFFECT.DROPEFFECT_NONE;
- if (dobj is IComDataObject cdo)
- {
- var fc = MakeFORMATETC(ShellClipboardFormat.CFSTR_PREFERREDDROPEFFECT);
- try
- {
- cdo.GetData(ref fc, out var medium);
- if (medium.unionmember != default)
- eff = (DROPEFFECT)medium.unionmember.ToStructure();
- ReleaseStgMedium(medium);
- }
- catch { }
- }
- return eff;
}
- public static IReadOnlyCollection GetShellIdList(this DataObject dobj)
+ ///
+ /// Initializes a new instance of the class and adds the specified object in the specified format.
+ ///
+ ///
+ /// The format of the specified data. See for predefined formats.
+ ///
+ /// The data to store.
+ public ShellDataObject(string format, object data) : base(format, data)
{
- var l = new List();
- if (dobj is IComDataObject cdo)
+ }
+
+ /// This format identifier is used by a data object to indicate whether it is in a drag-and-drop loop.
+ ///
+ /// Some drop targets might call IDataObject::GetData and attempt to extract data while the object is still within the drag-and-drop
+ /// loop. Fully rendering the object for each such occurrence might cause the drag cursor to stall. If the data object supports
+ /// CFSTR_INDRAGLOOP, the target can instead use that format to check the status of the drag-and-drop loop and avoid memory
+ /// intensive rendering of the object until it is actually dropped. The formats that are memory intensive to render should still be
+ /// included in the FORMATETC enumerator and in calls to IDataObject::QueryGetData. If the data object does not set
+ /// CFSTR_INDRAGLOOP, it should act as if the value is set to zero.
+ ///
+ /// if the data object is within a drag-and-drop loop; otherwise, .
+ public bool InDragLoop
+ {
+ get => base.GetData(ShellClipboardFormat.CFSTR_INDRAGLOOP) is int i && i != 0;
+ set => base.SetData(ShellClipboardFormat.CFSTR_INDRAGLOOP, value ? 1 : 0);
+ }
+
+ ///
+ ///
+ /// This value is used by a drop source to specify whether its preferred method of data transfer is move or copy. A drop target
+ /// requests this format by calling the data object's IDataObject::GetData method. This value is set to if a move operation is preferred or if a copy operation is preferred.
+ ///
+ ///
+ /// This feature is used when a source can support either a move or copy operation. It uses the CFSTR_PREFERREDDROPEFFECT format to
+ /// communicate its preference to the target. Because the target is not obligated to honor the request, the target must call the
+ /// source's IDataObject::SetData method with a CFSTR_PERFORMEDDROPEFFECT format to tell the data object which operation was
+ /// actually performed.
+ ///
+ ///
+ /// With a delete-on-paste operation, the CFSTR_PREFERREDDROPFORMAT format is used to tell the target whether the source did a cut
+ /// or copy. With a drag-and-drop operation, you can use CFSTR_PREFERREDDROPFORMAT to specify the Shell's action. If this format is
+ /// not present, the Shell performs a default action, based on context. For instance, if a user drags a file from one volume and
+ /// drops it on another volume, the Shell's default action is to copy the file. By including a CFSTR_PREFERREDDROPFORMAT format in
+ /// the data object, you can override the default action and explicitly tell the Shell to copy, move, or link the file. If the user
+ /// chooses to drag with the right button, CFSTR_PREFERREDDROPFORMAT specifies the default command on the drag-and-drop shortcut
+ /// menu. The user is still free to choose other commands on the menu.
+ ///
+ ///
+ /// Before Microsoft Internet Explorer 4.0, an application indicated that it was transferring shortcut file types by setting
+ /// FD_LINKUI in the dwFlags member of the FILEDESCRIPTOR structure. Targets then had to use a potentially time-consuming call to
+ /// IDataObject::GetData to find out if the FD_LINKUI flag was set. Now, the preferred way to indicate that shortcuts are being
+ /// transferred is to use the CFSTR_PREFERREDDROPEFFECT format set to DROPEFFECT_LINK. However, for backward compatibility with
+ /// older systems, sources should still set the FD_LINKUI flag.
+ ///
+ ///
+ /// Specifies whether its preferred method of data transfer is move or copy.
+ public DragDropEffects PreferredDropEffect
+ {
+ get => base.GetData(ShellClipboardFormat.CFSTR_PREFERREDDROPEFFECT) is int i ? (DragDropEffects)i : 0;
+ set => base.SetData(ShellClipboardFormat.CFSTR_PREFERREDDROPEFFECT, (int)value);
+ }
+
+ ///
+ /// This format identifier is used by a target to provide its CLSID to the source.
+ ///
+ /// This format is used primarily to allow objects to be deleted by dragging them to the Recycle Bin. When an object is dropped in
+ /// the Recycle Bin, the source's IDataObject::SetData method is called with a CFSTR_TARGETCLSID format set to the Recycle Bin's
+ /// CLSID (CLSID_RecycleBin). The source can then delete the original object.
+ ///
+ ///
+ /// The CLSID.
+ public Guid TargetClsid
+ {
+ get => base.GetData(ShellClipboardFormat.CFSTR_TARGETCLSID) is Guid g ? g : default;
+ set => base.SetData(ShellClipboardFormat.CFSTR_TARGETCLSID, value);
+ }
+
+ ///
+ public override object GetData(string format, bool autoConvert)
+ {
+ var obj = base.GetData(format);
+ switch (format)
{
- var fc = MakeFORMATETC(ShellClipboardFormat.CFSTR_SHELLIDLIST);
- try
- {
- cdo.GetData(ref fc, out var medium);
- if (medium.unionmember != default)
+ case ShellClipboardFormat.CFSTR_FILEDESCRIPTORA:
+ case ShellClipboardFormat.CFSTR_FILEDESCRIPTORW:
+ //override the default handling of FileGroupDescriptor which returns a
+ //MemoryStream and instead return a string array of file names
+
+ // Pick format based on CharSet.Auto size
+ format = StringHelper.GetCharSize() == 1 ? ShellClipboardFormat.CFSTR_FILEDESCRIPTORA : ShellClipboardFormat.CFSTR_FILEDESCRIPTORW;
+
+ // Get the FileGroupDescriptor as a MemoryStream
+ var fileGroupDescriptorStream = (MemoryStream)base.GetData(format, autoConvert);
+ var fileGroupDescriptorBytes = new byte[fileGroupDescriptorStream.Length];
+ fileGroupDescriptorStream.Read(fileGroupDescriptorBytes, 0, fileGroupDescriptorBytes.Length);
+ fileGroupDescriptorStream.Close();
+
+ // copy the file group descriptor into unmanaged memory
+ using (var fileGroupDescriptorAPointer = new SafeHGlobalHandle(fileGroupDescriptorBytes))
{
- var cnt = (int)medium.unionmember.ToStructure() + 1;
- foreach (var offset in medium.unionmember.Offset(sizeof(uint)).ToArray(cnt))
- l.Add(new PIDL(medium.unionmember.Offset(offset), true));
+ Marshal.Copy(fileGroupDescriptorBytes, 0, fileGroupDescriptorAPointer, fileGroupDescriptorBytes.Length);
+
+ //marshal the unmanaged memory to to FILEGROUPDESCRIPTOR struct
+ var fileGroupDescriptor = fileGroupDescriptorAPointer.ToStructure();
+
+ // Extract and return filenames from each FILEDESCRIPTOR
+ return fileGroupDescriptor.Select(fd => new ShellFileDescriptor(fd)).ToArray();
}
- ReleaseStgMedium(medium);
- }
- catch { }
+
+ case ShellClipboardFormat.CFSTR_FILECONTENTS:
+ //override the default handling of FileContents which returns the
+ //contents of the first file as a memory stream and instead return
+ //a array of MemoryStreams containing the data to each file dropped
+
+ //get the array of filenames which lets us know how many file contents exist
+ var fileContentNames = (string[])GetData(ShellClipboardFormat.CFSTR_FILEDESCRIPTORA);
+
+ //create a MemoryStream array to store the file contents
+ var fileContents = new MemoryStream[fileContentNames.Length];
+
+ //loop for the number of files acording to the file names
+ for (var fileIndex = 0; fileIndex < fileContentNames.Length; fileIndex++)
+ {
+ //get the data at the file index and store in array
+ fileContents[fileIndex] = GetData(format, fileIndex) as MemoryStream;
+ }
+
+ //return array of MemoryStreams containing file contents
+ return fileContents;
}
- return (IReadOnlyList)l;
+ //if (format == DataFormats.FileDrop && (int)base.GetData(ShellClipboardFormat.CFSTR_INDRAGLOOP) != 0 && obj is StringCollection s)
+ //{
+ //}
+ return obj;
}
- public static void SetTargetClsid(this DataObject dobj, in Guid clsid) => dobj.SetData(ShellClipboardFormat.CFSTR_TARGETCLSID, clsid);
-
- internal static FORMATETC MakeFORMATETC(string fmt, TYMED tymed = TYMED.TYMED_HGLOBAL) => new FORMATETC
+ /// Retrieves the data associated with the specified data format at the specified index.
+ ///
+ /// The format of the data to retrieve. See for predefined formats.
+ ///
+ /// The index of the data to retrieve.
+ /// An object containing the raw data for the specified data format at the specified index.
+ public object GetData(string format, int index)
{
- cfFormat = (short)GetFormat(fmt).Id,
- dwAspect = DVASPECT.DVASPECT_CONTENT,
- lindex = -1,
- tymed = tymed
- };
+ //create a FORMATETC struct to request the data with
+ var formatetc = new FORMATETC
+ {
+ cfFormat = (short)DataFormats.GetFormat(format).Id,
+ dwAspect = DVASPECT.DVASPECT_CONTENT,
+ lindex = index,
+ ptd = new IntPtr(0),
+ tymed = TYMED.TYMED_ISTREAM | TYMED.TYMED_ISTORAGE | TYMED.TYMED_HGLOBAL
+ };
+
+ //using the Com IDataObject interface get the data using the defined FORMATETC
+ ((IComDataObject)this).GetData(ref formatetc, out var medium);
+
+ //retrieve the data depending on the returned store type
+ switch (medium.tymed)
+ {
+ case TYMED.TYMED_ISTORAGE:
+ //to handle a IStorage it needs to be written into a second unmanaged
+ //memory mapped storage and then the data can be read from memory into
+ //a managed byte and returned as a MemoryStream
+ {
+ //marshal the returned pointer to a IStorage object
+ using var pStorage = ComReleaserFactory.Create((IStorage)Marshal.GetObjectForIUnknown(medium.unionmember));
+ Marshal.Release(medium.unionmember);
+
+ //create a ILockBytes (unmanaged byte array) and then create a IStorage using the byte array as a backing store
+ CreateILockBytesOnHGlobal(IntPtr.Zero, true, out var iLockBytes).ThrowIfFailed();
+ using var pLockBytes = ComReleaserFactory.Create(iLockBytes);
+ StgCreateDocfileOnILockBytes(iLockBytes, STGM.STGM_CREATE | STGM.STGM_WRITE | STGM.STGM_READWRITE, default, out var iStorage2).ThrowIfFailed();
+ using var pStorage2 = ComReleaserFactory.Create(iStorage2);
+
+ //copy the returned IStorage into the new IStorage
+ pStorage.Item.CopyTo(0, null, IntPtr.Zero, iStorage2);
+ iLockBytes.Flush();
+ iStorage2.Commit(0);
+
+ //get the STATSTG of the ILockBytes to determine how many bytes were written to it
+ iLockBytes.Stat(out var iLockBytesStat, STATFLAG.STATFLAG_NONAME);
+
+ //read the data from the ILockBytes (unmanaged byte array) into a managed byte array
+ using var iLockBytesContent = new SafeHGlobalHandle(iLockBytesStat.cbSize);
+ iLockBytes.ReadAt(0, iLockBytesContent, iLockBytesContent.Size, out _);
+
+ //wrapped the managed byte array into a memory stream and return it
+ return new MemoryStream(iLockBytesContent.GetBytes(0, iLockBytesContent.Size));
+ }
+
+ case TYMED.TYMED_ISTREAM:
+ //to handle a IStream it needs to be read into a managed byte and
+ //returned as a MemoryStream
+ {
+ //marshal the returned pointer to a IStream object
+ using var pStream = ComReleaserFactory.Create((IStream)Marshal.GetObjectForIUnknown(medium.unionmember));
+ Marshal.Release(medium.unionmember);
+
+ //get the STATSTG of the IStream to determine how many bytes are in it
+ pStream.Item.Stat(out var iStreamStat, 0);
+
+ //read the data from the IStream into a managed byte array
+ var iStreamContent = new byte[((int)iStreamStat.cbSize)];
+ pStream.Item.Read(iStreamContent, iStreamContent.Length, IntPtr.Zero);
+
+ //wrapped the managed byte array into a memory stream and return it
+ return new MemoryStream(iStreamContent);
+ }
+
+ case TYMED.TYMED_HGLOBAL:
+ //to handle a HGlobal the exisitng "GetDataFromHGLOBLAL" method is invoked via
+ //reflection
+ return GetObjectFromHGlobal(DataFormats.GetFormat(formatetc.cfFormat).Name, medium.unionmember);
+ }
+
+ return null;
+ }
+
+ ///
+ /// This is used when a group of files in CF_HDROP format is being renamed as well as transferred. The data consists of an STGMEDIUM
+ /// structure that contains a global memory object. The structure's hGlobal member points to a double null-terminated character
+ /// array. This array contains a new name for each file, in the same order that the files are listed in the accompanying CF_HDROP
+ /// format. The format of the character array is the same as that used by CF_HDROP to list the transferred files.
+ ///
+ /// A list of strings containing a name for each file.
+ public string[] GetFileNameMap()
+ {
+ if (GetDataPresent(ShellClipboardFormat.CFSTR_FILENAMEMAPW) && GetData(ShellClipboardFormat.CFSTR_FILENAMEMAPW, true) is StringCollection dataw)
+ return dataw.Cast().ToArray();
+ else if (GetDataPresent(ShellClipboardFormat.CFSTR_FILENAMEMAPA) && GetData(ShellClipboardFormat.CFSTR_FILENAMEMAPA, true) is StringCollection data)
+ return data.Cast().ToArray();
+ return new string[0];
+ }
+
+ ///
+ ///
+ /// This format identifier is used when transferring the locations of one or more existing namespace objects. It is used in much the
+ /// same way as CF_HDROP, but it contains PIDLs instead of file system paths. Using PIDLs allows the CFSTR_SHELLIDLIST format to
+ /// handle virtual objects as well as file system objects. The data is an STGMEDIUM structure that contains a global memory object.
+ /// The structure's hGlobal member points to a CIDA structure.
+ ///
+ ///
+ /// The aoffset member of the CIDA structure is an array containing offsets to the beginning of the ITEMIDLIST structure for each
+ /// PIDL that is being transferred. To extract a particular PIDL, first determine its index. Then, add the aoffset value that
+ /// corresponds to that index to the address of the CIDA structure.
+ ///
+ ///
+ /// The first element of aoffset contains an offset to the fully qualified PIDL of a parent folder. If this PIDL is empty, the
+ /// parent folder is the desktop. Each of the remaining elements of the array contains an offset to one of the PIDLs to be
+ /// transferred. All of these PIDLs are relative to the PIDL of the parent folder.
+ ///
+ ///
+ /// The following two macros can be used to retrieve PIDLs from a CIDA structure. The first takes a pointer to the structure and
+ /// retrieves the PIDL of the parent folder. The second takes a pointer to the structure and retrieves one of the other PIDLs,
+ /// identified by its zero-based index.
+ ///
+ /// #define GetPIDLFolder(pida) (LPCITEMIDLIST)(((LPBYTE)pida)+(pida)->aoffset[0])
+ ///#define GetPIDLItem(pida, i) (LPCITEMIDLIST)(((LPBYTE)pida)+(pida)->aoffset[i+1])
+ /// The value that is returned by these macros is a pointer to the PIDL's ITEMIDLIST structure. Since these
+ /// structures vary in length, you must determine the end of the structure by walking through each of the ITEMIDLIST structure's
+ /// SHITEMID structures until you reach the two-byte NULL that marks the end.
+ ///
+ /// A list of strings containing a name for each file.
+ public PIDL[] GetShellIdList() => GetComData(ShellClipboardFormat.CFSTR_SHELLIDLIST,
+ p => Array.ConvertAll(p.Offset(sizeof(uint)).ToArray((int)p.ToStructure() + 1), u => new PIDL(p.Offset(u), true)), new PIDL[0]);
private static DataFormats.Format GetFormat(string format) => DataFormats.GetFormat(format);
+
+ private T GetComData(string fmt, Func convert, T defValue = default)
+ {
+ var ret = defValue;
+ var fc = new FORMATETC { cfFormat = (short)GetFormat(fmt).Id, dwAspect = DVASPECT.DVASPECT_CONTENT, lindex = -1, tymed = TYMED.TYMED_HGLOBAL };
+ try
+ {
+ ((IComDataObject)this).GetData(ref fc, out var medium);
+ if (medium.unionmember != default)
+ ret = convert(medium.unionmember);
+ ReleaseStgMedium(medium);
+ }
+ catch { }
+ return ret;
+ }
+
+ private object GetObjectFromHGlobal(string format, IntPtr hGlobal)
+ {
+ var ptr = Win32Error.ThrowLastErrorIfNull(Kernel32.GlobalLock(hGlobal));
+ try
+ {
+ if (format == DataFormats.Text || format == DataFormats.Rtf || format == DataFormats.CommaSeparatedValue || format == DataFormats.OemText)
+ return StringHelper.GetString(ptr, CharSet.Ansi);
+ else if (format == DataFormats.UnicodeText)
+ return StringHelper.GetString(ptr, CharSet.Unicode);
+ else if (format == DataFormats.Html)
+ return NativeClipboard.GetHtml(ptr);
+ else if (format == ShellClipboardFormat.CFSTR_FILENAMEA)
+ return new[] { StringHelper.GetString(ptr, CharSet.Ansi) };
+ else if (format == ShellClipboardFormat.CFSTR_FILENAMEW)
+ return new[] { StringHelper.GetString(ptr, CharSet.Unicode) };
+ else if (format == DataFormats.FileDrop)
+ // TODO
+ return new MemoryStream();
+ else
+ // TODO
+ return new MemoryStream();
+ }
+ finally
+ {
+ Kernel32.GlobalUnlock(hGlobal);
+ }
+ }
}
-}
+
+ ///
+ /// Describes the properties of a file that is being copied by means of the clipboard during a Microsoft ActiveX drag-and-drop operation.
+ ///
+ public class ShellFileDescriptor
+ {
+ /// Initializes a new instance of the class.
+ /// The file information.
+ public ShellFileDescriptor(FileInfo fileInfo)
+ {
+ Info = fileInfo;
+ }
+
+ internal ShellFileDescriptor(in FILEDESCRIPTOR fd)
+ {
+ Info = new FileInfo(fd.cFileName);
+ if (fd.dwFlags.IsFlagSet(FD_FLAGS.FD_CLSID))
+ TypeIdClsid = fd.clsid;
+ if (fd.dwFlags.IsFlagSet(FD_FLAGS.FD_SIZEPOINT))
+ {
+ IconSize = fd.sizel;
+ ScreenPosition = fd.pointl;
+ }
+ ShowProgressUI = fd.dwFlags.IsFlagSet(FD_FLAGS.FD_PROGRESSUI);
+ IsShortcut = fd.dwFlags.IsFlagSet(FD_FLAGS.FD_LINKUI);
+ }
+
+ /// The width and height of the file icon.
+ public Size? IconSize { get; set; }
+
+ /// Gets the file information.
+ /// The file information.
+ public FileInfo Info { get; }
+
+ /// Treat the operation as a shortcut.
+ public bool IsShortcut { get; set; }
+
+ /// The screen coordinates of the file object.
+ public Point? ScreenPosition { get; set; }
+
+ /// progress indicator is shown with drag-and-drop operations.
+ public bool ShowProgressUI { get; set; }
+
+ /// The file type identifier.
+ public Guid? TypeIdClsid { get; set; }
+
+ internal FILEDESCRIPTOR ToFileDesc()
+ {
+ return new FILEDESCRIPTOR
+ {
+ dwFlags = FD_FLAGS.FD_ATTRIBUTES | FD_FLAGS.FD_WRITESTIME | FD_FLAGS.FD_FILESIZE | FD_FLAGS.FD_ACCESSTIME | FD_FLAGS.FD_CREATETIME |
+ (ShowProgressUI ? FD_FLAGS.FD_PROGRESSUI : 0) | (IsShortcut ? FD_FLAGS.FD_LINKUI : 0) | (TypeIdClsid.HasValue ? FD_FLAGS.FD_CLSID : 0) |
+ (IconSize.HasValue ? FD_FLAGS.FD_SIZEPOINT : 0),
+ clsid = TypeIdClsid ?? Guid.Empty,
+ cFileName = Info.FullName,
+ dwFileAttributes = (FileFlagsAndAttributes)Info.Attributes,
+ nFileSIze = unchecked((ulong)Info.Length),
+ ftCreationTime = Info.CreationTimeUtc.ToFileTimeStruct(),
+ ftLastAccessTime = Info.LastAccessTimeUtc.ToFileTimeStruct(),
+ ftLastWriteTime = Info.LastWriteTimeUtc.ToFileTimeStruct(),
+ sizel = IconSize ?? SIZE.Empty,
+ pointl = ScreenPosition ?? Point.Empty
+ };
+ }
+ }
+}
\ No newline at end of file