Vanara/Windows.Shell/ShellObjects/ShellItem.cs

883 lines
39 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using Vanara.Extensions;
using Vanara.InteropServices;
using Vanara.PInvoke;
using static Vanara.PInvoke.Gdi32;
using static Vanara.PInvoke.Kernel32;
using static Vanara.PInvoke.Ole32;
using static Vanara.PInvoke.PropSys;
using static Vanara.PInvoke.Shell32;
using static Vanara.PInvoke.ShlwApi;
namespace Vanara.Windows.Shell
{
/// <summary>Attributes that can be retrieved on an item (file or folder) or set of items using <see cref="ShellItem.Attributes"/>.</summary>
[Flags]
public enum ShellItemAttribute : uint
{
/// <summary>The specified items can be copied.</summary>
CanCopy = 0x00000001,
/// <summary>The specified items can be moved.</summary>
CanMove = 0x00000002,
/// <summary>
/// Shortcuts can be created for the specified items. If a namespace extension returns this attribute, a Create Shortcut entry with a
/// default handler is added to the shortcut menu that is displayed during drag-and-drop operations. The extension can also implement
/// its own handler for the link verb in place of the default. If the extension does so, it is responsible for creating the shortcut.
/// A Create Shortcut item is also added to the Windows Explorer File menu and to normal shortcut menus. If the item is selected,
/// your application's IContextMenu::InvokeCommand method is invoked with the lpVerb member of the CMINVOKECOMMANDINFO structure set
/// to link. Your application is responsible for creating the link.
/// </summary>
CanLink = 0x00000004,
/// <summary>
/// The specified items can be bound to an IStorage object throughIShellFolder::BindToObject. For more information about namespace
/// manipulation capabilities, see IStorage.
/// </summary>
Storage = 0x00000008,
/// <summary>
/// The specified items can be renamed. Note that this value is essentially a suggestion; not all namespace clients allow items to be
/// renamed. However, those that do must have this attribute set.
/// </summary>
CanRename = 0x00000010,
/// <summary>The specified items can be deleted.</summary>
CanDelete = 0x00000020,
/// <summary>The specified items have property sheets.</summary>
HasPropSheet = 0x00000040,
/// <summary>The specified items are drop targets.</summary>
DropTarget = 0x00000100,
/// <summary>
/// This flag is a mask for the capability attributes: CANCOPY, CANMOVE, CANLINK, CANRENAME, CANDELETE, HASPROPSHEET, and DROPTARGET.
/// Callers normally do not use this value.
/// </summary>
CapabilityMask = 0x00000177,
/// <summary>Windows 7 and later. The specified items are system items.</summary>
System = 0x00001000,
/// <summary>The specified items are encrypted and might require special presentation.</summary>
Encrypted = 0x00002000,
/// <summary>
/// Accessing the item (through IStream or other storage interfaces) is expected to be a slow operation. Applications should avoid
/// accessing items flagged with ISSLOW. Note: Opening a stream for an item is generally a slow operation at all times. ISSLOW
/// indicates that it is expected to be especially slow, for example in the case of slow network connections or offline
/// (FILE_ATTRIBUTE_OFFLINE) files. However, querying ISSLOW is itself a slow operation. Applications should query ISSLOW only on a
/// background thread. An alternate method, such as retrieving the PKEY_FileAttributes property and testing for
/// FILE_ATTRIBUTE_OFFLINE, could be used in place of a method call that involves ISSLOW.
/// </summary>
IsSlow = 0x00004000,
/// <summary>The specified items are shown as dimmed and unavailable to the user.</summary>
Ghosted = 0x00008000,
/// <summary>The specified items are shortcuts.</summary>
Link = 0x00010000,
/// <summary>The specified objects are shared.</summary>
Share = 0x00020000,
/// <summary>
/// The specified items are read-only. In the case of folders, this means that new items cannot be created in those folders. This
/// should not be confused with the behavior specified by the FILE_ATTRIBUTE_READONLY flag retrieved by IColumnProvider::GetItemData
/// in a SHCOLUMNDATAstructure. FILE_ATTRIBUTE_READONLY has no meaning for Win32 file system folders.
/// </summary>
ReadOnly = 0x00040000,
/// <summary>
/// The item is hidden and should not be displayed unless the Show hidden files and folders option is enabled in Folder Settings.
/// </summary>
Hidden = 0x00080000,
/// <summary>Do not use.</summary>
DisplayAttrMask = 0x000FC000,
/// <summary>
/// The items are non-enumerated items and should be hidden. They are not returned through an enumerator such as that created by
/// theIShellFolder::EnumObjects method.
/// </summary>
NonEnumerated = 0x00100000,
/// <summary>The items contain new content, as defined by the particular application.</summary>
NewContent = 0x00200000,
/// <summary>Not supported.</summary>
CanMoniker = 0x00400000,
/// <summary>Not supported.</summary>
HasStorage = 0x00400000,
/// <summary>
/// Indicates that the item has a stream associated with it. That stream can be accessed through a call to IShellFolder::BindToObject
/// orIShellItem::BindToHandler with IID_IStream in the riid parameter.
/// </summary>
Stream = 0x00400000,
/// <summary>Children of this item are accessible through IStream or IStorage. Those children are flagged with STORAGE or STREAM.</summary>
StorageAncestor = 0x00800000,
/// <summary>
/// When specified as input, VALIDATE instructs the folder to validate that the items contained in a folder or Shell item array
/// exist. If one or more of those items do not exist, IShellFolder::GetAttributesOf and IShellItemArray::GetAttributes return a
/// failure code. This flag is never returned as an [out] value. When used with the file system folder, VALIDATE instructs the folder
/// to discard cached properties retrieved by clients of IShellFolder2::GetDetailsEx that might have accumulated for the specified items.
/// </summary>
Validate = 0x01000000,
/// <summary>The specified items are on removable media or are themselves removable devices.</summary>
Removable = 0x02000000,
/// <summary>The specified items are compressed.</summary>
Compressed = 0x04000000,
/// <summary>The specified items can be hosted inside a web browser or Windows Explorer frame.</summary>
Browsable = 0x08000000,
/// <summary>
/// The specified folders are either file system folders or contain at least one descendant (child, grandchild, or later) that is a
/// file system (FILESYSTEM) folder.
/// </summary>
FileSysAncestor = 0x10000000,
/// <summary>
/// The specified items are folders. Some items can be flagged with both STREAM and FOLDER, such as a compressed file with a .zip
/// file name extension. Some applications might include this flag when testing for items that are both files and containers.
/// </summary>
Folder = 0x20000000,
/// <summary>
/// The specified folders or files are part of the file system (that is, they are files, directories, or root directories). The
/// parsed names of the items can be assumed to be valid Win32 file system paths. These paths can be either UNC or drive-letter based.
/// </summary>
FileSystem = 0x40000000,
/// <summary>
/// This flag is a mask for the storage capability attributes: STORAGE, LINK, READONLY, STREAM, STORAGEANCESTOR, FILESYSANCESTOR,
/// FOLDER, and FILESYSTEM. Callers normally do not use this value.
/// </summary>
StorageCapMask = 0x70C50008,
/// <summary>
/// The specified folders have subfolders. The HASSUBFOLDER attribute is only advisory and might be returned by Shell folder
/// implementations even if they do not contain subfolders. Note, however, that the converse—failing to return
/// HASSUBFOLDER—definitively states that the folder objects do not have subfolders. Returning HASSUBFOLDER is recommended whenever a
/// significant amount of time is required to determine whether any subfolders exist. For example, the Shell always returns
/// HASSUBFOLDER when a folder is located on a network drive.
/// </summary>
HasSubfolder = 0x80000000,
/// <summary>This flag is a mask for content attributes, at present only HASSUBFOLDER. Callers normally do not use this value.</summary>
ContentsMask = 0x80000000,
/// <summary>
/// Mask used by the PKEY_SFGAOFlags property to determine attributes that are considered to cause slow calculations or lack context:
/// ISSLOW, READONLY, HASSUBFOLDER, and VALIDATE. Callers normally do not use this value.
/// </summary>
PKEYMask = 0x81044000
}
/// <summary>Used to determine how to compare two Shell items. ShellItem.Compare uses this enumerated type.</summary>
[Flags]
public enum ShellItemComparison : uint
{
/// <summary>Exact comparison of two instances of a Shell item.</summary>
AllFields = 0x80000000,
/// <summary>Indicates that the comparison is based on a canonical name.</summary>
Canonical = 0x10000000,
/// <summary>Indicates that the comparison is based on the display in a folder view.</summary>
Display = 0,
/// <summary>Windows 7 and later. If the Shell items are not the same, test the file system paths.</summary>
SecondaryFileSystemPath = 0x20000000
}
/// <summary>Requests the form of an item's display name to retrieve through <see cref="ShellItem.GetDisplayName(ShellItemDisplayString)"/>.</summary>
public enum ShellItemDisplayString : uint
{
/// <summary>Returns the display name relative to the parent folder. In UI this name is generally ideal for display to the user.</summary>
NormalDisplay = 0x00000000,
/// <summary>Returns the parsing name relative to the parent folder. This name is not suitable for use in UI.</summary>
ParentRelativeParsing = 0x80018001,
/// <summary>Returns the parsing name relative to the desktop. This name is not suitable for use in UI.</summary>
DesktopAbsoluteParsing = 0x80028000,
/// <summary>Returns the editing name relative to the parent folder. In UI this name is suitable for display to the user.</summary>
ParentRelativeEditing = 0x80031001,
/// <summary>Returns the editing name relative to the desktop. In UI this name is suitable for display to the user.</summary>
DesktopAbsoluteEditing = 0x8004c000,
/// <summary>
/// Returns the item's file system path, if it has one. Only items that report SFGAO_FILESYSTEM have a file system path. When an item
/// does not have a file system path, a call to IShellItem::GetDisplayName on that item will fail. In UI this name is suitable for
/// display to the user in some cases, but note that it might not be specified for all items.
/// </summary>
FileSysPath = 0x80058000,
/// <summary>
/// Returns the item's URL, if it has one. Some items do not have a URL, and in those cases a call to IShellItem::GetDisplayName will
/// fail. This name is suitable for display to the user in some cases, but note that it might not be specified for all items.
/// </summary>
Url = 0x80068000,
/// <summary>
/// Returns the path relative to the parent folder in a friendly format as displayed in an address bar. This name is suitable for
/// display to the user.
/// </summary>
ParentRelativeForAddressBar = 0x8007c001,
/// <summary>Returns the path relative to the parent folder.</summary>
ParentRelative = 0x80080001,
/// <summary>Introduced in Windows 8.</summary>
ParentRelativeForUI = 0x80094001
}
/// <summary>Options for retrieving images from a <see cref="ShellItem"/>.</summary>
[Flags]
public enum ShellItemGetImageOptions
{
/// <summary>Shrink the bitmap as necessary to fit, preserving its aspect ratio.</summary>
ResizeToFit = 0x00000000,
/// <summary>
/// Passed by callers if they want to stretch the returned image themselves. For example, if the caller passes an icon size of 80x80,
/// a 96x96 thumbnail could be returned. This action can be used as a performance optimization if the caller expects that they will
/// need to stretch the image.
/// </summary>
BiggerSizeOk = 0x00000001,
/// <summary>
/// Return the item only if it is already in memory. Do not access the disk even if the item is cached. Note that this only returns
/// an already-cached icon and can fall back to a per-class icon if an item has a per-instance icon that has not been cached.
/// Retrieving a thumbnail, even if it is cached, always requires the disk to be accessed, so GetImage should not be called from the
/// UI thread without passing MemoryOnly.
/// </summary>
MemoryOnly = 0x00000002,
/// <summary>Return only the icon, never the thumbnail.</summary>
IconOnly = 0x00000004,
/// <summary>
/// Return only the thumbnail, never the icon. Note that not all items have thumbnails, so ThumbnailOnly will cause the method to
/// fail in these cases.
/// </summary>
ThumbnailOnly = 0x00000008,
/// <summary>
/// Allows access to the disk, but only to retrieve a cached item. This returns a cached thumbnail if it is available. If no cached
/// thumbnail is available, it returns a cached per-instance icon but does not extract a thumbnail or icon.
/// </summary>
InCacheOnly = 0x00000010,
/// <summary>Introduced in Windows 8. If necessary, crop the bitmap to a square.</summary>
CropToSquare = 0x00000020,
/// <summary>Introduced in Windows 8. Stretch and crop the bitmap to a 0.7 aspect ratio.</summary>
WideThumbnails = 0x00000040,
/// <summary>
/// Introduced in Windows 8. If returning an icon, paint a background using the associated app's registered background color.
/// </summary>
IconBackground = 0x00000080,
/// <summary>Introduced in Windows 8. If necessary, stretch the bitmap so that the height and width fit the given size.</summary>
ScaleUp = 0x00000100,
}
/// <summary>Flags that direct the handling of the item from which you're retrieving the info tip text.</summary>
[Flags]
public enum ShellItemToolTipOptions
{
/// <summary>No special handling.</summary>
Default = 0x00000000,
/// <summary>Provide the name of the item in ppwszTip rather than the info tip text.</summary>
Name = 0x00000001,
/// <summary>If the item is a shortcut, retrieve the info tip text of the shortcut rather than its target.</summary>
LinkNotTarget = 0x00000002,
/// <summary>If the item is a shortcut, retrieve the info tip text of the shortcut's target.</summary>
LinkTarget = 0x00000004,
/// <summary>Search the entire namespace for the information. This value can result in a delayed response time.</summary>
AllowDelay = 0x00000008,
/// <summary><c>Windows Vista and later.</c> Put the info tip on a single line.</summary>
SingleLine = 0x00000010,
}
// TODO: object GetPropertyStoreForKeys(IntPtr rgKeys, uint cKeys, GPS flags, in Guid riid);
// TODO: object GetPropertyStoreWithCreateObject(GPS flags, object punkCreateObject, in Guid riid);
/// <summary>Encapsulates an item in the Windows Shell.</summary>
/// <seealso cref="System.IComparable{ShellItem}"/>
/// <seealso cref="System.IDisposable"/>
/// <seealso cref="System.IEquatable{IShellItem}"/>
/// <seealso cref="System.IEquatable{ShellItem}"/>
public class ShellItem : IComparable<ShellItem>, IDisposable, IEquatable<IShellItem>, IEquatable<ShellItem>, INotifyPropertyChanged
{
internal static readonly bool IsMin7 = Environment.OSVersion.Version >= new Version(6, 1);
internal static readonly bool IsMinVista = Environment.OSVersion.Version.Major >= 6;
internal IShellItem iShellItem;
internal IShellItem2 iShellItem2;
private static Dictionary<Type, BHID> bhidLookup;
private PropertyDescriptionList propDescList;
private ShellItemPropertyStore props;
private IQueryInfo qi;
/// <summary>Initializes a new instance of the <see cref="ShellItem"/> class.</summary>
/// <param name="path">The file system path of the item.</param>
public ShellItem(string path) => Init(ShellUtil.GetShellItemForPath(path));
/// <summary>Initializes a new instance of the <see cref="ShellItem"/> class.</summary>
/// <param name="idList">The ID list.</param>
public ShellItem(PIDL idList)
{
if (idList == null || idList.IsInvalid) throw new ArgumentNullException(nameof(idList));
if (IsMinVista)
{
SHCreateItemFromIDList(idList, typeof(IShellItem).GUID, out var obj).ThrowIfFailed();
Init((IShellItem)obj);
}
else
{
Init(new ShellItemImpl(idList, false));
}
}
/// <summary>Initializes a new instance of the <see cref="ShellItem"/> class.</summary>
/// <param name="si">An existing IShellItem instance.</param>
protected ShellItem(IShellItem si) => Init(si);
/// <summary>Initializes a new instance of the <see cref="ShellItem"/> class.</summary>
/// <param name="knownFolder">A known folder reference.</param>
protected ShellItem(KNOWNFOLDERID knownFolder)
{
if (IsMin7)
{
SHGetKnownFolderItem(knownFolder.Guid(), KNOWN_FOLDER_FLAG.KF_FLAG_DEFAULT, HTOKEN.NULL, typeof(IShellItem).GUID, out var ppv).ThrowIfFailed();
Init((IShellItem)ppv);
}
else
{
var csidl = knownFolder.SpecialFolder();
if (csidl == null) throw new ArgumentOutOfRangeException(nameof(knownFolder), @"Cannot translate this known folder to a value understood by systems prior to Windows 7.");
var path = new StringBuilder(MAX_PATH);
SHGetFolderPath(IntPtr.Zero, (int)csidl.Value, HTOKEN.NULL, SHGFP.SHGFP_TYPE_CURRENT, path).ThrowIfFailed();
Init(ShellUtil.GetShellItemForPath(path.ToString()));
}
}
/// <summary>Initializes a new instance of the <see cref="ShellItem"/> class.</summary>
protected ShellItem() { }
/// <summary>Occurs when a property value changes.</summary>
public event PropertyChangedEventHandler PropertyChanged
{
add { ((INotifyPropertyChanged)Properties).PropertyChanged += value; }
remove { ((INotifyPropertyChanged)Properties).PropertyChanged -= value; }
}
/// <summary>Gets the attributes for the Shell item.</summary>
/// <value>The attributes of the Shell item.</value>
public ShellItemAttribute Attributes => (ShellItemAttribute)iShellItem.GetAttributes((SFGAO)0xFFFFFFFF);
/// <summary>Gets the <see cref="ShellFileInfo"/> corresponding to this instance.</summary>
public ShellFileInfo FileInfo => IsFileSystem ? new ShellFileInfo(PIDL) : throw new InvalidOperationException("Not file system objects do not have associated ShellFileInfo objects");
/// <summary>Gets the file system path if this item is part of the file system.</summary>
/// <value>The file system path.</value>
public string FileSystemPath => GetDisplayName(SIGDN.SIGDN_FILESYSPATH);
/// <summary>Gets a value indicating whether this instance is part of the file system.</summary>
/// <value><c>true</c> if this instance is part of the file system; otherwise, <c>false</c>.</value>
public bool IsFileSystem => iShellItem.GetAttributes(SFGAO.SFGAO_FILESYSTEM) != 0;
/// <summary>Gets a value indicating whether this instance is folder.</summary>
/// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
public bool IsFolder => iShellItem.GetAttributes(SFGAO.SFGAO_FOLDER) != 0;
/// <summary>Gets the IShellItem instance of the current ShellItem.</summary>
public IShellItem IShellItem => iShellItem;
/// <summary>Gets a value indicating whether this instance is link.</summary>
/// <value><c>true</c> if this instance is link; otherwise, <c>false</c>.</value>
public bool IsLink => iShellItem.GetAttributes(SFGAO.SFGAO_LINK) != 0;
/// <summary>Gets the name relative to the parent for the item.</summary>
public virtual string Name
{
get => GetDisplayName(SIGDN.SIGDN_NORMALDISPLAY);
protected set { }
}
/// <summary>Gets the parent for the current item.</summary>
/// <value>The parent item. If this is the desktop, this property will return <c>null</c>.</value>
public ShellFolder Parent
{
get
{
try { return new ShellFolder(iShellItem.GetParent()); } catch { }
return null;
}
}
/// <summary>Gets a string that can be used to parse an absolute value from the Desktop.</summary>
/// <value>A parsable name for the item.</value>
public string ParsingName => GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING);
/// <summary>Gets the item's ID list.</summary>
/// <value>The ID list for the item.</value>
public PIDL PIDL
{
get
{
SHGetIDListFromObject(iShellItem, out var pidl).ThrowIfFailed();
return pidl;
}
}
/// <summary>Gets the property store for the item.</summary>
/// <value>The dictionary of properties.</value>
public ShellItemPropertyStore Properties => props ?? (props = new ShellItemPropertyStore(this));
/// <summary>Gets a property description list object containing descriptions of all properties.</summary>
/// <returns>A complete <see cref="PropertyDescriptionList"/> instance.</returns>
public PropertyDescriptionList PropertyDescriptions => propDescList ?? (propDescList = GetPropertyDescriptionList(PROPERTYKEY.System.PropList.FullDetails));
/// <summary>Gets the normal tool tip text associated with this item.</summary>
/// <value>The tool tip text.</value>
public string ToolTipText => GetToolTip();
/// <summary>Gets the system bind context.</summary>
/// <value>The bind context.</value>
protected static IBindCtx BindContext => ShellUtil.CreateBindCtx();
/// <summary>Creates the most specialized derivative of ShellItem from an IShellItem object.</summary>
/// <param name="iItem">The IShellItem object.</param>
/// <returns>A ShellItem derivative for the supplied IShellItem.</returns>
public static ShellItem Open(IShellItem iItem)
{
if (iItem.GetAttributes(SFGAO.SFGAO_LINK) != 0)
return new ShellLink(iItem);
// If not a folder, get the ShellItem
if (iItem.GetAttributes(SFGAO.SFGAO_FOLDER) == 0)
return new ShellItem(iItem);
// Try to get specialized folder type from property
var pk = PROPERTYKEY.System.ItemType;
string itemType = null;
try { itemType = (iItem as IShellItem2)?.GetString(pk)?.ToString().ToLowerInvariant(); } catch { }
switch (itemType)
{
case ".library-ms":
return new ShellLibrary(iItem);
case ".searchconnector-ms":
// TODO: Return a search connector
case ".search-ms":
// TODO: Return a saved search connection
default:
return new ShellFolder(iItem);
}
}
/// <summary>Implements the operator !=.</summary>
/// <param name="left">The left operand.</param>
/// <param name="right">The right operand.</param>
/// <returns>The result of the operator.</returns>
public static bool operator !=(ShellItem left, ShellItem right) => !(left == right);
/// <summary>Implements the operator ==.</summary>
/// <param name="left">The left operand.</param>
/// <param name="right">The right operand.</param>
/// <returns>The result of the operator.</returns>
public static bool operator ==(ShellItem left, ShellItem right) => Equals(left?.iShellItem, right?.iShellItem);
/// <summary>
/// Compares the current instance with another object of the same type and returns an integer that indicates whether the current
/// instance precedes, follows, or occurs in the same position in the sort order as the other object.
/// </summary>
/// <param name="other">An object to compare with this instance.</param>
/// <param name="hint">Optional hint value that determines how to perform the comparison. The default compares all fields.</param>
/// <returns>
/// A value that indicates the relative order of the objects being compared. If the two items are the same this parameter equals
/// zero; if they are different the parameter is nonzero.
/// </returns>
public int CompareTo(ShellItem other, ShellItemComparison hint = ShellItemComparison.SecondaryFileSystemPath) => iShellItem.Compare(other?.iShellItem, (SICHINTF)hint);
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public virtual void Dispose()
{
if (props != null) { props?.Dispose(); props = null; }
if (propDescList != null) { propDescList?.Dispose(); propDescList = null; }
if (qi != null) { Marshal.ReleaseComObject(qi); qi = null; }
if (iShellItem2 != null) { Marshal.ReleaseComObject(iShellItem2); iShellItem2 = null; }
if (iShellItem != null) { Marshal.ReleaseComObject(iShellItem); iShellItem = null; }
}
/// <summary>Determines whether the specified <see cref="System.Object"/>, is equal to this instance.</summary>
/// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
/// <returns><c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.</returns>
public override bool Equals(object obj) => Equals(iShellItem, obj as IShellItem);
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
/// <param name="other">An object to compare with this object.</param>
/// <returns>true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.</returns>
public bool Equals(IShellItem other) => Equals(iShellItem, other);
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
/// <param name="other">An object to compare with this object.</param>
/// <returns>true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.</returns>
public bool Equals(ShellItem other) => Equals(iShellItem, other?.iShellItem);
/// <summary>Gets a formatted display name for this item.</summary>
/// <param name="option">The formatting options.</param>
/// <returns>A string with the formatted display name if successful; otherwise <c>null</c>.</returns>
public string GetDisplayName(ShellItemDisplayString option) => GetDisplayName((SIGDN)option);
/// <summary>Gets a handler interface.</summary>
/// <typeparam name="TInterface">The interface of the handler to return.</typeparam>
/// <param name="handler">The bind handler to retrieve.</param>
/// <returns>The requested interface.</returns>
public TInterface GetHandler<TInterface>(BHID handler = 0) where TInterface : class => GetHandler<TInterface>(BindContext, handler);
/// <summary>Gets a handler interface.</summary>
/// <typeparam name="TInterface">The interface of the handler to return.</typeparam>
/// <param name="bindCtx">The bind context.</param>
/// <param name="handler">The bind handler to retrieve.</param>
/// <returns>The requested interface.</returns>
/// <exception cref="ArgumentOutOfRangeException">handler</exception>
public TInterface GetHandler<TInterface>(IBindCtx bindCtx, BHID handler = 0) where TInterface : class
{
if (handler == 0)
handler = GetBHIDForInterface<TInterface>();
if (handler == 0)
throw new ArgumentOutOfRangeException(nameof(handler));
return iShellItem.BindToHandler(bindCtx, handler.Guid(), typeof(TInterface).GUID) as TInterface;
}
/// <summary>Returns a hash code for this instance.</summary>
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
public override int GetHashCode() => GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING).GetHashCode();
/// <summary>
/// Gets an image that represents this item. The default behavior is to load a thumbnail. If there is no thumbnail for the current
/// item, it retrieves the icon of the item. The thumbnail or icon is extracted if it is not currently cached.
/// </summary>
/// <param name="size">A structure that specifies the size of the image to be received.</param>
/// <param name="flags">One or more of the option flags.</param>
/// <returns>The resulting image.</returns>
/// <exception cref="PlatformNotSupportedException"></exception>
public Image GetImage(Size size, ShellItemGetImageOptions flags)
{
if (!IsMinVista) throw new PlatformNotSupportedException();
if (iShellItem is IShellItemImageFactory fctry)
{
var hres = fctry.GetImage(size, (SIIGBF)flags, out var hbitmap);
if (hres == 0x8004B200 && flags.IsFlagSet(ShellItemGetImageOptions.ThumbnailOnly))
throw new InvalidOperationException("Thumbnails are not supported by this item.");
hres.ThrowIfFailed();
//Marshal.ReleaseComObject(fctry);
return GetTransparentBitmap(hbitmap);
}
if (!flags.IsFlagSet(ShellItemGetImageOptions.IconOnly))
return GetThumbnail(size.Width);
throw new InvalidOperationException("Unable to retrieve an image for this item.");
}
/// <summary>Gets a property description list object given a reference to a property key.</summary>
/// <param name="keyType">
/// A reference to a PROPERTYKEY structure. The values in <see cref="PROPERTYKEY.System.PropList"/> are all valid.
/// <see cref="PROPERTYKEY.System.PropList.FullDetails"/> will return all properties.
/// </param>
/// <returns>A <see cref="PropertyDescriptionList"/> instance for the supplied key.</returns>
public PropertyDescriptionList GetPropertyDescriptionList(PROPERTYKEY keyType)
{
ThrowIfNoShellItem2();
return new PropertyDescriptionList(iShellItem2.GetPropertyDescriptionList(keyType, typeof(IPropertyDescriptionList).GUID));
}
/// <summary>Gets the stream of the file contents.</summary>
/// <param name="mode">Flags that should be used when opening the file.</param>
/// <returns>The file stream.</returns>
public ComStream GetStream(STGM mode = STGM.STGM_READWRITE)
{
//using (var ctx = new BindContext(mode))
var ctx = ShellUtil.CreateBindCtx(mode);
return new ComStream(GetHandler<IStream>(ctx));
}
/// <summary>Gets the formatted tool tip text associated with this item.</summary>
/// <param name="options">The option flags.</param>
/// <returns>The tool tip text formatted as per <paramref name="options"/>.</returns>
public string GetToolTip(ShellItemToolTipOptions options = ShellItemToolTipOptions.Default)
{
if (qi == null)
try
{
qi = (Parent ?? ShellFolder.Desktop).GetChildrenUIObjects<IQueryInfo>(null, this);
}
catch { }
if (qi == null) return "";
qi.GetInfoTip((QITIP)options, out var ret);
return ret ?? "";
}
/// <summary>Returns a <see cref="System.String"/> that represents this instance.</summary>
/// <returns>A <see cref="System.String"/> that represents this instance.</returns>
public override string ToString() => Name;
/// <summary>Ensures that all cached information for this item is updated.</summary>
public void Update()
{
props?.Commit();
ThrowIfNoShellItem2();
iShellItem2.Update(BindContext);
}
/// <summary>Open a new Windows Explorer window with this item selected.</summary>
public void ViewInExplorer() => SHOpenFolderAndSelectItems(Parent.PIDL, 1, new IntPtr[] { (IntPtr)PIDL }, OFASI.OFASI_NONE);
/// <summary>
/// Compares the current instance with another object of the same type and returns an integer that indicates whether the current
/// instance precedes, follows, or occurs in the same position in the sort order as the other object.
/// </summary>
/// <param name="other">An object to compare with this instance.</param>
/// <returns>
/// A value that indicates the relative order of the objects being compared. If the two items are the same this parameter equals
/// zero; if they are different the parameter is nonzero.
/// </returns>
int IComparable<ShellItem>.CompareTo(ShellItem other) => CompareTo(other);
/// <summary>Gets the BHID for the supplied <typeparamref name="TInterface"/>.</summary>
/// <typeparam name="TInterface">The type of the interface to lookup.</typeparam>
/// <returns>The related BHID if found, 0 if not.</returns>
internal static BHID GetBHIDForInterface<TInterface>()
{
if (bhidLookup == null)
bhidLookup = new Dictionary<Type, BHID>
{
//{ typeof(IShellFolder??, IStream??), BHID.BHID_SFObject },
{ typeof(IShellLinkW), BHID.BHID_SFUIObject },
//{ typeof(Others??), BHID.BHID_SFUIObject },
//{ typeof(IShellItemResources??, IShellView??), BHID.BHID_SFViewObject },
{ typeof(IStorage), BHID.BHID_Storage },
{ typeof(IStream), BHID.BHID_Stream },
//{ typeof(IShellItem??), BHID.BHID_LinkTargetItem },
//{ typeof(IEnumShellItems), BHID.BHID_StorageEnum }, // Can't have multiple keys
// TODO: { typeof(ITransferSource), BHID.BHID_Transfer },
// TODO: { typeof(ITransferDestination), BHID.BHID_Transfer },
{ typeof(IPropertyStore), BHID.BHID_PropertyStore },
{ typeof(IPropertyStoreFactory), BHID.BHID_PropertyStore },
{ typeof(IExtractImage), BHID.BHID_ThumbnailHandler },
{ typeof(IThumbnailProvider), BHID.BHID_ThumbnailHandler },
{ typeof(IEnumShellItems), BHID.BHID_EnumItems },
{ typeof(IDataObject), BHID.BHID_DataObject },
{ typeof(IQueryAssociations), BHID.BHID_AssociationArray },
// IGNORE: Not supported { typeof(IFilter), BHID.BHID_Filter },
{ typeof(IEnumAssocHandlers), BHID.BHID_EnumAssocHandlers },
// TODO: Win8+ { typeof(IRandomAccessStream), BHID.BHID_RandomAccessStream },
//{ typeof(??), BHID.BHID_FilePlaceholder },
};
return bhidLookup.TryGetValue(typeof(TInterface), out var value) ? value : 0;
}
internal static string GetStringValue(Action<StringBuilder, int> method, int buffSize = MAX_PATH)
{
while (true)
{
var ret = new StringBuilder(buffSize, buffSize);
try { method(ret, ret.Capacity); }
catch (COMException ex) { if (ex.ErrorCode == unchecked((int)0x8007007A) || ex.ErrorCode == unchecked((int)0x800700EA) || buffSize <= 8192) buffSize *= 2; else throw ex; }
return ret.ToString();
}
}
/// <summary>Throws an exception if no IShellItem2 instance can be retrieved.</summary>
internal void ThrowIfNoShellItem2()
{
if (iShellItem2 == null)
throw new InvalidOperationException("Unable to get access to this detail.");
}
/// <summary>Enumerates all the children of the current item. Not valid before Vista.</summary>
/// <returns>An enumeration of the child objects.</returns>
protected virtual IEnumerable<ShellItem> EnumerateChildren()
{
if (!IsMinVista) yield break;
IEnumShellItems ie = null;
try
{
ie = GetHandler<IEnumShellItems>(BHID.BHID_EnumItems);
}
catch (Exception e) { Debug.WriteLine($"Unable to enum children: {e.Message}"); }
if (ie != null)
{
var a = new IShellItem[1];
while (ie.Next(1, a, out var f).Succeeded && f == 1)
{
ShellItem i = null;
try { i = Open(a[0]); } catch (Exception e) { Debug.WriteLine($"Unable to open child: {e.Message}"); }
if (i != null) yield return i;
}
Marshal.ReleaseComObject(ie);
}
}
/// <summary>Gets the display name.</summary>
/// <param name="dn">The display name option.</param>
/// <returns>The display name.</returns>
protected virtual string GetDisplayName(SIGDN dn)
{
try { return iShellItem?.GetDisplayName(dn); } catch { }
return null;
}
/// <summary>Initializes this instance with the specified IShellItem.</summary>
/// <param name="si">The IShellItem object.</param>
protected void Init(IShellItem si)
{
iShellItem = si ?? throw new ArgumentNullException(nameof(si));
iShellItem2 = si as IShellItem2;
}
private static bool Equals(IShellItem left, IShellItem right)
{
if (ReferenceEquals(left, right)) return true;
if (left is null || right is null) return false;
return left.Compare(right, SICHINTF.SICHINT_TEST_FILESYSPATH_IF_NOT_EQUAL) == 0;
}
private static Bitmap GetTransparentBitmap(HBITMAP hbitmap)
{
var dibsection = GetObject<DIBSECTION>(hbitmap);
var bitmap = new Bitmap(dibsection.dsBm.bmWidth, dibsection.dsBm.bmHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using (var mstr = new MarshalingStream(dibsection.dsBm.bmBits, (long)dibsection.dsBm.bmBits))
{
for (var x = 0; x < dibsection.dsBmih.biWidth; x++)
for (var y = 0; y < dibsection.dsBmih.biHeight; y++)
{
var rgbquad = mstr.Read<RGBQUAD>();
if (rgbquad.rgbReserved != 0)
bitmap.SetPixel(x, y, rgbquad.Color);
}
}
return bitmap;
}
/// <summary>Gets the thumbnail image for the item using the specified characteristics.</summary>
/// <param name="width">The width, in pixels, of the Bitmap.</param>
/// <returns>The resulting Bitmap, on success, or <c>null</c> on failure.</returns>
private Image GetThumbnail(int width = 32)
{
IThumbnailProvider provider = null;
try
{
provider = GetHandler<IThumbnailProvider>(BHID.BHID_ThumbnailHandler);
if (provider == null) return null;
provider.GetThumbnail((uint)width, out var hbmp, out var alpha);
return hbmp.ToBitmap();
}
catch
{
return null;
}
finally
{
if (provider != null) Marshal.ReleaseComObject(provider);
}
}
protected class ShellItemImpl : IDisposable, IShellItem
{
public ShellItemImpl(PIDL pidl, bool owner) => PIDL = owner ? pidl : new PIDL(pidl);
private PIDL PIDL { get; set; }
[return: MarshalAs(UnmanagedType.Interface)]
public object BindToHandler(IBindCtx pbc, in Guid bhid, in Guid riid)
{
if (riid == typeof(IShellFolder).GUID)
return Marshal.GetIUnknownForObject(GetIShellFolder());
throw new InvalidCastException();
}
public int Compare(IShellItem psi, SICHINTF hint)
{
var other = (ShellItemImpl)psi;
var p1 = InternalGetParent();
var p2 = other.InternalGetParent();
if (p1.PIDL.Equals(p2.PIDL))
return p1.GetIShellFolder().CompareIDs((IntPtr)(int)hint, PIDL.LastId, other.PIDL.LastId).Code;
return 1;
}
public void Dispose() => PIDL = null;
public SFGAO GetAttributes(SFGAO sfgaoMask)
{
var parentFolder = InternalGetParent().GetIShellFolder();
var result = sfgaoMask;
parentFolder.GetAttributesOf(1, new[] { (IntPtr)PIDL.LastId }, ref result);
return result & sfgaoMask;
}
public SafeCoTaskMemString GetDisplayName(SIGDN sigdnName)
{
if (sigdnName == SIGDN.SIGDN_FILESYSPATH)
{
var result = new StringBuilder(512);
if (!SHGetPathFromIDList(PIDL, result))
throw new ArgumentException();
return new SafeCoTaskMemString(result.ToString(), CharSet.Unicode);
}
var parentFolder = InternalGetParent().GetIShellFolder();
var child = PIDL.LastId;
return new SafeCoTaskMemString(parentFolder.GetDisplayNameOf(child, (SHGDNF)((int)sigdnName & 0xffff)), CharSet.Unicode);
}
public IShellItem GetParent()
{
var pidlCopy = new PIDL(PIDL);
if (!pidlCopy.RemoveLastId())
Marshal.ThrowExceptionForHR((int)new HRESULT(HRESULT.MK_E_NOOBJECT));
return new ShellItemImpl(pidlCopy, true);
}
private IShellFolder GetIShellFolder()
{
SHGetFolderLocation(IntPtr.Zero, 0, HTOKEN.NULL, 0, out var dtPidl);
if (ShellFolder.Desktop.PIDL.Equals(dtPidl))
return ShellFolder.Desktop.iShellFolder;
return (IShellFolder)ShellFolder.Desktop.iShellFolder.BindToObject(PIDL, null, typeof(IShellFolder).GUID);
}
private ShellItemImpl InternalGetParent()
{
var pidlCopy = new PIDL(PIDL);
return pidlCopy.RemoveLastId() ? new ShellItemImpl(pidlCopy, true) : this;
}
}
}
}