Files
Shrlalgo.RvKits/NeuWPF/NeoUI/Controls/UploadArea.xaml.cs

396 lines
14 KiB
C#
Raw Normal View History

2025-08-12 23:08:54 +08:00
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using Microsoft.Win32;
2025-08-20 12:10:35 +08:00
using NeoUI.Utilities;
2025-08-20 12:10:13 +08:00
2025-08-12 23:08:54 +08:00
2025-08-20 12:10:35 +08:00
namespace NeoUI.Controls;
2025-08-12 23:08:54 +08:00
2025-08-20 12:10:13 +08:00
/// <summary>
/// 上传状态枚举用于表示文件上传过程中的不同状态。该枚举包含三个可能的值Pending待处理、Success成功和Error错误
/// Pending 表示文件正在等待被上传Success 表示文件已经成功上传;而 Error 则表示在尝试上传文件时发生了错误。
/// </summary>
2025-08-12 23:08:54 +08:00
public enum UploadStatus
{
2025-08-20 12:10:13 +08:00
/// <summary>
/// 表示文件或文件夹正处于待上传状态。当此枚举成员被设置时,意味着相关联的文件或文件夹已经准备好但还未开始实际的上传过程。
/// </summary>
2025-08-12 23:08:54 +08:00
Pending,
2025-08-20 12:10:13 +08:00
/// <summary>
/// 表示文件已成功上传。当此枚举成员被设置时,意味着所选文件或文件夹已经顺利完成上传过程。
/// </summary>
2025-08-12 23:08:54 +08:00
Success,
2025-08-20 12:10:13 +08:00
/// <summary>
/// 表示在尝试上传文件时发生了错误。当此枚举成员被设置时,表示文件上传过程中遇到了问题,未能成功完成。
/// </summary>
2025-08-12 23:08:54 +08:00
Error
}
2025-08-20 12:10:13 +08:00
/// <summary>
/// 上传文件项类用于表示单个待上传的文件。此类实现了INotifyPropertyChanged接口以便在属性更改时通知UI进行更新。
/// 每个实例包含文件路径、文件名及当前上传状态等信息。通过构造函数初始化时会自动设置文件名并将上传状态设为Pending待处理
/// </summary>
2025-08-12 23:08:54 +08:00
public class UploadFileItem : INotifyPropertyChanged
{
2025-08-20 12:10:13 +08:00
private string filePath;
/// <summary>
/// 获取或设置当前文件项的完整路径。
/// </summary>
2025-08-12 23:08:54 +08:00
public string FilePath
{
2025-08-20 12:10:13 +08:00
get => filePath;
set => SetField(ref filePath, value);
2025-08-12 23:08:54 +08:00
}
2025-08-20 12:10:13 +08:00
private string fileName;
/// <summary>
/// 获取或设置当前文件项的文件名。
/// </summary>
2025-08-12 23:08:54 +08:00
public string FileName
{
2025-08-20 12:10:13 +08:00
get => fileName;
set => SetField(ref fileName, value);
2025-08-12 23:08:54 +08:00
}
2025-08-20 12:10:13 +08:00
private UploadStatus status;
/// <summary>
/// 获取或设置当前文件项的上传状态。状态可以是Pending待处理、Success成功或Error错误
/// </summary>
2025-08-12 23:08:54 +08:00
public UploadStatus Status
{
2025-08-20 12:10:13 +08:00
get => status;
set => SetField(ref status, value);
2025-08-12 23:08:54 +08:00
}
2025-08-20 12:10:13 +08:00
/// <summary>
/// 上传文件项类用于表示单个待上传的文件。此类实现了INotifyPropertyChanged接口以便在属性更改时通知UI进行更新。
/// 每个实例包含文件路径、文件名及当前上传状态等信息。通过构造函数初始化时会自动设置文件名并将上传状态设为Pending待处理
/// </summary>
2025-08-12 23:08:54 +08:00
public UploadFileItem(string filePath)
{
FilePath = filePath;
FileName = Path.GetFileName(filePath);
Status = UploadStatus.Pending;
}
2025-08-20 12:10:13 +08:00
/// <inheritdoc />
2025-08-12 23:08:54 +08:00
public event PropertyChangedEventHandler PropertyChanged;
2025-08-20 12:10:13 +08:00
/// <summary>
/// 当属性值更改时调用此方法以通知UI进行相应的更新。
/// 该方法会触发PropertyChanged事件并传递更改的属性名称作为参数。
/// </summary>
/// <param name="propertyName">可选参数,指定触发属性更改事件的属性名。如果未提供,则默认使用调用者成员名称。</param>
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
2025-08-12 23:08:54 +08:00
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
2025-08-20 12:10:13 +08:00
/// <summary>
/// 设置指定字段的值,并在值更改时触发属性更改通知。
/// 如果新值与旧值相同,则不会进行任何操作也不会触发通知。
/// </summary>
/// <typeparam name="T">字段的数据类型。</typeparam>
/// <param name="field">要设置值的字段。</param>
/// <param name="value">要赋予字段的新值。</param>
/// <param name="propertyName">可选参数,指定触发属性更改事件的属性名。如果未提供,则默认使用调用者成员名称。</param>
/// <returns>如果值被成功设置且不等于旧值返回true否则返回false。</returns>
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
2025-08-12 23:08:54 +08:00
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
2025-08-20 12:10:13 +08:00
if (propertyName != null) OnPropertyChanged(propertyName);
2025-08-12 23:08:54 +08:00
return true;
}
}
2025-08-20 12:10:13 +08:00
/// <summary>
/// 上传区域控件,用于实现文件或文件夹的拖拽上传功能。支持设置可接受的上传内容类型(仅文件或仅文件夹)、是否允许多选、文件过滤器以及主提示文字等属性。
/// 控件还提供了对拖放事件的支持,允许用户通过拖拽文件到该区域内来完成上传操作,并且能够显示已选择的文件列表。
/// </summary>
2025-08-12 23:08:54 +08:00
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
[TemplateVisualState(Name = "DragOver", GroupName = "DragDropStates")]
[TemplateVisualState(Name = "DragLeave", GroupName = "DragDropStates")]
public class UploadArea : Control
{
2025-08-20 12:10:13 +08:00
private bool isDragOver;
2025-08-12 23:08:54 +08:00
#region Commands
2025-08-20 12:10:13 +08:00
/// <summary>
/// 定义一个命令,用于从文件列表中移除指定的文件项。
/// </summary>
2025-08-12 23:08:54 +08:00
public ICommand RemoveItemCommand { get; }
2025-08-20 12:10:13 +08:00
/// <summary>
/// 获取用于触发选择文件或文件夹操作的命令。此命令绑定到控件,允许用户通过点击来选择文件或文件夹。
/// </summary>
2025-08-12 23:08:54 +08:00
public ICommand SelectCommand { get; }
#endregion
static UploadArea()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(UploadArea), new FrameworkPropertyMetadata(typeof(UploadArea)));
}
2025-08-20 12:10:13 +08:00
/// <summary>
/// 上传区域控件,允许用户通过点击或拖拽方式上传文件或文件夹。支持设置接受的文件类型、是否允许多选以及提示文本等属性。
/// </summary>
2025-08-12 23:08:54 +08:00
public UploadArea()
{
2025-08-20 12:10:13 +08:00
FileList = [];
2025-08-12 23:08:54 +08:00
RemoveItemCommand = new RelayCommand(p => FileList.Remove(p as UploadFileItem));
SelectCommand = new RelayCommand<object>(_ => OnSelectTriggered(), _ => IsEnabled);
}
#region Dependency Properties
/// <summary>
/// 定义上传内容的类型:仅文件 或 仅文件夹。
/// </summary>
public enum UploadContentType
{
2025-08-20 12:10:13 +08:00
/// <summary>
///
/// </summary>
2025-08-12 23:08:54 +08:00
FilesOnly,
2025-08-20 12:10:13 +08:00
/// <summary>
/// 指示上传内容类型仅限文件夹。当此枚举成员被设置时,用户只能选择文件夹进行上传。
/// </summary>
2025-08-12 23:08:54 +08:00
FoldersOnly
}
2025-08-20 12:10:13 +08:00
/// <summary>
/// 定义上传内容的类型依赖属性,用于指定可以上传的内容是仅文件还是仅文件夹。
/// </summary>
2025-08-12 23:08:54 +08:00
public static readonly DependencyProperty ModeProperty =
DependencyProperty.Register(nameof(Mode), typeof(UploadContentType), typeof(UploadArea), new PropertyMetadata(UploadContentType.FilesOnly));
/// <summary>
/// 获取或设置上传的类型(文件/文件夹)。
/// 结合 'Multiple' 属性可实现四种模式。
/// </summary>
public UploadContentType Mode
{
get => (UploadContentType)GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}
/// <summary>
/// 获取或设置是否允许选择多个文件或文件夹。
/// </summary>
public static readonly DependencyProperty MultipleProperty =
DependencyProperty.Register(nameof(Multiple), typeof(bool), typeof(UploadArea), new PropertyMetadata(false));
2025-08-20 12:10:13 +08:00
/// <summary>
/// 获取或设置是否允许多个文件或文件夹的选择。
/// </summary>
2025-08-12 23:08:54 +08:00
public bool Multiple
{
get => (bool)GetValue(MultipleProperty);
set => SetValue(MultipleProperty, value);
}
/// <summary>
/// 获取或设置文件选择对话框的文件类型过滤器。
/// 例如: "Image Files|*.jpg;*.png|All Files|*.*"
/// </summary>
public static readonly DependencyProperty AcceptProperty =
DependencyProperty.Register(nameof(Accept), typeof(string), typeof(UploadArea), new PropertyMetadata("All files (*.*)|*.*"));
2025-08-20 12:10:13 +08:00
/// <summary>
/// 获取或设置文件选择对话框中允许选择的文件类型过滤器。
/// 该属性用于定义用户在打开文件对话框时可以看到和选择的文件类型。支持通过指定文件扩展名来过滤显示的文件。
/// </summary>
2025-08-12 23:08:54 +08:00
public string Accept
{
get => (string)GetValue(AcceptProperty);
set => SetValue(AcceptProperty, value);
}
/// <summary>
/// 上传区域的主提示文字。用户可直接设置。
/// </summary>
public static readonly DependencyProperty HintTextProperty =
DependencyProperty.Register(nameof(HintText), typeof(string), typeof(UploadArea), new PropertyMetadata("点击或拖拽到此区域"));
2025-08-20 12:10:13 +08:00
/// <summary>
/// 获取或设置当用户未与控件交互时显示的提示文本。
/// </summary>
2025-08-12 23:08:54 +08:00
public string HintText
{
get => (string)GetValue(HintTextProperty);
set => SetValue(HintTextProperty, value);
}
/// <summary>
/// 已选择的文件/文件夹列表。
/// </summary>
public static readonly DependencyProperty FileListProperty =
DependencyProperty.Register(nameof(FileList), typeof(ObservableCollection<UploadFileItem>), typeof(UploadArea), new PropertyMetadata(null));
2025-08-20 12:10:13 +08:00
/// <summary>
/// 获取或设置当前上传区域中的文件项列表。
/// </summary>
2025-08-12 23:08:54 +08:00
public ObservableCollection<UploadFileItem> FileList
{
get => (ObservableCollection<UploadFileItem>)GetValue(FileListProperty);
set => SetValue(FileListProperty, value);
}
#endregion
#region Overrides (Drag & Drop, Visual State)
2025-08-20 12:10:13 +08:00
/// <inheritdoc />
2025-08-12 23:08:54 +08:00
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
UpdateVisualState(false);
}
2025-08-20 12:10:13 +08:00
/// <inheritdoc />
2025-08-12 23:08:54 +08:00
protected override void OnDragEnter(DragEventArgs e)
{
base.OnDragEnter(e);
if (IsEnabled && e.Data.GetDataPresent(DataFormats.FileDrop))
{
2025-08-20 12:10:13 +08:00
isDragOver = true;
2025-08-12 23:08:54 +08:00
UpdateVisualState(true);
}
e.Handled = true;
}
2025-08-20 12:10:13 +08:00
/// <inheritdoc />
2025-08-12 23:08:54 +08:00
protected override void OnDragLeave(DragEventArgs e)
{
base.OnDragLeave(e);
2025-08-20 12:10:13 +08:00
isDragOver = false;
2025-08-12 23:08:54 +08:00
UpdateVisualState(true);
e.Handled = true;
}
2025-08-20 12:10:13 +08:00
/// <inheritdoc />
2025-08-12 23:08:54 +08:00
protected override void OnDrop(DragEventArgs e)
{
base.OnDrop(e);
if (IsEnabled && e.Data.GetDataPresent(DataFormats.FileDrop))
{
var paths = (string[])e.Data.GetData(DataFormats.FileDrop);
AddPaths(paths);
}
2025-08-20 12:10:13 +08:00
isDragOver = false;
2025-08-12 23:08:54 +08:00
UpdateVisualState(true);
e.Handled = true;
}
2025-08-20 12:10:13 +08:00
/// <inheritdoc />
2025-08-12 23:08:54 +08:00
protected override void OnDragOver(DragEventArgs e)
{
base.OnDragOver(e);
e.Effects = IsEnabled && e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
e.Handled = true;
}
#endregion
#region Core Logic
private void OnSelectTriggered()
{
switch (Mode)
{
case UploadContentType.FilesOnly:
SelectFiles();
break;
case UploadContentType.FoldersOnly:
SelectFolders();
break;
2025-08-20 12:10:13 +08:00
default:
throw new ArgumentOutOfRangeException();
2025-08-12 23:08:54 +08:00
}
}
private void SelectFiles()
{
var dialog = new OpenFileDialog
{
Multiselect = this.Multiple,
Title = this.Multiple ? "选择一个或多个文件" : "选择一个文件",
Filter = this.Accept
};
if (dialog.ShowDialog() == true)
{
AddPaths(dialog.FileNames);
}
}
private void SelectFolders()
{
// 注意VistaFolderBrowserDialog 不是 .NET Core/5+ 的标准部分。
// 您可能需要使用第三方库或针对不同平台进行实现。
var dialog = new VistaFolderBrowserDialog
{
Title = this.Multiple ? "选择一个或多个文件夹" : "选择一个文件夹",
Multiselect = this.Multiple
};
if (dialog.ShowDialog() == true)
{
AddPaths(dialog.SelectedPaths);
}
}
2025-08-20 12:10:35 +08:00
private void AddPaths(string?[] paths)
2025-08-12 23:08:54 +08:00
{
2025-08-20 12:10:13 +08:00
FileList ??= [];
2025-08-12 23:08:54 +08:00
// 如果不允许选择多个,则先清空列表
if (!this.Multiple)
{
FileList.Clear();
}
foreach (var path in paths)
{
// 根据文件/文件夹模式进行过滤
2025-08-20 12:10:13 +08:00
var isFile = File.Exists(path);
var isDirectory = Directory.Exists(path);
2025-08-12 23:08:54 +08:00
if ((Mode == UploadContentType.FilesOnly && isFile) ||
(Mode == UploadContentType.FoldersOnly && isDirectory))
{
if (!FileList.Any(item => item.FilePath.Equals(path, StringComparison.OrdinalIgnoreCase)))
{
FileList.Add(new UploadFileItem(path));
}
}
}
}
private void UpdateVisualState(bool useTransitions)
{
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", useTransitions);
2025-08-20 12:10:13 +08:00
if (IsEnabled && isDragOver)
2025-08-12 23:08:54 +08:00
{
VisualStateManager.GoToState(this, "DragOver", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "DragLeave", useTransitions);
}
}
#endregion
}