Files
Explorer-Workbench/src/Explorer.App/ShellIconConverter.cs

169 lines
6.0 KiB
C#

using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Data;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Explorer.Domain;
using Explorer.Presentation;
namespace Explorer.App;
public sealed class ShellIconConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var large = parameter is string p && p.Equals("large", StringComparison.OrdinalIgnoreCase);
return value switch
{
FolderItemViewModel item => ShellIconCache.Get(item.FullPath, item.IsDirectory, large, item.MayHydrateOnRead),
FileSystemItem fs => ShellIconCache.Get(fs.FullPath, fs.IsDirectory, large, AttributeFlags.MayHydrateOnRead(fs.Attributes)),
string path => ShellIconCache.Get(path, Directory.Exists(path), large),
_ => ShellIconCache.Get(null, true, large)
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
internal static class ShellIconCache
{
private static readonly ConcurrentDictionary<string, ImageSource> Cache = new(StringComparer.OrdinalIgnoreCase);
public static ImageSource Get(string? path, bool isDirectory, bool large = false, bool avoidHydration = false)
{
var key = (large ? "L:" : "S:") + (avoidHydration ? "A:" : "") + CacheKey(path, isDirectory);
return Cache.GetOrAdd(key, _ => Load(path, isDirectory, large, avoidHydration));
}
private static string CacheKey(string? path, bool isDirectory)
{
if (string.IsNullOrWhiteSpace(path) || path == "This PC")
{
return isDirectory ? "dir:generic" : "file:generic";
}
if (path == LocationRoots.RecycleBin)
{
return "recycle";
}
if (isDirectory)
{
return PathRules.IsDriveRoot(path) || (path.Length <= 3 && path.Contains(':', StringComparison.Ordinal))
? "drive:" + path.TrimEnd('\\').ToUpperInvariant()
: "dir:generic";
}
var ext = Path.GetExtension(path);
if (ext.Equals(".exe", StringComparison.OrdinalIgnoreCase)
|| ext.Equals(".lnk", StringComparison.OrdinalIgnoreCase)
|| ext.Equals(".ico", StringComparison.OrdinalIgnoreCase)
|| ext.Equals(".dll", StringComparison.OrdinalIgnoreCase))
{
return "path:" + path;
}
return "ext:" + (string.IsNullOrEmpty(ext) ? ".file" : ext.ToLowerInvariant());
}
private static ImageSource Load(string? path, bool isDirectory, bool large, bool avoidHydration = false)
{
try
{
var info = new ShFileInfo();
uint flags = ShgfiIcon | (large ? ShgfiLargeIcon : ShgfiSmallIcon);
uint attrs = isDirectory ? FileAttributeDirectory : FileAttributeNormal;
string psz;
if (string.IsNullOrWhiteSpace(path) || path == "This PC")
{
psz = isDirectory ? "folder" : "file";
flags |= ShgfiUseFileAttributes;
}
else if (path == LocationRoots.RecycleBin)
{
psz = @"::{645FF040-5081-101B-9F08-00AA002F954E}";
}
else if (isDirectory && PathRules.IsDriveRoot(path))
{
psz = PathRules.EnsureDirectoryTrailingSlashIfRoot(path);
}
else if (isDirectory)
{
psz = path;
flags |= ShgfiUseFileAttributes;
}
else
{
psz = path;
var ext = Path.GetExtension(path);
if (avoidHydration
|| !ext.Equals(".exe", StringComparison.OrdinalIgnoreCase)
&& !ext.Equals(".lnk", StringComparison.OrdinalIgnoreCase)
&& !ext.Equals(".ico", StringComparison.OrdinalIgnoreCase)
&& File.Exists(path) == false)
{
flags |= ShgfiUseFileAttributes;
}
}
SHGetFileInfo(psz, attrs, ref info, (uint)Marshal.SizeOf<ShFileInfo>(), flags);
if (info.hIcon == 0)
{
return Fallback();
}
try
{
var source = Imaging.CreateBitmapSourceFromHIcon(info.hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
source.Freeze();
return source;
}
finally
{
DestroyIcon(info.hIcon);
}
}
catch
{
return Fallback();
}
}
private static ImageSource Fallback()
{
var bmp = BitmapSource.Create(16, 16, 96, 96, PixelFormats.Bgra32, null, new byte[16 * 16 * 4], 16 * 4);
bmp.Freeze();
return bmp;
}
private const uint ShgfiIcon = 0x100;
private const uint ShgfiLargeIcon = 0x0;
private const uint ShgfiSmallIcon = 0x1;
private const uint ShgfiUseFileAttributes = 0x10;
private const uint FileAttributeDirectory = 0x10;
private const uint FileAttributeNormal = 0x80;
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern nint SHGetFileInfo(string pszPath, uint dwFileAttributes, ref ShFileInfo psfi, uint cbFileInfo, uint uFlags);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool DestroyIcon(nint hIcon);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct ShFileInfo
{
public nint hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string szTypeName;
}
}