From 61c26627b5f7cd9f24f98d3bcc13949b9e8a14b6 Mon Sep 17 00:00:00 2001 From: dahall Date: Sun, 26 Jul 2020 13:55:23 -0600 Subject: [PATCH] Incomplete work on ShellDataObject --- Windows.Shell/ShellDataObject.cs | 434 ++++++++++++++++++++++++++++++++++----- 1 file changed, 383 insertions(+), 51 deletions(-) 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