SystemTrayMenu/Utilities/File/IconReader.cs

287 lines
11 KiB
C#
Raw Normal View History

2023-04-17 09:27:27 +12:00
// <copyright file="IconReader.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
// see also: https://www.codeproject.com/Articles/2532/Obtaining-and-managing-file-and-folder-icons-using.
namespace SystemTrayMenu.Utilities
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
2023-04-17 09:27:27 +12:00
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Media.Imaging;
2023-04-17 09:27:27 +12:00
using SystemTrayMenu.DllImports;
using SystemTrayMenu.Helpers;
2023-04-17 09:27:27 +12:00
/// <summary>
/// Provides static methods to read system icons for folders and files.
/// </summary>
internal static class IconReader
2023-04-17 09:27:27 +12:00
{
private static readonly ConcurrentDictionary<string, BitmapSource> IconDictPersistent = new();
private static readonly ConcurrentDictionary<string, BitmapSource> IconDictCache = new();
private static readonly BitmapSource? OverlayImage = (BitmapSource?)Application.Current.Resources["LinkArrowIconImage"];
2023-04-17 09:27:27 +12:00
// see https://github.com/Hofknecht/SystemTrayMenu/issues/209.
internal static bool IsPreloading { get; set; } = true;
2023-04-17 09:27:27 +12:00
internal static void ClearCacheWhenLimitReached()
2023-04-17 09:27:27 +12:00
{
if (IconDictCache.Count > Properties.Settings.Default.ClearCacheIfMoreThanThisNumberOfItems)
2023-04-17 09:27:27 +12:00
{
IconDictCache.Clear();
GC.Collect();
2023-04-17 09:27:27 +12:00
}
}
internal static void RemoveIconFromCache(string path) => IconDictPersistent.Remove(path, out _);
2023-04-17 09:27:27 +12:00
internal static bool GetFileIconWithCache(
string path,
string resolvedPath,
2023-04-17 09:27:27 +12:00
bool linkOverlay,
bool checkPersistentFirst,
Action<BitmapSource?> onIconLoaded)
2023-04-17 09:27:27 +12:00
{
bool cacheHit;
string key;
string extension = Path.GetExtension(path);
if (IsExtensionWithSameIcon(extension))
2022-12-06 09:46:53 +13:00
{
key = extension + linkOverlay;
}
else
{
key = path;
}
2022-12-06 09:46:53 +13:00
if (!DictIconCache(checkPersistentFirst).TryGetValue(key, out BitmapSource? icon) &&
!DictIconCache(!checkPersistentFirst).TryGetValue(key, out icon))
{
cacheHit = false;
new Thread(UpdateIconInBackground).Start();
void UpdateIconInBackground()
2022-12-06 09:46:53 +13:00
{
BitmapSource icon = DictIconCache(checkPersistentFirst).GetOrAdd(key, FactoryIconFile);
onIconLoaded(icon);
}
2022-12-06 09:46:53 +13:00
BitmapSource FactoryIconFile(string keyExtension)
{
return GetIconAsBitmapSource(path, resolvedPath, linkOverlay, false);
2022-12-06 09:46:53 +13:00
}
2023-04-17 09:27:27 +12:00
}
else
{
cacheHit = true;
onIconLoaded(icon);
}
2023-04-17 09:27:27 +12:00
return cacheHit;
2023-04-17 09:27:27 +12:00
}
internal static bool GetFolderIconWithCache(
string path,
2023-04-17 09:27:27 +12:00
bool linkOverlay,
bool checkPersistentFirst,
Action<BitmapSource?> onIconLoaded)
2023-04-17 09:27:27 +12:00
{
bool cacheHit;
string key = path;
if (!DictIconCache(checkPersistentFirst).TryGetValue(key, out BitmapSource? icon) &&
!DictIconCache(!checkPersistentFirst).TryGetValue(key, out icon))
2022-12-06 09:46:53 +13:00
{
cacheHit = false;
2022-12-06 09:46:53 +13:00
if (IsPreloading)
2022-12-06 09:46:53 +13:00
{
cacheHit = true;
icon = DictIconCache(checkPersistentFirst).GetOrAdd(key, FactoryIconFolder);
onIconLoaded(icon);
}
else
{
new Thread(UpdateIconInBackground).Start();
void UpdateIconInBackground()
{
BitmapSource icon = DictIconCache(checkPersistentFirst).GetOrAdd(key, FactoryIconFolder);
onIconLoaded(icon);
}
}
2022-12-06 09:46:53 +13:00
BitmapSource FactoryIconFolder(string keyExtension)
{
return GetIconAsBitmapSource(path, path, linkOverlay, true);
2022-12-06 09:46:53 +13:00
}
2023-04-17 09:27:27 +12:00
}
else
{
cacheHit = true;
onIconLoaded(icon);
}
2023-04-17 09:27:27 +12:00
return cacheHit;
2023-04-17 09:27:27 +12:00
}
internal static Icon? GetRootFolderIcon(string path)
2023-04-17 09:27:27 +12:00
{
NativeMethods.SHFILEINFO shFileInfo = default;
bool linkOverlay = false;
bool largeIcon = false;
uint flags = GetFlags(linkOverlay, largeIcon);
uint attribute = NativeMethods.FileAttributeDirectory;
IntPtr imageList = NativeMethods.Shell32SHGetFileInfo(path, attribute, ref shFileInfo, (uint)Marshal.SizeOf(shFileInfo), flags);
return GetIcon(path, linkOverlay, shFileInfo, imageList);
}
internal static BitmapSource? TryGetIconAsBitmapSource(string path, string resolvedPath, bool linkOverlay, bool isFolder)
{
BitmapSource? result = null;
Icon? icon;
if (!isFolder && Path.GetExtension(path).Equals(".ico", StringComparison.InvariantCultureIgnoreCase))
{
icon = Icon.ExtractAssociatedIcon(path);
if (icon != null)
{
result = CreateBitmapSourceFromIcon(icon);
icon.Dispose();
}
}
else if (!isFolder && File.Exists(resolvedPath) &&
Path.GetExtension(resolvedPath).Equals(".ico", StringComparison.InvariantCultureIgnoreCase))
{
icon = Icon.ExtractAssociatedIcon(resolvedPath);
if (icon != null)
2023-04-17 09:27:27 +12:00
{
result = CreateBitmapSourceFromIcon(icon);
icon.Dispose();
if (linkOverlay && OverlayImage != null)
{
result = ImagingHelper.CreateIconWithOverlay(result, OverlayImage);
}
2023-04-17 09:27:27 +12:00
}
}
else
{
NativeMethods.SHFILEINFO shFileInfo = default;
bool largeIcon = // Note: large returns another folder icon than windows explorer
Scaling.Factor >= 1.25f ||
Scaling.FactorByDpi >= 1.25f ||
Properties.Settings.Default.IconSizeInPercent / 100f >= 1.25f;
uint flags = GetFlags(linkOverlay, largeIcon);
uint attribute = isFolder ? NativeMethods.FileAttributeDirectory : NativeMethods.FileAttributeNormal;
IntPtr imageList = NativeMethods.Shell32SHGetFileInfo(path, attribute, ref shFileInfo, (uint)Marshal.SizeOf(shFileInfo), flags);
icon = GetIcon(path, linkOverlay, shFileInfo, imageList);
if (icon != null)
{
result = CreateBitmapSourceFromIcon(icon);
icon.Dispose();
}
2023-04-17 09:27:27 +12:00
}
return result;
2023-04-17 09:27:27 +12:00
}
private static BitmapSource GetIconAsBitmapSource(string path, string resolvedPath, bool linkOverlay, bool isFolder)
{
BitmapSource? bitmapSource = TryGetIconAsBitmapSource(path, resolvedPath, linkOverlay, isFolder);
bitmapSource ??= (BitmapSource)Application.Current.Resources["NotFoundIconImage"];
bitmapSource.Freeze(); // Make it accessible for any thread
return bitmapSource;
}
private static ConcurrentDictionary<string, BitmapSource> DictIconCache(bool checkPersistentFirst)
=> checkPersistentFirst ? IconDictPersistent : IconDictCache;
2023-04-17 09:27:27 +12:00
private static bool IsExtensionWithSameIcon(string fileExtension)
{
bool isExtensionWithSameIcon = true;
List<string> extensionsWithDiffIcons = new() { string.Empty, ".EXE", ".LNK", ".ICO", ".URL" };
if (extensionsWithDiffIcons.Contains(fileExtension.ToUpperInvariant()))
{
isExtensionWithSameIcon = false;
}
return isExtensionWithSameIcon;
}
private static uint GetFlags(bool linkOverlay, bool largeIcon)
2023-04-17 09:27:27 +12:00
{
uint flags = NativeMethods.ShgfiIcon | NativeMethods.ShgfiSYSICONINDEX;
if (linkOverlay)
{
flags += NativeMethods.ShgfiLINKOVERLAY;
}
if (!largeIcon)
2023-04-17 09:27:27 +12:00
{
flags += NativeMethods.ShgfiSMALLICON;
}
else
{
flags += NativeMethods.ShgfiLARGEICON;
}
return flags;
}
private static Icon? GetIcon(
string path, bool linkOverlay, NativeMethods.SHFILEINFO shFileInfo, IntPtr imageList)
{
Icon? icon = null;
if (imageList != IntPtr.Zero)
{
IntPtr hIcon;
if (linkOverlay)
{
hIcon = shFileInfo.hIcon;
}
else
{
hIcon = NativeMethods.ImageList_GetIcon(imageList, shFileInfo.iIcon, NativeMethods.IldTransparent);
2023-04-17 09:27:27 +12:00
}
try
{
// Note: Destroying hIcon after FromHandle will invalidate the Icon, so we do NOT destroy it, despite the request from documentation:
// https://learn.microsoft.com/en-us/dotnet/api/system.drawing.icon.fromhandle?view=dotnet-plat-ext-7.0
// Reason is https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/Icon.cs,555 (FromHandle)
// It is not taking over the ownership, data will be deleted upon destroying the original icon, so a clone is required.
// With Clone we actually get a new handle, so we can free up the original handle without killing our copy.
// Using Clone will also restore the ownership of the new icon handle, so we do not have to call DestroyIcon on it by ourself.
2023-04-17 09:27:27 +12:00
icon = (Icon)Icon.FromHandle(hIcon).Clone();
}
catch (Exception ex)
{
Log.Warn($"path:'{path}'", ex);
}
if (!linkOverlay)
{
_ = NativeMethods.User32DestroyIcon(hIcon);
2023-04-17 09:27:27 +12:00
}
_ = NativeMethods.User32DestroyIcon(shFileInfo.hIcon);
2023-04-17 09:27:27 +12:00
}
return icon;
}
private static BitmapSource CreateBitmapSourceFromIcon(Icon icon)
{
return (BitmapSource)new IconToImageSourceConverter().Convert(
icon,
typeof(BitmapSource),
null!,
CultureInfo.InvariantCulture);
}
2023-04-17 09:27:27 +12:00
}
}