Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)

This commit is contained in:
WpyQwq
2026-09-19 11:54:03 +08:00
commit 5780fde61a
60 changed files with 15035 additions and 0 deletions
+269
View File
@@ -0,0 +1,269 @@
using FluidExplorer.Services;
using FluidExplorer.Services.Operations;
using FluidExplorer.ViewModels;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Windows.Graphics;
using Windows.System;
namespace FluidExplorer;
public sealed partial class MainWindow : Window
{
private readonly AppServices _services;
private readonly AppSettings _settings;
public MainWindow(AppServices services)
{
_services = services;
_settings = services.Settings;
ViewModel = services.Main;
InitializeComponent();
Shell.ViewModel = ViewModel;
Title = "文件资源管理器";
SystemBackdrop = new MicaBackdrop();
ExtendsContentIntoTitleBar = true;
SetTitleBar(Shell.DragRegion);
RestoreWindowPlacement();
ApplyCaptionButtonColors();
HookServices();
Shell.ThemedRoot.ActualThemeChanged += (_, _) => ApplyCaptionButtonColors();
Shell.SizeChanged += (_, _) => UpdateTitleBarInsets();
AppWindow.Changed += (_, _) => UpdateTitleBarInsets();
AppWindow.Closing += OnClosing;
RegisterAccelerators();
// 启动:恢复标签页 + 建立索引(都在后台,窗口先出来)
_ = DispatcherQueue.TryEnqueue(async () => await ViewModel.InitializeAsync());
}
public MainViewModel ViewModel { get; }
public void ShowOperationQueue() => Shell.ShowOperationQueue();
// ── 窗口外观 ────────────────────────────────────────────────────────────
private void RestoreWindowPlacement()
{
if (!double.IsNaN(_settings.WindowWidth) && _settings.WindowWidth > 400)
{
AppWindow.ResizeClient(new SizeInt32((int)_settings.WindowWidth, (int)_settings.WindowHeight));
}
if (!double.IsNaN(_settings.WindowLeft) && !double.IsNaN(_settings.WindowTop) && IsOnScreen(_settings.WindowLeft, _settings.WindowTop))
{
AppWindow.Move(new PointInt32((int)_settings.WindowLeft, (int)_settings.WindowTop));
}
if (_settings.WindowMaximized && AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Maximize();
}
UpdateTitleBarInsets();
}
private static bool IsOnScreen(double left, double top)
{
var display = DisplayArea.GetFromPoint(new PointInt32((int)left, (int)top), DisplayAreaFallback.Nearest);
return display is not null && left >= display.WorkArea.X - 40 && top >= display.WorkArea.Y - 40;
}
/// <summary>把标签栏右侧的留白让给系统标题栏按钮,保证标签不被按钮压住。</summary>
private void UpdateTitleBarInsets()
{
try
{
var scale = Shell.XamlRoot?.RasterizationScale ?? 1.0;
var rightInset = AppWindow.TitleBar.RightInset / scale;
if (rightInset < 0) rightInset = 0;
if (Shell.DragRegion is FrameworkElement region) region.Margin = new Thickness(0, 0, rightInset, 0);
}
catch
{
// 最小化/全屏切换等瞬态下取不到 TitleBar 信息,忽略
}
}
/// <summary>标题栏按钮颜色跟随当前主题。</summary>
private void ApplyCaptionButtonColors()
{
var titleBar = AppWindow.TitleBar;
titleBar.ButtonBackgroundColor = Colors.Transparent;
titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
if (Shell.ThemedRoot.ActualTheme == ElementTheme.Dark)
{
titleBar.ButtonForegroundColor = Colors.White;
titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(24, 255, 255, 255);
titleBar.ButtonHoverForegroundColor = Colors.White;
titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(48, 255, 255, 255);
titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(200, 255, 255, 255);
titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(120, 255, 255, 255);
}
else
{
titleBar.ButtonForegroundColor = Windows.UI.Color.FromArgb(230, 0, 0, 0);
titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(20, 0, 0, 0);
titleBar.ButtonHoverForegroundColor = Colors.Black;
titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(40, 0, 0, 0);
titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(160, 0, 0, 0);
titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(100, 0, 0, 0);
}
}
/// <summary>主题切换(用户手动指定时覆盖系统跟随)。</summary>
public void ApplyThemeMode(AppThemeMode mode)
{
Shell.ThemedRoot.RequestedTheme = mode switch
{
AppThemeMode.Light => ElementTheme.Light,
AppThemeMode.Dark => ElementTheme.Dark,
_ => ElementTheme.Default
};
}
// ── 服务接线 ────────────────────────────────────────────────────────────
private void HookServices()
{
_services.Operations.ConflictResolver = ShowConflictDialogAsync;
ViewModel.ThemeModeChanged += (_, mode) => ApplyThemeMode(mode);
ViewModel.AnimationsToggled += (_, enabled) =>
{
// 关掉动画时禁用所有依赖动画(WinUI 原生总开关,克制且彻底)
Microsoft.UI.Xaml.Media.Animation.Timeline.AllowDependentAnimations = enabled;
};
ViewModel.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(MainViewModel.ErrorMessage) && ViewModel.ErrorMessage is { Length: > 0 } message)
{
ViewModel.ActivePane?.ReportError(message);
ViewModel.ErrorMessage = null;
}
};
}
/// <summary>文件冲突对话框:不弹系统模态框,全部由应用自己处理。</summary>
private async Task<ConflictResolution> ShowConflictDialogAsync(ConflictInfo info)
{
var dialog = new ContentDialog
{
XamlRoot = Shell.XamlRoot,
Title = "目标位置已有同名项目",
DefaultButton = ContentDialogButton.Primary
};
var keepBoth = new RadioButton { Content = "同时保留两个文件(推荐)", IsChecked = true };
var replace = new RadioButton { Content = "替换目标中的文件" };
var skip = new RadioButton { Content = "跳过此文件" };
var applyAll = new CheckBox { Content = "为后续所有冲突执行相同操作" };
var detail = new StackPanel { Spacing = 8 };
detail.Children.Add(new TextBlock
{
Text = Path.GetFileName(info.SourcePath),
TextWrapping = TextWrapping.Wrap,
Style = (Style)Application.Current.Resources["BodyStrongTextBlockStyle"]
});
detail.Children.Add(new TextBlock
{
Text = $"源:{info.SourcePath}\n{Models.FileEntry.FormatSize(info.SourceSize)} · {info.SourceModifiedUtc.ToLocalTime():yyyy/MM/dd HH:mm}\n\n" +
$"目标:{info.DestinationPath}\n{Models.FileEntry.FormatSize(info.DestinationSize)} · {info.DestinationModifiedUtc.ToLocalTime():yyyy/MM/dd HH:mm}",
TextWrapping = TextWrapping.Wrap,
Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"]
});
detail.Children.Add(keepBoth);
detail.Children.Add(replace);
detail.Children.Add(skip);
detail.Children.Add(applyAll);
dialog.Content = detail;
dialog.PrimaryButtonText = "继续";
dialog.CloseButtonText = "取消本次操作";
var result = await dialog.ShowAsync();
info.ApplyToAll = applyAll.IsChecked == true;
if (result != ContentDialogResult.Primary) return ConflictResolution.Cancel;
if (replace.IsChecked == true) return ConflictResolution.Replace;
if (skip.IsChecked == true) return ConflictResolution.Skip;
return ConflictResolution.KeepBoth;
}
// ── 快捷键 ──────────────────────────────────────────────────────────────
private void RegisterAccelerators()
{
AddAccelerator(VirtualKey.T, VirtualKeyModifiers.Control, () => ViewModel.NewTabCommand.Execute(null));
AddAccelerator(VirtualKey.W, VirtualKeyModifiers.Control, () => ViewModel.CloseTabCommand.Execute(ViewModel.SelectedTab));
AddAccelerator(VirtualKey.Z, VirtualKeyModifiers.Control, () => ViewModel.UndoCommand.Execute(null));
AddAccelerator(VirtualKey.D, VirtualKeyModifiers.Control | VirtualKeyModifiers.Shift, () => ViewModel.ToggleDualPaneCommand.Execute(null));
AddAccelerator(VirtualKey.F, VirtualKeyModifiers.Control, () => ViewModel.ActivePane?.RequestSearchFocus());
AddAccelerator(VirtualKey.Tab, VirtualKeyModifiers.Control, CycleTab);
}
private void AddAccelerator(VirtualKey key, VirtualKeyModifiers modifiers, Action action)
{
var accelerator = new KeyboardAccelerator { Key = key, Modifiers = modifiers };
accelerator.Invoked += (_, args) =>
{
// 文本输入状态下不抢 Ctrl+Z / Ctrl+F
if (key is VirtualKey.Z or VirtualKey.F
&& modifiers == VirtualKeyModifiers.Control
&& FocusManager.GetFocusedElement(Shell.XamlRoot) is TextBox)
{
return;
}
args.Handled = true;
action();
};
Shell.ThemedRoot.KeyboardAccelerators.Add(accelerator);
}
private void CycleTab()
{
if (ViewModel.Tabs.Count < 2 || ViewModel.SelectedTab is null) return;
var index = ViewModel.Tabs.IndexOf(ViewModel.SelectedTab);
ViewModel.SelectedTab = ViewModel.Tabs[(index + 1) % ViewModel.Tabs.Count];
}
// ── 关闭 ────────────────────────────────────────────────────────────────
private void OnClosing(AppWindow sender, AppWindowClosingEventArgs args)
{
try
{
// 有正在进行的复制/移动时不静默退出,避免用户以为文件已搬完
var hasActiveJob = _services.Operations.Jobs.Any(j => !j.IsFinished);
if (hasActiveJob && !_settings.KeepWindowOpenDuringOperations)
{
args.Cancel = true;
return;
}
ViewModel.SaveOpenTabs();
// 索引持有卷句柄,退出时显式释放
foreach (var index in _services.Search.AllIndexes)
{
if (index is IDisposable disposable) disposable.Dispose();
}
_settings.WindowMaximized = AppWindow.Presenter is OverlappedPresenter { State: OverlappedPresenterState.Maximized };
if (!_settings.WindowMaximized)
{
_settings.WindowLeft = AppWindow.Position.X;
_settings.WindowTop = AppWindow.Position.Y;
_settings.WindowWidth = AppWindow.Size.Width;
_settings.WindowHeight = AppWindow.Size.Height;
}
_settings.Save();
}
catch
{
// 关闭时保存失败不应阻塞退出
}
}
}