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.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 { /// Shell extended . // TODO: Finish adding ShellClipboardFormat handling, tests and release class ShellDataObject : DataObject { /// Initializes a new instance of the class. public ShellDataObject() : base() { } /// Initializes a new instance of the class and adds the specified object to it. /// The data to store. public ShellDataObject(object data) : base(data) { } /// /// 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) { } /// Initializes a new instance of the class. /// A list of ShellItem instances. public ShellDataObject(ShellItem[] items) : base(ShellClipboardFormat.CFSTR_SHELLIDLIST, GetPIDLArrayStream(items)) { } private static Stream GetPIDLArrayStream(ShellItem[] items) { var str = new MemoryStream(); var pidlBytes = Array.ConvertAll(items, i => i.PIDL.GetBytes()); int offset = 0; str.Write(BitConverter.GetBytes((uint)pidlBytes.Length), offset, sizeof(uint)); offset += sizeof(uint); for (var i = 0; i < pidlBytes.Length; i++, offset += sizeof(uint)) str.Write(BitConverter.GetBytes((uint)pidlBytes[i].Length), offset, sizeof(uint)); for (var i = 0; i < pidlBytes.Length; i++) { str.Write(pidlBytes[i], offset, pidlBytes[i].Length); offset += pidlBytes[i].Length; } return str; } /// 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) { 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)) { 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(); } 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; } //if (format == DataFormats.FileDrop && (int)base.GetData(ShellClipboardFormat.CFSTR_INDRAGLOOP) != 0 && obj is StringCollection s) //{ //} return obj; } /// 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) { //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 }; } } }