Files

895 lines
36 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using BgraBuffer = FluidExplorer.Services.Icons.ShellNative.BgraBuffer;
namespace FluidExplorer.Services.Icons;
/// <summary>
/// 系统原版图标 / 缩略图服务。
///
/// 设计要点:
/// 1) 取图全部在外壳与 GDI 侧完成(可后台线程),只有 <see cref="SoftwareBitmapSource"/> 的创建被派回 UI 线程;
/// SoftwareBitmapSource 是 UI 线程亲和对象,后台线程碰它会直接崩。
/// 2) 结果带 LRU 缓存 + 同键请求合并(Lazy&lt;Task&gt;),列表快速滚动时不会重复解码同一张图。
/// 3) 任何未预期异常都会被吞成 null 并写 Debug 输出,服务层绝不把异常抛进 UI 线程。
/// </summary>
public sealed class ShellIconService : IIconService
{
private const int IconCacheCapacity = 512;
private const int ThumbnailCacheCapacity = 256;
private const int MaxConcurrentNativeWork = 4;
/// <summary>缩略图 E_PENDING 重试参数。</summary>
private const int ThumbnailRetryCount = 3;
/// <summary>缩略图 E_PENDING 重试间隔(毫秒)。</summary>
private const int ThumbnailRetryDelayMs = 200;
/// <summary>UI 调度器;极端情况下可能为 null(此时退化为在调用线程创建 ImageSource)。</summary>
private readonly DispatcherQueue? _uiDispatcher;
private readonly Func<double> _scaleProvider;
/// <summary>
/// 图标缓存:值类型是 <see cref="Task{TResult}"/>。
/// 缓存"共享的 Task"而不是最终 ImageSource,才能在解码未完成时就完成同键请求合并;
/// 这也是 Lazy&lt;Task&gt; 模式的落点(LruCache 内层用 Lazy 保证工厂只跑一次)。
/// </summary>
private readonly LruCache<Task<ImageSource?>> _iconCache = new(IconCacheCapacity);
/// <summary>缩略图缓存,容量更小(缩略图位图大得多,全部是 256px 级别的位图)。</summary>
private readonly LruCache<Task<ImageSource?>> _thumbnailCache = new(ThumbnailCacheCapacity);
/// <summary>把纯 GDI 取图/解码节流在 4 路,避免整屏滚动时把 CPU 打满。</summary>
private readonly SemaphoreSlim _nativeThrottle = new(MaxConcurrentNativeWork, MaxConcurrentNativeWork);
/// <summary>默认的 DPI 缩放(rasterizationScale 为 null 时按 1.0 处理)。</summary>
private static readonly Func<double> DefaultScale = static () => 1.0;
/// <summary>
/// 构造。签名固定为 (DispatcherQueue, Func&lt;double&gt;?),见 Services/AppServices.cs 的调用。
/// </summary>
/// <remarks>
/// 构造函数**不抛异常**:uiDispatcher 为 null 时回退到当前线程的 DispatcherQueue,
/// 再拿不到就退化为"直接在调用线程创建 ImageSource"。
/// 宁可降级也不要让 App 启动阶段直接崩掉。
/// </remarks>
public ShellIconService(DispatcherQueue uiDispatcher, Func<double>? rasterizationScale = null)
{
_uiDispatcher = uiDispatcher ?? TryGetCurrentDispatcher();
_scaleProvider = rasterizationScale ?? DefaultScale;
}
/// <summary>尽力拿到当前线程的 DispatcherQueue;拿不到返回 null(构造期不抛异常)。</summary>
private static DispatcherQueue? TryGetCurrentDispatcher()
{
try
{
return DispatcherQueue.GetForCurrentThread();
}
catch (Exception ex)
{
Debug.WriteLine($"[ShellIconService] GetForCurrentThread 失败: {ex.Message}");
return null;
}
}
/// <summary>当前构造时注入的 UI 调度器(ViewModel 可用它确认自己在哪个线程上调用)。</summary>
public DispatcherQueue? UiDispatcher => _uiDispatcher;
// ─────────────────────────────────────────────────────────────
// IIconService
// ─────────────────────────────────────────────────────────────
/// <inheritdoc />
public Task<ImageSource?> GetIconAsync(string path, bool isDirectory, int size, CancellationToken cancellationToken)
{
try
{
if (string.IsNullOrWhiteSpace(path)) return Task.FromResult<ImageSource?>(null);
int logical = NormalizeSize(size);
// 缓存键规则(关键,不要随意改动):
// - 文件 / 目录 / 驱动器一律用完整路径。因为 .exe 会带自身嵌入图标,
// .lnk 指向不同目标,文件夹可能带 OneDrive / 共享 / 快捷方式角标,
// 只有按路径缓存才不会串图。
// - 扩展名走 ".ext" 键(见 GetExtensionIconAsync),SHGFI_USEFILEATTRIBUTES 不碰磁盘。
string key = BuildIconKey(path, isDirectory, logical);
return GetOrAddAsync(_iconCache, key, ct => LoadIconCoreAsync(path, isDirectory, logical, ct), cancellationToken);
}
catch (Exception ex)
{
// 公开方法绝不抛异常给调用方(列表滚动会崩主程序)
Debug.WriteLine($"[ShellIconService] GetIconAsync 失败 {path}: {ex}");
return Task.FromResult<ImageSource?>(null);
}
}
/// <inheritdoc />
public Task<ImageSource?> GetThumbnailAsync(string path, int size, CancellationToken cancellationToken)
{
try
{
if (string.IsNullOrWhiteSpace(path)) return Task.FromResult<ImageSource?>(null);
int logical = NormalizeSize(size);
string key = "thumb|" + logical.ToString() + "|" + path;
return GetOrAddAsync(_thumbnailCache, key, ct => LoadThumbnailCoreAsync(path, logical, ct), cancellationToken);
}
catch (Exception ex)
{
Debug.WriteLine($"[ShellIconService] GetThumbnailAsync 失败 {path}: {ex}");
return Task.FromResult<ImageSource?>(null);
}
}
/// <inheritdoc />
public Task<ImageSource?> GetExtensionIconAsync(string extension, int size, CancellationToken cancellationToken)
{
try
{
string ext = NormalizeExtension(extension);
if (ext.Length == 0) return Task.FromResult<ImageSource?>(null);
int logical = NormalizeSize(size);
// ".ext" 键:同类型文件共用一个位图,命中率极高,且不需要访问磁盘
string key = "ext|" + logical.ToString() + "|." + ext;
return GetOrAddAsync(_iconCache, key, ct => LoadExtensionIconCoreAsync(ext, logical, ct), cancellationToken);
}
catch (Exception ex)
{
Debug.WriteLine($"[ShellIconService] GetExtensionIconAsync 失败 {extension}: {ex}");
return Task.FromResult<ImageSource?>(null);
}
}
/// <inheritdoc />
/// <remarks>
/// 注意:本方法是同步签名(见 IIconService)。它只应在 UI 线程调用,内部会同步等待取图完成,
/// 因此仅适合启动阶段取少量侧边栏图标,不要在滚动/渲染路径里按行调用。
/// </remarks>
public ImageSource? GetSpecialFolderIcon(string parsingName, int size)
{
try
{
if (string.IsNullOrWhiteSpace(parsingName)) return null;
// 该重载不会死锁:内部从不依赖调用方线程继续泵消息(软件位图那一段走 TryEnqueue)
return GetIconAsync(parsingName, isDirectory: true, size, CancellationToken.None)
.WaitAsync(TimeSpan.FromSeconds(5))
.GetAwaiter()
.GetResult();
}
catch (TimeoutException ex)
{
Debug.WriteLine($"[ShellIconService] 特殊文件夹图标超时 {parsingName}: {ex.Message}");
return null;
}
catch (Exception ex)
{
// 含 OperationCanceledException:公开方法一律不外抛
Debug.WriteLine($"[ShellIconService] 特殊文件夹图标失败 {parsingName}: {ex}");
return null;
}
}
/// <inheritdoc />
public void ClearCache()
{
try
{
_iconCache.Clear();
_thumbnailCache.Clear();
}
catch (Exception ex)
{
Debug.WriteLine($"[ShellIconService] ClearCache 失败: {ex}");
}
}
// ─────────────────────────────────────────────────────────────
// 取图主流程(全部在后台线程执行)
// ─────────────────────────────────────────────────────────────
/// <summary>文件 / 目录 / 驱动器 / 已知外壳对象的图标。</summary>
private Task<ImageSource?> LoadIconCoreAsync(string path, bool isDirectory, int logicalSize, CancellationToken ct)
{
return RunSafeAsync(async () =>
{
await _nativeThrottle.WaitAsync(ct).ConfigureAwait(false);
try
{
BgraBuffer? pixels = null;
if (IsSpecialParsingName(path))
{
// shell:RecycleBinFolder、::{20D04FE0-...}(此电脑)之类:
// 必须先解析成 PIDL,再交给 SHGetFileInfoW 的 PIDL 重载
pixels = ShellParsingName.IsShellPrefix(path)
? LoadIconFromShellParsingName(path, logicalSize)
: LoadIconFromParsingName(path, logicalSize);
}
else if (!isDirectory && logicalSize >= 48 && File.Exists(path))
{
// 大尺寸文件图标优先走 IShellItemImageFactory + SIIGBF_ICONONLY:
// 它返回的是外壳为该文件类型选定的高清图标(含 .exe/.lnk 的个性化图标),
// 比把 32x32 拉伸到 48 清晰得多。
// File.Exists 先挡一道:SHCreateItemFromParsingName 对不存在的路径会抛/失败,避免无谓开销。
pixels = LoadIconViaShellItemImageFactory(path, logicalSize);
}
// 回退 / 小尺寸路径:SHGetFileInfoW。
// 小尺寸用这条更贴近资源管理器的列表视图(16/32 就是系统图像列表原生尺寸,不经缩放)。
pixels ??= LoadIconViaShellFileInfo(path, isDirectory, logicalSize);
ct.ThrowIfCancellationRequested();
if (pixels is null) return null;
return await CreateImageSourceAsync(pixels).ConfigureAwait(false);
}
finally
{
_nativeThrottle.Release();
}
});
}
/// <summary>扩展名通用图标:SHGFI_USEFILEATTRIBUTES + FILE_ATTRIBUTE_NORMAL,不访问磁盘。</summary>
private Task<ImageSource?> LoadExtensionIconCoreAsync(string extension, int logicalSize, CancellationToken ct)
{
return RunSafeAsync(async () =>
{
await _nativeThrottle.WaitAsync(ct).ConfigureAwait(false);
try
{
// 路径传 "x.ext" 纯粹是为了让外壳从扩展名推断类型;
// SHGFI_USEFILEATTRIBUTES 保证它不会去访问磁盘(这也是它比按路径取快一个数量级的原因)。
string fakePath = "x." + extension;
BgraBuffer? pixels = LoadIconViaShellFileInfoCore(
fakePath,
ShellNative.FILE_ATTRIBUTE_NORMAL,
ShellNative.SHGFI_USEFILEATTRIBUTES,
logicalSize);
ct.ThrowIfCancellationRequested();
if (pixels is null) return null;
return await CreateImageSourceAsync(pixels).ConfigureAwait(false);
}
finally
{
_nativeThrottle.Release();
}
});
}
/// <summary>
/// 缩略图:SHCreateItemFromParsingName + IShellItemImageFactory.GetImage。
/// 失败一律返回 null(不抛异常);E_PENDING 表示外壳正在后台解码,等 200ms 重试,最多 3 次。
/// </summary>
private Task<ImageSource?> LoadThumbnailCoreAsync(string path, int logicalSize, CancellationToken ct)
{
return RunSafeAsync(async () =>
{
await _nativeThrottle.WaitAsync(ct).ConfigureAwait(false);
try
{
int pixelSize = ToPixelSize(logicalSize);
Guid iid = typeof(ShellNative.IShellItemImageFactory).GUID;
ShellNative.IShellItemImageFactory? factory = null;
try
{
int hr = ShellNative.SHCreateItemFromParsingName(path, IntPtr.Zero, ref iid, out factory);
if (hr < 0 || factory is null)
{
Debug.WriteLine($"[ShellIconService] SHCreateItemFromParsingName 失败 0x{hr:X8}: {path}");
return null;
}
// THUMBNAILONLY:没有真实缩略图就直接失败(交给上层显示图标);
// BIGGERSIZEOK:允许外壳返回更大的缓存图,由 XAML 侧缩放,反而更清晰。
const int flags = ShellNative.SIIGBF_THUMBNAILONLY | ShellNative.SIIGBF_BIGGERSIZEOK;
BgraBuffer? pixels = null;
for (int attempt = 0; attempt <= ThumbnailRetryCount; attempt++)
{
ct.ThrowIfCancellationRequested();
var size = new ShellNative.SIZE(pixelSize, pixelSize);
int hrImage = factory.GetImage(size, flags, out IntPtr hBitmap);
if (hrImage == ShellNative.E_PENDING)
{
// 外壳正在生成缩略图:等一下再来
await Task.Delay(ThumbnailRetryDelayMs, ct).ConfigureAwait(false);
continue;
}
if (hrImage < 0 || hBitmap == IntPtr.Zero)
{
// 没有缩略图(例如 .txt / 未知类型)——这是正常情况,不记错误
if (hBitmap != IntPtr.Zero) ShellNative.DeleteObject(hBitmap);
return null;
}
try
{
pixels = ShellNative.BitmapToBgra(hBitmap, pixelSize, pixelSize);
}
finally
{
// IShellItemImageFactory 返回的 HBITMAP 归调用方所有,必须释放
ShellNative.DeleteObject(hBitmap);
}
break;
}
ct.ThrowIfCancellationRequested();
if (pixels is null) return null;
// 空位图保护:尺寸为 0 或无像素时不产出 ImageSource
if (pixels.Width <= 0 || pixels.Height <= 0 || pixels.Pixels.Length == 0) return null;
return await CreateImageSourceAsync(pixels).ConfigureAwait(false);
}
finally
{
if (factory is not null)
{
try
{
Marshal.FinalReleaseComObject(factory);
}
catch (ArgumentException)
{
// 已被释放:忽略
}
}
}
}
finally
{
_nativeThrottle.Release();
}
});
}
// ─────────────────────────────────────────────────────────────
// 各条取图路径的 Win32 细节
// ─────────────────────────────────────────────────────────────
/// <summary>SHGetFileInfoW 路径:目录走真实路径(保角标/个性化图标),文件走属性推断。</summary>
private BgraBuffer? LoadIconViaShellFileInfo(string path, bool isDirectory, int logicalSize)
{
if (isDirectory)
{
// 目录必须用真实路径:这样 OneDrive / 共享 / 快捷方式角标才会被带上
return LoadIconViaShellFileInfoCore(path, ShellNative.FILE_ATTRIBUTE_DIRECTORY, 0, logicalSize);
}
// 文件用 FILE_ATTRIBUTE_NORMAL 推断:不访问磁盘,速度极快。
// 注意路径仍然参与取值,所以 .exe 的嵌入图标、.lnk 的目标图标依然正确。
return LoadIconViaShellFileInfoCore(path, ShellNative.FILE_ATTRIBUTE_NORMAL, ShellNative.SHGFI_USEFILEATTRIBUTES, logicalSize);
}
/// <summary>
/// SHGetFileInfoW 核心:先拿系统图像列表索引 → 按 size 选 SHIL 取对应尺寸 HICON;
/// 拿不到索引就退回 SHGetFileInfoW 直接给的 HICON(32x32)。
/// </summary>
private BgraBuffer? LoadIconViaShellFileInfoCore(string path, uint attributes, uint extraFlags, int logicalSize)
{
int pixelSize = ToPixelSize(logicalSize);
int imageListKind = SelectImageList(logicalSize);
IntPtr hIcon = IntPtr.Zero;
try
{
int index = ShellNative.GetSystemIconIndexByPath(path, attributes, extraFlags);
if (index >= 0)
{
hIcon = ShellNative.GetHIconFromSystemImageList(imageListKind, index);
}
if (hIcon == IntPtr.Zero)
{
// 回退:直接取 HICON(32x32 或 16x16),大尺寸就走高质量缩放
uint iconFlags = logicalSize <= 16 ? ShellNative.SHGFI_SMALLICON : ShellNative.SHGFI_LARGEICON;
hIcon = ShellNative.GetHIconByPath(path, attributes, extraFlags | iconFlags);
}
if (hIcon == IntPtr.Zero) return null;
return ShellNative.IconToBgra(hIcon, pixelSize, pixelSize);
}
finally
{
// 每一次取到的 HICON 都必须销毁,否则 GDI 句柄会持续增长
if (hIcon != IntPtr.Zero) ShellNative.DestroyIcon(hIcon);
}
}
/// <summary>shell: / ::{CLSID} 解析名的图标:SHParseDisplayName → PIDL → 系统图像列表。</summary>
private BgraBuffer? LoadIconFromShellParsingName(string parsingName, int logicalSize)
{
IntPtr pidl = ShellNative.ParseDisplayNameToPidl(parsingName);
if (pidl == IntPtr.Zero) return null;
try
{
int pixelSize = ToPixelSize(logicalSize);
int imageListKind = SelectImageList(logicalSize);
IntPtr hIcon = IntPtr.Zero;
try
{
int index = ShellNative.GetSystemIconIndexByPidl(pidl, 0);
if (index >= 0)
{
hIcon = ShellNative.GetHIconFromSystemImageList(imageListKind, index);
}
if (hIcon == IntPtr.Zero)
{
uint iconFlags = logicalSize <= 16 ? ShellNative.SHGFI_SMALLICON : ShellNative.SHGFI_LARGEICON;
hIcon = ShellNative.GetHIconByPidl(pidl, iconFlags);
}
if (hIcon == IntPtr.Zero) return null;
return ShellNative.IconToBgra(hIcon, pixelSize, pixelSize);
}
finally
{
if (hIcon != IntPtr.Zero) ShellNative.DestroyIcon(hIcon);
}
}
finally
{
// PIDL 由 SHParseDisplayName 用 CoTaskMemAlloc 分配,必须 CoTaskMemFree
ShellNative.CoTaskMemFree(pidl);
}
}
/// <summary>带 "::{CLSID}" 前缀但不带 shell: 前缀的解析名。</summary>
private BgraBuffer? LoadIconFromParsingName(string parsingName, int logicalSize)
=> LoadIconFromShellParsingName(parsingName, logicalSize);
/// <summary>IShellItemImageFactory + SIIGBF_ICONONLY:大尺寸文件/扩展名的高清原版图标。</summary>
private BgraBuffer? LoadIconViaShellItemImageFactory(string path, int logicalSize)
{
int pixelSize = ToPixelSize(logicalSize);
Guid iid = typeof(ShellNative.IShellItemImageFactory).GUID;
ShellNative.IShellItemImageFactory? factory = null;
IntPtr hBitmap = IntPtr.Zero;
try
{
int hr = ShellNative.SHCreateItemFromParsingName(path, IntPtr.Zero, ref iid, out factory);
if (hr < 0 || factory is null) return null;
// ICONONLY:只要图标不要缩略图;BIGGERSIZEOK:允许外壳给更大的原版图标
const int flags = ShellNative.SIIGBF_ICONONLY | ShellNative.SIIGBF_BIGGERSIZEOK;
hr = factory.GetImage(new ShellNative.SIZE(pixelSize, pixelSize), flags, out hBitmap);
if (hr < 0 || hBitmap == IntPtr.Zero)
{
if (hBitmap != IntPtr.Zero) ShellNative.DeleteObject(hBitmap);
return null;
}
return ShellNative.BitmapToBgra(hBitmap, pixelSize, pixelSize);
}
catch (COMException ex)
{
Debug.WriteLine($"[ShellIconService] IShellItemImageFactory(icon) 0x{ex.HResult:X8}: {path}");
return null;
}
finally
{
if (hBitmap != IntPtr.Zero) ShellNative.DeleteObject(hBitmap);
if (factory is not null)
{
try
{
Marshal.FinalReleaseComObject(factory);
}
catch (ArgumentException)
{
// 已被释放:忽略
}
}
}
}
// ─────────────────────────────────────────────────────────────
// 像素 → WinUI ImageSource(唯一需要 UI 线程的一段)
// ─────────────────────────────────────────────────────────────
/// <summary>
/// BGRA 像素 → <see cref="SoftwareBitmapSource"/>。
/// SoftwareBitmap 可在任意线程构造,但 SoftwareBitmapSource 必须在 UI 线程创建并 SetBitmapAsync,
/// 因此这里统一通过 <see cref="_uiDispatcher"/> 派回 UI 线程。
/// </summary>
private Task<ImageSource?> CreateImageSourceAsync(BgraBuffer buffer)
{
// SoftwareBitmap 是自由线程的,先在当前(后台)线程建好。
// 用 DataWriter 把 BGRA 字节装进 WinRT IBuffer(比 byte[].AsBuffer() 更省一次拷贝,
// 而且后者依赖 System.Runtime.InteropServices.WindowsRuntime 扩展,在部分 TFM 下不可用)。
var writer = new DataWriter();
writer.WriteBytes(buffer.Pixels);
IBuffer winrtBuffer = writer.DetachBuffer();
var bitmap = SoftwareBitmap.CreateCopyFromBuffer(
winrtBuffer,
BitmapPixelFormat.Bgra8,
buffer.Width,
buffer.Height,
// 图标/缩略图带 alpha,必须用预乘格式,否则半透明边缘会出现黑边
BitmapAlphaMode.Premultiplied);
return RunOnUiThreadAsync(async () =>
{
var source = new SoftwareBitmapSource();
try
{
await source.SetBitmapAsync(bitmap);
}
finally
{
// SetBitmapAsync 会拷贝像素,之后即可释放 SoftwareBitmap
bitmap.Dispose();
}
return (ImageSource)source;
});
}
/// <summary>
/// 把一段"必须跑在 UI 线程"的工作派回去执行,用 TaskCompletionSource 桥接。
/// 如果调用方已经在 UI 线程上,则直接同步执行,省掉一次调度往返。
/// </summary>
private Task<T?> RunOnUiThreadAsync<T>(Func<Task<T?>> work) where T : class
{
var dispatcher = _uiDispatcher;
if (dispatcher is null)
{
// 没有调度器(构造时未注入且当前线程也没有):退化为直接在调用线程创建,
// 总比直接失败好 —— 调用方若本来就在 UI 线程,这里完全正确。
return work();
}
if (dispatcher.HasThreadAccess)
{
// 已在 UI 线程:直接执行(SoftwareBitmapSource 亲和 UI 线程,此处满足条件)
return work();
}
var tcs = new TaskCompletionSource<T?>(TaskCreationOptions.RunContinuationsAsynchronously);
bool enqueued;
try
{
enqueued = dispatcher.TryEnqueue(async void () =>
{
try
{
tcs.TrySetResult(await work().ConfigureAwait(true));
}
catch (Exception ex)
{
// 派回 UI 线程的工作失败:以异常结束 Task,由上层 RunSafeAsync 统一吞掉
tcs.TrySetException(ex);
}
});
}
catch (Exception ex) when (ex is COMException or InvalidOperationException)
{
// DispatcherQueue 正在关闭(应用退出中)
Debug.WriteLine($"[ShellIconService] TryEnqueue 失败: {ex.Message}");
return Task.FromResult<T?>(null);
}
if (!enqueued)
{
Debug.WriteLine("[ShellIconService] TryEnqueue 返回 false(消息循环已停止)");
return Task.FromResult<T?>(null);
}
return tcs.Task;
}
// ─────────────────────────────────────────────────────────────
// 缓存 / 并发 / 异常兜底
// ─────────────────────────────────────────────────────────────
/// <summary>
/// 同键请求合并 + LRU 缓存。
/// Lazy&lt;Task&gt; 保证同一个键只会解码一次,其余调用方 await 同一个 Task(快速滚动不会重复解码)。
/// </summary>
private static Task<ImageSource?> GetOrAddAsync(
LruCache<Task<ImageSource?>> cache,
string key,
Func<CancellationToken, Task<ImageSource?>> factory,
CancellationToken ct)
{
// 缓存命中时必须尊重取消:已取消就直接返回 null
if (cache.TryGet(key, out var cachedTask))
{
return ct.IsCancellationRequested ? Task.FromResult<ImageSource?>(null) : cachedTask;
}
// 注意 T 是 Task<ImageSource?>:Lazy 的工厂返回的就是这个共享 Task
var lazy = cache.GetOrCreate(
key,
_ => new Lazy<Task<ImageSource?>>(
() => factory(CancellationToken.None),
LazyThreadSafetyMode.ExecutionAndPublication));
return AwaitLazyAsync(lazy, ct);
}
/// <summary>等待共享的 Lazy&lt;Task&gt;,但让"本次调用"的取消能立刻返回 null(不打断别人共享的那次解码)。</summary>
private static async Task<ImageSource?> AwaitLazyAsync(Lazy<Task<ImageSource?>> lazy, CancellationToken ct)
{
Task<ImageSource?> task;
try
{
task = lazy.Value;
}
catch (Exception ex)
{
// Lazy 的工厂本身抛异常(极少见,主要来自 factory 构造阶段)
Debug.WriteLine($"[ShellIconService] 缓存工厂异常: {ex}");
return null;
}
if (task.IsCompleted)
{
if (ct.IsCancellationRequested) return null;
return await AwaitSharedTaskAsync(task).ConfigureAwait(false);
}
if (!ct.CanBeCanceled)
{
return await AwaitSharedTaskAsync(task).ConfigureAwait(false);
}
using var cts = new CancellationTokenSource();
using (ct.Register(static state => ((CancellationTokenSource)state!).Cancel(), cts))
{
var cancelTask = Task.Delay(Timeout.Infinite, cts.Token);
var finished = await Task.WhenAny(task, cancelTask).ConfigureAwait(false);
if (finished != task)
{
// 本调用被取消:吞掉 OperationCanceledException 语义,返回 null。
// 注意共享的那次解码仍在后台继续,其它调用方仍能拿到结果。
return null;
}
}
return await AwaitSharedTaskAsync(task).ConfigureAwait(false);
}
/// <summary>
/// await 共享 Task 的最后一层兜底。
/// 正常情况这里不会抛(各 Load*CoreAsync 都套了 RunSafeAsync),
/// 但共享 Task 一旦带异常,所有同键调用方都会中招,所以在出口再兜一次,确保公开 API 永不外抛。
/// </summary>
private static async Task<ImageSource?> AwaitSharedTaskAsync(Task<ImageSource?> task)
{
try
{
return await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return null;
}
catch (Exception ex)
{
Debug.WriteLine($"[ShellIconService] 共享取图任务异常: {ex}");
return null;
}
}
/// <summary>
/// 服务层统一兜底:取消 → null;其它任何异常(含 COMException / SEHException / 未知)记 Debug 后返回 null。
/// 这里绝不让异常逃出去把 UI 线程打崩。
/// </summary>
private static async Task<ImageSource?> RunSafeAsync(Func<Task<ImageSource?>> work)
{
try
{
return await work().ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// 取消是正常路径(列表滚过去了),静默返回 null
return null;
}
catch (Exception ex)
{
Debug.WriteLine($"[ShellIconService] 取图失败: {ex}");
return null;
}
}
/// <summary>逻辑尺寸归一到合法范围(16..1024)。</summary>
private static int NormalizeSize(int size)
{
if (size <= 0) return 32;
if (size < 16) return 16;
if (size > 1024) return 1024;
return size;
}
/// <summary>归一化扩展名:去掉前导点、转小写;空则返回空串。</summary>
private static string NormalizeExtension(string? extension)
{
if (string.IsNullOrWhiteSpace(extension)) return string.Empty;
string ext = extension.Trim();
if (ext.StartsWith('.')) ext = ext[1..];
return ext.ToLowerInvariant();
}
/// <summary>按显示尺寸 × DPI 缩放算出实际渲染像素尺寸(保证高 DPI 不糊)。</summary>
private int ToPixelSize(int logicalSize)
{
double scale = 1.0;
try
{
scale = _scaleProvider();
}
catch (Exception ex)
{
Debug.WriteLine($"[ShellIconService] rasterizationScale 取值失败: {ex.Message}");
}
if (double.IsNaN(scale) || double.IsInfinity(scale) || scale <= 0) scale = 1.0;
if (scale > 4.0) scale = 4.0;
int pixels = (int)Math.Round(logicalSize * scale, MidpointRounding.AwayFromZero);
if (pixels < 1) pixels = 1;
if (pixels > 2048) pixels = 2048;
return pixels;
}
/// <summary>
/// 按显示尺寸挑系统图像列表,保证拿到的是「原生该尺寸」的图标,而不是放大出来的。
/// 实测各列表的 HICON 原生尺寸:SHIL_LARGE(0)=32、SHIL_SMALL(1)=16、SHIL_EXTRALARGE(2)=48、SHIL_JUMBO(4)=256。
/// </summary>
private static int SelectImageList(int logicalSize) => logicalSize switch
{
>= 256 => ShellNative.SHIL_JUMBO,
>= 48 => ShellNative.SHIL_EXTRALARGE,
>= 32 => ShellNative.SHIL_LARGE,
_ => ShellNative.SHIL_SYSSMALL,
};
private static string BuildIconKey(string path, bool isDirectory, int logicalSize)
=> (isDirectory ? "dir|" : "file|") + logicalSize.ToString() + "|" + path;
/// <summary>判断是否为已知外壳解析名(shell: 或 ::{CLSID})。</summary>
private static bool IsSpecialParsingName(string path)
=> ShellParsingName.IsShellPrefix(path) || path.StartsWith("::", StringComparison.Ordinal);
}
/// <summary>
/// 极简 LRU 缓存:ConcurrentDictionary 负责高并发读取,LinkedList 记录使用顺序,
/// 超出容量后从链表尾部淘汰最少使用的项。
/// </summary>
/// <typeparam name="T">缓存值类型(本工程里是 ImageSource?)。</typeparam>
internal sealed class LruCache<T>
{
private readonly int _capacity;
private readonly ConcurrentDictionary<string, Entry> _map = new(StringComparer.OrdinalIgnoreCase);
private readonly LinkedList<string> _order = new();
private readonly object _orderGate = new();
internal LruCache(int capacity)
{
_capacity = capacity > 0 ? capacity : 1;
}
private sealed class Entry
{
internal Entry(Lazy<T> value) => Value = value;
internal Lazy<T> Value { get; }
internal LinkedListNode<string>? Node { get; set; }
}
/// <summary>取缓存值(命中即刷新使用顺序)。</summary>
internal bool TryGet(string key, out T value)
{
if (_map.TryGetValue(key, out var entry))
{
Touch(entry);
value = entry.Value.Value;
return true;
}
value = default!;
return false;
}
/// <summary>
/// 取或创建。注意这里返回的是 Lazy&lt;T&gt; 本身:同一键的并发调用方会拿到同一个实例,
/// 因此 T 为 Task 时天然实现了"同键请求合并"。
/// </summary>
internal Lazy<T> GetOrCreate(string key, Func<string, Lazy<T>> factory)
{
var entry = _map.GetOrAdd(key, k => new Entry(factory(k)));
if (entry.Node is null)
{
lock (_orderGate)
{
if (entry.Node is null)
{
entry.Node = _order.AddFirst(key);
Trim();
}
}
}
return entry.Value;
}
internal void Clear()
{
lock (_orderGate)
{
_map.Clear();
_order.Clear();
}
}
/// <summary>把命中的键移到链表头部(最近使用)。</summary>
private void Touch(Entry entry)
{
var node = entry.Node;
if (node is null) return;
lock (_orderGate)
{
if (node.List is null) return;
_order.Remove(node);
_order.AddFirst(node);
}
}
/// <summary>超出容量时从尾部淘汰(调用方已持有 _orderGate)。</summary>
private void Trim()
{
while (_order.Count > _capacity)
{
var last = _order.Last;
if (last is null) return;
_order.RemoveLast();
if (_map.TryRemove(last.Value, out var removed))
{
// 置空节点引用,防止被淘汰的 Entry 仍被 Remove 时的 node.List 判空逻辑误解
removed.Node = null;
}
}
}
}