196 lines
6.5 KiB
C#
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;
|
|
});
|
|
}
|
|
}
|
|
} |