SystemTrayMenu/Utilities/File/IconReader.cs
Peter Kirmeier da4ec8bec4 Fix issue of some icons not being loaded properly
Make sure native Icon calls are running within STA threads (resolves loading issues).
Make sure bitmaps are created on UI thread to ensure ownership (resolves display/update issues).
Remove obsolete helper routines (GetParentWindow and IconToImageSourceConverter).
2023-08-13 02:13:18 +02:00

289 lines
12 KiB
C#

// <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.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using SystemTrayMenu.DllImports;
using SystemTrayMenu.Helpers;
/// <summary>
/// Provides static methods to read system icons for folders and files.
/// </summary>
internal static class IconReader
{
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"];
// see https://github.com/Hofknecht/SystemTrayMenu/issues/209.
internal static bool IsPreloading { get; set; } = true;
internal static void ClearCacheWhenLimitReached()
{
if (IconDictCache.Count > Properties.Settings.Default.ClearCacheIfMoreThanThisNumberOfItems)
{
IconDictCache.Clear();
GC.Collect();
}
}
internal static void RemoveIconFromCache(string path) => IconDictPersistent.Remove(path, out _);
internal static bool GetFileIconWithCache(
string path,
string resolvedPath,
bool linkOverlay,
bool checkPersistentFirst,
Action<BitmapSource?> onIconLoaded)
{
bool cacheHit;
string key;
string extension = Path.GetExtension(path);
if (IsExtensionWithSameIcon(extension))
{
key = extension + linkOverlay;
}
else
{
key = path;
}
if (!DictIconCache(checkPersistentFirst).TryGetValue(key, out BitmapSource? icon) &&
!DictIconCache(!checkPersistentFirst).TryGetValue(key, out icon))
{
cacheHit = false;
Thread thread = new(UpdateIconInBackgroundSTA);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
void UpdateIconInBackgroundSTA()
{
BitmapSource icon = DictIconCache(checkPersistentFirst).GetOrAdd(key, FactoryIconFolderSTA);
onIconLoaded(icon);
}
BitmapSource FactoryIconFolderSTA(string keyExtension)
{
return GetIconAsBitmapSourceSTA(path, resolvedPath, linkOverlay, false);
}
}
else
{
cacheHit = true;
onIconLoaded(icon);
}
return cacheHit;
}
internal static bool GetFolderIconWithCache(
string path,
bool linkOverlay,
bool checkPersistentFirst,
Action<BitmapSource?> onIconLoaded)
{
bool cacheHit;
string key = path;
if (!DictIconCache(checkPersistentFirst).TryGetValue(key, out BitmapSource? icon) &&
!DictIconCache(!checkPersistentFirst).TryGetValue(key, out icon))
{
cacheHit = false;
if (IsPreloading)
{
cacheHit = true;
icon = DictIconCache(checkPersistentFirst).GetOrAdd(key, FactoryIconFolderSTA);
onIconLoaded(icon);
}
else
{
Thread thread = new (UpdateIconInBackgroundSTA);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
void UpdateIconInBackgroundSTA()
{
BitmapSource icon = DictIconCache(checkPersistentFirst).GetOrAdd(key, FactoryIconFolderSTA);
onIconLoaded(icon);
}
}
BitmapSource FactoryIconFolderSTA(string keyExtension)
{
return GetIconAsBitmapSourceSTA(path, path, linkOverlay, true);
}
}
else
{
cacheHit = true;
onIconLoaded(icon);
}
return cacheHit;
}
internal static Icon? GetRootFolderIcon(string path)
{
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);
}
private static BitmapSource CreateBitmapSourceFromIcon(Icon icon) => Application.Current.Dispatcher.Invoke(() =>
{
BitmapSource bitmap = Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
bitmap.Freeze();
return bitmap;
});
private static BitmapSource? TryGetIconAsBitmapSourceSTA(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);
}
}
else if (!isFolder && File.Exists(resolvedPath) &&
Path.GetExtension(resolvedPath).Equals(".ico", StringComparison.InvariantCultureIgnoreCase))
{
icon = Icon.ExtractAssociatedIcon(resolvedPath);
if (icon != null)
{
result = CreateBitmapSourceFromIcon(icon);
if (linkOverlay && OverlayImage != null)
{
result = ImagingHelper.CreateIconWithOverlay(result, OverlayImage);
}
}
}
else
{
// This code block must run in an STA thread otherwise the results may be incorrectly loaded icons!
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();
return result;
}
private static BitmapSource GetIconAsBitmapSourceSTA(string path, string resolvedPath, bool linkOverlay, bool isFolder)
{
BitmapSource? bitmapSource = TryGetIconAsBitmapSourceSTA(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;
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)
{
uint flags = NativeMethods.ShgfiIcon | NativeMethods.ShgfiSYSICONINDEX;
if (linkOverlay)
{
flags += NativeMethods.ShgfiLINKOVERLAY;
}
if (!largeIcon)
{
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);
}
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.
icon = (Icon)Icon.FromHandle(hIcon).Clone();
}
catch (Exception ex)
{
Log.Warn($"path:'{path}'", ex);
}
if (!linkOverlay)
{
_ = NativeMethods.User32DestroyIcon(hIcon);
}
_ = NativeMethods.User32DestroyIcon(shFileInfo.hIcon);
}
return icon;
}
}
}