Files
29WxOrder/Main.Test/StyledDataGridImageColumn.cs
T
2026-10-22 13:42:04 +08:00

170 lines
8.7 KiB
C#

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;
/// <summary>配套图片列:自动使用所属 StyledDataGrid 的拖动暂停和图片缓存能力。</summary>
public class StyledDataGridImageColumn : DataGridBoundColumn
{
private static readonly Style DefaultImageStyle = CreateDefaultStyle();
/// <summary>可选的逐行占位图;未设置时显示空白。</summary>
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;
}
}
/// <summary>仅为已生成的可见单元格加载图片;回收时取消,避免旧图片覆盖新行。</summary>
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<string, BitmapSource> Cache = new();
private static readonly Queue<string> 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<BitmapSource> 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);
}
}
}