Files
29WxOrder/29WxOrderWpf/Converters/ImageUrlToBitmapImageConverter.cs

196 lines
6.5 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Work;
namespace _29WxOrderWpf.Converters
{
public class ImageUrlToBitmapImageConverter : IMultiValueConverter
{
/// <summary> 缓存上限,防止大量缩略图重复下载导致内存膨胀 </summary>
private const int MaxCacheCount = 200;
private readonly ConcurrentDictionary<string, BitmapImage> _imageCache = new ConcurrentDictionary<string, BitmapImage>();
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 0 || values[0] == DependencyProperty.UnsetValue)
return null;
var url = values[0] as string;
var isLoad = values[1] is bool b && b;
if (!isLoad || string.IsNullOrEmpty(url))
return null; // 或返回占位图
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
if (_imageCache.TryGetValue(url, out BitmapImage cached))
{
return cached;
}
try
{
// 直接加载绑定如果遇到有问题的网络图片会直接崩溃。
BitmapImage bitMap = AllBase.ImageBuild.LoadBitmipImageFromUrl(url);
if (bitMap != null)
{
if (_imageCache.Count >= MaxCacheCount)
{
_imageCache.Clear();
}
_imageCache[url] = bitMap;
}
return bitMap;
}
catch
{
}
}
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
// 建议作为静态字段复用,不要每次调用都 new HttpClient()
private static readonly HttpClient _httpClient = new HttpClient();
}
public class ImageLoadRequest
{
public string Url { get; set; }
public bool IsLoad { get; set; }
// 用于避免相同状态重复触发下载逻辑
public override bool Equals(object obj) =>
obj is ImageLoadRequest other && other.Url == Url && other.IsLoad == IsLoad;
public override int GetHashCode() => Url.GetHashCode();
}
public class ImageLoadRequestConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var url = values[0] as string;
var isLoad = values.Length > 1 && values[1] is bool b && b;
return new ImageLoadRequest { Url = url, IsLoad = isLoad };
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public static class AsyncImageLoader
{
private static readonly HttpClient _httpClient = new HttpClient();
private static readonly ConcurrentDictionary<string, Task<BitmapImage>> _cache
= new ConcurrentDictionary<string, Task<BitmapImage>>();
public static readonly DependencyProperty RequestProperty =
DependencyProperty.RegisterAttached(
"Request",
typeof(ImageLoadRequest),
typeof(AsyncImageLoader),
new PropertyMetadata(null, OnRequestChanged));
public static ImageLoadRequest GetRequest(DependencyObject obj) => (ImageLoadRequest)obj.GetValue(RequestProperty);
public static void SetRequest(DependencyObject obj, ImageLoadRequest value) => obj.SetValue(RequestProperty, value);
private static readonly DependencyProperty CurrentUrlProperty =
DependencyProperty.RegisterAttached("CurrentUrl", typeof(string), typeof(AsyncImageLoader), new PropertyMetadata(null));
private static string GetCurrentUrl(DependencyObject obj) => (string)obj.GetValue(CurrentUrlProperty);
private static void SetCurrentUrl(DependencyObject obj, string value) => obj.SetValue(CurrentUrlProperty, value);
private static readonly DependencyProperty CtsProperty =
DependencyProperty.RegisterAttached("Cts", typeof(CancellationTokenSource), typeof(AsyncImageLoader), new PropertyMetadata(null));
private static CancellationTokenSource GetCts(DependencyObject obj) => (CancellationTokenSource)obj.GetValue(CtsProperty);
private static void SetCts(DependencyObject obj, CancellationTokenSource value) => obj.SetValue(CtsProperty, value);
private static void OnRequestChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is Image image)) return;
// 值没变(Equals 重写过),不做任何事,避免无意义触发
if (Equals(e.OldValue, e.NewValue)) return;
var request = e.NewValue as ImageLoadRequest;
GetCts(image)?.Cancel();
if (request == null || !request.IsLoad || string.IsNullOrEmpty(request.Url))
{
return; // 滚动中,不加载,保留原图,不产生任何开销
}
string url = request.Url;
SetCurrentUrl(image, url);
var cts = new CancellationTokenSource();
SetCts(image, cts);
// 关键:把真正的下载动作丢到 Dispatcher 队列里、用较低优先级去做,
// 避免在当前这一帧的布局/绑定更新过程中做任何"重"的同步开销,
// 也顺便起到一个天然的"防抖"效果——如果这一帧内 Request 又变了
// (比如滚动中又切回别的 item),上面的 Cancel() 已经先取消了。
image.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(async () =>
{
if (cts.IsCancellationRequested) return;
try
{
var bitmap = await LoadBitmapAsync(url, cts.Token);
if (bitmap != null && GetCurrentUrl(image) == url && !cts.IsCancellationRequested)
{
image.Source = bitmap;
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error loading image from {url}: {ex.Message}");
}
}));
}
private static Task<BitmapImage> LoadBitmapAsync(string url, CancellationToken token)
{
return _cache.GetOrAdd(url, async u =>
{
using var response = await _httpClient.GetAsync(u, token);
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadAsByteArrayAsync();
using var stream = new MemoryStream(data);
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CreateOptions = BitmapCreateOptions.PreservePixelFormat | BitmapCreateOptions.IgnoreColorProfile;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.DecodePixelWidth = 200;
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
});
}
}
}