using System.Diagnostics; using System.IO; using System.Net.Http; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media.Imaging; using System.Windows.Media; using System.Windows; namespace Main.Test; /// 配套图片列:自动使用所属 StyledDataGrid 的拖动暂停和图片缓存能力。 public class StyledDataGridImageColumn : DataGridBoundColumn { private static readonly Style DefaultImageStyle = CreateDefaultStyle(); /// 可选的逐行占位图;未设置时显示空白。 public BindingBase? PlaceholderBinding { get; set; } protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) { var image = new NetworkThumbnail { Style = ElementStyle ?? DefaultImageStyle }; if (Binding != null) BindingOperations.SetBinding(image, NetworkThumbnail.UrlProperty, Binding); if (PlaceholderBinding != null) BindingOperations.SetBinding(image, NetworkThumbnail.PlaceholderProperty, PlaceholderBinding); return image; } protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) => GenerateElement(cell, dataItem); private static Style CreateDefaultStyle() { var style = new Style(typeof(NetworkThumbnail)); style.Setters.Add(new Setter(FrameworkElement.WidthProperty, 58d)); style.Setters.Add(new Setter(FrameworkElement.HeightProperty, 58d)); style.Setters.Add(new Setter(System.Windows.Controls.Image.StretchProperty, Stretch.Uniform)); style.Seal(); return style; } } /// 仅为已生成的可见单元格加载图片;回收时取消,避免旧图片覆盖新行。 public sealed class NetworkThumbnail : Image { public static readonly DependencyProperty CacheEnabledProperty = DependencyProperty.RegisterAttached("CacheEnabled", typeof(bool), typeof(NetworkThumbnail), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits, Changed)); public static bool GetCacheEnabled(DependencyObject target) => (bool)target.GetValue(CacheEnabledProperty); public static void SetCacheEnabled(DependencyObject target, bool value) => target.SetValue(CacheEnabledProperty, value); public static readonly DependencyProperty IsLoadingSuspendedProperty = DependencyProperty.RegisterAttached( "IsLoadingSuspended", typeof(bool), typeof(NetworkThumbnail), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits, SuspensionChanged)); public static bool GetIsLoadingSuspended(DependencyObject target) => (bool)target.GetValue(IsLoadingSuspendedProperty); public static void SetIsLoadingSuspended(DependencyObject target, bool value) => target.SetValue(IsLoadingSuspendedProperty, value); private static void SuspensionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not NetworkThumbnail image) return; image.CancelLoading(); image.Source = null; if (!(bool)e.NewValue) image.QueueRefresh(); } public static readonly DependencyProperty UrlProperty = DependencyProperty.Register(nameof(Url), typeof(string), typeof(NetworkThumbnail), new PropertyMetadata(null, Changed)); public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register(nameof(Placeholder), typeof(ImageSource), typeof(NetworkThumbnail), new PropertyMetadata(null, Changed)); public string? Url { get => (string?)GetValue(UrlProperty); set => SetValue(UrlProperty, value); } public ImageSource? Placeholder { get => (ImageSource?)GetValue(PlaceholderProperty); set => SetValue(PlaceholderProperty, value); } private CancellationTokenSource? loading; private bool refreshPending; public NetworkThumbnail() { Loaded += (_, _) => QueueRefresh(); Unloaded += (_, _) => CancelLoading(); } private static void Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not NetworkThumbnail image) return; image.CancelLoading(); image.Source = GetIsLoadingSuspended(image) ? null : image.Placeholder; // 回收行时多个绑定会依次变更,等本轮绑定完成后只加载一次新 URL。 image.QueueRefresh(); } private void QueueRefresh() { if (!IsLoaded || refreshPending) return; refreshPending = true; Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, new Action(() => { refreshPending = false; if (IsLoaded) StartLoading(); })); } private void CancelLoading() { loading?.Cancel(); loading?.Dispose(); loading = null; } private async void StartLoading() { CancelLoading(); ToolTip = null; if (GetIsLoadingSuspended(this)) { Source = null; return; } Source = Placeholder; if (string.IsNullOrWhiteSpace(Url)) return; var request = new CancellationTokenSource(); loading = request; var token = request.Token; try { var bitmap = await ThumbnailLoader.LoadAsync(Url, GetCacheEnabled(this), token); if (!token.IsCancellationRequested && ReferenceEquals(loading, request)) Source = bitmap; } catch (OperationCanceledException) when (token.IsCancellationRequested) { } catch (Exception ex) { if (!token.IsCancellationRequested && ReferenceEquals(loading, request)) ToolTip = $"图片加载失败:{ex.Message}"; } } } internal static class ThumbnailLoader { private static readonly HttpClient Client = new() { Timeout = TimeSpan.FromSeconds(12) }; private static readonly SemaphoreSlim Slots = new(4); private static readonly object CacheLock = new(); private static readonly Dictionary Cache = new(); private static readonly Queue CacheOrder = new(); private static long started, succeeded, failed, canceled, cacheHits, bytes, totalTicks; private static int active, pending; public static int Pending => Volatile.Read(ref pending); public static string Statistics => $"网络图片:请求 {Interlocked.Read(ref started)} / 成功 {Interlocked.Read(ref succeeded)} / 失败 {Interlocked.Read(ref failed)} / 取消 {Interlocked.Read(ref canceled)} / 缓存命中 {Interlocked.Read(ref cacheHits)} | 活跃 {Volatile.Read(ref active)},待完成 {Pending} | 下载 {Interlocked.Read(ref bytes) / 1024d:N0} KB | 成功平均 {AverageMilliseconds:N0} ms"; private static double AverageMilliseconds => Interlocked.Read(ref succeeded) == 0 ? 0 : Interlocked.Read(ref totalTicks) * 1000d / Stopwatch.Frequency / Interlocked.Read(ref succeeded); public static async Task LoadAsync(string url, bool cacheEnabled, CancellationToken token) { var cacheKey = url; lock (CacheLock) if (cacheEnabled && Cache.TryGetValue(cacheKey, out var cached)) { Interlocked.Increment(ref cacheHits); return cached; } Interlocked.Increment(ref pending); var acquired = false; try { await Slots.WaitAsync(token).ConfigureAwait(false); acquired = true; Interlocked.Increment(ref active); Interlocked.Increment(ref started); var watch = Stopwatch.StartNew(); var data = await Client.GetByteArrayAsync(url, token).ConfigureAwait(false); Interlocked.Add(ref bytes, data.Length); var bitmap = await Task.Run(() => { token.ThrowIfCancellationRequested(); using var stream = new MemoryStream(data); var image = new BitmapImage(); image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad; image.DecodePixelWidth = 72; image.StreamSource = stream; image.EndInit(); image.Freeze(); return image; }, token).ConfigureAwait(false); token.ThrowIfCancellationRequested(); lock (CacheLock) { if (cacheEnabled && !Cache.ContainsKey(cacheKey)) { if (Cache.Count >= 128) Cache.Remove(CacheOrder.Dequeue()); Cache.Add(cacheKey, bitmap); CacheOrder.Enqueue(cacheKey); } } Interlocked.Increment(ref succeeded); Interlocked.Add(ref totalTicks, watch.ElapsedTicks); return bitmap; } catch (OperationCanceledException) when (token.IsCancellationRequested) { Interlocked.Increment(ref canceled); throw; } catch { Interlocked.Increment(ref failed); throw; } finally { if (acquired) { Interlocked.Decrement(ref active); Slots.Release(); } Interlocked.Decrement(ref pending); } } }