You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
258 lines
7.7 KiB
258 lines
7.7 KiB
|
1 month ago
|
using Avalonia.Data.Converters;
|
||
|
|
using Avalonia.Media.Imaging;
|
||
|
|
using System;
|
||
|
|
using System.Collections.Concurrent;
|
||
|
|
using System.Globalization;
|
||
|
|
using System.IO;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
|
||
|
|
namespace AuroraDesk.Presentation.Converters;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 图片相关转换器
|
||
|
|
/// </summary>
|
||
|
|
public static class ImageConverters
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// 文件路径到图片源的转换器
|
||
|
|
/// </summary>
|
||
|
|
public static readonly IValueConverter FilePathToImageSourceConverter = new FilePathToImageSourceConverter();
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 大于零的转换器(用于数字到布尔值)
|
||
|
|
/// </summary>
|
||
|
|
public static readonly IValueConverter IsGreaterThanZeroConverter = new IsGreaterThanZeroConverter();
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 反转布尔值转换器
|
||
|
|
/// </summary>
|
||
|
|
public static readonly IValueConverter InvertedBoolConverter = new InvertedBoolConverter();
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 是否为零的转换器
|
||
|
|
/// </summary>
|
||
|
|
public static readonly IValueConverter IsZeroConverter = new IsZeroConverter();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 文件路径到图片源转换器(异步加载缩略图,类似 Windows 11)
|
||
|
|
/// 特点:
|
||
|
|
/// 1. 异步加载,不阻塞UI线程
|
||
|
|
/// 2. 只加载缩略图(184x184),不加载完整图片
|
||
|
|
/// 3. 缓存已加载的缩略图,避免重复加载
|
||
|
|
/// 4. 延迟加载,只在需要时才加载
|
||
|
|
/// </summary>
|
||
|
|
public class FilePathToImageSourceConverter : IValueConverter
|
||
|
|
{
|
||
|
|
// 缩略图缓存(线程安全)
|
||
|
|
private static readonly ConcurrentDictionary<string, Bitmap?> _thumbnailCache = new();
|
||
|
|
|
||
|
|
// 正在加载的任务字典,避免重复加载
|
||
|
|
private static readonly ConcurrentDictionary<string, Task<Bitmap?>> _loadingTasks = new();
|
||
|
|
|
||
|
|
// 缩略图大小(与UI中的184x184一致)
|
||
|
|
private const int ThumbnailSize = 184;
|
||
|
|
|
||
|
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||
|
|
{
|
||
|
|
if (value is string filePath && !string.IsNullOrEmpty(filePath) && File.Exists(filePath))
|
||
|
|
{
|
||
|
|
// 先返回 null,让 UI 显示占位符
|
||
|
|
// 然后异步加载缩略图
|
||
|
|
_ = LoadThumbnailAsync(filePath);
|
||
|
|
return null; // 初始返回 null,异步加载完成后通过绑定更新
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 异步加载缩略图
|
||
|
|
/// </summary>
|
||
|
|
private static async Task<Bitmap?> LoadThumbnailAsync(string filePath)
|
||
|
|
{
|
||
|
|
// 检查缓存
|
||
|
|
if (_thumbnailCache.TryGetValue(filePath, out var cached))
|
||
|
|
{
|
||
|
|
return cached;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查是否正在加载
|
||
|
|
if (_loadingTasks.TryGetValue(filePath, out var loadingTask))
|
||
|
|
{
|
||
|
|
return await loadingTask;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 创建新的加载任务
|
||
|
|
var task = Task.Run(async () =>
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// 在后台线程加载图片
|
||
|
|
using var imageStream = File.OpenRead(filePath);
|
||
|
|
var bitmap = new Bitmap(imageStream);
|
||
|
|
|
||
|
|
// 如果图片尺寸小于缩略图尺寸,直接返回原图(克隆)
|
||
|
|
if (bitmap.PixelSize.Width <= ThumbnailSize && bitmap.PixelSize.Height <= ThumbnailSize)
|
||
|
|
{
|
||
|
|
// 克隆位图以避免释放原始资源
|
||
|
|
using var memoryStream = new MemoryStream();
|
||
|
|
bitmap.Save(memoryStream);
|
||
|
|
memoryStream.Position = 0;
|
||
|
|
var result = new Bitmap(memoryStream);
|
||
|
|
_thumbnailCache.TryAdd(filePath, result);
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 创建缩略图(保持宽高比)
|
||
|
|
var sourceSize = bitmap.PixelSize;
|
||
|
|
var scale = Math.Min(
|
||
|
|
(double)ThumbnailSize / sourceSize.Width,
|
||
|
|
(double)ThumbnailSize / sourceSize.Height);
|
||
|
|
|
||
|
|
var thumbnailWidth = (int)(sourceSize.Width * scale);
|
||
|
|
var thumbnailHeight = (int)(sourceSize.Height * scale);
|
||
|
|
|
||
|
|
// 创建缩略图
|
||
|
|
var thumbnail = bitmap.CreateScaledBitmap(
|
||
|
|
new Avalonia.PixelSize(thumbnailWidth, thumbnailHeight),
|
||
|
|
Avalonia.Media.Imaging.BitmapInterpolationMode.HighQuality);
|
||
|
|
|
||
|
|
// 释放原图资源
|
||
|
|
bitmap.Dispose();
|
||
|
|
|
||
|
|
// 缓存缩略图
|
||
|
|
_thumbnailCache.TryAdd(filePath, thumbnail);
|
||
|
|
return thumbnail;
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
// 加载失败,缓存 null 避免重复尝试
|
||
|
|
_thumbnailCache.TryAdd(filePath, null);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
// 移除加载任务
|
||
|
|
_loadingTasks.TryRemove(filePath, out _);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
_loadingTasks.TryAdd(filePath, task);
|
||
|
|
return await task;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 获取缩略图(同步方法,用于异步绑定)
|
||
|
|
/// </summary>
|
||
|
|
public static Bitmap? GetThumbnail(string filePath)
|
||
|
|
{
|
||
|
|
if (_thumbnailCache.TryGetValue(filePath, out var cached))
|
||
|
|
{
|
||
|
|
return cached;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 触发异步加载
|
||
|
|
_ = LoadThumbnailAsync(filePath);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 清除缓存
|
||
|
|
/// </summary>
|
||
|
|
public static void ClearCache()
|
||
|
|
{
|
||
|
|
foreach (var bitmap in _thumbnailCache.Values)
|
||
|
|
{
|
||
|
|
bitmap?.Dispose();
|
||
|
|
}
|
||
|
|
_thumbnailCache.Clear();
|
||
|
|
_loadingTasks.Clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||
|
|
{
|
||
|
|
throw new NotImplementedException();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 大于零转换器
|
||
|
|
/// </summary>
|
||
|
|
public class IsGreaterThanZeroConverter : IValueConverter
|
||
|
|
{
|
||
|
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||
|
|
{
|
||
|
|
if (value is int intValue)
|
||
|
|
{
|
||
|
|
return intValue > 0;
|
||
|
|
}
|
||
|
|
if (value is long longValue)
|
||
|
|
{
|
||
|
|
return longValue > 0;
|
||
|
|
}
|
||
|
|
if (value is double doubleValue)
|
||
|
|
{
|
||
|
|
return doubleValue > 0;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||
|
|
{
|
||
|
|
throw new NotImplementedException();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 反转布尔值转换器
|
||
|
|
/// </summary>
|
||
|
|
public class InvertedBoolConverter : IValueConverter
|
||
|
|
{
|
||
|
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||
|
|
{
|
||
|
|
if (value is bool boolValue)
|
||
|
|
{
|
||
|
|
return !boolValue;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||
|
|
{
|
||
|
|
if (value is bool boolValue)
|
||
|
|
{
|
||
|
|
return !boolValue;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 是否为零转换器
|
||
|
|
/// </summary>
|
||
|
|
public class IsZeroConverter : IValueConverter
|
||
|
|
{
|
||
|
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||
|
|
{
|
||
|
|
if (value is int intValue)
|
||
|
|
{
|
||
|
|
return intValue == 0;
|
||
|
|
}
|
||
|
|
if (value is long longValue)
|
||
|
|
{
|
||
|
|
return longValue == 0;
|
||
|
|
}
|
||
|
|
if (value is double doubleValue)
|
||
|
|
{
|
||
|
|
return doubleValue == 0;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||
|
|
{
|
||
|
|
throw new NotImplementedException();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|