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; /// /// 系统原版图标 / 缩略图服务。 /// /// 设计要点: /// 1) 取图全部在外壳与 GDI 侧完成(可后台线程),只有 的创建被派回 UI 线程; /// SoftwareBitmapSource 是 UI 线程亲和对象,后台线程碰它会直接崩。 /// 2) 结果带 LRU 缓存 + 同键请求合并(Lazy<Task>),列表快速滚动时不会重复解码同一张图。 /// 3) 任何未预期异常都会被吞成 null 并写 Debug 输出,服务层绝不把异常抛进 UI 线程。 /// public sealed class ShellIconService : IIconService { private const int IconCacheCapacity = 512; private const int ThumbnailCacheCapacity = 256; private const int MaxConcurrentNativeWork = 4; /// 缩略图 E_PENDING 重试参数。 private const int ThumbnailRetryCount = 3; /// 缩略图 E_PENDING 重试间隔(毫秒)。 private const int ThumbnailRetryDelayMs = 200; /// UI 调度器;极端情况下可能为 null(此时退化为在调用线程创建 ImageSource)。 private readonly DispatcherQueue? _uiDispatcher; private readonly Func _scaleProvider; /// /// 图标缓存:值类型是 。 /// 缓存"共享的 Task"而不是最终 ImageSource,才能在解码未完成时就完成同键请求合并; /// 这也是 Lazy<Task> 模式的落点(LruCache 内层用 Lazy 保证工厂只跑一次)。 /// private readonly LruCache> _iconCache = new(IconCacheCapacity); /// 缩略图缓存,容量更小(缩略图位图大得多,全部是 256px 级别的位图)。 private readonly LruCache> _thumbnailCache = new(ThumbnailCacheCapacity); /// 把纯 GDI 取图/解码节流在 4 路,避免整屏滚动时把 CPU 打满。 private readonly SemaphoreSlim _nativeThrottle = new(MaxConcurrentNativeWork, MaxConcurrentNativeWork); /// 默认的 DPI 缩放(rasterizationScale 为 null 时按 1.0 处理)。 private static readonly Func DefaultScale = static () => 1.0; /// /// 构造。签名固定为 (DispatcherQueue, Func<double>?),见 Services/AppServices.cs 的调用。 /// /// /// 构造函数**不抛异常**:uiDispatcher 为 null 时回退到当前线程的 DispatcherQueue, /// 再拿不到就退化为"直接在调用线程创建 ImageSource"。 /// 宁可降级也不要让 App 启动阶段直接崩掉。 /// public ShellIconService(DispatcherQueue uiDispatcher, Func? rasterizationScale = null) { _uiDispatcher = uiDispatcher ?? TryGetCurrentDispatcher(); _scaleProvider = rasterizationScale ?? DefaultScale; } /// 尽力拿到当前线程的 DispatcherQueue;拿不到返回 null(构造期不抛异常)。 private static DispatcherQueue? TryGetCurrentDispatcher() { try { return DispatcherQueue.GetForCurrentThread(); } catch (Exception ex) { Debug.WriteLine($"[ShellIconService] GetForCurrentThread 失败: {ex.Message}"); return null; } } /// 当前构造时注入的 UI 调度器(ViewModel 可用它确认自己在哪个线程上调用)。 public DispatcherQueue? UiDispatcher => _uiDispatcher; // ───────────────────────────────────────────────────────────── // IIconService // ───────────────────────────────────────────────────────────── /// public Task GetIconAsync(string path, bool isDirectory, int size, CancellationToken cancellationToken) { try { if (string.IsNullOrWhiteSpace(path)) return Task.FromResult(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(null); } } /// public Task GetThumbnailAsync(string path, int size, CancellationToken cancellationToken) { try { if (string.IsNullOrWhiteSpace(path)) return Task.FromResult(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(null); } } /// public Task GetExtensionIconAsync(string extension, int size, CancellationToken cancellationToken) { try { string ext = NormalizeExtension(extension); if (ext.Length == 0) return Task.FromResult(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(null); } } /// /// /// 注意:本方法是同步签名(见 IIconService)。它只应在 UI 线程调用,内部会同步等待取图完成, /// 因此仅适合启动阶段取少量侧边栏图标,不要在滚动/渲染路径里按行调用。 /// 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; } } /// public void ClearCache() { try { _iconCache.Clear(); _thumbnailCache.Clear(); } catch (Exception ex) { Debug.WriteLine($"[ShellIconService] ClearCache 失败: {ex}"); } } // ───────────────────────────────────────────────────────────── // 取图主流程(全部在后台线程执行) // ───────────────────────────────────────────────────────────── /// 文件 / 目录 / 驱动器 / 已知外壳对象的图标。 private Task 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(); } }); } /// 扩展名通用图标:SHGFI_USEFILEATTRIBUTES + FILE_ATTRIBUTE_NORMAL,不访问磁盘。 private Task 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(); } }); } /// /// 缩略图:SHCreateItemFromParsingName + IShellItemImageFactory.GetImage。 /// 失败一律返回 null(不抛异常);E_PENDING 表示外壳正在后台解码,等 200ms 重试,最多 3 次。 /// private Task 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 细节 // ───────────────────────────────────────────────────────────── /// SHGetFileInfoW 路径:目录走真实路径(保角标/个性化图标),文件走属性推断。 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); } /// /// SHGetFileInfoW 核心:先拿系统图像列表索引 → 按 size 选 SHIL 取对应尺寸 HICON; /// 拿不到索引就退回 SHGetFileInfoW 直接给的 HICON(32x32)。 /// 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); } } /// shell: / ::{CLSID} 解析名的图标:SHParseDisplayName → PIDL → 系统图像列表。 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); } } /// 带 "::{CLSID}" 前缀但不带 shell: 前缀的解析名。 private BgraBuffer? LoadIconFromParsingName(string parsingName, int logicalSize) => LoadIconFromShellParsingName(parsingName, logicalSize); /// IShellItemImageFactory + SIIGBF_ICONONLY:大尺寸文件/扩展名的高清原版图标。 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 线程的一段) // ───────────────────────────────────────────────────────────── /// /// BGRA 像素 → 。 /// SoftwareBitmap 可在任意线程构造,但 SoftwareBitmapSource 必须在 UI 线程创建并 SetBitmapAsync, /// 因此这里统一通过 派回 UI 线程。 /// private Task 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; }); } /// /// 把一段"必须跑在 UI 线程"的工作派回去执行,用 TaskCompletionSource 桥接。 /// 如果调用方已经在 UI 线程上,则直接同步执行,省掉一次调度往返。 /// private Task RunOnUiThreadAsync(Func> 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(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(null); } if (!enqueued) { Debug.WriteLine("[ShellIconService] TryEnqueue 返回 false(消息循环已停止)"); return Task.FromResult(null); } return tcs.Task; } // ───────────────────────────────────────────────────────────── // 缓存 / 并发 / 异常兜底 // ───────────────────────────────────────────────────────────── /// /// 同键请求合并 + LRU 缓存。 /// Lazy<Task> 保证同一个键只会解码一次,其余调用方 await 同一个 Task(快速滚动不会重复解码)。 /// private static Task GetOrAddAsync( LruCache> cache, string key, Func> factory, CancellationToken ct) { // 缓存命中时必须尊重取消:已取消就直接返回 null if (cache.TryGet(key, out var cachedTask)) { return ct.IsCancellationRequested ? Task.FromResult(null) : cachedTask; } // 注意 T 是 Task:Lazy 的工厂返回的就是这个共享 Task var lazy = cache.GetOrCreate( key, _ => new Lazy>( () => factory(CancellationToken.None), LazyThreadSafetyMode.ExecutionAndPublication)); return AwaitLazyAsync(lazy, ct); } /// 等待共享的 Lazy<Task>,但让"本次调用"的取消能立刻返回 null(不打断别人共享的那次解码)。 private static async Task AwaitLazyAsync(Lazy> lazy, CancellationToken ct) { Task 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); } /// /// await 共享 Task 的最后一层兜底。 /// 正常情况这里不会抛(各 Load*CoreAsync 都套了 RunSafeAsync), /// 但共享 Task 一旦带异常,所有同键调用方都会中招,所以在出口再兜一次,确保公开 API 永不外抛。 /// private static async Task AwaitSharedTaskAsync(Task task) { try { return await task.ConfigureAwait(false); } catch (OperationCanceledException) { return null; } catch (Exception ex) { Debug.WriteLine($"[ShellIconService] 共享取图任务异常: {ex}"); return null; } } /// /// 服务层统一兜底:取消 → null;其它任何异常(含 COMException / SEHException / 未知)记 Debug 后返回 null。 /// 这里绝不让异常逃出去把 UI 线程打崩。 /// private static async Task RunSafeAsync(Func> work) { try { return await work().ConfigureAwait(false); } catch (OperationCanceledException) { // 取消是正常路径(列表滚过去了),静默返回 null return null; } catch (Exception ex) { Debug.WriteLine($"[ShellIconService] 取图失败: {ex}"); return null; } } /// 逻辑尺寸归一到合法范围(16..1024)。 private static int NormalizeSize(int size) { if (size <= 0) return 32; if (size < 16) return 16; if (size > 1024) return 1024; return size; } /// 归一化扩展名:去掉前导点、转小写;空则返回空串。 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(); } /// 按显示尺寸 × DPI 缩放算出实际渲染像素尺寸(保证高 DPI 不糊)。 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; } /// /// 按显示尺寸挑系统图像列表,保证拿到的是「原生该尺寸」的图标,而不是放大出来的。 /// 实测各列表的 HICON 原生尺寸:SHIL_LARGE(0)=32、SHIL_SMALL(1)=16、SHIL_EXTRALARGE(2)=48、SHIL_JUMBO(4)=256。 /// 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; /// 判断是否为已知外壳解析名(shell: 或 ::{CLSID})。 private static bool IsSpecialParsingName(string path) => ShellParsingName.IsShellPrefix(path) || path.StartsWith("::", StringComparison.Ordinal); } /// /// 极简 LRU 缓存:ConcurrentDictionary 负责高并发读取,LinkedList 记录使用顺序, /// 超出容量后从链表尾部淘汰最少使用的项。 /// /// 缓存值类型(本工程里是 ImageSource?)。 internal sealed class LruCache { private readonly int _capacity; private readonly ConcurrentDictionary _map = new(StringComparer.OrdinalIgnoreCase); private readonly LinkedList _order = new(); private readonly object _orderGate = new(); internal LruCache(int capacity) { _capacity = capacity > 0 ? capacity : 1; } private sealed class Entry { internal Entry(Lazy value) => Value = value; internal Lazy Value { get; } internal LinkedListNode? Node { get; set; } } /// 取缓存值(命中即刷新使用顺序)。 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; } /// /// 取或创建。注意这里返回的是 Lazy<T> 本身:同一键的并发调用方会拿到同一个实例, /// 因此 T 为 Task 时天然实现了"同键请求合并"。 /// internal Lazy GetOrCreate(string key, Func> 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(); } } /// 把命中的键移到链表头部(最近使用)。 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); } } /// 超出容量时从尾部淘汰(调用方已持有 _orderGate)。 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; } } } }