170 lines
8.7 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|