diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml
index f08cf1a52..a569bff2d 100644
--- a/.github/workflows/reusable-build.yml
+++ b/.github/workflows/reusable-build.yml
@@ -59,7 +59,7 @@ jobs:
PCL_LOBBY_DEFAULT_SECRET: ${{ secrets.LOBBY_DEFAULT_SECRET }}
PCL_GITHUB_SHA: ${{ github.sha }}
run: |
- dotnet publish "Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj" \
+ dotnet publish "Plain Craft Launcher 2/Plain Craft Launcher 2.csproj" \
-p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.architecture }} \
-p:DeleteExistingFiles=true -o ./artifact --no-self-contained
diff --git a/Plain Craft Launcher 2.slnx b/Plain Craft Launcher 2.slnx
index 815a16a64..cbe70b35c 100644
--- a/Plain Craft Launcher 2.slnx
+++ b/Plain Craft Launcher 2.slnx
@@ -12,6 +12,6 @@
-
-
-
\ No newline at end of file
+
+
+
diff --git a/Plain Craft Launcher 2/Application.xaml b/Plain Craft Launcher 2/Application.xaml
index a006290e4..138a25548 100644
--- a/Plain Craft Launcher 2/Application.xaml
+++ b/Plain Craft Launcher 2/Application.xaml
@@ -1,12 +1,12 @@
-
+
@@ -15,14 +15,14 @@
0.0
0.7
Gaussian
-
+
-
-
-
-
-
-
+
+
+
+
+
+
Resources/#PCL English, Microsoft YaHei UI
@@ -99,43 +99,43 @@
-
+
-
+
-
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Application.xaml.cs b/Plain Craft Launcher 2/Application.xaml.cs
new file mode 100644
index 000000000..1e029bda3
--- /dev/null
+++ b/Plain Craft Launcher 2/Application.xaml.cs
@@ -0,0 +1,250 @@
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Threading;
+using PCL.Core.App;
+using PCL.Core.App.IoC;
+using PCL.Core.Logging;
+using PCL.Core.Utils;
+using PCL.Core.Utils.OS;
+
+namespace PCL;
+
+public partial class Application
+{
+ public static readonly List ShowingTooltips = new();
+
+ public Application()
+ {
+ // 注册生命周期事件
+ Lifecycle.When(LifecycleState.Loaded, Application_Startup);
+ SessionEnding += Application_SessionEnding;
+ }
+
+ // 开始
+ private void Application_Startup() // (sender As Object, e As StartupEventArgs) Handles Me.Startup
+ {
+ try
+ {
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+ // 创建自定义跟踪监听器,用于检测是否存在 Binding 失败
+ PresentationTraceSources.DataBindingSource.Listeners.Add(new BindingErrorTraceListener());
+ PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.Error;
+ ModSecret.SecretOnApplicationStart();
+ // 检查参数调用
+ var args = Basics.CommandLineArguments;
+ if (args.Length > 0)
+ {
+ if (args[0] == "--gpu")
+ {
+ // 调整显卡设置
+ try
+ {
+ ModMain.SetGPUPreference(args[1].Trim('"'));
+ Environment.Exit((int)ModBase.ProcessReturnValues.TaskDone);
+ }
+ catch (Exception ex)
+ {
+ Environment.Exit((int)ModBase.ProcessReturnValues.Fail);
+ }
+ }
+ /* TODO ERROR: Skipped IfDirectiveTrivia
+ #If DEBUGRESERVED Then
+ */ /* TODO ERROR: Skipped DisabledTextTrivia
+ '制作更新包
+ ElseIf args(0) = "--edit1" Then
+ ExeEdit(args(1), True)
+ Environment.Exit(ProcessReturnValues.TaskDone)
+ ElseIf args(0) = "--edit2" Then
+ ExeEdit(args(1), False)
+ Environment.Exit(ProcessReturnValues.TaskDone)
+ */ /* TODO ERROR: Skipped EndIfDirectiveTrivia
+ #End If
+ */
+ }
+
+ // 初始化文件结构
+ Directory.CreateDirectory(ModBase.ExePath + @"PCL\Pictures");
+ Directory.CreateDirectory(ModBase.ExePath + @"PCL\Musics");
+ Directory.CreateDirectory(ModBase.PathTemp + "Cache");
+ Directory.CreateDirectory(ModBase.PathTemp + "Download");
+ Directory.CreateDirectory(ModBase.PathAppdata);
+ /* TODO ERROR: Skipped IfDirectiveTrivia
+ #If False Then
+ */ /* TODO ERROR: Skipped DisabledTextTrivia
+ '检测单例
+ Dim ShouldWaitForExit As Boolean = args.Length > 0 AndAlso args(0) = "--wait" '要求等待已有的 PCL 退出
+ Dim WaitRetryCount As Integer = 0
+ WaitRetry:
+ Dim WindowHwnd As IntPtr = FindWindow(Nothing, "Plain Craft Launcher Community Edition ")
+ If WindowHwnd = IntPtr.Zero Then FindWindow(Nothing, "Plain Craft Launcher 2 Community Edition ")
+ If WindowHwnd <> IntPtr.Zero Then
+ If ShouldWaitForExit AndAlso WaitRetryCount < 20 Then '至多等待 10 秒
+ WaitRetryCount += 1
+ Thread.Sleep(500)
+ GoTo WaitRetry
+ End If
+ '将已有的 PCL 窗口拖出来
+ ShowWindowToTop(WindowHwnd)
+ '播放提示音并退出
+ Beep()
+ Environment.[Exit](ProcessReturnValues.Cancel)
+ End If
+ */ /* TODO ERROR: Skipped EndIfDirectiveTrivia
+ #End If
+ */ // 设置 ToolTipService 默认值
+ ToolTipService.InitialShowDelayProperty.OverrideMetadata(typeof(DependencyObject),
+ new FrameworkPropertyMetadata(300));
+ ToolTipService.BetweenShowDelayProperty.OverrideMetadata(typeof(DependencyObject),
+ new FrameworkPropertyMetadata(400));
+ ToolTipService.ShowDurationProperty.OverrideMetadata(typeof(DependencyObject),
+ new FrameworkPropertyMetadata(9999999));
+ ToolTipService.PlacementProperty.OverrideMetadata(typeof(DependencyObject),
+ new FrameworkPropertyMetadata(PlacementMode.Bottom));
+ ToolTipService.HorizontalOffsetProperty.OverrideMetadata(typeof(DependencyObject),
+ new FrameworkPropertyMetadata(8.0d));
+ ToolTipService.VerticalOffsetProperty.OverrideMetadata(typeof(DependencyObject),
+ new FrameworkPropertyMetadata(4.0d));
+ // 设置初始窗口
+ if (Config.Preference.ShowStartupLogo)
+ {
+ ModMain.FrmStart = new SplashScreen(@"Images\icon.ico");
+ ModMain.FrmStart.Show(false, true);
+ }
+
+ // 检测异常环境
+ var problemList = new List();
+ var currentOSVersion = NtInterop.GetCurrentOsVersion();
+ if (currentOSVersion.Build < 17763)
+ problemList.Add("- Windows 版本不满足推荐要求,推荐至少 Windows 10 1809,建议考虑升级 Windows 系统");
+ if (ModBase.Is32BitSystem)
+ problemList.Add("- 当前系统为 32 位,不受 PCL 和新版 Minecraft 支持,非常建议重装为 64 位系统后再进行游戏");
+ if (ModBase.ExePath.Contains(Path.GetTempPath()) || ModBase.ExePath.Contains(@"AppData\Local\Temp\"))
+ problemList.Add("- PCL 正在临时目录运行,请将 PCL 从压缩包中解压之后再使用,否则可能导致游戏存档或设置丢失");
+ if (ModBase.ExePath.ContainsF("wechat_files", true) || ModBase.ExePath.ContainsF("WeChat Files", true) ||
+ ModBase.ExePath.ContainsF("Tencent Files", true))
+ problemList.Add("- PCL 正在 QQ、微信、TIM 等社交软件的下载目录运行,请考虑移动到其他位置,否则可能导致游戏存档或设置丢失");
+ if (problemList.Count != 0)
+ ModMain.MyMsgBox(
+ "PCL CE 在启动时检测到环境问题:" + "\r\n" + "\r\n" + problemList.Join("\r\n") +
+ "\r\n" + "\r\n" + "不解决这些问题可能会导致部分功能无法正常工作……", "环境警告", "我知道了", IsWarn: true);
+ // 设置初始化
+ ModBase.Setup.Load("SystemDebugMode");
+ ModBase.Setup.Load("SystemDebugAnim");
+ ModBase.Setup.Load("SystemHttpProxy");
+ ModBase.Setup.Load("SystemHttpProxyCustomUsername");
+ ModBase.Setup.Load("SystemHttpProxyType");
+ ModBase.Setup.Load("ToolDownloadThread");
+ ModBase.Setup.Load("ToolDownloadSpeed");
+ ModBase.Setup.Load("UiFont");
+ var updateBranchCfg = Config.Update.UpdateChannelConfig;
+ if (updateBranchCfg.IsDefault())
+ updateBranchCfg.SetValue(ModBase.VersionBaseName.Contains("beta")
+ ? Core.App.UpdateChannel.Beta
+ : Core.App.UpdateChannel.Release);
+ // 删除旧日志
+ for (var i = 1; i <= 5; i++)
+ {
+ var oldLogFile = $@"{ModBase.ExePath}PCL\Log-CE{i}.log";
+ if (File.Exists(oldLogFile))
+ File.Delete(oldLogFile);
+ }
+
+ // 计时
+ ModBase.Log("[Start] 第一阶段加载用时:" + (TimeUtils.GetTimeTick() - ModBase.ApplicationStartTick) + " ms");
+ ModBase.ApplicationStartTick = TimeUtils.GetTimeTick();
+ // 执行测试
+ /* TODO ERROR: Skipped IfDirectiveTrivia
+ #If DEBUGRESERVED Then
+ */ /* TODO ERROR: Skipped DisabledTextTrivia
+ Test()
+ */ /* TODO ERROR: Skipped EndIfDirectiveTrivia
+ #End If
+ */
+ ModAnimation.AniControlEnabled += 1;
+ }
+ catch (Exception ex)
+ {
+ var FilePath = ModBase.ExePathWithName;
+ MessageBox.Show(ex + "\r\n" + "PCL 所在路径:" + (string.IsNullOrEmpty(FilePath) ? "获取失败" : FilePath),
+ "PCL 初始化错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ FormMain.EndProgramForce(ModBase.ProcessReturnValues.Exception);
+ }
+ }
+
+ // 结束
+ private void Application_SessionEnding(object sender, SessionEndingCancelEventArgs e)
+ {
+ ModMain.FrmMain.EndProgram(false);
+ }
+
+// Error handling for unhandled exceptions
+ private void Application_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
+ {
+ try
+ {
+ e.Handled = true;
+ if (ModBase.IsProgramEnded) return;
+
+ ModBase.FeedbackInfo();
+
+ var detail = e.Exception.ToString();
+
+ // Automatic error analysis for environment issues
+ if (detail.Contains("System.Windows.Threading.Dispatcher.Invoke") ||
+ detail.Contains("MS.Internal.AppModel.ITaskbarList.HrInit") ||
+ detail.Contains("未能加载文件或程序集"))
+ {
+ ModBase.OpenWebsite("https://get.dot.net/8");
+ LogWrapper.Error(e.Exception,
+ "Your .NET Desktop Runtime is outdated or corrupted. Please reinstall .NET 8!");
+ }
+ else
+ {
+ LogWrapper.Error(e.Exception, "An unexpected error occurred");
+ }
+ }
+ catch
+ {
+ // Equivalent to On Error Resume Next for safety in the global handler
+ }
+ }
+
+ // Win32 API declaration for DLL directory configuration
+ [DllImport("kernel32", EntryPoint = "SetDllDirectoryA", CharSet = CharSet.Ansi)]
+ private static extern bool SetDllDirectory(string lpPathName);
+ // 切换窗口
+
+ // 控件模板事件
+ private void MyIconButton_Click(object sender, EventArgs e)
+ {
+ }
+
+ private void TooltipLoaded(object sender, EventArgs e)
+ {
+ ShowingTooltips.Add((Border)sender);
+ }
+
+ private void TooltipUnloaded(object sender, RoutedEventArgs e)
+ {
+ ShowingTooltips.Remove((Border)sender);
+ }
+
+ // 自定义监听器类
+ public class BindingErrorTraceListener : TraceListener
+ {
+ public override void Write(string message)
+ {
+ ModBase.Log($"警告,检测到 Binding 失败:{message}");
+ }
+
+ public override void WriteLine(string message)
+ {
+ ModBase.Log($"警告,检测到 Binding 失败:{message}");
+ }
+ }
+}
diff --git a/Plain Craft Launcher 2/Controls/AnimatedBackgroundGrid.cs b/Plain Craft Launcher 2/Controls/AnimatedBackgroundGrid.cs
new file mode 100644
index 000000000..c9a3ec533
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/AnimatedBackgroundGrid.cs
@@ -0,0 +1,78 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace PCL;
+
+public class AnimatedBackgroundGrid : Grid
+{
+ public static readonly DependencyProperty BackgroundBrushProperty = DependencyProperty.Register("BackgroundBrush",
+ typeof(SolidColorBrush), typeof(AnimatedBackgroundGrid),
+ new PropertyMetadata(new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)), _BackgroundBrushChanged));
+
+ private readonly DependencyProperty _animatableBrushProperty;
+
+ public readonly int Uuid = ModBase.GetUuid();
+
+ private bool _isAnimating;
+
+ public AnimatedBackgroundGrid(DependencyProperty brushDp)
+ {
+ _animatableBrushProperty = brushDp;
+ Loaded += (_, _) => Init();
+ }
+
+ public AnimatedBackgroundGrid() : this(BackgroundProperty)
+ {
+ }
+
+ protected virtual FrameworkElement AnimatableElement => this;
+
+ protected virtual SolidColorBrush AnimatableBrush
+ {
+ get => (SolidColorBrush)Background;
+ set => Background = value;
+ }
+
+ protected bool IsAnimating
+ {
+ get => _isAnimating;
+ private set => _isAnimating = value;
+ }
+
+ public SolidColorBrush BackgroundBrush
+ {
+ get => (SolidColorBrush)GetValue(BackgroundBrushProperty);
+ set => SetValue(BackgroundBrushProperty, value);
+ }
+
+ private static void _BackgroundBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var grid = (AnimatedBackgroundGrid)d;
+ var brush = (SolidColorBrush)e.NewValue;
+ if (!(grid.IsLoaded & grid.IsVisible))
+ {
+ grid.AnimatableBrush = brush;
+ return;
+ }
+
+ grid.Dispatcher.BeginInvoke(new Func(async () =>
+ {
+ grid.IsAnimating = true;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(grid.AnimatableElement, grid._animatableBrushProperty,
+ new ModBase.MyColor(brush) - grid.AnimatableBrush, 300)
+ }, "MyCard Theme " + grid.Uuid);
+ await Task.Delay(300);
+ grid.AnimatableBrush = brush;
+ grid.IsAnimating = false;
+ }));
+ }
+
+ private void Init()
+ {
+ AnimatableBrush = BackgroundBrush;
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/Behaviors/ClipboardInterceptor.cs b/Plain Craft Launcher 2/Controls/Behaviors/ClipboardInterceptor.cs
new file mode 100644
index 000000000..223b20bd6
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/Behaviors/ClipboardInterceptor.cs
@@ -0,0 +1,240 @@
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Input;
+using Clipboard = System.Windows.Forms.Clipboard;
+
+// Author: uye (owner of the MaaAssistantArknights team)
+// Original Source: MaaAssistantArknights project - https://github.com/MaaAssistantArknights/MaaAssistantArknights
+// License: Apache License 2.0 (this file only)
+//
+// This file is based on work originally developed in the MaaAssistantArknights project,
+// which is licensed under the GNU AGPL v3.0 only.
+//
+// As the original author of this code, I am re-licensing this specific file under
+// the Apache License 2.0 for use in PCL2-CE.
+//
+// Description:
+// Implements a WPF clipboard fix to handle OpenClipboard failures in TextBox,
+// RichTextBox, and DataGrid, typically caused by focus issues or external hooks.
+//
+// Date: 2025-07-03
+
+namespace PCL.Controls.Behaviors;
+
+public sealed class ClipboardInterceptor
+{
+ public static readonly DependencyProperty EnableSafeClipboardProperty =
+ DependencyProperty.RegisterAttached("EnableSafeClipboard", typeof(bool), typeof(ClipboardInterceptor),
+ new PropertyMetadata(false, OnEnableSafeClipboardChanged));
+
+ private ClipboardInterceptor()
+ {
+ }
+
+ public static void SetEnableSafeClipboard(DependencyObject element, bool value)
+ {
+ element.SetValue(EnableSafeClipboardProperty, value);
+ }
+
+ public static bool GetEnableSafeClipboard(DependencyObject element)
+ {
+ return (bool)element.GetValue(EnableSafeClipboardProperty);
+ }
+
+ private static void OnEnableSafeClipboardChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ switch (d)
+ {
+ case TextBox box when (bool)e.NewValue:
+ AddCommandBindingsToTextBox(box);
+ break;
+ case RichTextBox box when (bool)e.NewValue:
+ AddCommandBindingsToRichTextBox(box);
+ break;
+ case DataGrid grid when (bool)e.NewValue:
+ AddCommandBindingsToDataGrid(grid);
+ break;
+ }
+ }
+
+ private static void AddCommandBindingsToTextBox(TextBox tb)
+ {
+ tb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopyTextBox));
+ tb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCutTextBox));
+ tb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPasteTextBox));
+ }
+
+ private static void AddCommandBindingsToRichTextBox(RichTextBox rtb)
+ {
+ rtb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopyRichTextBox));
+ rtb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCutRichTextBox));
+ rtb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPasteRichTextBox));
+ }
+
+ private static void AddCommandBindingsToDataGrid(DataGrid dg)
+ {
+ dg.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopyDataGrid));
+ }
+
+ private static void OnCopyTextBox(object sender, ExecutedRoutedEventArgs e)
+ {
+ var tb = sender as TextBox;
+ if (tb is null || tb.SelectionLength <= 0)
+ return;
+
+ try
+ {
+ Clipboard.Clear();
+ Clipboard.SetDataObject(tb.SelectedText, true);
+ }
+ catch
+ {
+ }
+
+ e.Handled = true;
+ }
+
+ private static void OnCutTextBox(object sender, ExecutedRoutedEventArgs e)
+ {
+ var tb = sender as TextBox;
+ if (tb is null || tb.SelectionLength <= 0)
+ return;
+
+ try
+ {
+ Clipboard.Clear();
+ Clipboard.SetDataObject(tb.SelectedText, true);
+ }
+ catch
+ {
+ }
+
+ tb.SelectedText = string.Empty;
+ e.Handled = true;
+ }
+
+ private static void OnPasteTextBox(object sender, ExecutedRoutedEventArgs e)
+ {
+ var tb = sender as TextBox;
+ if (tb is null)
+ return;
+
+ if (Clipboard.ContainsText())
+ {
+ var pasteText = Clipboard.GetText();
+
+ if (!tb.AcceptsReturn)
+ pasteText = pasteText.Replace("\r\n", " ").Replace("\r", " ")
+ .Replace("\n", " ");
+
+ var start = tb.SelectionStart;
+
+ tb.SelectedText = pasteText;
+ tb.CaretIndex = start + pasteText.Length;
+ tb.SelectionLength = 0;
+ }
+
+ e.Handled = true;
+ }
+
+ private static void OnCopyRichTextBox(object sender, ExecutedRoutedEventArgs e)
+ {
+ var rtb = sender as RichTextBox;
+ if (rtb is null)
+ return;
+
+ var textRange = new TextRange(rtb.Selection.Start, rtb.Selection.End);
+ if (string.IsNullOrEmpty(textRange.Text))
+ return;
+
+ try
+ {
+ Clipboard.Clear();
+ Clipboard.SetDataObject(textRange.Text, true);
+ }
+ catch
+ {
+ }
+
+ e.Handled = true;
+ }
+
+ private static void OnCutRichTextBox(object sender, ExecutedRoutedEventArgs e)
+ {
+ var rtb = sender as RichTextBox;
+ if (rtb is null)
+ return;
+
+ var selection = new TextRange(rtb.Selection.Start, rtb.Selection.End);
+ if (string.IsNullOrEmpty(selection.Text))
+ return;
+
+ try
+ {
+ Clipboard.Clear();
+ Clipboard.SetDataObject(selection.Text, true);
+ }
+ catch
+ {
+ }
+
+ selection.Text = string.Empty;
+ e.Handled = true;
+ }
+
+ private static void OnPasteRichTextBox(object sender, ExecutedRoutedEventArgs e)
+ {
+ var rtb = sender as RichTextBox;
+ if (rtb is null)
+ return;
+
+ if (!Clipboard.ContainsText())
+ return;
+
+ var pasteText = Clipboard.GetText();
+ var selection = rtb.Selection;
+
+ selection.Text = pasteText;
+
+ var caretPos = selection.End;
+ rtb.CaretPosition = caretPos;
+ rtb.Selection.Select(caretPos, caretPos);
+
+ e.Handled = true;
+ }
+
+ private static void OnCopyDataGrid(object sender, ExecutedRoutedEventArgs e)
+ {
+ var dg = sender as DataGrid;
+ if (dg is null || dg.SelectedCells is null || dg.SelectedCells.Count == 0)
+ return;
+
+ var sb = new StringBuilder();
+ var rowGroups = dg.SelectedCells.GroupBy(c => c.Item);
+
+ foreach (var row in rowGroups)
+ {
+ var rowText = string.Join("\t", row.Select(cell =>
+ {
+ var tb = cell.Column.GetCellContent(cell.Item) as TextBlock;
+ return tb is not null ? tb.Text : "";
+ }));
+ sb.AppendLine(rowText);
+ }
+
+ var sbStr = sb.ToString().TrimEnd('\r', '\n');
+
+ try
+ {
+ Clipboard.Clear();
+ Clipboard.SetDataObject(sbStr, true);
+ }
+ catch
+ {
+ }
+
+ e.Handled = true;
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/Behaviors/LazyLoadBehavior.cs b/Plain Craft Launcher 2/Controls/Behaviors/LazyLoadBehavior.cs
new file mode 100644
index 000000000..1af433fe3
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/Behaviors/LazyLoadBehavior.cs
@@ -0,0 +1,75 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using Microsoft.Xaml.Behaviors;
+
+namespace PCL;
+
+internal static class LazyLoader
+{
+ public static void EnableLazyLoad(this FrameworkElement element, Action action)
+ {
+ var behavior = new LazyLoadBehavior();
+ behavior.Action = action;
+ Interaction.GetBehaviors(element).Add(behavior);
+ }
+}
+
+public class LazyLoadBehavior : Behavior
+{
+ public static readonly DependencyProperty ActionProperty = DependencyProperty.Register(nameof(Action),
+ typeof(Action), typeof(LazyLoadBehavior), new PropertyMetadata(null));
+
+ public Action Action
+ {
+ get => (Action)GetValue(ActionProperty);
+ set => SetValue(ActionProperty, value);
+ }
+
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+ AssociatedObject.LayoutUpdated += OnLayoutUpdated;
+ }
+
+ protected override void OnDetaching()
+ {
+ AssociatedObject.LayoutUpdated -= OnLayoutUpdated;
+ base.OnDetaching();
+ }
+
+ private void OnLayoutUpdated(object sender, EventArgs e)
+ {
+ if (AssociatedObject.RenderSize.Width < double.Epsilon)
+ return;
+ if (!AssociatedObject.IsVisible)
+ return;
+
+ var scrollViewer = FindParentScrollViewer(AssociatedObject);
+ if (scrollViewer is null)
+ return;
+
+ var elementBounds = AssociatedObject.TransformToAncestor(scrollViewer)
+ .TransformBounds(new Rect(new Point(0d, 0d), AssociatedObject.RenderSize));
+ var viewport = new Rect(0d, 0d, scrollViewer.ViewportWidth, scrollViewer.ViewportHeight);
+
+ if (viewport.IntersectsWith(elementBounds))
+ {
+ Action?.Invoke();
+ // 仅执行一次
+ AssociatedObject.LayoutUpdated -= OnLayoutUpdated;
+ }
+ }
+
+ private ScrollViewer FindParentScrollViewer(DependencyObject d)
+ {
+ while (d is not null)
+ {
+ if (d is ScrollViewer)
+ return (ScrollViewer)d;
+ d = VisualTreeHelper.GetParent(d);
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/FontSelector.xaml b/Plain Craft Launcher 2/Controls/FontSelector.xaml
index f66cdab6b..41d96da2c 100644
--- a/Plain Craft Launcher 2/Controls/FontSelector.xaml
+++ b/Plain Craft Launcher 2/Controls/FontSelector.xaml
@@ -1,24 +1,24 @@
-
-
+
+ FontFamily="{Binding Font}" />
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/FontSelector.xaml.cs b/Plain Craft Launcher 2/Controls/FontSelector.xaml.cs
new file mode 100644
index 000000000..1084b918b
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/FontSelector.xaml.cs
@@ -0,0 +1,168 @@
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using PCL.Core.Logging;
+using PCL.Core.Utils.Exts;
+
+namespace PCL;
+
+public partial class FontSelector
+{
+ public delegate void SelectionChangedEventHandler(object sender, SelectionChangedEventArgs e);
+
+ public new static readonly DependencyProperty TooltipProperty = DependencyProperty.Register("Tooltip",
+ typeof(string), typeof(FontSelector), new PropertyMetadata(null, OnTooltipChanged));
+
+ private bool _isInitializing;
+ private string _pendingFontTag;
+
+ public FontSelector()
+ {
+ InitializeComponent();
+ Loaded += FontSelector_Loaded;
+ ComboFont.SelectionChanged += ComboFont_SelectionChanged;
+ }
+
+
+ public new string Tooltip
+ {
+ get => (string)GetValue(TooltipProperty);
+ set => SetValue(TooltipProperty, value);
+ }
+
+ public ObservableCollection CustomFontCollection { get; } = new();
+
+ public string SelectedFontTag
+ {
+ get
+ {
+ if (ComboFont.SelectedItem is null)
+ return "";
+ var selectedFont = ComboFont.SelectedItem as CustomFontProperties;
+ if (selectedFont is null)
+ return "";
+ return selectedFont.Tag;
+ }
+ set
+ {
+ // 如果字体还在加载中,延迟设置
+ if (CustomFontCollection.Count == 0 ||
+ (CustomFontCollection.Count == 1 && CustomFontCollection[0].Name == "加载中..."))
+ {
+ _pendingFontTag = value;
+ return;
+ }
+
+ _isInitializing = true;
+
+ var targetSelection = CustomFontCollection.FirstOrDefault(i => (i.Tag ?? "") == (value ?? ""));
+ if (targetSelection is null)
+ ComboFont.SelectedIndex = 0;
+ else
+ ComboFont.SelectedItem = targetSelection;
+
+ _isInitializing = false;
+ }
+ }
+
+ public int SelectedIndex
+ {
+ get => ComboFont.SelectedIndex;
+ set => ComboFont.SelectedIndex = value;
+ }
+
+ public new bool IsEnabled
+ {
+ get => ComboFont.IsEnabled;
+ set => ComboFont.IsEnabled = value;
+ }
+
+ private static void OnTooltipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = d as FontSelector;
+ if (control is not null) control.ComboFont.ToolTip = e.NewValue;
+ }
+
+ public event SelectionChangedEventHandler? SelectionChanged;
+
+ private void FontSelector_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (CustomFontCollection.Count == 0) LoadFonts();
+ }
+
+ private void LoadFonts()
+ {
+ Dispatcher.BeginInvoke(async () =>
+ {
+ ComboFont.IsEnabled = false;
+ _isInitializing = true;
+ CustomFontCollection.Add(new CustomFontProperties { Name = "加载中..." });
+ ComboFont.SelectedIndex = 0;
+
+ var availableFonts = new List<(string Name, FontFamily Font)>();
+
+ await Task.Run(() =>
+ {
+ foreach (var font in Fonts.SystemFontFamilies)
+ try
+ {
+ if (font.Source.StartsWith("Global ")) continue;
+
+ foreach (var typeface in font.GetTypefaces())
+ {
+ if (!typeface.TryGetGlyphTypeface(out var glyph))
+ throw new NullReferenceException(
+ $"字形 {typeface.FaceNames.GetForCurrentUiCulture("(unknown)")} 无法加载");
+
+ _ = new GlyphTypeface(glyph.FontUri);
+ }
+
+ availableFonts.Add((font.FamilyNames.GetForCurrentUiCulture(), font));
+ }
+ catch (Exception ex)
+ {
+ LogWrapper.Error(ex, $"发现了一个无法加载的异常的字体:{font.Source}");
+ }
+
+ availableFonts.Sort((l, r) => string.Compare(l.Name, r.Name, StringComparison.Ordinal));
+ });
+
+ CustomFontCollection.Clear();
+ CustomFontCollection.Add(new CustomFontProperties
+ {
+ Name = "默认",
+ Font = new FontFamily(new Uri("pack://application:,,,/"),
+ "./Resources/#PCL English, Segoe UI, Microsoft YaHei UI"),
+ Tag = ""
+ });
+
+ foreach (var font in availableFonts)
+ CustomFontCollection.Add(new CustomFontProperties
+ { Name = font.Name, Font = font.Font, Tag = font.Font.Source });
+
+ ComboFont.IsEnabled = true;
+
+ if (_pendingFontTag != null)
+ {
+ var pendingTag = _pendingFontTag;
+ _pendingFontTag = null;
+ SelectedFontTag = pendingTag;
+ }
+
+ _isInitializing = false;
+ });
+ }
+
+ private void ComboFont_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (!_isInitializing) SelectionChanged?.Invoke(sender, e);
+ }
+
+ public class CustomFontProperties
+ {
+ public string Name { get; set; }
+ public FontFamily Font { get; set; }
+ public string Tag { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/IMyRadio.cs b/Plain Craft Launcher 2/Controls/IMyRadio.cs
new file mode 100644
index 000000000..063906dfe
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/IMyRadio.cs
@@ -0,0 +1,11 @@
+namespace PCL;
+
+public interface IMyRadio
+{
+ delegate void ChangedEventHandler(object sender, ModBase.RouteEventArgs e);
+
+ delegate void CheckEventHandler(object sender, ModBase.RouteEventArgs e);
+
+ event CheckEventHandler Check;
+ event ChangedEventHandler Changed;
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MinecraftServer.xaml b/Plain Craft Launcher 2/Controls/MinecraftServer.xaml
index aefaa6fe8..b9cf8d22c 100644
--- a/Plain Craft Launcher 2/Controls/MinecraftServer.xaml
+++ b/Plain Craft Launcher 2/Controls/MinecraftServer.xaml
@@ -1,19 +1,20 @@
-
-
-
-
+
+
+
-
-
+
+
-
-
+ IsHitTestVisible="False" Width="300" Height="Auto" VerticalAlignment="Center" />
+
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MinecraftServer.xaml.cs b/Plain Craft Launcher 2/Controls/MinecraftServer.xaml.cs
new file mode 100644
index 000000000..c638aa2d4
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MinecraftServer.xaml.cs
@@ -0,0 +1,98 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Media;
+using PCL.Core.Link.McPing;
+using PCL.Core.Link.McPing.Model;
+using PCL.Core.Minecraft;
+using PCL.Core.UI;
+
+namespace PCL;
+
+public partial class MinecraftServer : Grid
+{
+ private const string FallbackImageUri =
+ "pack://application:,,,/Plain Craft Launcher 2;component/Images/Icons/DefaultServer.png";
+
+ private static readonly DependencyProperty AddressProperty = DependencyProperty.Register(nameof(Address),
+ typeof(string), typeof(MinecraftServer), new PropertyMetadata(string.Empty, OnAddressChanged));
+
+ public MinecraftServer()
+ {
+ InitializeComponent();
+ }
+
+ public string Address
+ {
+ get => (string)(GetValue(AddressProperty));
+ set => SetValue(AddressProperty, value);
+ }
+
+ private static void OnAddressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var server = (MinecraftServer)d;
+ d.Dispatcher.BeginInvoke(new Func(() => server.UpdateServerInfoAsync(e.NewValue?.ToString())));
+ }
+
+ public async Task UpdateServerInfoAsync(string address)
+ {
+ if (address is null)
+ return;
+ address = address.Replace(":", ":");
+ // 预先重置UI状态
+ LabServerDesc.Foreground = Brushes.White;
+ LabServerDesc.Text = "查询中...";
+ LabServerPlayer.Text = "-/-";
+ LabServerPlayer.ToolTip = null;
+ ImageLoaderHelper.SetFallbackImage(ImgServerLogo, FallbackImageUri);
+
+ try
+ {
+ // 获取可达地址(DNS解析)
+ var addr = await ServerAddressResolver.GetReachableAddressAsync(address);
+
+ // Ping服务器
+ using (var query = McPingServiceFactory.CreateService(addr.Ip, addr.Port))
+ {
+ var ret = await query.PingAsync();
+
+ if (ret is null) throw new Exception("未返回服务器信息");
+
+ // 处理服务器图标
+ await ImageLoaderHelper.SetServerLogoAsync(ret.Favicon, ImgServerLogo);
+
+ // 更新UI
+ UpdateServerStatus(ret);
+ }
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "[MinecraftServer] 信息查询失败");
+ LabServerDesc.Text = $"无法连接: {ex.Message}";
+ LabServerDesc.Foreground = Brushes.Red;
+ ImageLoaderHelper.SetFallbackImage(ImgServerLogo, FallbackImageUri);
+ }
+ }
+
+ private void UpdateServerStatus(McPingResult ret)
+ {
+ // 延迟颜色判断
+ var latencyColor = ret.Latency < 150 ? "a" : ret.Latency < 400 ? "6" : "c";
+
+ // 更新描述
+ LabServerDesc.Text = "Minecraft 服务器";
+ MotdRenderer.RenderMotd(ret.Description, false, 2, 14);
+ MotdRenderer.RenderCanvas();
+
+ // 更新玩家信息
+ var playerText = $"{ret.Players.Online}/{ret.Players.Max}{"\r\n"}§{latencyColor}{ret.Latency}ms";
+ ModStyle.MinecraftFormatter.SetColorfulTextLab(playerText, LabServerPlayer, false);
+
+ // 玩家列表提示
+ if (ret.Players.Samples.Any())
+ {
+ LabServerPlayer.ToolTip = string.Join("\r\n", ret.Players.Samples.Select(x => x.Name));
+ ToolTipService.SetPlacement(LabServerPlayer, PlacementMode.Mouse);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml b/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml
index e187330e8..5fa616528 100644
--- a/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml
+++ b/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml
@@ -1,34 +1,37 @@
-
-
-
+
+
-
-
+
+
-
-
+
+
/
-
-
+
+
-
-
+
+
-
+
-
+
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml.cs b/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml.cs
new file mode 100644
index 000000000..8b8158d2d
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml.cs
@@ -0,0 +1,24 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace PCL;
+
+public partial class MinecraftServerQuery : Grid
+{
+ public MinecraftServerQuery()
+ {
+ InitializeComponent();
+ BtnServerQuery.Click += BtnServerQuery_Click;
+ }
+ private void BtnServerQuery_Click(object sender, MouseButtonEventArgs e)
+ {
+ Dispatcher.BeginInvoke(new Func(() => ServerQueryAsync()));
+ }
+
+ private async Task ServerQueryAsync()
+ {
+ await PanMcServer.UpdateServerInfoAsync(LabServerIp.Text);
+ ServerInfo.Visibility = Visibility.Visible;
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyButton.xaml b/Plain Craft Launcher 2/Controls/MyButton.xaml
index 52c44aea1..7ba584a10 100644
--- a/Plain Craft Launcher 2/Controls/MyButton.xaml
+++ b/Plain Craft Launcher 2/Controls/MyButton.xaml
@@ -1,13 +1,19 @@
-
-
+
+
-
+
diff --git a/Plain Craft Launcher 2/Controls/MyButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyButton.xaml.cs
new file mode 100644
index 000000000..a009ff34b
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyButton.xaml.cs
@@ -0,0 +1,283 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Markup;
+using System.Windows.Media;
+
+namespace PCL;
+
+[ContentProperty("Inlines")]
+public partial class MyButton
+{
+ public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); // 自定义事件
+
+ public enum ColorState
+ {
+ Normal = 0,
+ Highlight = 1,
+ Red = 2
+ }
+
+ // 自定义事件
+ private const int AnimationColorIn = 100;
+ private const int AnimationColorOut = 200;
+
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
+ typeof(MyButton), new PropertyMetadata((sender, e) =>
+ {
+ if (sender is not null) ((MyButton)sender).LabText.Text = (string)e.NewValue;
+ }));
+
+ // 属性穿透
+ public new static readonly DependencyProperty PaddingProperty = DependencyProperty.Register("Padding",
+ typeof(Thickness), typeof(MyButton), new PropertyMetadata((sender, e) =>
+ {
+ if (sender is not null) ((MyButton)sender).PanFore.Padding = (Thickness)e.NewValue;
+ }));
+
+ private ColorState _ColorType = ColorState.Normal; // 配色方案
+
+ // 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+
+
+ // 自定义属性
+ public int Uuid = ModBase.GetUuid();
+
+ public MyButton()
+ {
+ InitializeComponent();
+
+ MouseEnter += RefreshColor;
+ MouseLeave += RefreshColor;
+ Loaded += RefreshColor;
+ IsEnabledChanged += (_, _) => RefreshColor();
+ MouseLeftButtonUp += Button_MouseUp;
+ MouseLeftButtonDown += Button_MouseDown;
+ MouseEnter += (_, _) => Button_MouseEnter();
+ MouseLeftButtonUp += (_, _) => Button_MouseUp();
+ MouseLeave += (_, _) => Button_MouseLeave();
+ }
+
+ public InlineCollection Inlines => LabText.Inlines;
+
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ } // 显示文本
+
+ public Thickness TextPadding
+ {
+ get => LabText.Padding;
+ set => LabText.Padding = value;
+ }
+
+ public ColorState ColorType
+ {
+ get => _ColorType;
+ set
+ {
+ _ColorType = value;
+ RefreshColor();
+ }
+ }
+
+ public new Thickness Padding
+ {
+ get => PanFore.Padding;
+ set => PanFore.Padding = value;
+ }
+
+ public Transform RealRenderTransform
+ {
+ get => PanFore.RenderTransform;
+ set => PanFore.RenderTransform = value;
+ }
+
+ // 声明
+ public event ClickEventHandler? Click;
+
+ private void RefreshColor(object obj = null, object e = null)
+ {
+ try
+ {
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
+ {
+ if (IsEnabled)
+ switch (ColorType)
+ {
+ case ColorState.Normal:
+ {
+ if (IsMouseOver)
+ // 指向(Main 3)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrush3",
+ AnimationColorIn)
+ }, "MyButton Color " + Uuid);
+ else
+ // 普通(Main 1)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrush1",
+ AnimationColorOut)
+ }, "MyButton Color " + Uuid);
+
+ break;
+ }
+ case ColorState.Highlight:
+ {
+ if (IsMouseOver)
+ // 指向(Main 3)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrush3",
+ AnimationColorIn)
+ }, "MyButton Color " + Uuid);
+ else
+ // 高亮(Main 2)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrush2",
+ AnimationColorOut)
+ }, "MyButton Color " + Uuid);
+
+ break;
+ }
+ case ColorState.Red:
+ {
+ if (IsMouseOver)
+ // 红色指向
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrushRedLight",
+ AnimationColorIn)
+ }, "MyButton Color " + Uuid);
+ else
+ // 红色
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrushRedDark",
+ AnimationColorOut)
+ }, "MyButton Color " + Uuid);
+
+ break;
+ }
+ }
+ else
+ // 不可用(Gray 4)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(PanFore, BorderBrushProperty,
+ ModSecret.ColorGray4 - PanFore.BorderBrush, AnimationColorOut)
+ }, "MyButton Color " + Uuid);
+ }
+ else
+ {
+ ModAnimation.AniStop("MyButton Color " + Uuid);
+ if (IsEnabled)
+ switch (ColorType)
+ {
+ case ColorState.Normal:
+ {
+ if (IsMouseOver)
+ PanFore.SetResourceReference(BorderBrushProperty, "ColorBrush3");
+ else
+ PanFore.SetResourceReference(BorderBrushProperty, "ColorBrush1");
+
+ break;
+ }
+ case ColorState.Highlight:
+ {
+ if (IsMouseOver)
+ PanFore.SetResourceReference(BorderBrushProperty, "ColorBrush3");
+ else
+ PanFore.SetResourceReference(BorderBrushProperty, "ColorBrush2");
+
+ break;
+ }
+ case ColorState.Red:
+ {
+ if (IsMouseOver)
+ PanFore.SetResourceReference(BorderBrushProperty, "ColorBrushRedLight");
+ else
+ PanFore.SetResourceReference(BorderBrushProperty, "ColorBrushRedDark");
+
+ break;
+ }
+ }
+ else
+ PanFore.BorderBrush = ModSecret.ColorGray4;
+ }
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "刷新按钮颜色出错");
+ }
+ }
+
+ // 实现自定义事件
+ private bool IsMouseDown = false;
+ private void Button_MouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (!IsMouseDown)
+ return;
+ ModBase.Log("[Control] 按下按钮:" + Text);
+ Click?.Invoke(sender, e);
+ ModMain.RaiseCustomEvent(this);
+ }
+
+ private void Button_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ IsMouseDown = true;
+ Focus();
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanFore, 0.955d - ((ScaleTransform)PanFore.RenderTransform).ScaleX, 80,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)),
+ ModAnimation.AaScaleTransform(PanFore, -0.01d, 700, Ease: new ModAnimation.AniEaseOutFluent())
+ }, "MyButton Scale " + Uuid);
+ }
+
+ private void Button_MouseEnter()
+ {
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(PanFore, BackgroundProperty,
+ _ColorType == ColorState.Red ? "ColorBrushRedBack" : "ColorBrush7", AnimationColorIn),
+ "MyButton Background " + Uuid);
+ }
+
+ private void Button_MouseUp()
+ {
+ if (!IsMouseDown)
+ return;
+ IsMouseDown = false;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanFore, 1d - ((ScaleTransform)PanFore.RenderTransform).ScaleX, 300, 10,
+ new ModAnimation.AniEaseOutFluent())
+ }, "MyButton Scale " + Uuid);
+ }
+
+ private void Button_MouseLeave()
+ {
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(PanFore, BackgroundProperty, "ColorBrushHalfWhite", AnimationColorOut),
+ "MyButton Background " + Uuid);
+ if (!IsMouseDown)
+ return;
+ IsMouseDown = false;
+ ModAnimation.AniStart(
+ ModAnimation.AaScaleTransform(PanFore, 1d - ((ScaleTransform)PanFore.RenderTransform).ScaleX, 800,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), "MyButton Scale " + Uuid);
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyCard.cs b/Plain Craft Launcher 2/Controls/MyCard.cs
new file mode 100644
index 000000000..b619b2c8d
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyCard.cs
@@ -0,0 +1,508 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Shapes;
+using PCL.Core.UI.Controls;
+
+namespace PCL;
+
+public class MyCard : AnimatedBackgroundGrid
+{
+ // 动画
+ private const double DropShadowIdleOpacity = 0.07d;
+ private const double DropShadowHoverOpacity = 0.4d;
+
+ public static readonly DependencyProperty TitleProperty =
+ DependencyProperty.Register("Title", typeof(string), typeof(MyCard), new PropertyMetadata(""));
+
+ private readonly BlurBorder MainBorder;
+
+ // 控件
+ private readonly Grid MainGrid;
+ private Path _MainSwap;
+ private TextBlock _MainTextBlock;
+ private bool IsLoad;
+
+ // UI 建立
+ public MyCard() : base(BlurBorder.BackgroundProperty)
+ {
+ MainChrome = new MyDropShadow
+ {
+ Margin = new Thickness(-3, -3, -3, -3 - ModBase.GetWPFSize(1d)), ShadowRadius = 3d,
+ Opacity = DropShadowIdleOpacity, CornerRadius = new CornerRadius(5d)
+ };
+ MainChrome.SetResourceReference(MyDropShadow.ColorProperty, "ColorObject1");
+ Children.Insert(0, MainChrome);
+ MainBorder = new BlurBorder { CornerRadius = new CornerRadius(5d), IsHitTestVisible = false };
+ Children.Insert(1, MainBorder);
+ MainGrid = new Grid();
+ Children.Add(MainGrid);
+ // 设置背景色
+ SetResourceReference(BackgroundBrushProperty, "ColorBrushTransparentBackground");
+ Loaded += (_, _) => Init();
+ MouseEnter += MyCard_MouseEnter;
+ MouseLeave += MyCard_MouseLeave;
+ SizeChanged += MySizeChanged;
+ MouseLeftButtonDown += MyCard_MouseLeftButtonDown;
+ MouseLeftButtonUp += MyCard_MouseLeftButtonUp;
+ MouseLeave += MyCard_MouseLeave_Swap;
+ }
+
+ public MyDropShadow MainChrome { get; }
+
+ public UIElement BorderChild
+ {
+ get => MainBorder.Child;
+ set => MainBorder.Child = value;
+ }
+
+ public TextBlock MainTextBlock
+ {
+ get
+ {
+ Init(); // 当父级触发 Loaded 时,本卡片可能尚未触发 Loaded(该事件从父级向子级调用),因此这会是 null。手动触发以确保控件已加载。
+ return _MainTextBlock;
+ }
+ set => _MainTextBlock = value;
+ }
+
+ public Path MainSwap
+ {
+ get
+ {
+ Init();
+ return _MainSwap;
+ }
+ set => _MainSwap = value;
+ }
+
+ // 属性
+ public InlineCollection Inlines => MainTextBlock.Inlines;
+
+ public CornerRadius CornerRadius
+ {
+ get => MainChrome.CornerRadius;
+ set
+ {
+ MainChrome.CornerRadius = value;
+ MainBorder.CornerRadius = value;
+ }
+ }
+
+ public string Title
+ {
+ get => (string)GetValue(TitleProperty);
+ set
+ {
+ SetValue(TitleProperty, value);
+ if (_MainTextBlock is not null)
+ MainTextBlock.Text = value;
+ }
+ }
+
+ protected override SolidColorBrush AnimatableBrush
+ {
+ get => (SolidColorBrush)MainBorder.Background;
+ set => MainBorder.Background = value;
+ }
+
+ protected override FrameworkElement AnimatableElement => MainBorder;
+ public bool HasMouseAnimation { get; set; } = true;
+
+ private void Init()
+ {
+ if (IsLoad)
+ return;
+ IsLoad = true;
+ // AddHandler ThemeChanged, AddressOf _BackgroundBrushChanged '已在依赖属性中实现
+ // 初次加载限定
+ if (MainTextBlock is null)
+ {
+ MainTextBlock = new TextBlock
+ {
+ HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top,
+ Margin = new Thickness(15d, 12d, 0d, 0d), FontWeight = FontWeights.Bold, FontSize = 13d,
+ IsHitTestVisible = false
+ };
+ MainTextBlock.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1");
+ MainTextBlock.SetBinding(TextBlock.TextProperty,
+ new Binding("Title") { Source = this, Mode = BindingMode.OneWay });
+ MainGrid.Children.Add(MainTextBlock);
+ }
+
+ if (CanSwap || SwapControl is not null)
+ {
+ if (SwapControl is null && Children.Count > 3)
+ SwapControl = Children[3];
+ MainSwap = new Path
+ {
+ HorizontalAlignment = HorizontalAlignment.Right, Stretch = Stretch.Uniform, Height = 6d, Width = 10d,
+ VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0d, 17d, 16d, 0d),
+ Data =
+ (Geometry)new GeometryConverter().ConvertFromString("M2,4 l-2,2 10,10 10,-10 -2,-2 -8,8 -8,-8 z"),
+ RenderTransform = new RotateTransform(180d), RenderTransformOrigin = new Point(0.5d, 0.5d)
+ };
+ MainSwap.SetResourceReference(Shape.FillProperty, "ColorBrush1");
+ MainGrid.Children.Add(MainSwap);
+ }
+
+ // 改变默认的折叠
+ if (IsSwapped && SwapControl is not null)
+ {
+ MainSwap.RenderTransform = new RotateTransform(SwapLogoRight ? 270 : 0);
+ SwapControl.Visibility = Visibility.Collapsed;
+ // 取消由于高度变化被迫触发的高度动画
+ var RawUseAnimation = UseAnimation;
+ UseAnimation = false;
+ Height = SwapedHeight;
+ ModAnimation.AniStop("MyCard Height " + Uuid);
+ IsHeightAnimating = false;
+ ModBase.RunInUi(() => UseAnimation = RawUseAnimation, true);
+ }
+ }
+
+ // 已在依赖属性中实现
+ // Private Sub Dispose() Handles Me.Unloaded
+ // If Parent Is Nothing Then
+ // RemoveHandler ThemeChanged, AddressOf _BackgroundBrushChanged
+ // End If
+ // End Sub
+ public void StackInstall()
+ {
+ var argstack = (StackPanel)SwapControl;
+ StackInstall(ref argstack, InstallMethod);
+ SwapControl = argstack;
+ TriggerForceResize();
+ }
+
+ public static void StackInstall(ref StackPanel stack, Action installMethod)
+ {
+ if (stack.Tag is null)
+ return;
+ try
+ {
+ installMethod(stack);
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "[MyCard] InstallMethod 调用失败");
+ }
+
+ stack.Children.Add(new FrameworkElement { Height = 18d }); // 下边距,同时适应折叠
+ stack.Tag = null;
+ }
+
+ private void MyCard_MouseEnter(object sender, MouseEventArgs e)
+ {
+ if (!HasMouseAnimation)
+ return;
+ var AniList = new List();
+ if (!(MainTextBlock == null))
+ AniList.Add(ModAnimation.AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush2", 90));
+ if (!(MainSwap == null))
+ AniList.Add(ModAnimation.AaColor(MainSwap, Shape.FillProperty, "ColorBrush2", 90));
+ AniList.AddRange(new[]
+ {
+ ModAnimation.AaColor(MainChrome, MyDropShadow.ColorProperty, "ColorObject4", 90),
+ ModAnimation.AaOpacity(MainChrome, DropShadowHoverOpacity - MainChrome.Opacity, 90)
+ });
+ if (!IsAnimating)
+ ModAnimation.AniStart(AniList, "MyCard Mouse " + Uuid);
+ }
+
+ private void MyCard_MouseLeave(object sender, MouseEventArgs e)
+ {
+ if (!HasMouseAnimation)
+ return;
+ var AniList = new List();
+ if (!(MainTextBlock == null))
+ AniList.Add(ModAnimation.AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush1", 90));
+ if (!(MainSwap == null))
+ AniList.Add(ModAnimation.AaColor(MainSwap, Shape.FillProperty, "ColorBrush1", 90));
+ AniList.AddRange(new[]
+ {
+ ModAnimation.AaColor(MainChrome, MyDropShadow.ColorProperty, "ColorObject1", 90),
+ ModAnimation.AaOpacity(MainChrome, DropShadowIdleOpacity - MainChrome.Opacity, 90)
+ });
+ if (!IsAnimating)
+ ModAnimation.AniStart(AniList, "MyCard Mouse " + Uuid);
+ }
+
+ #region 高度改变动画
+
+ ///
+ /// 是否启用高度改变动画。
+ ///
+ public bool UseAnimation { get; set; } = true;
+
+ private bool IsHeightAnimating;
+ private double ActualUsedHeight; // 回滚实际高度(例如 NaN)
+
+ private void MySizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (!UseAnimation)
+ return;
+ var DeltaHeight = (IsSwapped ? SwapedHeight : e.NewSize.Height) - e.PreviousSize.Height;
+ // 卡片的进入时动画已被页面通用切换动画替代
+ if (e.PreviousSize.Height == 0d || IsHeightAnimating || Math.Abs(DeltaHeight) < 1d || ActualHeight == 0d)
+ return;
+ StartHeightAnimation(DeltaHeight, e.PreviousSize.Height, false);
+ }
+
+ ///
+ /// 启动卡片高度变化的动画效果
+ /// 根据变化距离的大小采用不同的动画策略:短距离使用简单缓动,长距离使用分段动画
+ ///
+ /// 高度变化量
+ /// 之前的高度
+ /// 是否为加载动画
+ private void StartHeightAnimation(double Delta, double PreviousHeight, bool IsLoadAnimation)
+ {
+ if (IsHeightAnimating || ModMain.FrmMain is null)
+ return; // 避免 XAML 设计器出错
+
+ var AnimList = new List();
+ var AbsDelta = Math.Abs(Delta);
+
+ if (AbsDelta <= 800d)
+ {
+ // 短距离,直接使用 150ms 的缓动动画
+ AnimList.Add(ModAnimation.AaHeight(this, Delta, 150,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)));
+ }
+ else
+ {
+ var EaseLength = default(int);
+ int EaseTime;
+ int InitSpeed; // 到达缓动区前的初速度
+ if (Delta < 0d && AbsDelta - EaseLength > 5000d * 0.1d)
+ {
+ // 收回距离过长 (>0.1s),强制以 100ms 完成匀速段,然后让减速段更长
+ EaseLength = 200;
+ EaseTime = 150;
+ InitSpeed = (int)Math.Round((AbsDelta - EaseLength) / 0.1d);
+ }
+ else if (Delta > 0d && AbsDelta - EaseLength > 5000d * 0.6d)
+ {
+ // 展开距离过长 (>0.6s),以 5000 速度展示 300ms 匀速段,剩下的距离全部归入减速段
+ InitSpeed = 5000;
+ EaseLength = (int)Math.Round(AbsDelta - InitSpeed * 0.3d);
+ EaseTime = 400;
+ }
+ else
+ {
+ // 中程,匀速地快速展开(或收回)
+ EaseLength = 150;
+ EaseTime = 200;
+ InitSpeed = 4000;
+ }
+
+ // 匀速段
+ AnimList.Add(ModAnimation.AaHeight(this, (AbsDelta - EaseLength) * Math.Sign(Delta),
+ (int)Math.Round((AbsDelta - EaseLength) / InitSpeed * 1000d)));
+ // 减速段
+ AnimList.Add(ModAnimation.AaHeight(this, EaseLength * Math.Sign(Delta), EaseTime,
+ Ease: new ModAnimation.AniEaseOutFluentWithInitial(InitSpeed, EaseTime / 1000d, EaseLength),
+ After: true));
+ }
+
+ AnimList.Add(ModAnimation.AaCode(() =>
+ {
+ IsHeightAnimating = false;
+ Height = ActualUsedHeight;
+ if (IsSwapped && SwapControl is not null)
+ SwapControl.Visibility = Visibility.Collapsed;
+ }, After: true));
+ ModAnimation.AniStart(AnimList, "MyCard Height " + Uuid);
+ IsHeightAnimating = true;
+ ActualUsedHeight = IsSwapped ? SwapedHeight : Height;
+ Height = PreviousHeight;
+ }
+
+ ///
+ /// 通知 MyCard,控件内容已改变,需要中断动画并瞬间更新高度。
+ ///
+ public void TriggerForceResize()
+ {
+ Height = IsSwapped ? SwapedHeight : double.NaN;
+ ModAnimation.AniStop("MyCard Height " + Uuid);
+ IsHeightAnimating = false;
+ }
+
+ #endregion
+
+ #region 折叠
+
+ // 若设置了 CanSwap,或 SwapControl 不为空,则判定为会进行折叠
+ // 这是因为不能直接在 XAML 中设置 SwapControl
+ public UIElement SwapControl;
+ public bool CanSwap { get; set; } = false;
+
+ ///
+ /// 数据转为列表项的转换方法
+ ///
+ ///
+ public Action InstallMethod { get; set; }
+
+ ///
+ /// 是否已被折叠。
+ ///
+ public bool IsSwapped
+ {
+ get => _IsSwapped;
+ set
+ {
+ if (_IsSwapped == value)
+ return;
+ _IsSwapped = value;
+ if (SwapControl is null)
+ return;
+
+ // 当卡片展开时,如果SwapControl是StackPanel类型,则执行安装方法
+ // 这通常用于动态添加内容到折叠卡片中
+ if (!IsSwapped && SwapControl is StackPanel)
+ {
+ var argstack = (StackPanel)SwapControl;
+ StackInstall(ref argstack, InstallMethod);
+ SwapControl = argstack;
+ }
+
+ // 若尚未加载,会在 Loaded 事件中触发无动画的折叠,不需要在这里进行
+ if (!IsLoaded)
+ return;
+
+ // 更新控件的可见性和高度
+ SwapControl.Visibility = Visibility.Visible;
+ TriggerForceResize();
+
+ // 根据折叠状态旋转箭头图标
+ // 折叠时箭头指向右侧或向上(根据SwapLogoRight设置),展开时指向下方
+ ModAnimation.AniStart(
+ ModAnimation.AaRotateTransform(MainSwap,
+ (_IsSwapped ? SwapLogoRight ? 270 : 0 : 180) - ((RotateTransform)MainSwap.RenderTransform).Angle,
+ 250, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)),
+ "MyCard Swap " + Uuid, true);
+ }
+ }
+
+ private bool _IsSwapped;
+
+ ///
+ /// 是否已被折叠。(已过时,请使用 IsSwapped)
+ ///
+ [Obsolete("请使用 IsSwapped 属性,IsSwaped 存在拼写错误")]
+ public bool IsSwaped
+ {
+ get => IsSwapped;
+ set => IsSwapped = value;
+ }
+
+ public bool SwapLogoRight { get; set; } = false;
+ private bool IsSwapMouseDown = false; //用于触发卡片展开/折叠的 MouseDown
+ private bool IsCustomMouseDown = false; //用于触发自定义事件的 MouseDown
+ public event PreviewSwapEventHandler? PreviewSwap;
+
+ public delegate void PreviewSwapEventHandler(object sender, ModBase.RouteEventArgs e);
+
+ public event SwapEventHandler? Swap;
+
+ public delegate void SwapEventHandler(object sender, ModBase.RouteEventArgs e);
+
+ public const int SwapedHeight = 40;
+
+ private void MyCard_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ double Pos = Mouse.GetPosition(this).Y;
+ if (!IsSwapped && (Pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || (Pos == 0 && !IsMouseDirectlyOver)))
+ return;
+ IsCustomMouseDown = true;
+ if (!IsSwapped && (SwapControl == null || Pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || (Pos == 0 && !IsMouseDirectlyOver)))
+ return;
+ IsSwapMouseDown = true;
+ }
+
+ private void MyCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ if (!IsCustomMouseDown) return;
+ IsCustomMouseDown = false;
+ ModMain.RaiseCustomEvent(this);
+
+ if (!IsSwapMouseDown) return;
+ IsSwapMouseDown = false;
+
+ double Pos = Mouse.GetPosition(this).Y;
+ if (!IsSwapped && (SwapControl == null || Pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || (Pos == 0 && !IsMouseDirectlyOver)))
+ return; // 检测点击位置;或已经不在可视树上的误判
+
+ var e2 = new ModBase.RouteEventArgs(true);
+ PreviewSwap?.Invoke(this, e2);
+ if (e2.Handled)
+ {
+ IsSwapMouseDown = false;
+ return;
+ }
+
+ IsSwapped = !IsSwapped;
+ ModBase.Log("[Control] " + (IsSwapped ? "折叠卡片" : "展开卡片") + (Title == null ? "" : ":" + Title));
+ Swap?.Invoke(this, e2);
+ }
+
+ private void MyCard_MouseLeave_Swap(object sender, MouseEventArgs e)
+ {
+ IsSwapMouseDown = false;
+ }
+
+ #endregion
+}
+
+public static partial class ModAnimation
+{
+ public static void AniDispose(MyCard Control, bool RemoveFromChildren, ParameterizedThreadStart CallBack = null)
+ {
+ if (Control.IsHitTestVisible)
+ {
+ Control.IsHitTestVisible = false;
+ AniStart(new[]
+ {
+ AaScaleTransform(Control, -0.08d, 200, Ease: new AniEaseInFluent()),
+ AaOpacity(Control, -1, 200, Ease: new AniEaseOutFluent()),
+ AaHeight(Control, -Control.ActualHeight, 150, 100, new AniEaseOutFluent()),
+ AaCode(() =>
+ {
+ if (RemoveFromChildren)
+ {
+ if (Control.Parent is null)
+ return;
+ ((Panel)Control.Parent).Children.Remove(Control);
+ }
+ else
+ {
+ Control.Visibility = Visibility.Collapsed;
+ }
+
+ if (CallBack is not null)
+ CallBack(Control);
+ }, After: true)
+ }, "MyCard Dispose " + Control.Uuid);
+ }
+ else
+ {
+ if (RemoveFromChildren)
+ {
+ if (Control.Parent is null)
+ return;
+ ((Panel)Control.Parent).Children.Remove(Control);
+ }
+ else
+ {
+ Control.Visibility = Visibility.Collapsed;
+ }
+
+ if (CallBack is not null)
+ CallBack(Control);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyCheckBox.xaml b/Plain Craft Launcher 2/Controls/MyCheckBox.xaml
index 1813032a0..150c1bc6e 100644
--- a/Plain Craft Launcher 2/Controls/MyCheckBox.xaml
+++ b/Plain Craft Launcher 2/Controls/MyCheckBox.xaml
@@ -1,19 +1,30 @@
-
-
-
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="PCL.MyCheckBox"
+ FocusVisualStyle="{x:Null}"
+ MinWidth="20" x:Name="PanBack" UseLayoutRounding="False" SnapsToDevicePixels="False" MinHeight="20"
+ Background="{StaticResource ColorBrushSemiTransparent}" Focusable="True" d:DesignWidth="126.4"
+ d:DesignHeight="44.8">
+
+
+
-
+
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyCheckBox.xaml.cs b/Plain Craft Launcher 2/Controls/MyCheckBox.xaml.cs
new file mode 100644
index 000000000..0d532d3ab
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyCheckBox.xaml.cs
@@ -0,0 +1,503 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Markup;
+using System.Windows.Media;
+
+namespace PCL;
+
+[ContentProperty("Inlines")]
+public partial class MyCheckBox
+{
+ public delegate void ChangeEventHandler(object sender, bool user);
+
+ public delegate void PreviewChangeEventHandler(object sender, ModBase.RouteEventArgs e);
+
+ private const int AnimationTimeOfCheck = 150; // 勾选状态变更动画长度
+
+ // 指向动画
+
+ private const int AnimationTimeOfMouseIn = 100;
+
+ private const int AnimationTimeOfMouseOut = 200;
+
+ // 在使用 XAML 设置 Checked 属性时,不会触发 Checked_Set 方法,所以需要在这里手动触发 UI 改变
+ public static readonly DependencyProperty CheckedProperty = DependencyProperty.Register("Checked", typeof(bool?),
+ typeof(MyCheckBox), new PropertyMetadata(false, (d, e) =>
+ {
+ var obj = (MyCheckBox)d;
+ if (!obj.IsLoaded) obj.SyncUI();
+ }));
+
+ ///
+ /// 是否为三态复选框。
+ ///
+ public static readonly DependencyProperty IsThreeStateProperty =
+ DependencyProperty.Register("IsThreeState", typeof(bool), typeof(MyCheckBox), new PropertyMetadata(false));
+
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
+ typeof(MyCheckBox), new PropertyMetadata((sender, e) =>
+ {
+ if (sender is not null) ((MyCheckBox)sender).LabText.Text = (string)e.NewValue;
+ }));
+
+ private bool? _previousState = false; // 上一次的勾选状态
+ private bool AllowMouseDown = true;
+
+ // 点击事件
+
+ private bool MouseDowned;
+
+ // 基础
+
+ public int Uuid = ModBase.GetUuid();
+
+ public MyCheckBox()
+ {
+ InitializeComponent();
+
+ MouseLeftButtonUp += (_, _) => Checkbox_MouseUp();
+ MouseLeftButtonDown += (_, _) => Checkbox_MouseDown();
+ MouseLeave += (_, _) => Checkbox_MouseLeave();
+ IsEnabledChanged += (_, _) => Checkbox_IsEnabledChanged();
+ MouseEnter += (_, _) => Checkbox_MouseEnterAnimation();
+ MouseLeave += (_, _) => Checkbox_MouseLeaveAnimation();
+ }
+
+ // 自定义属性
+ public bool? Checked
+ {
+ get => (bool?)GetValue(CheckedProperty);
+ set => SetChecked(value, false);
+ }
+
+ public bool IsThreeState
+ {
+ get => (bool)GetValue(IsThreeStateProperty);
+ set => SetValue(IsThreeStateProperty, value);
+ } // 是否为三态复选框
+
+ public InlineCollection Inlines => LabText.Inlines;
+
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ } // 内容
+
+ ///
+ /// 复选框勾选状态改变。
+ ///
+ /// 是否为用户手动改变的勾选状态。
+ public event ChangeEventHandler? Change;
+
+ public event PreviewChangeEventHandler? PreviewChange;
+
+ ///
+ /// 手动设置 Checked 属性。
+ ///
+ /// 新的 Checked 属性。
+ /// 是否由用户引发。
+ public void SetChecked(bool? value, bool user)
+ {
+ try
+ {
+ if (Checked is var arg1 && value.HasValue && arg1.HasValue && value.Value == arg1.Value)
+ return;
+
+ // Preview 事件
+ if ((!value.HasValue || value.Value) && user && value.HasValue)
+ {
+ var e = new ModBase.RouteEventArgs(user);
+ PreviewChange?.Invoke(this, e);
+ if (e.Handled)
+ {
+ MouseDowned = true;
+ Checkbox_MouseLeave();
+ MouseDowned = false;
+ return;
+ }
+ }
+
+ // 判断真实勾选状态
+ var isChecked = GetFinalState(value, IsThreeState);
+
+ _previousState = Checked; // 记录上一次的勾选状态
+ SetValue(CheckedProperty, isChecked);
+ if (IsLoaded)
+ Change?.Invoke(this, user);
+
+ // 更改动画
+ SyncUI();
+ ModMain.RaiseCustomEvent(this);
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "设置 Checked 失败");
+ }
+ }
+
+ private void SyncUI()
+ {
+ if (ModAnimation.AniControlEnabled == 0 && IsLoaded) // 防止默认属性变更触发动画
+ {
+ AllowMouseDown = false;
+
+ var isChecked = GetFinalState(Checked, IsThreeState);
+
+ switch (isChecked, _previousState)
+ {
+ case (true, false):
+ AniBackgroundScale();
+ AniCheckShow();
+ AniColorChecked();
+ AniAllowMouseDown();
+ break;
+
+ case (true, null):
+ AniBackgroundScale();
+ AniIndeterminateHide();
+ AniCheckShow();
+ AniColorChecked();
+ AniAllowMouseDown();
+ break;
+
+ case (false, true):
+ AniBackgroundScale();
+ AniCheckHide();
+ AniColorUnchecked();
+ AniAllowMouseDown();
+ break;
+
+ case (false, null):
+ AniBackgroundScale();
+ AniIndeterminateHide();
+ AniCheckHide();
+ AniColorUnchecked();
+ AniAllowMouseDown();
+ break;
+
+ case (null, true):
+ AniBackgroundScale();
+ AniCheckHide();
+ AniIndeterminateShow();
+ AniColorUnchecked();
+ AniAllowMouseDown();
+ break;
+
+ case (null, false):
+ AniBackgroundScale();
+ AniIndeterminateShow();
+ AniColorUnchecked();
+ AniAllowMouseDown();
+ break;
+ }
+ }
+
+ // If Checked Then
+ // '由无变有
+ // AniStart({
+ // AaScale(ShapeBorder, 12 - ShapeBorder.Width, AnimationTimeOfCheck, , New AniEaseOutFluent, , True),
+ // AaScaleTransform(ShapeCheck, 1 - CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleX, AnimationTimeOfCheck * 2, AnimationTimeOfCheck * 0.7, New AniEaseOutBack(AniEasePower.Weak)),
+ // AaScale(ShapeBorder, 6, AnimationTimeOfCheck * 2, AnimationTimeOfCheck * 0.7, New AniEaseOutBack, , True)
+ // }, "MyCheckBox Scale " & Uuid)
+ // AniStart({
+ // AaColor(ShapeBorder, Border.BorderBrushProperty, If(IsEnabled, If(IsMouseOver, "ColorBrush3", "ColorBrush2"), "ColorBrushGray4"), AnimationTimeOfCheck)
+ // }, "MyCheckBox BorderColor " & Uuid)
+ // AniStart({
+ // AaCode(Sub() AllowMouseDown = True, AnimationTimeOfCheck * 2)
+ // }, "MyCheckBox AllowMouseDown " & Uuid)
+ // Else
+ // '由有变无
+ // AniStart({
+ // AaScale(ShapeBorder, 12 - ShapeBorder.Width, AnimationTimeOfCheck, , New AniEaseOutFluent, , True),
+ // AaScaleTransform(ShapeCheck, -CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleX, AnimationTimeOfCheck * 0.9, , New AniEaseInFluent(AniEasePower.Weak)),
+ // AaScale(ShapeBorder, 6, AnimationTimeOfCheck * 2, AnimationTimeOfCheck * 0.7, New AniEaseOutBack, , True)
+ // }, "MyCheckBox Scale " & Uuid)
+ // AniStart({
+ // AaColor(ShapeBorder, Border.BorderBrushProperty, If(IsEnabled, If(IsMouseOver, "ColorBrush3", "ColorBrush1"), "ColorBrushGray4"), AnimationTimeOfCheck)
+ // }, "MyCheckBox BorderColor " & Uuid)
+ // AniStart({
+ // AaCode(Sub() AllowMouseDown = True, AnimationTimeOfCheck * 2)
+ // }, "MyCheckBox AllowMouseDown " & Uuid)
+ // End If
+ else
+ {
+ // 不使用动画
+ ModAnimation.AniStop("MyCheckBox Background Scale " + Uuid);
+ ModAnimation.AniStop("MyCheckBox Check Scale Show" + Uuid);
+ ModAnimation.AniStop("MyCheckBox Check Scale Hide" + Uuid);
+ ModAnimation.AniStop("MyCheckBox Indeterminate Scale Show" + Uuid);
+ ModAnimation.AniStop("MyCheckBox Indeterminate Scale Hide" + Uuid);
+ ModAnimation.AniStop("MyCheckBox BorderColor " + Uuid);
+ ModAnimation.AniStop("MyCheckBox AllowMouseDown " + Uuid);
+ if (Checked == true)
+ {
+ ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX = 1d;
+ ((ScaleTransform)ShapeCheck.RenderTransform).ScaleY = 1d;
+ ShapeBorder.SetResourceReference(Border.BorderBrushProperty,
+ IsEnabled ? "ColorBrush2" : "ColorBrushGray4");
+ }
+ else
+ {
+ ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX = 0d;
+ ((ScaleTransform)ShapeCheck.RenderTransform).ScaleY = 0d;
+ ShapeBorder.SetResourceReference(Border.BorderBrushProperty,
+ IsEnabled ? "ColorBrush1" : "ColorBrushGray4");
+ }
+ }
+ }
+
+ private void Checkbox_MouseUp()
+ {
+ if (!MouseDowned)
+ return;
+ ModBase.Log("[Control] 按下复选框(" + !Checked + "):" + Text);
+ MouseDowned = false;
+ if (IsThreeState)
+ {
+ switch (Checked)
+ {
+ case true:
+ SetChecked(null, true);
+ break;
+ case false:
+ SetChecked(true, true);
+ break;
+ case null:
+ SetChecked(false, true);
+ break;
+ }
+
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100),
+ "MyCheckBox Background " + Uuid);
+ return;
+ }
+
+ SetChecked(!Checked, true);
+ ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100),
+ "MyCheckBox Background " + Uuid);
+ }
+
+ private void Checkbox_MouseDown()
+ {
+ if (!AllowMouseDown)
+ return;
+ MouseDowned = true;
+ Focus();
+ ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushBg1", 100),
+ "MyCheckBox Background " + Uuid);
+ if (Checked == true)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScale(ShapeBorder, 16.5d - ShapeBorder.Width, 1000,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), Absolute: true),
+ ModAnimation.AaScaleTransform(ShapeCheck,
+ 0.9d - ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX, 1000,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong))
+ }, "MyCheckBox Scale " + Uuid);
+ else
+ ModAnimation.AniStart(
+ ModAnimation.AaScale(ShapeBorder, 16.5d - ShapeBorder.Width, 1000,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), Absolute: true),
+ "MyCheckBox Scale " + Uuid);
+ }
+
+ private void Checkbox_MouseLeave()
+ {
+ if (!MouseDowned)
+ return;
+ MouseDowned = false;
+ ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100),
+ "MyCheckBox Background " + Uuid);
+ if (Checked == true)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScale(ShapeBorder, 18d - ShapeBorder.Width,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), Absolute: true),
+ ModAnimation.AaScaleTransform(ShapeCheck, 1d - ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX,
+ 500, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong))
+ }, "MyCheckBox Scale " + Uuid);
+ else
+ ModAnimation.AniStart(
+ ModAnimation.AaScale(ShapeBorder, 18d - ShapeBorder.Width,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), Absolute: true),
+ "MyCheckBox Scale " + Uuid);
+ }
+
+ private void Checkbox_IsEnabledChanged()
+ {
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
+ {
+ // 有动画
+ if (IsEnabled)
+ {
+ // 可用
+ Checkbox_MouseLeaveAnimation();
+ }
+ else
+ {
+ // 不可用
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty,
+ ModSecret.ColorGray4 - ShapeBorder.BorderBrush, AnimationTimeOfMouseOut)
+ }, "MyCheckBox BorderColor " + Uuid);
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty,
+ ModSecret.ColorGray4 - LabText.Foreground, AnimationTimeOfMouseOut)
+ }, "MyCheckBox TextColor " + Uuid);
+ }
+ }
+ else
+ {
+ // 无动画
+ ModAnimation.AniStop("MyCheckBox TextColor " + Uuid);
+ ModAnimation.AniStop("MyCheckBox BorderColor " + Uuid);
+ LabText.SetResourceReference(TextBlock.ForegroundProperty, IsEnabled ? "ColorBrush1" : "ColorBrushGray4");
+ ShapeBorder.SetResourceReference(Border.BorderBrushProperty,
+ IsEnabled ? Checked == true ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4");
+ }
+ }
+
+ private void Checkbox_MouseEnterAnimation()
+ {
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfMouseIn)
+ }, "MyCheckBox TextColor " + Uuid);
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty, "ColorBrush3", AnimationTimeOfMouseIn)
+ }, "MyCheckBox BorderColor " + Uuid);
+ }
+
+ private void Checkbox_MouseLeaveAnimation()
+ {
+ if (!IsEnabled)
+ return; // MouseLeave 比 IsEnabledChanged 后执行,所以如果自定义事件修改了 IsEnabled,将导致显示错误
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty,
+ IsEnabled ? "ColorBrush1" : "ColorBrushGray4", AnimationTimeOfMouseOut)
+ }, "MyCheckBox TextColor " + Uuid);
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty,
+ IsEnabled ? Checked == true ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4",
+ AnimationTimeOfMouseOut)
+ }, "MyCheckBox BorderColor " + Uuid);
+ }
+
+ // 动画
+ private void AniBackgroundScale()
+ {
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScale(ShapeBorder, 12d - ShapeBorder.Width, AnimationTimeOfCheck,
+ Ease: new ModAnimation.AniEaseOutFluent(), Absolute: true),
+ ModAnimation.AaScale(ShapeBorder, 6d, AnimationTimeOfCheck * 2,
+ (int)Math.Round(AnimationTimeOfCheck * 0.7d), new ModAnimation.AniEaseOutBack(), Absolute: true)
+ }, "MyCheckBox Background Scale " + Uuid);
+ }
+
+ private void AniCheckShow()
+ {
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(ShapeCheck, 1d - ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX,
+ AnimationTimeOfCheck * 2, (int)Math.Round(AnimationTimeOfCheck * 0.7d),
+ new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak))
+ }, "MyCheckBox Check Scale Show" + Uuid);
+ }
+
+ private void AniCheckHide()
+ {
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(ShapeCheck, -((ScaleTransform)ShapeCheck.RenderTransform).ScaleX,
+ (int)Math.Round(AnimationTimeOfCheck * 0.9d),
+ Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak))
+ }, "MyCheckBox Check Scale Hide" + Uuid);
+ }
+
+ private void AniIndeterminateShow()
+ {
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(ShapeIndeterminate,
+ 1d - ((ScaleTransform)ShapeIndeterminate.RenderTransform).ScaleX, AnimationTimeOfCheck * 2,
+ (int)Math.Round(AnimationTimeOfCheck * 0.7d),
+ new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak))
+ }, "MyCheckBox Indeterminate Scale Show" + Uuid);
+ }
+
+ private void AniIndeterminateHide()
+ {
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(ShapeIndeterminate,
+ -((ScaleTransform)ShapeIndeterminate.RenderTransform).ScaleX,
+ (int)Math.Round(AnimationTimeOfCheck * 0.9d),
+ Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak))
+ }, "MyCheckBox Indeterminate Scale Hide" + Uuid);
+ }
+
+ private void AniAllowMouseDown()
+ {
+ ModAnimation.AniStart(new[] { ModAnimation.AaCode(() => AllowMouseDown = true, AnimationTimeOfCheck * 2) },
+ "MyCheckBox AllowMouseDown " + Uuid);
+ }
+
+ private void AniColorChecked()
+ {
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty,
+ IsEnabled ? IsMouseOver ? "ColorBrush3" : "ColorBrush2" : "ColorBrushGray4", AnimationTimeOfCheck)
+ }, "MyCheckBox BorderColor " + Uuid);
+ }
+
+ private void AniColorUnchecked()
+ {
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty,
+ IsEnabled ? IsMouseOver ? "ColorBrush3" : "ColorBrush1" : "ColorBrushGray4", AnimationTimeOfCheck)
+ }, "MyCheckBox BorderColor " + Uuid);
+ }
+
+ private bool? GetFinalState(bool? value, bool isThreeState)
+ {
+ if (isThreeState)
+ {
+ // 三态复选框
+ if (value.HasValue && value.Value) return true;
+
+ if (value.HasValue && !value.Value) return false;
+
+ return default;
+ // 空值表示未选中状态
+ }
+
+ // 二态复选框
+ return value == true ? true : false;
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyComboBox.cs b/Plain Craft Launcher 2/Controls/MyComboBox.cs
new file mode 100644
index 000000000..fcaa40d43
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyComboBox.cs
@@ -0,0 +1,235 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+
+namespace PCL;
+
+public class MyComboBox : ComboBox
+{
+ public delegate void TextChangedEventHandler(object sender, TextChangedEventArgs e);
+
+ public static readonly DependencyProperty HintTextProperty = DependencyProperty.Register("HintText", typeof(string),
+ typeof(MyComboBox), new PropertyMetadata("", (d, e) =>
+ {
+ var c = (MyComboBox)d;
+ if (c.TextBox is not null)
+ c.TextBox.HintText = (string)e.NewValue;
+ }));
+
+ private string _Text;
+
+ // 鼠标按下接口
+ private bool IsMouseDown;
+
+ // 修复 WPF Bug:下拉框文本修改后,依然误认为还选择着此前的选项,导致再次点击该选项时内容不变
+ private bool IsTextChanging;
+ private double RealWidth; // 由于下拉框 Popup 宽度与 Width 一致,故不能为 NaN(Auto)
+ private MyTextBox TextBox;
+
+ // 基础
+ public int Uuid = ModBase.GetUuid();
+
+ public MyComboBox()
+ {
+ _Text = SelectedItem?.ToString() ?? "";
+ PreviewMouseLeftButtonDown += MyComboBox_PreviewMouseLeftButtonDown;
+ PreviewMouseLeftButtonUp += MyComboBox_PreviewMouseLeftButtonUp;
+ MouseLeave += MyComboBox_PreviewMouseLeftButtonUp;
+ IsEnabledChanged += (_, _) => RefreshColor();
+ MouseEnter += (_, _) => RefreshColor();
+ MouseLeave += (_, _) => RefreshColor();
+ PreviewMouseLeftButtonDown += (_, _) => RefreshColor();
+ PreviewMouseLeftButtonUp += (_, _) => RefreshColor();
+ GotKeyboardFocus += (_, _) => RefreshColor();
+ DropDownOpened += MyComboBox_DropDownOpened;
+ DropDownClosed += MyComboBox_DropDownClosed;
+ TextChanged += MyComboBox_TextChanged;
+ }
+
+ public string HintText
+ {
+ get => (string)GetValue(HintTextProperty);
+ set => SetValue(HintTextProperty, value);
+ }
+
+ public new string Text
+ {
+ get
+ {
+ if (IsEditable)
+ {
+ if (TextBox is null) return _Text ?? "";
+
+ return TextBox.Text ?? "";
+ }
+
+ return (SelectedItem ?? "").ToString();
+ }
+ set
+ {
+ if (IsEditable)
+ {
+ if (TextBox == null)
+ _Text = value;
+ else
+ TextBox.Text = value;
+ }
+ else
+ {
+ throw new NotSupportedException("该 ComboBox 不支持修改文本。");
+ }
+ }
+ }
+
+ public bool DropDownWidthSync { get; set; } = true;
+
+ public ContentPresenter ContentPresenter => (ContentPresenter)Template.FindName("PART_Content", this);
+ public event TextChangedEventHandler? TextChanged;
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+ if (!IsEditable)
+ return;
+ try
+ {
+ TextBox = (MyTextBox)Template.FindName("PART_EditableTextBox", this);
+ TextBox.AddHandler(LostFocusEvent, new RoutedEventHandler((_, _) => RefreshColor()));
+ TextBox.ChangedEventList.Add((sender, e) => TextChanged?.Invoke(sender, (TextChangedEventArgs)e));
+ TextBox.Tag = Tag; // 有时需要用文本框的 Tag 来写入设置
+ if (string.IsNullOrEmpty(Text))
+ TextBox.Text = _Text;
+ else
+ TextChanged?.Invoke(this, null);
+ if (HintText.Length > 0)
+ TextBox.HintText = HintText;
+ TextBox.SetResourceReference(TextBoxBase.CaretBrushProperty, "ColorBrushGray1");
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "初始化可编辑文本框失败(" + (Name ?? "") + ")", ModBase.LogLevel.Feedback);
+ }
+ }
+
+ private void MyComboBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ IsMouseDown = true;
+ }
+
+ private void MyComboBox_PreviewMouseLeftButtonUp(object sender, EventArgs e)
+ {
+ IsMouseDown = false;
+ }
+
+ // 指向动画
+ public void RefreshColor()
+ {
+ // 判断当前颜色
+ string ForeColorName;
+ string BackColorName;
+ int Time;
+ if (IsEnabled)
+ {
+ if (IsMouseDown || IsDropDownOpen ||
+ (IsEditable && ((MyTextBox)Template.FindName("PART_EditableTextBox", this)).IsFocused))
+ {
+ ForeColorName = "ColorBrush3";
+ BackColorName = "ColorBrush7";
+ Time = 10;
+ }
+ else if (IsMouseOver)
+ {
+ ForeColorName = "ColorBrush4";
+ BackColorName = "ColorBrush7";
+ Time = 100;
+ }
+ else
+ {
+ ForeColorName = "ColorBrushBg0";
+ BackColorName = "ColorBrushHalfWhite";
+ Time = 100;
+ }
+ }
+ else
+ {
+ ForeColorName = "ColorBrushGray5";
+ BackColorName = "ColorBrushGray6";
+ Time = 200;
+ }
+
+ // 触发颜色动画
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
+ {
+ // 有动画
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(this, ForegroundProperty, ForeColorName, Time),
+ ModAnimation.AaColor(this, BackgroundProperty, BackColorName, Time)
+ }, "MyComboBox Color " + Uuid);
+ }
+ else
+ {
+ // 无动画
+ ModAnimation.AniStop("MyComboBox Color " + Uuid);
+ SetResourceReference(ForegroundProperty, ForeColorName);
+ SetResourceReference(BackgroundProperty, BackColorName);
+ }
+ }
+
+ private void MyComboBox_DropDownOpened(object sender, EventArgs e)
+ {
+ RealWidth = Width;
+ if (DropDownWidthSync)
+ Width = ActualWidth;
+ try
+ {
+ var popup = (Grid)Template.FindName("PanPopup", this);
+ popup.Opacity = ModMain.FrmMain.Opacity;
+ if (!DropDownWidthSync)
+ popup.MinWidth = ActualWidth;
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "设置下拉框属性失败", ModBase.LogLevel.Feedback);
+ }
+ }
+
+ private void MyComboBox_DropDownClosed(object sender, EventArgs e)
+ {
+ Width = RealWidth;
+ }
+
+ private void MyComboBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ if (IsTextChanging || !IsEditable)
+ return;
+ if (SelectedItem == null || Text == SelectedItem.ToString()) return;
+ {
+ var RawText = Text;
+ var RawSelectionStart = TextBox.SelectionStart;
+ IsTextChanging = true;
+ SelectedItem = null;
+ Text = RawText;
+ TextBox.SelectionStart = RawSelectionStart;
+ IsTextChanging = false;
+ }
+ }
+
+ // 用于 ItemsSource 的自定义容器
+ protected override DependencyObject GetContainerForItemOverride()
+ {
+ return new MyComboBoxItem();
+ }
+
+ private void MyComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0) ModMain.RaiseCustomEvent(this);
+ }
+
+ protected override bool IsItemItsOwnContainerOverride(object item)
+ {
+ return item is MyComboBoxItem || base.IsItemItsOwnContainerOverride(item);
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyComboBoxItem.cs b/Plain Craft Launcher 2/Controls/MyComboBoxItem.cs
new file mode 100644
index 000000000..4c5ed2aa9
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyComboBoxItem.cs
@@ -0,0 +1,100 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace PCL;
+
+public class MyComboBoxItem : ComboBoxItem
+{
+ // 指向动画
+
+ private const int AnimationTimeIn = 100;
+ private const int AnimationTimeOut = 300;
+ private string BackColorName;
+ private double FontOpacity;
+
+ // 基础
+
+ public int Uuid = ModBase.GetUuid();
+
+ public MyComboBoxItem()
+ {
+ Style = (Style)FindResource("MyComboBoxItem");
+ Unselected += (_, _) => RefreshColor();
+ MouseMove += (_, _) => RefreshColor();
+ MouseLeave += (_, _) => RefreshColor();
+ Selected += (_, _) => RefreshColor();
+ IsEnabledChanged += (_, _) => RefreshColor();
+ MouseLeftButtonUp += MyComboBoxItem_MouseLeftButtonUp;
+ }
+
+ private void RefreshColor()
+ {
+ // 判断当前颜色
+ string NewBackColorName;
+ double NewFontOpacity;
+ int Time;
+ if (IsSelected)
+ {
+ NewBackColorName = "ColorBrush6";
+ NewFontOpacity = 1d;
+ Time = AnimationTimeIn;
+ }
+ else if (IsMouseOver)
+ {
+ NewBackColorName = "ColorBrush8";
+ NewFontOpacity = 1d;
+ Time = AnimationTimeIn;
+ }
+ else if (IsEnabled)
+ {
+ NewBackColorName = "ColorBrushTransparent";
+ NewFontOpacity = 1d;
+ Time = AnimationTimeOut;
+ }
+ else
+ {
+ NewBackColorName = "ColorBrushTransparent";
+ NewFontOpacity = 0.4d;
+ Time = AnimationTimeOut;
+ }
+
+ if ((BackColorName ?? "") == (NewBackColorName ?? "") && FontOpacity == NewFontOpacity)
+ return;
+ BackColorName = NewBackColorName;
+ FontOpacity = NewFontOpacity;
+ // 触发颜色动画
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
+ {
+ // 有动画
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(this, BackgroundProperty, BackColorName, Time),
+ ModAnimation.AaOpacity(this, FontOpacity - Opacity, Time)
+ }, "ComboBoxItem Color " + Uuid);
+ }
+ else
+ {
+ // 无动画
+ ModAnimation.AniStop("ComboBoxItem Color " + Uuid);
+ SetResourceReference(BackgroundProperty, BackColorName);
+ Opacity = FontOpacity;
+ }
+ }
+
+ public override string ToString()
+ {
+ return Content.ToString();
+ }
+
+ public static implicit operator string(MyComboBoxItem Value)
+ {
+ return Value.Content.ToString();
+ }
+
+ private void MyComboBoxItem_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ ModBase.Log("[Control] 选择下拉列表项:" + ToString());
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyDropShadow.cs b/Plain Craft Launcher 2/Controls/MyDropShadow.cs
new file mode 100644
index 000000000..5a3646b2c
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyDropShadow.cs
@@ -0,0 +1,373 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace PCL;
+
+public class MyDropShadow : Decorator
+{
+ public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(Color),
+ typeof(MyDropShadow),
+ new FrameworkPropertyMetadata(Color.FromArgb(0x71, 0x0, 0x0, 0x0),
+ FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes));
+
+ public static readonly DependencyProperty ShadowRadiusProperty = DependencyProperty.Register("ShadowRadius",
+ typeof(double), typeof(MyDropShadow),
+ new FrameworkPropertyMetadata(5d, FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes));
+
+ public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius",
+ typeof(CornerRadius), typeof(MyDropShadow),
+ new FrameworkPropertyMetadata(new CornerRadius(), FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes),
+ IsCornerRadiusValid);
+
+ private static Brush[] _commonBrushes;
+ private static CornerRadius _commonCornerRadius;
+ private static readonly object _resourceAccess = new();
+ private Brush[] _brushes;
+
+ ///
+ /// 阴影颜色。
+ ///
+ public Color Color
+ {
+ get => (Color)GetValue(ColorProperty);
+ set => SetValue(ColorProperty, value);
+ }
+
+ ///
+ /// 阴影模糊半径。
+ ///
+ public double ShadowRadius
+ {
+ get => (double)GetValue(ShadowRadiusProperty);
+ set => SetValue(ShadowRadiusProperty, value);
+ }
+
+ ///
+ /// 圆角大小。
+ ///
+ public CornerRadius CornerRadius
+ {
+ get => (CornerRadius)GetValue(CornerRadiusProperty);
+ set => SetValue(CornerRadiusProperty, value);
+ }
+
+ private static bool IsCornerRadiusValid(object value)
+ {
+ var cr = (CornerRadius)value;
+ return !(cr.TopLeft < 0.0d || cr.TopRight < 0.0d || cr.BottomLeft < 0.0d || cr.BottomRight < 0.0d ||
+ double.IsNaN(cr.TopLeft) || double.IsNaN(cr.TopRight) || double.IsNaN(cr.BottomLeft) ||
+ double.IsNaN(cr.BottomRight) || double.IsInfinity(cr.TopLeft) || double.IsInfinity(cr.TopRight) ||
+ double.IsInfinity(cr.BottomLeft) || double.IsInfinity(cr.BottomRight));
+ }
+
+
+ // =======================================
+ // 渲染
+ // =======================================
+
+
+ protected override void OnRender(DrawingContext drawingContext)
+ {
+ var cornerRadius = CornerRadius;
+ var shadowBounds = new Rect(0d, 0d, RenderSize.Width, RenderSize.Height);
+ var color = Color;
+
+ if (shadowBounds.Width > 0d && shadowBounds.Height > 0d && color.A > 0)
+ {
+ var centerWidth = shadowBounds.Right - shadowBounds.Left - 2d * ShadowRadius;
+ var centerHeight = shadowBounds.Bottom - shadowBounds.Top - 2d * ShadowRadius;
+ var maxRadius = Math.Min(centerWidth * 0.5d, centerHeight * 0.5d);
+ cornerRadius.TopLeft = Math.Min(cornerRadius.TopLeft, maxRadius);
+ cornerRadius.TopRight = Math.Min(cornerRadius.TopRight, maxRadius);
+ cornerRadius.BottomLeft = Math.Min(cornerRadius.BottomLeft, maxRadius);
+ cornerRadius.BottomRight = Math.Min(cornerRadius.BottomRight, maxRadius);
+ var brushes = GetBrushes(color, cornerRadius);
+ var centerTop = shadowBounds.Top + ShadowRadius;
+ var centerLeft = shadowBounds.Left + ShadowRadius;
+ var centerRight = shadowBounds.Right - ShadowRadius;
+ var centerBottom = shadowBounds.Bottom - ShadowRadius;
+ var guidelineSetX = new[]
+ {
+ centerLeft, centerLeft + cornerRadius.TopLeft, centerRight - cornerRadius.TopRight,
+ centerLeft + cornerRadius.BottomLeft, centerRight - cornerRadius.BottomRight, centerRight
+ };
+ var guidelineSetY = new[]
+ {
+ centerTop, centerTop + cornerRadius.TopLeft, centerTop + cornerRadius.TopRight,
+ centerBottom - cornerRadius.BottomLeft, centerBottom - cornerRadius.BottomRight, centerBottom
+ };
+ drawingContext.PushGuidelineSet(new GuidelineSet(guidelineSetX, guidelineSetY));
+ cornerRadius.TopLeft += ShadowRadius;
+ cornerRadius.TopRight += ShadowRadius;
+ cornerRadius.BottomLeft += ShadowRadius;
+ cornerRadius.BottomRight += ShadowRadius;
+ var topLeft = new Rect(shadowBounds.Left, shadowBounds.Top, cornerRadius.TopLeft, cornerRadius.TopLeft);
+ drawingContext.DrawRectangle(brushes[(int)Placement.TopLeft], null, topLeft);
+ var topWidth = guidelineSetX[2] - guidelineSetX[1];
+
+ if (topWidth > 0d)
+ {
+ var top = new Rect(guidelineSetX[1], shadowBounds.Top, topWidth, ShadowRadius);
+ drawingContext.DrawRectangle(brushes[(int)Placement.Top], null, top);
+ }
+
+ var topRight = new Rect(guidelineSetX[2], shadowBounds.Top, cornerRadius.TopRight, cornerRadius.TopRight);
+ drawingContext.DrawRectangle(brushes[(int)Placement.TopRight], null, topRight);
+ var leftHeight = guidelineSetY[3] - guidelineSetY[1];
+
+ if (leftHeight > 0d)
+ {
+ var left = new Rect(shadowBounds.Left, guidelineSetY[1], ShadowRadius, leftHeight);
+ drawingContext.DrawRectangle(brushes[(int)Placement.Left], null, left);
+ }
+
+ var rightHeight = guidelineSetY[4] - guidelineSetY[2];
+
+ if (rightHeight > 0d)
+ {
+ var right = new Rect(guidelineSetX[5], guidelineSetY[2], ShadowRadius, rightHeight);
+ drawingContext.DrawRectangle(brushes[(int)Placement.Right], null, right);
+ }
+
+ var bottomLeft = new Rect(shadowBounds.Left, guidelineSetY[3], cornerRadius.BottomLeft,
+ cornerRadius.BottomLeft);
+ drawingContext.DrawRectangle(brushes[(int)Placement.BottomLeft], null, bottomLeft);
+ var bottomWidth = guidelineSetX[4] - guidelineSetX[3];
+
+ if (bottomWidth > 0d)
+ {
+ var bottom = new Rect(guidelineSetX[3], guidelineSetY[5], bottomWidth, ShadowRadius);
+ drawingContext.DrawRectangle(brushes[(int)Placement.Bottom], null, bottom);
+ }
+
+ var bottomRight = new Rect(guidelineSetX[4], guidelineSetY[4], cornerRadius.BottomRight,
+ cornerRadius.BottomRight);
+ drawingContext.DrawRectangle(brushes[(int)Placement.BottomRight], null, bottomRight);
+
+ if (cornerRadius.TopLeft == ShadowRadius && cornerRadius.TopLeft == cornerRadius.TopRight &&
+ cornerRadius.TopLeft == cornerRadius.BottomLeft && cornerRadius.TopLeft == cornerRadius.BottomRight)
+ {
+ var center = new Rect(guidelineSetX[0], guidelineSetY[0], centerWidth, centerHeight);
+ drawingContext.DrawRectangle(brushes[(int)Placement.Center], null, center);
+ }
+ else
+ {
+ var figure = new PathFigure();
+
+ if (cornerRadius.TopLeft > ShadowRadius)
+ {
+ figure.StartPoint = new Point(guidelineSetX[1], guidelineSetY[0]);
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[1], guidelineSetY[1]), true));
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[1]), true));
+ }
+ else
+ {
+ figure.StartPoint = new Point(guidelineSetX[0], guidelineSetY[0]);
+ }
+
+ if (cornerRadius.BottomLeft > ShadowRadius)
+ {
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[3]), true));
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[3], guidelineSetY[3]), true));
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[3], guidelineSetY[5]), true));
+ }
+ else
+ {
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[5]), true));
+ }
+
+ if (cornerRadius.BottomRight > ShadowRadius)
+ {
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[4], guidelineSetY[5]), true));
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[4], guidelineSetY[4]), true));
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[4]), true));
+ }
+ else
+ {
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[5]), true));
+ }
+
+ if (cornerRadius.TopRight > ShadowRadius)
+ {
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[2]), true));
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[2], guidelineSetY[2]), true));
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[2], guidelineSetY[0]), true));
+ }
+ else
+ {
+ figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[0]), true));
+ }
+
+ figure.IsClosed = true;
+ figure.Freeze();
+ var geometry = new PathGeometry();
+ geometry.Figures.Add(figure);
+ geometry.Freeze();
+ drawingContext.DrawGeometry(brushes[(int)Placement.Center], null, geometry);
+ }
+
+ drawingContext.Pop();
+ }
+ }
+
+ private static void ClearBrushes(DependencyObject o, DependencyPropertyChangedEventArgs e)
+ {
+ ((MyDropShadow)o)._brushes = null;
+ }
+
+ private GradientStopCollection CreateStops(Color c, double cornerRadius)
+ {
+ var gradientScale = 1d / (ShadowRadius + cornerRadius);
+ var gsc = new GradientStopCollection();
+ var stopColor = c;
+ gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.1d + cornerRadius) * gradientScale));
+ stopColor.A = (byte)Math.Round(0.74336d * c.A);
+ gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.3d + cornerRadius) * gradientScale));
+ stopColor.A = (byte)Math.Round(0.38053d * c.A);
+ gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.5d + cornerRadius) * gradientScale));
+ stopColor.A = (byte)Math.Round(0.12389d * c.A);
+ gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.7d + cornerRadius) * gradientScale));
+ stopColor.A = (byte)Math.Round(0.02654d * c.A);
+ gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.9d + cornerRadius) * gradientScale));
+ stopColor.A = 0;
+ gsc.Add(new GradientStop(stopColor, (ShadowRadius + cornerRadius) * gradientScale));
+ gsc.Freeze();
+ return gsc;
+ }
+
+ private Brush[] CreateBrushes(Color c, CornerRadius cornerRadius)
+ {
+ var brushes = new Brush[9];
+ brushes[(int)Placement.Center] = new SolidColorBrush(c);
+ brushes[(int)Placement.Center].Freeze();
+ var sideStops = CreateStops(c, 0d);
+ var top = new LinearGradientBrush(sideStops, new Point(0d, 1d), new Point(0d, 0d));
+ top.Freeze();
+ brushes[(int)Placement.Top] = top;
+ var left = new LinearGradientBrush(sideStops, new Point(1d, 0d), new Point(0d, 0d));
+ left.Freeze();
+ brushes[(int)Placement.Left] = left;
+ var right = new LinearGradientBrush(sideStops, new Point(0d, 0d), new Point(1d, 0d));
+ right.Freeze();
+ brushes[(int)Placement.Right] = right;
+ var bottom = new LinearGradientBrush(sideStops, new Point(0d, 0d), new Point(0d, 1d));
+ bottom.Freeze();
+ brushes[(int)Placement.Bottom] = bottom;
+ GradientStopCollection topLeftStops;
+
+ if (cornerRadius.TopLeft == 0d)
+ topLeftStops = sideStops;
+ else
+ topLeftStops = CreateStops(c, cornerRadius.TopLeft);
+
+ var topLeft = new RadialGradientBrush(topLeftStops)
+ {
+ RadiusX = 1d,
+ RadiusY = 1d,
+ Center = new Point(1d, 1d),
+ GradientOrigin = new Point(1d, 1d)
+ };
+ topLeft.Freeze();
+ brushes[(int)Placement.TopLeft] = topLeft;
+ GradientStopCollection topRightStops;
+
+ if (cornerRadius.TopRight == 0d)
+ topRightStops = sideStops;
+ else if (cornerRadius.TopRight == cornerRadius.TopLeft)
+ topRightStops = topLeftStops;
+ else
+ topRightStops = CreateStops(c, cornerRadius.TopRight);
+
+ var topRight = new RadialGradientBrush(topRightStops)
+ {
+ RadiusX = 1d,
+ RadiusY = 1d,
+ Center = new Point(0d, 1d),
+ GradientOrigin = new Point(0d, 1d)
+ };
+ topRight.Freeze();
+ brushes[(int)Placement.TopRight] = topRight;
+ GradientStopCollection bottomLeftStops;
+
+ if (cornerRadius.BottomLeft == 0d)
+ bottomLeftStops = sideStops;
+ else if (cornerRadius.BottomLeft == cornerRadius.TopLeft)
+ bottomLeftStops = topLeftStops;
+ else if (cornerRadius.BottomLeft == cornerRadius.TopRight)
+ bottomLeftStops = topRightStops;
+ else
+ bottomLeftStops = CreateStops(c, cornerRadius.BottomLeft);
+
+ var bottomLeft = new RadialGradientBrush(bottomLeftStops)
+ {
+ RadiusX = 1d,
+ RadiusY = 1d,
+ Center = new Point(1d, 0d),
+ GradientOrigin = new Point(1d, 0d)
+ };
+ bottomLeft.Freeze();
+ brushes[(int)Placement.BottomLeft] = bottomLeft;
+ GradientStopCollection bottomRightStops;
+
+ if (cornerRadius.BottomRight == 0d)
+ bottomRightStops = sideStops;
+ else if (cornerRadius.BottomRight == cornerRadius.TopLeft)
+ bottomRightStops = topLeftStops;
+ else if (cornerRadius.BottomRight == cornerRadius.TopRight)
+ bottomRightStops = topRightStops;
+ else if (cornerRadius.BottomRight == cornerRadius.BottomLeft)
+ bottomRightStops = bottomLeftStops;
+ else
+ bottomRightStops = CreateStops(c, cornerRadius.BottomRight);
+
+ var bottomRight = new RadialGradientBrush(bottomRightStops)
+ {
+ RadiusX = 1d,
+ RadiusY = 1d,
+ Center = new Point(0d, 0d),
+ GradientOrigin = new Point(0d, 0d)
+ };
+ bottomRight.Freeze();
+ brushes[(int)Placement.BottomRight] = bottomRight;
+ return brushes;
+ }
+
+ private Brush[] GetBrushes(Color c, CornerRadius cornerRadius)
+ {
+ if (_commonBrushes is null)
+ lock (_resourceAccess)
+ {
+ if (_commonBrushes is null)
+ {
+ _commonBrushes = CreateBrushes(c, cornerRadius);
+ _commonCornerRadius = cornerRadius;
+ }
+ }
+
+ if (c == ((SolidColorBrush)_commonBrushes[(int)Placement.Center]).Color && cornerRadius == _commonCornerRadius)
+ {
+ _brushes = null;
+ return _commonBrushes;
+ }
+
+ if (_brushes is null) _brushes = CreateBrushes(c, cornerRadius);
+
+ return _brushes;
+ }
+
+ private enum Placement
+ {
+ TopLeft = 0,
+ Top = 1,
+ TopRight = 2,
+ Left = 3,
+ Center = 4,
+ Right = 5,
+ BottomLeft = 6,
+ Bottom = 7,
+ BottomRight = 8
+ }
+}
+
+// 参考自:https://referencesource.microsoft.com/#PresentationFramework.Aero/parent/Shared/Microsoft/Windows/Themes/SystemDropShadowChrome.cs,6d9c27d92a8128c1
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyExtraButton.xaml b/Plain Craft Launcher 2/Controls/MyExtraButton.xaml
index f8a0d88ca..2dfd00789 100644
--- a/Plain Craft Launcher 2/Controls/MyExtraButton.xaml
+++ b/Plain Craft Launcher 2/Controls/MyExtraButton.xaml
@@ -1,30 +1,40 @@
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="PanBack" mc:Ignorable="d"
+ x:Class="PCL.MyExtraButton"
+ Width="40" RenderTransformOrigin="0.5,0.5" ToolTipService.Placement="Left" ToolTipService.VerticalOffset="16"
+ ToolTipService.HorizontalOffset="-8" Height="0">
-
+
-
+
-
+
-
+
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyExtraButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyExtraButton.xaml.cs
new file mode 100644
index 000000000..f519d851f
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyExtraButton.xaml.cs
@@ -0,0 +1,307 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace PCL;
+
+public partial class MyExtraButton
+{
+ public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); // 自定义事件
+
+ public delegate void RightClickEventHandler(object sender, MouseButtonEventArgs e);
+
+ public delegate bool ShowCheckDelegate();
+
+ // 自定义事件
+ // 务必放在 IsMouseDown 更新之后
+ private const int AnimationColorIn = 120;
+ private const int AnimationColorOut = 150;
+ private string _Logo = "";
+ private double _LogoScale = 1d;
+
+ // 进度条
+ private double _Progress;
+ private bool _Show;
+
+ // 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+ private bool IsLeftMouseHeld;
+ private bool IsRightMouseHeld;
+ public ShowCheckDelegate ShowCheck = null;
+
+ // 自定义属性
+ public int Uuid = ModBase.GetUuid();
+
+ public MyExtraButton()
+ {
+ Loaded += (_, _) => RefreshColor();
+ IsEnabledChanged += (_, _) => RefreshColor();
+ InitializeComponent();
+ PanClick.MouseLeave += (_, _) => Button_MouseLeave();
+ }
+
+ public double Progress
+ {
+ get => _Progress;
+ set
+ {
+ if (_Progress == value)
+ return;
+ _Progress = value;
+ if (value < 0.0001d)
+ {
+ PanProgress.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ PanProgress.Visibility = Visibility.Visible;
+ RectProgress.Rect = new Rect(0d, 40d * (1d - value), 40d, 40d * value);
+ }
+ }
+ }
+
+ public string Logo
+ {
+ get => _Logo;
+ set
+ {
+ if ((value ?? "") == (_Logo ?? ""))
+ return;
+ _Logo = value;
+ Path.Data = (Geometry)new GeometryConverter().ConvertFromString(value);
+ }
+ }
+
+ public double LogoScale
+ {
+ get => _LogoScale;
+ set
+ {
+ _LogoScale = value;
+ if (!(Path == null))
+ Path.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale };
+ }
+ }
+
+ public bool Show
+ {
+ get => _Show;
+ set
+ {
+ if (_Show == value)
+ return;
+ _Show = value;
+ ModBase.RunInUi(() =>
+ {
+ if (value)
+ {
+ // 有了
+ Visibility = Visibility.Visible;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(this, 0.3d - ((ScaleTransform)RenderTransform).ScaleX, 500,
+ 60, new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaScaleTransform(this, 0.7d, 500, 60,
+ new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaHeight(this, 50d - Height, 200,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
+ }, "MyExtraButton MainScale " + Uuid);
+ }
+ else
+ {
+ // 没了
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(this, -((ScaleTransform)RenderTransform).ScaleX, 100,
+ Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaHeight(this, -Height, 400, 100, new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaCode(() => Visibility = Visibility.Collapsed, After: true)
+ }, "MyExtraButton MainScale " + Uuid);
+ }
+
+ IsHitTestVisible = value; // 防止缩放动画中依然可以点进去
+ });
+ }
+ }
+
+ public bool CanRightClick { get; set; }
+
+ // 声明
+ public event ClickEventHandler? Click;
+ public event RightClickEventHandler? RightClick;
+
+ public void ShowRefresh()
+ {
+ if (ShowCheck is not null)
+ Show = ShowCheck();
+ }
+
+ // 触发点击事件
+ private void Button_LeftMouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (IsLeftMouseHeld)
+ {
+ ModBase.Log("[Control] 按下附加按钮" +
+ (ToolTip is null or "" ? "" : ":" + ToolTip));
+ Click?.Invoke(sender, e);
+ e.Handled = true;
+ Button_LeftMouseUp();
+ }
+ }
+
+ private void Button_RightMouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (IsRightMouseHeld)
+ {
+ ModBase.Log("[Control] 右键按下附加按钮" +
+ (ToolTip is null or "" ? "" : ":" + ToolTip));
+ RightClick?.Invoke(sender, e);
+ e.Handled = true;
+ Button_RightMouseUp();
+ }
+ }
+
+ private void Button_LeftMouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (!IsLeftMouseHeld && !IsRightMouseHeld)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanScale, 0.85d - ((ScaleTransform)PanScale.RenderTransform).ScaleX,
+ 800, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)),
+ ModAnimation.AaScaleTransform(PanScale, -0.05d, 60, Ease: new ModAnimation.AniEaseOutFluent())
+ }, "MyExtraButton Scale " + Uuid);
+ IsLeftMouseHeld = true;
+ Focus();
+ }
+
+ private void Button_RightMouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (!CanRightClick)
+ return;
+ if (!IsLeftMouseHeld && !IsRightMouseHeld)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanScale, 0.85d - ((ScaleTransform)PanScale.RenderTransform).ScaleX,
+ 800, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)),
+ ModAnimation.AaScaleTransform(PanScale, -0.05d, 60, Ease: new ModAnimation.AniEaseOutFluent())
+ }, "MyExtraButton Scale " + Uuid);
+ IsRightMouseHeld = true;
+ Focus();
+ }
+
+ private void Button_LeftMouseUp()
+ {
+ if (!IsRightMouseHeld)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 300,
+ Ease: new ModAnimation.AniEaseOutBack())
+ }, "MyExtraButton Scale " + Uuid);
+ if (IsLeftMouseHeld) ModMain.RaiseCustomEvent(this);
+ IsLeftMouseHeld = false;
+ RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
+ }
+
+ private void Button_RightMouseUp()
+ {
+ if (!CanRightClick)
+ return;
+ if (!IsLeftMouseHeld)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 300,
+ Ease: new ModAnimation.AniEaseOutBack())
+ }, "MyExtraButton Scale " + Uuid);
+ IsRightMouseHeld = false;
+ RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
+ }
+
+ private void Button_MouseLeave()
+ {
+ IsLeftMouseHeld = false;
+ IsRightMouseHeld = false;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 500,
+ Ease: new ModAnimation.AniEaseOutFluent())
+ }, "MyExtraButton Scale " + Uuid);
+ RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
+ }
+
+ public void RefreshColor()
+ {
+ try
+ {
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
+ {
+ if (!IsEnabled)
+ // 禁用
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrushGray4", AnimationColorIn),
+ "MyExtraButton Color " + Uuid);
+ else if (IsMouseOver)
+ // 指向
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush4", AnimationColorIn),
+ "MyExtraButton Color " + Uuid);
+ else
+ // 普通
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush3", AnimationColorOut),
+ "MyExtraButton Color " + Uuid);
+ }
+
+ else
+ {
+ ModAnimation.AniStop("MyExtraButton Color " + Uuid);
+ if (!IsEnabled)
+ PanColor.SetResourceReference(BackgroundProperty, "ColorBrushGray4");
+ else if (IsMouseOver)
+ PanColor.SetResourceReference(BackgroundProperty, "ColorBrush4");
+ else
+ PanColor.SetResourceReference(BackgroundProperty, "ColorBrush3");
+ }
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "刷新图标按钮颜色出错");
+ }
+ }
+
+ ///
+ /// 发出一圈波浪效果提示。
+ ///
+ public void Ribble()
+ {
+ ModBase.RunInUi(() =>
+ {
+ var Shape = new Border
+ {
+ CornerRadius = new CornerRadius(1000d), BorderThickness = new Thickness(0.001d), Opacity = 0.5d,
+ RenderTransformOrigin = new Point(0.5d, 0.5d), RenderTransform = new ScaleTransform()
+ };
+ Shape.SetResourceReference(Border.BackgroundProperty, "ColorBrush5");
+ PanScale.Children.Insert(0, Shape);
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(Shape, 13d, 1000,
+ Ease: new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Strong, 0.3d)),
+ ModAnimation.AaOpacity(Shape, -Shape.Opacity, 1000),
+ ModAnimation.AaCode(() => PanScale.Children.Remove(Shape), After: true)
+ }, "ExtraButton Ribble " + ModBase.GetUuid());
+ });
+ }
+
+ private void PanClick_MouseEvent(object sender, MouseEventArgs e)
+ {
+ RefreshColor();
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml b/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml
index 795a7bd9d..d09b57c6f 100644
--- a/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml
+++ b/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml
@@ -1,15 +1,15 @@
-
+
@@ -21,12 +21,14 @@
-
+
-
diff --git a/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml.cs
new file mode 100644
index 000000000..03116a6c1
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml.cs
@@ -0,0 +1,229 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Markup;
+using System.Windows.Media;
+
+namespace PCL;
+
+[ContentProperty("Inlines")]
+public partial class MyExtraTextButton
+{
+ public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); // 自定义事件
+
+ // 自定义事件
+ // 务必放在 IsMouseDown 更新之后
+ private const int AnimationColorIn = 120;
+ private const int AnimationColorOut = 150;
+
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
+ typeof(MyExtraTextButton), new PropertyMetadata((sender, e) =>
+ {
+ if (sender is not null) ((MyExtraTextButton)sender).LabText.Text = (string)e.NewValue;
+ }));
+
+ private string _Logo = "";
+ private double _LogoScale = 1d;
+
+ // 动画
+ private bool _Show;
+
+ // 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+ private bool IsLeftMouseHeld;
+
+ // 自定义属性
+ public int Uuid = ModBase.GetUuid();
+
+ public MyExtraTextButton()
+ {
+ InitializeComponent();
+
+ Loaded += (_, _) => RefreshColor();
+ IsEnabledChanged += (_, _) => RefreshColor();
+ PanClick.MouseLeftButtonDown += Button_LeftMouseDown;
+ PanClick.MouseLeftButtonUp += Button_LeftMouseUp;
+ PanClick.MouseLeave += Button_MouseLeave;
+ PanClick.MouseRightButtonUp += Button_RightMouseUp;
+ PanClick.MouseEnter += (sender, e) => RefreshColor();
+ }
+
+ public string Logo
+ {
+ get => _Logo;
+ set
+ {
+ if ((value ?? "") == (_Logo ?? ""))
+ return;
+ _Logo = value;
+ Path.Data = (Geometry)new GeometryConverter().ConvertFromString(value);
+ }
+ }
+
+ public double LogoScale
+ {
+ get => _LogoScale;
+ set
+ {
+ _LogoScale = value;
+ if (Path is not null)
+ Path.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale };
+ }
+ }
+
+ // 显示文本
+ public InlineCollection Inlines => LabText.Inlines;
+
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+ set
+ {
+ if (value == null) return;
+ SetValue(TextProperty, value);
+ }
+ }
+
+ public bool Show
+ {
+ get => _Show;
+ set
+ {
+ if (_Show == value)
+ return;
+ _Show = value;
+ ModBase.RunInUi(() =>
+ {
+ if (value)
+ {
+ // 有了
+ Opacity = 0d;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaOpacity(this, 1d - Opacity, 80, 50),
+ ModAnimation.AaScaleTransform(this, 0.15d - ((ScaleTransform)RenderTransform).ScaleX, 400,
+ 50, new ModAnimation.AniEaseOutBack()),
+ ModAnimation.AaScaleTransform(this, 0.85d, 160, 50, new ModAnimation.AniEaseOutFluent())
+ }, "MyExtraTextButton MainScale " + Uuid);
+ }
+ else
+ {
+ // 没了
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaOpacity(this, -Opacity, 50, 50),
+ ModAnimation.AaScaleTransform(this, -((ScaleTransform)RenderTransform).ScaleX, 100,
+ Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak))
+ }, "MyExtraTextButton MainScale " + Uuid);
+ }
+
+ IsHitTestVisible = value; // 防止缩放动画中依然可以点进去
+ });
+ }
+ }
+
+ // 声明
+ public event ClickEventHandler? Click;
+
+ // 触发点击事件
+ private void Button_LeftMouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (!IsLeftMouseHeld) return;
+ ModBase.Log("[Control] 按下附加图标按钮:" + Text);
+ Click?.Invoke(sender, e);
+ e.Handled = true;
+ ModMain.RaiseCustomEvent(this);
+ Button_LeftMouseUp();
+ }
+
+ private void Button_LeftMouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (!IsLeftMouseHeld)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanScale, 0.85d - ((ScaleTransform)PanScale.RenderTransform).ScaleX,
+ 800, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)),
+ ModAnimation.AaScaleTransform(PanScale, -0.05d, 60, Ease: new ModAnimation.AniEaseOutFluent())
+ }, "MyExtraTextButton Scale " + Uuid);
+ IsLeftMouseHeld = true;
+ Focus();
+ }
+
+ private void Button_LeftMouseUp()
+ {
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 300,
+ Ease: new ModAnimation.AniEaseOutBack())
+ }, "MyExtraTextButton Scale " + Uuid);
+ IsLeftMouseHeld = false;
+ RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
+ }
+
+ private void Button_RightMouseUp(object sender, MouseEventArgs e)
+ {
+ if (!IsLeftMouseHeld)
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 300,
+ Ease: new ModAnimation.AniEaseOutBack())
+ }, "MyExtraTextButton Scale " + Uuid);
+ RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
+ }
+
+ private void Button_MouseLeave(object sender, MouseEventArgs e)
+ {
+ IsLeftMouseHeld = false;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 500,
+ Ease: new ModAnimation.AniEaseOutFluent())
+ }, "MyExtraTextButton Scale " + Uuid);
+ RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave
+ }
+
+ public void RefreshColor()
+ {
+ try
+ {
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
+ {
+ if (!IsEnabled)
+ // 禁用
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrushGray4", AnimationColorIn),
+ "MyExtraTextButton Color " + Uuid);
+ else if (IsMouseOver)
+ // 指向
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush4", AnimationColorIn),
+ "MyExtraTextButton Color " + Uuid);
+ else
+ // 普通
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush3", AnimationColorOut),
+ "MyExtraTextButton Color " + Uuid);
+ }
+
+ else
+ {
+ ModAnimation.AniStop("MyExtraTextButton Color " + Uuid);
+ if (!IsEnabled)
+ PanColor.SetResourceReference(BackgroundProperty, "ColorBrushGray4");
+ else if (IsMouseOver)
+ PanColor.SetResourceReference(BackgroundProperty, "ColorBrush4");
+ else
+ PanColor.SetResourceReference(BackgroundProperty, "ColorBrush3");
+ }
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "刷新附加图标按钮颜色出错");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyHint.xaml b/Plain Craft Launcher 2/Controls/MyHint.xaml
index 6395c28ef..b4dff47e9 100644
--- a/Plain Craft Launcher 2/Controls/MyHint.xaml
+++ b/Plain Craft Launcher 2/Controls/MyHint.xaml
@@ -1,16 +1,21 @@
-
+
-
-
+
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyHint.xaml.cs b/Plain Craft Launcher 2/Controls/MyHint.xaml.cs
new file mode 100644
index 000000000..f99485787
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyHint.xaml.cs
@@ -0,0 +1,256 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Markup;
+
+using PCL.Core.App;
+using PCL.Core.UI.Theme;
+using System.Windows.Controls;
+
+namespace PCL;
+
+[ContentProperty("Inlines")]
+public partial class MyHint
+{
+ // 配色
+ public enum Themes
+ {
+ Blue = 0,
+ Red = 1,
+ Yellow = 2
+ }
+
+ public static readonly DependencyProperty IsWarnProperty = DependencyProperty.Register("IsWarn", typeof(bool),
+ typeof(MyHint),
+ new PropertyMetadata(true,
+ (d, e) =>
+ {
+ var f = (MyHint)d;
+ f.Theme = e.NewValue != null ? Themes.Red : Themes.Blue;
+ }));
+
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
+ typeof(MyHint), new PropertyMetadata("", (d, e) =>
+ {
+ var f = (MyHint)d;
+ f.LabText.Text = (string)e.NewValue;
+ }));
+
+ private Themes _ColorType = Themes.Red;
+
+ // 触发点击事件
+ private bool IsMouseDown;
+ public int Uuid = ModBase.GetUuid();
+
+ public MyHint()
+ {
+ InitializeComponent();
+ UpdateUI();
+ Loaded += (_, _) => UpdateUI();
+ Loaded += MyHint_Loaded;
+ MouseLeftButtonUp += MyHint_MouseUp;
+ MouseLeftButtonDown += MyHint_MouseDown;
+ MouseLeave += (_, _) => MyHint_MouseLeave();
+ Unloaded += (_, _) => Dispose();
+ }
+
+ // 边框
+ public bool HasBorder
+ {
+ get => BorderThickness.Top > 0d;
+ set
+ {
+ if (value)
+ BorderThickness = new Thickness(3d, ModBase.GetWPFSize(1d), ModBase.GetWPFSize(1d),
+ ModBase.GetWPFSize(1d));
+ else
+ BorderThickness = new Thickness(3d, 0d, 0d, 0d);
+ }
+ }
+
+ public Themes Theme
+ {
+ get => _ColorType;
+ set
+ {
+ _ColorType = value;
+ UpdateUI();
+ }
+ }
+
+ [Obsolete("IsWarn 已过时。请换用 Theme 属性。")]
+ public bool IsWarn
+ {
+ get => Theme == Themes.Red;
+ set => Theme = value ? Themes.Red : Themes.Blue;
+ }
+
+ // 文本
+ public InlineCollection Inlines => LabText.Inlines;
+
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ // 关闭按钮
+ public bool CanClose
+ {
+ get => BtnClose.Visibility == Visibility.Visible;
+ set => BtnClose.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ public string RelativeSetup { get; set; } = "";
+
+ private void UpdateUI()
+ {
+ var hue = default(double);
+ switch (Theme)
+ {
+ case Themes.Blue:
+ {
+ hue = 210d;
+ break;
+ }
+ case Themes.Red:
+ {
+ hue = 355d;
+ break;
+ }
+ case Themes.Yellow:
+ {
+ hue = 40d;
+ break;
+ }
+ }
+
+ var s = ThemeService.CurrentTone;
+ Background = new ModBase.MyColor().FromHSL2(hue, 90, s.L7 * 100);
+ BorderBrush = new ModBase.MyColor().FromHSL2(hue, 90, s.L2 * 100);
+ LabText.Foreground = new ModBase.MyColor().FromHSL2(hue, 90, s.L2 * 100);
+ BtnClose.Foreground = new ModBase.MyColor().FromHSL2(hue, 90, s.L2 * 100);
+ }
+
+ private void MyHint_Loaded(object sender, RoutedEventArgs e)
+ {
+ ThemeService.ColorModeChanged += (v, theme) => _ThemeChanged(v, theme);
+ if (CanClose && ModBase.Setup.Get(RelativeSetup) != null)
+ Visibility = Visibility.Collapsed;
+ }
+
+ private void BtnClose_Click(object sender, EventArgs e)
+ {
+ ModBase.Setup.Set(RelativeSetup, true);
+ ModAnimation.AniDispose(this, false);
+ }
+
+ private void MyHint_MouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (!IsMouseDown)
+ return;
+ IsMouseDown = false;
+ ModBase.Log("[Control] 按下提示条" + (string.IsNullOrEmpty(Name) ? "" : ":" + Name));
+ e.Handled = true;
+ ModMain.RaiseCustomEvent(this);
+ }
+
+ private void MyHint_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ IsMouseDown = true;
+ }
+
+ private void MyHint_MouseLeave()
+ {
+ IsMouseDown = false;
+ }
+
+ private void _ThemeChanged(bool isDarkMode, ColorTheme theme)
+ {
+ UpdateUI();
+ }
+
+ private void Dispose()
+ {
+ ThemeService.ColorModeChanged -= _ThemeChanged;
+ }
+
+ // Private Sub SetStyle()
+ // If Type = HintType.Note Then
+ // If IsWarn Then
+ // BorderBrush = New MyColor("#CCFF4444")
+ // Gradient1.Color = New MyColor(CType(If(IsDarkMode, "#BBFF8888", "#BBFFBBBB"), String))
+ // Gradient2.Color = New MyColor(CType(If(IsDarkMode, "#BBFF6666", "#BBFF8888"), String))
+ // Path.Fill = New MyColor("#BF0000")
+ // LabText.Foreground = New MyColor("#BF0000")
+ // BtnClose.Foreground = New MyColor("#BF0000")
+ // Path.Data = (New GeometryConverter).ConvertFromString("F1 M 58.5832,55.4172L 17.4169,55.4171C 15.5619,53.5621 15.5619,50.5546 17.4168,48.6996L 35.201,15.8402C 37.056,13.9852 40.0635,13.9852 41.9185,15.8402L 58.5832,48.6997C 60.4382,50.5546 60.4382,53.5622 58.5832,55.4172 Z M 34.0417,25.7292L 36.0208,41.9584L 39.9791,41.9583L 41.9583,25.7292L 34.0417,25.7292 Z M 38,44.3333C 36.2511,44.3333 34.8333,45.7511 34.8333,47.5C 34.8333,49.2489 36.2511,50.6667 38,50.6667C 39.7489,50.6667 41.1666,49.2489 41.1666,47.5C 41.1666,45.7511 39.7489,44.3333 38,44.3333 Z ")
+ // Return
+ // Else
+ // BorderBrush = New MyColor("#CC4D76FF")
+ // Gradient1.Color = New MyColor("#BBB0D0FF")
+ // Gradient2.Color = New MyColor("#BB9EBAFF")
+ // Path.Fill = New MyColor("#0062BF")
+ // LabText.Foreground = New MyColor("#0062BF")
+ // BtnClose.Foreground = New MyColor("#0062BF")
+ // Path.Data = (New GeometryConverter).ConvertFromString("F1M38,19C48.4934,19 57,27.5066 57,38 57,48.4934 48.4934,57 38,57 27.5066,57 19,48.4934 19,38 19,27.5066 27.5066,19 38,19z M33.25,33.25L33.25,36.4167 36.4166,36.4167 36.4166,47.5 33.25,47.5 33.25,50.6667 44.3333,50.6667 44.3333,47.5 41.1666,47.5 41.1666,36.4167 41.1666,33.25 33.25,33.25z M38.7917,25.3333C37.48,25.3333 36.4167,26.3967 36.4167,27.7083 36.4167,29.02 37.48,30.0833 38.7917,30.0833 40.1033,30.0833 41.1667,29.02 41.1667,27.7083 41.1667,26.3967 40.1033,25.3333 38.7917,25.3333z")
+ // Return
+ // End If
+ // End If
+
+ // Select Case Type
+ // Case HintType.Warning
+ // BorderBrush = New MyColor("#CCE69900")
+ // Gradient1.Color = New MyColor("#BBFFF4CE")
+ // Gradient2.Color = New MyColor("#BBFFF5CE")
+ // Path.Fill = New MyColor("#957500")
+ // LabText.Foreground = New MyColor("#957500")
+ // BtnClose.Foreground = New MyColor("#957500")
+ // Path.Data = (New GeometryConverter).ConvertFromString("F1 M 58.5832,55.4172L 17.4169,55.4171C 15.5619,53.5621 15.5619,50.5546 17.4168,48.6996L 35.201,15.8402C 37.056,13.9852 40.0635,13.9852 41.9185,15.8402L 58.5832,48.6997C 60.4382,50.5546 60.4382,53.5622 58.5832,55.4172 Z M 34.0417,25.7292L 36.0208,41.9584L 39.9791,41.9583L 41.9583,25.7292L 34.0417,25.7292 Z M 38,44.3333C 36.2511,44.3333 34.8333,45.7511 34.8333,47.5C 34.8333,49.2489 36.2511,50.6667 38,50.6667C 39.7489,50.6667 41.1666,49.2489 41.1666,47.5C 41.1666,45.7511 39.7489,44.3333 38,44.3333 Z ")
+ // Return
+ // Case HintType.Caution
+ // BorderBrush = New MyColor("#CCFF4444")
+ // Gradient1.Color = New MyColor(CType(If(IsDarkMode, "#BBFF8888", "#BBFFBBBB"), String))
+ // Gradient2.Color = New MyColor(CType(If(IsDarkMode, "#BBFF6666", "#BBFF8888"), String))
+ // Path.Fill = New MyColor("#BF0000")
+ // LabText.Foreground = New MyColor("#BF0000")
+ // BtnClose.Foreground = New MyColor("#BF0000")
+ // Path.Data = (New GeometryConverter).ConvertFromString("F1 M1024,1024z M0,0z M512,0C229.23,0 0,229.23 0,512 0,794.77 229.23,1024 512,1024 794.768,1024 1024,794.77 1024,512 1024,229.23 794.77,0 512,0z M746.76,656.252C754.568,664.06,754.566,676.724,746.762,684.536L684.534,746.76C676.726,754.568,664.064,754.574,656.248,746.762L512,602.51 367.75,746.76C359.94,754.572,347.276,754.568,339.466,746.76L277.24,684.536C269.43,676.728,269.428,664.064,277.24,656.252L421.492,512 277.242,367.75C269.432,359.942,269.432,347.276,277.242,339.466L339.468,277.242C347.278,269.43,359.942,269.432,367.752,277.242L512,421.49 656.252,277.24C664.058,269.428,676.722,269.43,684.534,277.24L746.76,339.464C754.566,347.276,754.568,359.938,746.76,367.748L602.51,512 746.76,656.252z")
+ // Return
+ // Case Else
+ // BorderBrush = New MyColor("#CC4D76FF")
+ // Gradient1.Color = New MyColor("#BBB0D0FF")
+ // Gradient2.Color = New MyColor("#BB9EBAFF")
+ // Path.Fill = New MyColor("#0062BF")
+ // LabText.Foreground = New MyColor("#0062BF")
+ // BtnClose.Foreground = New MyColor("#0062BF")
+ // Path.Data = (New GeometryConverter).ConvertFromString("F1M38,19C48.4934,19 57,27.5066 57,38 57,48.4934 48.4934,57 38,57 27.5066,57 19,48.4934 19,38 19,27.5066 27.5066,19 38,19z M33.25,33.25L33.25,36.4167 36.4166,36.4167 36.4166,47.5 33.25,47.5 33.25,50.6667 44.3333,50.6667 44.3333,47.5 41.1666,47.5 41.1666,36.4167 41.1666,33.25 33.25,33.25z M38.7917,25.3333C37.48,25.3333 36.4167,26.3967 36.4167,27.7083 36.4167,29.02 37.48,30.0833 38.7917,30.0833 40.1033,30.0833 41.1667,29.02 41.1667,27.7083 41.1667,26.3967 40.1033,25.3333 38.7917,25.3333z")
+ // Return
+ // End Select
+ // End Sub
+}
+
+public static partial class ModAnimation
+{
+ public static void AniDispose(MyHint Control, bool RemoveFromChildren, ParameterizedThreadStart CallBack = null)
+ {
+ if (!Control.IsHitTestVisible)
+ return;
+ Control.IsHitTestVisible = false;
+ AniStart(new[]
+ {
+ AaScaleTransform(Control, -0.08d, 200, Ease: new AniEaseInFluent()),
+ AaOpacity(Control, -1, 200, Ease: new AniEaseOutFluent()),
+ AaHeight(Control, -Control.ActualHeight, 150, 100, new AniEaseOutFluent()),
+ AaCode(() =>
+ {
+ if (RemoveFromChildren)
+ ((Panel)Control.Parent).Children.Remove(Control);
+ else
+ Control.Visibility = Visibility.Collapsed;
+ if (CallBack is not null)
+ CallBack(Control);
+ }, After: true)
+ }, "MyCard Dispose " + Control.Uuid);
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyIconButton.xaml b/Plain Craft Launcher 2/Controls/MyIconButton.xaml
index 5b4d8a080..ff7f5f228 100644
--- a/Plain Craft Launcher 2/Controls/MyIconButton.xaml
+++ b/Plain Craft Launcher 2/Controls/MyIconButton.xaml
@@ -1,8 +1,9 @@
-
-
+
+
@@ -12,4 +13,4 @@
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyIconButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyIconButton.xaml.cs
new file mode 100644
index 000000000..0f599b6a8
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyIconButton.xaml.cs
@@ -0,0 +1,331 @@
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Shapes;
+using System.Windows.Controls;
+
+namespace PCL;
+
+public partial class MyIconButton
+{
+ public delegate void ClickEventHandler(object sender, EventArgs e);
+
+ public enum Themes
+ {
+ Color,
+ White,
+ Black,
+ Red,
+ Custom
+ }
+
+ // 务必放在 IsMouseDown 更新之后
+ private const int AnimationColorIn = 120;
+ private const int AnimationColorOut = 150;
+
+ private SolidColorBrush _Foreground = new(Color.FromRgb(128, 128, 128));
+
+ private double _LogoScale = 1d;
+
+ // 自定义属性
+
+ public int Uuid = ModBase.GetUuid();
+
+ public MyIconButton()
+ {
+ InitializeComponent();
+
+ MouseLeftButtonUp += Button_MouseUp;
+ MouseLeftButtonDown += Button_MouseDown;
+ MouseLeftButtonUp += (_, _) => Button_MouseUp();
+ MouseLeave += (_, _) => Button_MouseLeave();
+ MouseEnter += (_, _) => RefreshAnim();
+ MouseLeave += (_, _) => RefreshAnim();
+ Loaded += (_, _) => RefreshAnim();
+ }
+
+ public string Logo
+ {
+ get => Path.Data.ToString();
+ set
+ {
+ if (Path == null) return;
+ Path.Data = (Geometry)new GeometryConverter().ConvertFromString(value);
+ }
+ }
+
+ public double LogoScale
+ {
+ get => _LogoScale;
+ set
+ {
+ _LogoScale = value;
+ if (!(Path == null))
+ Path.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale };
+ }
+ }
+
+ public Themes Theme { get; set; } = Themes.Color;
+
+ public SolidColorBrush Foreground
+ {
+ get => _Foreground;
+ set
+ {
+ _Foreground = value;
+ ModAnimation.AniControlEnabled += 1;
+ RefreshAnim();
+ ModAnimation.AniControlEnabled -= 1;
+ }
+ }
+
+ // 自定义事件
+ public event ClickEventHandler? Click;
+
+ //鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+ private bool IsMouseDown = false;
+ private void Button_MouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (!IsMouseDown)
+ return;
+ ModBase.Log("[Control] 按下图标按钮" + (string.IsNullOrEmpty(Name) ? "" : ":" + Name));
+ Click?.Invoke(sender, e);
+ e.Handled = true;
+ Button_MouseUp();
+ ModMain.RaiseCustomEvent(this);
+ }
+
+ private void Button_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ IsMouseDown = true;
+ Focus();
+ // 指向
+ ModAnimation.AniStart(
+ ModAnimation.AaScaleTransform(PanBack, 0.8d - ((ScaleTransform)PanBack.RenderTransform).ScaleX,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)),
+ "MyIconButton Scale " + Uuid);
+ }
+
+ private void Button_MouseUp()
+ {
+ if (IsMouseDown)
+ {
+ IsMouseDown = false;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanBack, 1.05d - ((ScaleTransform)PanBack.RenderTransform).ScaleX,
+ 250, Ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaScaleTransform(PanBack, -0.05d, 250,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong))
+ }, "MyIconButton Scale " + Uuid);
+ }
+
+ RefreshAnim(); // 直接刷新颜色以判断是否已触发 MouseLeave
+ }
+
+ private void Button_MouseLeave()
+ {
+ IsMouseDown = false;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(PanBack, 1d - ((ScaleTransform)PanBack.RenderTransform).ScaleX, 250,
+ Ease: new ModAnimation.AniEaseOutFluent())
+ }, "MyIconButton Scale " + Uuid);
+ RefreshAnim(); // 直接刷新颜色以判断是否已触发 MouseLeave
+ }
+
+ public void RefreshAnim()
+ {
+ try
+ {
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
+ {
+ if (PanBack.Background is null)
+ PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d);
+ if (Path.Fill is null)
+ switch (Theme)
+ {
+ case Themes.Red:
+ {
+ Path.Fill = new ModBase.MyColor(160d, 255d, 76d, 76d);
+ break;
+ }
+ case Themes.Black:
+ {
+ if (ModSecret.IsDarkMode)
+ Path.Fill = new ModBase.MyColor(160d, 255d, 255d, 255d);
+ else
+ Path.Fill = new ModBase.MyColor(160d, 0d, 0d, 0d);
+
+ break;
+ }
+ case Themes.Custom:
+ {
+ Path.Fill = new ModBase.MyColor(160d, Foreground);
+ break;
+ }
+ }
+
+ if (IsMouseOver)
+ {
+ // 指向
+ var AnimList = new List();
+ switch (Theme)
+ {
+ case Themes.Color:
+ {
+ AnimList.Add(
+ ModAnimation.AaColor(Path, Shape.FillProperty, "ColorBrush2", AnimationColorIn));
+ break;
+ }
+ case Themes.White:
+ {
+ AnimList.Add(ModAnimation.AaColor(PanBack, BackgroundProperty,
+ new ModBase.MyColor(50d, 255d, 255d, 255d) - PanBack.Background, AnimationColorIn));
+ break;
+ }
+ case Themes.Red:
+ {
+ AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty,
+ new ModBase.MyColor(255d, 76d, 76d) - Path.Fill, AnimationColorIn));
+ break;
+ }
+ case Themes.Black:
+ {
+ AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty,
+ (ModSecret.IsDarkMode
+ ? new ModBase.MyColor(230d, 255d, 255d, 255d)
+ : new ModBase.MyColor(230d, 0d, 0d, 0d)) - Path.Fill, AnimationColorIn));
+ break;
+ }
+ case Themes.Custom:
+ {
+ AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty,
+ new ModBase.MyColor(255d, Foreground) - Path.Fill, AnimationColorIn));
+ break;
+ }
+ }
+
+ ModAnimation.AniStart(AnimList, "MyIconButton Color " + Uuid);
+ }
+ else
+ {
+ // 普通
+ var AnimList = new List();
+ switch (Theme)
+ {
+ case Themes.Color:
+ {
+ AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty, "ColorBrush4",
+ AnimationColorOut));
+ PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d);
+ break;
+ }
+ case Themes.White:
+ {
+ AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty,
+ new ModBase.MyColor(234d, 242d, 254d), AnimationColorOut));
+ AnimList.Add(ModAnimation.AaColor(PanBack, BackgroundProperty,
+ new ModBase.MyColor(0d, 255d, 255d, 255d) - PanBack.Background, AnimationColorOut));
+ break;
+ }
+ case Themes.Red:
+ {
+ AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty,
+ new ModBase.MyColor(160d, 255d, 76d, 76d) - Path.Fill, AnimationColorOut));
+ PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d);
+ break;
+ }
+ case Themes.Black:
+ {
+ AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty,
+ (ModSecret.IsDarkMode
+ ? new ModBase.MyColor(160d, 255d, 255d, 255d)
+ : new ModBase.MyColor(160d, 0d, 0d, 0d)) - Path.Fill, AnimationColorOut));
+ PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d);
+ break;
+ }
+ case Themes.Custom:
+ {
+ AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty,
+ new ModBase.MyColor(160d, Foreground) - Path.Fill, AnimationColorOut));
+ PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d);
+ break;
+ }
+ }
+
+ ModAnimation.AniStart(AnimList, "MyIconButton Color " + Uuid);
+ }
+ }
+
+ else
+ {
+ ModAnimation.AniStop("MyIconButton Color " + Uuid);
+ switch (Theme)
+ {
+ case Themes.Color:
+ {
+ Path.SetResourceReference(Shape.FillProperty, "ColorBrush5");
+ break;
+ }
+ case Themes.White:
+ {
+ Path.Fill = new ModBase.MyColor(234d, 242d, 254d);
+ break;
+ }
+ case Themes.Red:
+ {
+ Path.Fill = new ModBase.MyColor(160d, 255d, 76d, 76d);
+ break;
+ }
+ case Themes.Black:
+ {
+ if (ModSecret.IsDarkMode)
+ Path.Fill = new ModBase.MyColor(160d, 255d, 255d, 255d);
+ else
+ Path.Fill = new ModBase.MyColor(160d, 0d, 0d, 0d);
+
+ break;
+ }
+ case Themes.Custom:
+ {
+ Path.Fill = new ModBase.MyColor(160d, Foreground);
+ break;
+ }
+ }
+
+ PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d);
+ }
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "刷新图标按钮动画状态出错");
+ }
+ }
+}
+
+public static partial class ModAnimation
+{
+ public static void AniDispose(MyIconButton Control, bool RemoveFromChildren,
+ ParameterizedThreadStart CallBack = null)
+ {
+ if (!Control.IsHitTestVisible)
+ return;
+ Control.IsHitTestVisible = false;
+ AniStart(new[]
+ {
+ AaScaleTransform(Control, -1.5d, 200, Ease: new AniEaseInFluent()),
+ AaCode(() =>
+ {
+ if (RemoveFromChildren)
+ ((Panel)Control.Parent).Children.Remove(Control);
+ else
+ Control.Visibility = Visibility.Collapsed;
+ if (CallBack is not null)
+ CallBack(Control);
+ }, After: true)
+ }, "MyIconButton Dispose " + Control.Uuid);
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml
index abd786d14..5dcb78359 100644
--- a/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml
+++ b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml
@@ -1,9 +1,12 @@
-
+
-
-
+
+
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.cs
new file mode 100644
index 000000000..c44bab718
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.cs
@@ -0,0 +1,288 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Markup;
+using System.Windows.Media;
+using System.Windows.Shapes;
+
+namespace PCL;
+
+[ContentProperty("Inlines")]
+public partial class MyIconTextButton
+{
+ public delegate void ChangeEventHandler(object sender, bool raiseByMouse);
+
+ public delegate void CheckEventHandler(object sender, bool raiseByMouse);
+
+ public delegate void ClickEventHandler(object sender, ModBase.RouteEventArgs e);
+
+ public enum ColorState
+ {
+ Black,
+ Highlight
+ }
+
+ // 动画
+
+ private const int AnimationTimeOfMouseIn = 100; // 鼠标指向动画长度
+ private const int AnimationTimeOfMouseOut = 150; // 鼠标移出动画长度
+
+ public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string),
+ typeof(MyIconTextButton), new PropertyMetadata((sender, e) =>
+ {
+ if (sender is not null) ((MyIconTextButton)sender).LabText.Text = (string)e.NewValue;
+ }));
+
+ public static readonly DependencyProperty ColorTypeProperty = DependencyProperty.Register("ColorType",
+ typeof(ColorState), typeof(MyIconTextButton), new PropertyMetadata(ColorState.Black));
+
+ private double _LogoScale = 1d;
+ private bool IsMouseDown;
+
+ // 基础
+
+ public int Uuid = ModBase.GetUuid();
+
+ public MyIconTextButton()
+ {
+ InitializeComponent();
+
+ MouseLeftButtonUp += (_, _) => MyIconTextButton_MouseUp();
+ MouseLeftButtonDown += (_, _) => MyIconTextButton_MouseDown();
+ MouseLeave += (_, _) => MyIconTextButton_MouseLeave();
+ MouseEnter += RefreshColor;
+ Loaded += RefreshColor;
+ IsEnabledChanged += (_, _) => RefreshColor();
+ }
+
+ // 自定义属性
+
+ public string Logo
+ {
+ get => ShapeLogo.Data.ToString();
+ set
+ {
+ if (ShapeLogo == null) return;
+ ShapeLogo.Data = (Geometry)new GeometryConverter().ConvertFromString(value);
+ }
+ }
+
+ public double LogoScale
+ {
+ get => _LogoScale;
+ set
+ {
+ _LogoScale = value;
+ if (!(ShapeLogo == null))
+ ShapeLogo.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale };
+ }
+ }
+
+ public InlineCollection Inlines => LabText.Inlines;
+
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ } // 内容
+
+ public ColorState ColorType
+ {
+ get => (ColorState)GetValue(ColorTypeProperty);
+ set
+ {
+ if (ColorType == value)
+ return;
+ SetValue(ColorTypeProperty, value);
+ RefreshColor();
+ }
+ } // 颜色类别
+
+ public event CheckEventHandler? Check;
+ public event ChangeEventHandler? Change;
+
+ // 点击事件
+
+ public event ClickEventHandler? Click;
+
+ private void MyIconTextButton_MouseUp()
+ {
+ if (!IsMouseDown)
+ return;
+ ModBase.Log("[Control] 按下带图标按钮:" + Text);
+ IsMouseDown = false;
+ Click?.Invoke(this, new ModBase.RouteEventArgs(true));
+ ModMain.RaiseCustomEvent(this);
+ RefreshColor();
+ }
+
+ private void MyIconTextButton_MouseDown()
+ {
+ IsMouseDown = true;
+ RefreshColor();
+ }
+
+ private void MyIconTextButton_MouseLeave()
+ {
+ IsMouseDown = false;
+ RefreshColor();
+ }
+
+ private void RefreshColor(object obj = null, object e = null)
+ {
+ try
+ {
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0 &&
+ !false.Equals(e)) // 防止默认属性变更触发动画,若强制不执行动画,则 e 为 False
+ {
+ switch (ColorType)
+ {
+ case ColorState.Black:
+ {
+ if (IsMouseDown)
+ {
+ // 按下
+ ModAnimation.AniStart(ModAnimation.AaColor(this, BackgroundProperty, "ColorBrush6", 70),
+ "MyIconTextButton Color " + Uuid);
+ }
+ else if (IsMouseOver)
+ {
+ // 指向
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush3",
+ AnimationTimeOfMouseIn),
+ ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3",
+ AnimationTimeOfMouseIn)
+ }, "MyIconTextButton Checked " + Uuid);
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(this, BackgroundProperty, "ColorBrushBg1", AnimationTimeOfMouseIn),
+ "MyIconTextButton Color " + Uuid);
+ }
+ else if (IsEnabled)
+ {
+ // 正常
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush1",
+ AnimationTimeOfMouseOut),
+ ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush1",
+ AnimationTimeOfMouseOut)
+ }, "MyIconTextButton Checked " + Uuid);
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(this, BackgroundProperty,
+ ModSecret.ColorSemiTransparent - Background, AnimationTimeOfMouseOut),
+ "MyIconTextButton Color " + Uuid);
+ }
+ else
+ {
+ // 禁用
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrushGray5", 100),
+ ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrushGray5", 100)
+ }, "MyIconTextButton Checked " + Uuid);
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(this, BackgroundProperty,
+ ModSecret.ColorSemiTransparent - Background, AnimationTimeOfMouseOut),
+ "MyIconTextButton Color " + Uuid);
+ }
+
+ break;
+ }
+ case ColorState.Highlight:
+ {
+ if (IsMouseDown)
+ {
+ // 按下
+ ModAnimation.AniStart(ModAnimation.AaColor(this, BackgroundProperty, "ColorBrush6", 70),
+ "MyIconTextButton Color " + Uuid);
+ }
+ else if (IsMouseOver)
+ {
+ // 指向
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush3",
+ AnimationTimeOfMouseIn),
+ ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3",
+ AnimationTimeOfMouseIn)
+ }, "MyIconTextButton Checked " + Uuid);
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(this, BackgroundProperty, "ColorBrushBg1", AnimationTimeOfMouseIn),
+ "MyIconTextButton Color " + Uuid);
+ }
+ else if (IsEnabled)
+ {
+ // 正常
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush3",
+ AnimationTimeOfMouseOut),
+ ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3",
+ AnimationTimeOfMouseOut)
+ }, "MyIconTextButton Checked " + Uuid);
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(this, BackgroundProperty,
+ ModSecret.ColorSemiTransparent - Background, AnimationTimeOfMouseOut),
+ "MyIconTextButton Color " + Uuid);
+ }
+ else
+ {
+ // 禁用
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrushGray5", 100),
+ ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrushGray5", 100)
+ }, "MyIconTextButton Checked " + Uuid);
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(this, BackgroundProperty,
+ ModSecret.ColorSemiTransparent - Background, AnimationTimeOfMouseOut),
+ "MyIconTextButton Color " + Uuid);
+ }
+
+ break;
+ }
+ }
+ }
+
+ else
+ {
+ // 不使用动画
+ ModAnimation.AniStop("MyIconTextButton Checked " + Uuid);
+ ModAnimation.AniStop("MyIconTextButton Color " + Uuid);
+ switch (ColorType)
+ {
+ case ColorState.Black:
+ {
+ Background = ModSecret.ColorSemiTransparent;
+ ShapeLogo.SetResourceReference(Shape.FillProperty,
+ IsEnabled ? "ColorBrush1" : "ColorBrushGray5");
+ LabText.SetResourceReference(TextBlock.ForegroundProperty,
+ IsEnabled ? "ColorBrush1" : "ColorBrushGray5");
+ break;
+ }
+ case ColorState.Highlight:
+ {
+ Background = ModSecret.ColorSemiTransparent;
+ ShapeLogo.SetResourceReference(Shape.FillProperty,
+ IsEnabled ? "ColorBrush3" : "ColorBrushGray5");
+ LabText.SetResourceReference(TextBlock.ForegroundProperty,
+ IsEnabled ? "ColorBrush3" : "ColorBrushGray5");
+ break;
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "刷新带图标按钮颜色出错");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyImage.cs b/Plain Craft Launcher 2/Controls/MyImage.cs
new file mode 100644
index 000000000..04a1ce22f
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyImage.cs
@@ -0,0 +1,258 @@
+using System.Collections.Concurrent;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using PCL.Core.IO.Net.Http.Client.Request;
+using PCL.Core.Utils;
+
+namespace PCL;
+
+public class MyImage : Image
+{
+ private string _ActualSource;
+
+ public MyImage()
+ {
+ Initialized += (_, _) => Load();
+ SizeChanged += (_, _) => UpdateClip();
+ }
+
+ ///
+ /// 实际被呈现的图片地址。
+ ///
+ public string ActualSource
+ {
+ get => _ActualSource;
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ value = null;
+ if ((_ActualSource ?? "") == (value ?? ""))
+ return;
+ _ActualSource = value;
+ Dispatcher.BeginInvoke(new Func(async () =>
+ {
+ try
+ {
+ ImageSource bitmap = value is null ? null : await Task.Run(() => new MyBitmap(value));
+ base.Source = bitmap;
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, $"加载图片失败({value})");
+ try
+ {
+ if (value.StartsWithF(ModBase.PathTemp) && File.Exists(value)) File.Delete(value);
+ }
+ catch
+ {
+ }
+ }
+ })); // 在这里先触发可能的文件读取,尽量避免在 UI 线程中读取文件
+ // ignored
+ }
+ }
+
+ private void Load() // 属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868)
+ {
+ if (Source is null)
+ {
+ ActualSource = null;
+ return;
+ }
+
+ if (!Source.StartsWithF("http"))
+ {
+ ActualSource = Source;
+ return;
+ }
+
+ var Url = Source;
+ var TempPath = GetTempPath(Url);
+ var TempFile = new FileInfo(TempPath);
+ var EnableCache = this.EnableCache;
+ if (EnableCache && TempFile.Exists)
+ {
+ ActualSource = TempPath;
+ if (DateTime.Now - TempFile.LastWriteTime < FileCacheExpiredTime)
+ return; // 无需刷新缓存
+ }
+
+ Dispatcher.BeginInvoke(new Func(async () =>
+ {
+ try
+ {
+ // 下载
+ ActualSource = LoadingSource;
+
+ var resp = await DownloadImageAsync(Url);
+ if (!string.IsNullOrEmpty(resp))
+ {
+ ActualSource = resp;
+ return;
+ }
+
+ resp = await DownloadImageAsync(FallbackSource);
+ if (!string.IsNullOrEmpty(resp))
+ {
+ ActualSource = resp;
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ // 更换备用地址
+ ModBase.Log(ex, $"Online image get fail(source = {Url}, fallback = {FallbackSource})", ModBase.LogLevel.Developer);
+ TempPath = GetTempPath(Url);
+ TempFile = new FileInfo(TempPath);
+ if (EnableCache && TempFile.Exists)
+ {
+ ActualSource = TempPath;
+ if (DateTime.Now - TempFile.LastWriteTime < FileCacheExpiredTime)
+ return;
+ }
+ }
+ }));
+ }
+
+ public static Task DownloadImageAsync(string url)
+ {
+ return _downloadTasks.GetOrAdd(url, key =>
+ {
+ var t = DownloadImageInternalAsync(key);
+ t.ContinueWith(_ => _downloadTasks.TryRemove(url, out _));
+ return t;
+ });
+ }
+
+ public static string GetTempPath(string Url)
+ {
+ return Path.Combine(ModBase.PathTemp, "Cache", "Images", $"{ModBase.GetStringMD5(Url)}.png");
+ }
+
+ private static readonly ConcurrentDictionary> _downloadTasks = new();
+
+ private static async Task DownloadImageInternalAsync(string url)
+ {
+ var tempPath = GetTempPath(url);
+ var tempDownloadingPath = tempPath + RandomUtils.NextInt(0, 1000000);
+
+ try
+ {
+ Directory.CreateDirectory(ModBase.GetPathFromFullPath(tempPath)); // 重新实现下载,以避免携带 Header(#5072)
+ using (var fs = new FileStream(tempDownloadingPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read))
+ {
+ using (var response = await HttpRequest.Create(url)
+ .WithHttpVersionOption(HttpVersion.Version30)
+ .SendAsync(addMetedata: false))
+ {
+ response.EnsureSuccessStatusCode();
+
+ using (var nfs = await response.AsStreamAsync())
+ {
+ fs.SetLength(0L);
+ await nfs.CopyToAsync(fs);
+ }
+ }
+ }
+
+ File.Move(tempDownloadingPath, tempPath, true);
+ return tempPath;
+ }
+ catch (Exception ex)
+ {
+ if (File.Exists(tempPath)) File.Delete(tempPath);
+ if (File.Exists(tempDownloadingPath)) File.Delete(tempDownloadingPath);
+
+ ModBase.Log(ex, $"Try to get online image fail (url = {url}, dest = {tempPath})");
+ return string.Empty;
+ }
+ }
+
+ #region 公开属性
+
+ public TimeSpan FileCacheExpiredTime = TimeSpan.FromDays(14d);
+
+ public bool EnableCache
+ {
+ get => (bool)GetValue(EnableCacheProperty);
+ set => SetValue(EnableCacheProperty, value);
+ }
+
+ public new static readonly DependencyProperty EnableCacheProperty =
+ DependencyProperty.Register("EnableCache", typeof(bool), typeof(MyImage), new PropertyMetadata(true));
+
+ ///
+ /// 与 Image 的 Source 类似。
+ /// 若输入以 http 开头的字符串,则会尝试下载图片然后显示,图片会保存为本地缓存。
+ /// 支持 WebP 格式的图片。
+ ///
+ public new string Source // 覆写 Image 的 Source 属性
+ {
+ get => _Source;
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ value = null;
+ if ((_Source ?? "") == (value ?? ""))
+ return;
+ _Source = value;
+ if (!IsInitialized)
+ return; // 属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868)
+ Load();
+ }
+ }
+
+ private string _Source = "";
+
+ public new static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(string),
+ typeof(MyImage), new PropertyMetadata((sender, e) =>
+ {
+ if (sender is not null) ((MyImage)sender).Source = e.NewValue.ToString();
+ }));
+
+ ///
+ /// 当 Source 首次下载失败时,会从该备用地址加载图片。
+ ///
+ public string FallbackSource { get; set; }
+
+ ///
+ /// 正在下载网络图片时显示的本地图片。
+ ///
+ public string LoadingSource { get; set; } = "pack://application:,,,/images/Icons/NoIcon.png";
+ public CornerRadius CornerRadius
+ {
+ get => (CornerRadius)GetValue(CornerRadiusProperty);
+ set => SetValue(CornerRadiusProperty, value);
+ }
+ private static readonly DependencyProperty CornerRadiusProperty =
+ DependencyProperty.Register(
+ "CornerRadius",
+ typeof(CornerRadius),
+ typeof(MyImage),
+ new FrameworkPropertyMetadata(
+ new CornerRadius(-1),
+ OnCornerRadiusChanged)
+ );
+
+ private static void OnCornerRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ ((MyImage)d).UpdateClip();
+ }
+
+ private void UpdateClip() // Handles Me.SizeChanged will be added separately
+ {
+ if (ActualWidth > 0 && ActualHeight > 0 &&
+ CornerRadius.TopLeft >= 0 && CornerRadius.TopRight >= 0)
+ {
+ Clip = new RectangleGeometry(
+ new Rect(0, 0, ActualWidth, ActualHeight),
+ CornerRadius.TopLeft,
+ CornerRadius.TopRight);
+ }
+ }
+ #endregion
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyListItem.xaml b/Plain Craft Launcher 2/Controls/MyListItem.xaml
index 19b54c455..278fd5b5b 100644
--- a/Plain Craft Launcher 2/Controls/MyListItem.xaml
+++ b/Plain Craft Launcher 2/Controls/MyListItem.xaml
@@ -1,17 +1,17 @@
-
+
-
+
-
-
+
+
@@ -22,7 +22,11 @@
-
+
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyListItem.xaml.cs b/Plain Craft Launcher 2/Controls/MyListItem.xaml.cs
new file mode 100644
index 000000000..18b9d7340
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyListItem.xaml.cs
@@ -0,0 +1,992 @@
+using System.Collections;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Markup;
+using System.Windows.Media;
+using System.Windows.Shapes;
+
+namespace PCL;
+
+[ContentProperty("Inlines")]
+public partial class MyListItem : IMyRadio
+{
+ public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e);
+
+ public delegate void LogoClickEventHandler(object sender, MouseButtonEventArgs e);
+
+ public bool IsMouseOverAnimationEnabled = true;
+
+ private string StateLast;
+
+ public object tag { get; set; }
+ public event IMyRadio.CheckEventHandler? Check;
+ public event IMyRadio.ChangedEventHandler? Changed;
+
+ public event ClickEventHandler? Click;
+ public event LogoClickEventHandler? LogoClick;
+
+ public void RefreshColor(object sender, EventArgs e)
+ {
+ // 菜单虚拟化检测
+ if (ContentHandler is not null)
+ {
+ ContentHandler.Invoke(this, e);
+ ContentHandler = null;
+ }
+
+ // 判断当前颜色
+ string StateNew;
+ int Time;
+ if (IsMouseDown && !(Type == CheckType.RadioBox && Checked))
+ {
+ StateNew = "MouseDown";
+ Time = 120;
+ }
+ else if (IsMouseOver && IsMouseOverAnimationEnabled)
+ {
+ StateNew = "MouseOver";
+ Time = 120;
+ }
+ else
+ {
+ StateNew = "Idle";
+ Time = 180;
+ }
+
+ if ((StateLast ?? "") == (StateNew ?? ""))
+ return;
+ StateLast = StateNew;
+ // 触发颜色动画
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
+ {
+ // 有动画
+ var Ani = new List();
+ if (IsMouseOver && IsMouseOverAnimationEnabled)
+ {
+ if (ButtonStack is not null)
+ {
+ Ani.Add(ModAnimation.AaOpacity(ButtonStack, 1d - ButtonStack.Opacity, (int)Math.Round(Time * 0.7d),
+ (int)Math.Round(Time * 0.3d)));
+ Ani.Add(ModAnimation.AaDouble(
+ i => ColumnPaddingRight.Width =
+ new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)),
+ Math.Max(MinPaddingRight, 5 + Buttons.Count() * 25) - ColumnPaddingRight.Width.Value,
+ (int)Math.Round(Time * 0.3d), (int)Math.Round(Time * 0.7d)));
+ }
+
+ Ani.AddRange(new[]
+ {
+ ModAnimation.AaColor(RectBack, Border.BackgroundProperty,
+ IsMouseDown ? "ColorBrush6" : "ColorBrushBg1", Time),
+ ModAnimation.AaOpacity(RectBack, 1d - RectBack.Opacity, Time,
+ Ease: new ModAnimation.AniEaseOutFluent())
+ });
+ if (IsScaleAnimationEnabled)
+ {
+ Ani.Add(ModAnimation.AaScaleTransform(RectBack,
+ 1d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, (int)Math.Round(Time * 1.6d),
+ Ease: new ModAnimation.AniEaseOutFluent()));
+ if (IsMouseDown)
+ Ani.Add(ModAnimation.AaScaleTransform(this, 0.98d - ((ScaleTransform)RenderTransform).ScaleX,
+ (int)Math.Round(Time * 0.9d), Ease: new ModAnimation.AniEaseOutFluent()));
+ else
+ Ani.Add(ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX,
+ (int)Math.Round(Time * 1.2d), Ease: new ModAnimation.AniEaseOutFluent()));
+ }
+ }
+ else
+ {
+ if (ButtonStack is not null)
+ {
+ Ani.Add(ModAnimation.AaOpacity(ButtonStack, -ButtonStack.Opacity, (int)Math.Round(Time * 0.4d)));
+ Ani.Add(ModAnimation.AaDouble(
+ i => ColumnPaddingRight.Width =
+ new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)),
+ MinPaddingRight - ColumnPaddingRight.Width.Value, (int)Math.Round(Time * 0.4d)));
+ }
+
+ Ani.Add(ModAnimation.AaOpacity(RectBack, -RectBack.Opacity, Time));
+ if (IsScaleAnimationEnabled)
+ Ani.AddRange(new[]
+ {
+ ModAnimation.AaColor(RectBack, Border.BackgroundProperty,
+ IsMouseDown ? "ColorBrush6" : "ColorBrush7", Time),
+ ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, Time * 3,
+ Ease: new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaScaleTransform(RectBack,
+ 0.996d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, Time,
+ Ease: new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaScaleTransform(RectBack, -0.246d, 1, After: true)
+ });
+ }
+
+ ModAnimation.AniStart(Ani, "ListItem Color " + Uuid);
+ }
+ else
+ {
+ // 无动画
+ if (IsMouseOver && IsMouseOverAnimationEnabled)
+ {
+ if (ButtonStack is not null)
+ {
+ ButtonStack.Opacity = 1d;
+ ColumnPaddingRight.Width = new GridLength(Math.Max(MinPaddingRight, 5 + Buttons.Count() * 25));
+ }
+
+ // 由于鼠标已经移入,所以直接实例化 RectBack
+ RectBack.Background = (Brush)ModSecret.AppResources["ColorBrushBg1"];
+ RectBack.Opacity = 1d;
+ RectBack.RenderTransform = new ScaleTransform(1d, 1d);
+ RenderTransform = new ScaleTransform(1d, 1d);
+ }
+ else
+ {
+ if (ButtonStack is not null)
+ {
+ ButtonStack.Opacity = 0d;
+ ColumnPaddingRight.Width = new GridLength(MinPaddingRight);
+ }
+
+ RenderTransform = new ScaleTransform(1d, 1d);
+ if (_RectBack is not null)
+ {
+ if (IsScaleAnimationEnabled)
+ RectBack.RenderTransform = new ScaleTransform(0.75d, 0.75d);
+ RectBack.Background = (Brush)ModSecret.AppResources["ColorBrush7"];
+ RectBack.Opacity = 0d;
+ }
+ }
+
+ ModAnimation.AniStop("ListItem Color " + Uuid);
+ }
+ }
+
+ private void MyListItem_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (Checked)
+ SetResourceReference(ForegroundProperty, Height < 40d ? "ColorBrush3" : "ColorBrush2");
+ else
+ SetResourceReference(ForegroundProperty, "ColorBrush1");
+ ColumnPaddingRight.Width = new GridLength(MinPaddingRight);
+ if (CustomEventService.GetEventType(this) == CustomEvent.EventType.打开帮助 && !(Title != "" && Info != "")) // #3266
+ {
+ try
+ {
+ ModMain.HelpEntry entry = new ModMain.HelpEntry(CustomEvent.GetAbsoluteUrls(CustomEventService.GetEventData(this), CustomEventService.GetEventType(this))[0]);
+ entry.SetToListItem(this);
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "设置帮助 MyListItem 失败", ModBase.LogLevel.Msgbox);
+ CustomEventService.SetEventType(this, CustomEvent.EventType.None);
+ CustomEventService.SetEventData(this, "");
+ }
+ }
+ }
+
+ public override string ToString()
+ {
+ return Title;
+ }
+
+ #region 后加载控件
+
+ // 指向背景
+ private Border _RectBack;
+
+ public Border RectBack
+ {
+ get
+ {
+ if (_RectBack is null)
+ {
+ var Rect = new Border
+ {
+ Name = "RectBack",
+ CornerRadius = new CornerRadius(IsScaleAnimationEnabled || Height > 40d ? 6 : 0),
+ RenderTransform = IsScaleAnimationEnabled ? new ScaleTransform(0.8d, 0.8d) : null,
+ RenderTransformOrigin = new Point(0.5d, 0.5d),
+ BorderThickness = new Thickness(ModBase.GetWPFSize(1d)),
+ SnapsToDevicePixels = true,
+ IsHitTestVisible = false,
+ Opacity = 0d
+ };
+ Rect.SetResourceReference(Border.BackgroundProperty, "ColorBrush7");
+ Rect.SetResourceReference(Border.BorderBrushProperty, "ColorBrush6");
+ SetColumnSpan(Rect, 999);
+ SetRowSpan(Rect, 999);
+ Children.Insert(0, Rect);
+ _RectBack = Rect;
+ //
+ }
+
+ return _RectBack;
+ }
+ }
+
+ // 按钮
+ public FrameworkElement ButtonStack;
+
+ // 图标
+ public FrameworkElement PathLogo;
+
+ // 勾选条
+ public Border RectCheck;
+
+
+ ///
+ /// Tags 的存放 StackPanel
+ ///
+ public StackPanel _PanTags;
+
+ public StackPanel PanTags
+ {
+ get
+ {
+ if (_PanTags is not null)
+ return _PanTags;
+ var NewStack = new StackPanel
+ {
+ IsHitTestVisible = false,
+ Orientation = Orientation.Horizontal,
+ VerticalAlignment = VerticalAlignment.Bottom,
+ Margin = new Thickness(3.5d, 0d, -3, 0d)
+ };
+ SetColumn(NewStack, 3);
+ SetRow(NewStack, 2);
+ PanBack.Children.Add(NewStack);
+ _PanTags = NewStack;
+ return _PanTags;
+ }
+ }
+
+ ///
+ /// 标签,可以传入 String 和 List(Of String)
+ ///
+ public object Tags
+ {
+ set
+ {
+ var list = new List();
+ if (value is string str) list = str.Split("|").ToList();
+ if (value is List) list = (List)value;
+ PanTags.Children.Clear();
+ PanTags.Visibility = list.Any() ? Visibility.Visible : Visibility.Collapsed;
+ foreach (var TagText in list)
+ {
+ var NewTag = new Border
+ {
+ Background = new SolidColorBrush(Color.FromArgb(17, 0, 0, 0)),
+ Padding = new Thickness(3d, 1d, 3d, 1d),
+ CornerRadius = new CornerRadius(3d),
+ Margin = new Thickness(0d, 0d, 3d, 0d),
+ SnapsToDevicePixels = true,
+ UseLayoutRounding = false
+ };
+ var TagTextBlock = new TextBlock
+ {
+ Text = TagText,
+ Foreground = new SolidColorBrush(Color.FromRgb(134, 134, 134)),
+ FontSize = 11d
+ };
+ NewTag.Child = TagTextBlock;
+ PanTags.Children.Add(NewTag);
+ }
+ }
+ }
+
+ // 副文本
+ private TextBlock _LabInfo;
+
+ public TextBlock LabInfo
+ {
+ get
+ {
+ if (_LabInfo is null)
+ {
+ var Lab = new TextBlock
+ {
+ Name = "LabInfo",
+ SnapsToDevicePixels = false,
+ UseLayoutRounding = false,
+ HorizontalAlignment = HorizontalAlignment.Left,
+ IsHitTestVisible = false,
+ TextTrimming = TextTrimming.CharacterEllipsis,
+ Visibility = Visibility.Collapsed,
+ FontSize = 12d,
+ Margin = new Thickness(4d, 0d, 0d, 0d),
+ Opacity = 0.6d
+ };
+ SetColumn(Lab, 4);
+ SetRow(Lab, 2);
+ PanBack.Children.Add(Lab);
+ _LabInfo = Lab;
+ //
+ }
+
+ return _LabInfo;
+ }
+ }
+
+ #endregion
+
+ #region 自定义属性
+
+ // Uuid
+ public int Uuid = ModBase.GetUuid();
+
+ ///
+ /// 是否启用缩放动画。
+ ///
+ public bool IsScaleAnimationEnabled
+ {
+ get => _IsScaleAnimationEnabled;
+ set
+ {
+ _IsScaleAnimationEnabled = value;
+ if (_RectBack is not null)
+ RectBack.CornerRadius = new CornerRadius(value ? 6 : 0);
+ }
+ }
+
+ private bool _IsScaleAnimationEnabled = true;
+
+ // 边距
+ public int PaddingLeft
+ {
+ get => (int)Math.Round(ColumnPaddingLeft.Width.Value);
+ set => ColumnPaddingLeft.Width = new GridLength(value);
+ }
+
+ ///
+ /// 右边距的最小值。
+ /// 在存在右侧按钮时,右边距会被自动设置为 5 + 按钮数 * 25。
+ ///
+ public int MinPaddingRight { get; set; } = 4;
+
+ // 按钮
+ private IEnumerable _Buttons;
+
+ public IEnumerable Buttons
+ {
+ get => _Buttons;
+ set
+ {
+ _Buttons = value;
+ // 没有特殊按钮,移除原 Stack
+ if (ButtonStack is not null)
+ {
+ Children.Remove(ButtonStack);
+ ButtonStack = null;
+ }
+
+ // 添加新 Stack
+ switch (value.Count())
+ {
+ case 0:
+ {
+ break;
+ }
+ // 没有按钮,不添加新的
+ case 1:
+ {
+ // 只有一个按钮
+ foreach (var Btn in value)
+ {
+ if (Btn.Height.Equals(double.NaN))
+ Btn.Height = 25d;
+ if (Btn.Width.Equals(double.NaN))
+ Btn.Width = 25d;
+ Btn.Opacity = 0d;
+ Btn.Margin = new Thickness(0d, 0d, 5d, 0d);
+ Btn.SnapsToDevicePixels = false;
+ Btn.HorizontalAlignment = HorizontalAlignment.Right;
+ Btn.VerticalAlignment = VerticalAlignment.Center;
+ Btn.SnapsToDevicePixels = false;
+ Btn.UseLayoutRounding = false;
+ SetColumnSpan(Btn, 10);
+ SetRowSpan(Btn, 10);
+ Children.Add(Btn);
+ ButtonStack = Btn;
+ }
+
+ break;
+ }
+
+ default:
+ {
+ // 有复数按钮,使用 StackPanel
+ ButtonStack = new StackPanel
+ {
+ Opacity = 0d, Margin = new Thickness(0d, 0d, 5d, 0d), SnapsToDevicePixels = false,
+ Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Center, UseLayoutRounding = false
+ };
+ SetColumnSpan(ButtonStack, 10);
+ SetRowSpan(ButtonStack, 10);
+ // 构造按钮
+ foreach (var Btn in value)
+ {
+ if (Btn.Height.Equals(double.NaN))
+ Btn.Height = 25d;
+ if (Btn.Width.Equals(double.NaN))
+ Btn.Width = 25d;
+ ((StackPanel)ButtonStack).Children.Add(Btn);
+ }
+
+ Children.Add(ButtonStack);
+ break;
+ }
+ }
+ }
+ }
+
+ // 标题
+ public InlineCollection Inlines => LabTitle.Inlines;
+
+ public string Title
+ {
+ get => (string)GetValue(TitleProperty);
+ set => SetValue(TitleProperty, value.Replace("\r", "").Replace("\n", ""));
+ }
+
+ public static readonly DependencyProperty TitleProperty =
+ DependencyProperty.Register("Title", typeof(string), typeof(MyListItem));
+
+ // 字号
+ public double FontSize
+ {
+ get => (double)GetValue(FontSizeProperty);
+ set => SetValue(FontSizeProperty, value);
+ }
+
+ public static readonly DependencyProperty FontSizeProperty =
+ DependencyProperty.Register("FontSize", typeof(double), typeof(MyListItem), new PropertyMetadata(14d));
+
+ // 信息
+ public string Info
+ {
+ get => (string)GetValue(InfoProperty);
+ set
+ {
+ if (Info == value)
+ return;
+ value = value?.Replace("\r", "").Replace("\n", "");
+ SetValue(InfoProperty, value);
+ }
+ }
+
+ public static readonly DependencyProperty InfoProperty = DependencyProperty.Register("Info", typeof(string),
+ typeof(MyListItem), new PropertyMetadata("", OnInfoChanged));
+
+ public MyListItem()
+ {
+ InitializeComponent();
+
+ SizeChanged += (_, _) => OnSizeChanged();
+ PreviewMouseLeftButtonUp += Button_MouseUp;
+ PreviewMouseLeftButtonDown += Button_MouseDown;
+ MouseLeave += Button_MouseLeave;
+ PreviewMouseLeftButtonUp += Button_MouseLeave;
+ MouseEnter += RefreshColor;
+ MouseLeave += RefreshColor;
+ MouseLeftButtonDown += RefreshColor;
+ MouseLeftButtonUp += RefreshColor;
+ Loaded += MyListItem_Loaded;
+ }
+
+ private static void OnInfoChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = (MyListItem)d;
+ var value = e.NewValue as string;
+ control.LabInfo.Text = value;
+ control.LabInfo.Visibility = string.IsNullOrEmpty(value) ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+ // 图片
+ public string Logo
+ {
+ get => (string)GetValue(LogoProperty);
+ set
+ {
+ if (Logo == value)
+ return;
+ SetValue(LogoProperty, value);
+ }
+ }
+
+ public static readonly DependencyProperty LogoProperty = DependencyProperty.Register("Logo", typeof(string),
+ typeof(MyListItem), new PropertyMetadata("", OnLogoChanged));
+
+ private static void OnLogoChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = (MyListItem)d;
+ var value = e.NewValue as string;
+ control.UpdateLogo(value);
+ }
+
+ private void UpdateLogo(string _Logo)
+ {
+ // 删除旧 Logo
+ if (!(PathLogo == null))
+ Children.Remove(PathLogo);
+ // 添加新 Logo
+ if (!string.IsNullOrEmpty(_Logo))
+ {
+ if (_Logo.StartsWithF("http", true))
+ {
+ // 网络图片
+ PathLogo = new MyImage
+ {
+ Tag = this,
+ IsHitTestVisible = LogoClickable,
+ Source = _Logo,
+ RenderTransformOrigin = new Point(0.5d, 0.5d),
+ RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale },
+ SnapsToDevicePixels = true,
+ UseLayoutRounding = false
+ };
+ RenderOptions.SetBitmapScalingMode(PathLogo, BitmapScalingMode.Linear);
+ }
+ else if (_Logo.EndsWithF(".png", true) || _Logo.EndsWithF(".jpg", true) || _Logo.EndsWithF(".webp", true))
+ {
+ // 位图
+ PathLogo = new Canvas
+ {
+ Tag = this,
+ IsHitTestVisible = LogoClickable,
+ Background = new MyBitmap(_Logo),
+ RenderTransformOrigin = new Point(0.5d, 0.5d),
+ RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale },
+ SnapsToDevicePixels = true,
+ UseLayoutRounding = false,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ VerticalAlignment = VerticalAlignment.Stretch
+ };
+ if (_Logo.Contains(ModBase.PathTemp + @"Cache\Skin\Head") ||
+ _Logo.Contains(ModBase.PathTemp + @"Cache\Cape"))
+ RenderOptions.SetBitmapScalingMode(PathLogo, BitmapScalingMode.NearestNeighbor);
+ else
+ RenderOptions.SetBitmapScalingMode(PathLogo, BitmapScalingMode.Linear);
+ }
+ else
+ {
+ // 矢量图
+ PathLogo = new Path
+ {
+ Tag = this,
+ IsHitTestVisible = LogoClickable,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Stretch = Stretch.Uniform,
+ Data = (Geometry)new GeometryConverter().ConvertFromString(_Logo),
+ RenderTransformOrigin = new Point(0.5d, 0.5d),
+ RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale },
+ SnapsToDevicePixels = false,
+ UseLayoutRounding = false
+ };
+ PathLogo.SetBinding(Shape.FillProperty, new Binding("Foreground") { Source = this });
+ }
+
+ SetColumn(PathLogo, 2);
+ SetRowSpan(PathLogo, 4);
+ OnSizeChanged(); // 设置边距
+ Children.Add(PathLogo);
+ // 图标的点击事件
+ if (LogoClickable)
+ {
+ PathLogo.MouseLeave += (sender, e) => IsLogoDown = false;
+ PathLogo.MouseLeftButtonDown += (sender, e) => IsLogoDown = true;
+ PathLogo.MouseLeftButtonUp += (sender, e) =>
+ {
+ if (IsLogoDown)
+ {
+ IsLogoDown = false;
+ LogoClick?.Invoke(((FrameworkElement)sender).Tag, e);
+ }
+ };
+ }
+ }
+
+ // 改变行距
+ ColumnLogo.Width = new GridLength((string.IsNullOrEmpty(_Logo) ? 0 : 34) + (Height < 40d ? 0 : 4));
+ }
+
+ private double _LogoScale = 1d;
+
+ public double LogoScale
+ {
+ get => _LogoScale;
+ set
+ {
+ _LogoScale = value;
+ if (!(PathLogo == null))
+ PathLogo.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale };
+ }
+ }
+
+ // 图标的点击
+ ///
+ /// 该 Logo 是否可用点击触发事件。需要在 Logo 属性之前设置。
+ ///
+ public bool LogoClickable { get; set; } = false;
+
+ private bool IsLogoDown;
+
+ // 勾选选项
+ public enum CheckType
+ {
+ None,
+ Clickable,
+ RadioBox,
+ CheckBox
+ }
+
+ private CheckType _Type = CheckType.None;
+
+ public CheckType Type
+ {
+ get => _Type;
+ set
+ {
+ if (_Type == value)
+ return;
+ _Type = value;
+ // 切换左栏大小
+ ColumnCheck.Width =
+ new GridLength(_Type == CheckType.None || _Type == CheckType.Clickable ? Height < 40d ? 4 : 2 : 6);
+ // 切换竖条控件
+ if (_Type == CheckType.None || _Type == CheckType.Clickable)
+ {
+ // 移除竖条控件
+ if (!(RectCheck == null))
+ {
+ Children.Remove(RectCheck);
+ RectCheck = null;
+ }
+
+ SetChecked(false, false, false);
+ }
+ // 添加竖条控件
+ else if (RectCheck == null)
+ {
+ RectCheck = new Border
+ {
+ Width = 5d,
+ Height = Checked ? double.NaN : 0d,
+ CornerRadius = new CornerRadius(2d, 2d, 2d, 2d),
+ VerticalAlignment = Checked ? VerticalAlignment.Stretch : VerticalAlignment.Center,
+ HorizontalAlignment = HorizontalAlignment.Left,
+ UseLayoutRounding = false,
+ SnapsToDevicePixels = false,
+ Margin = Checked ? new Thickness(-1, 6d, 0d, 6d) : new Thickness(-1, 0d, 0d, 0d)
+ };
+ RectCheck.SetResourceReference(Border.BackgroundProperty, "ColorBrush3");
+ SetRowSpan(RectCheck, 4);
+ Children.Add(RectCheck);
+ }
+ }
+ }
+
+ // 适应尺寸
+ private void OnSizeChanged()
+ {
+ var _Logo = Logo;
+ ColumnCheck.Width =
+ new GridLength(_Type == CheckType.None || _Type == CheckType.Clickable ? Height < 40d ? 4 : 2 : 6);
+ ColumnLogo.Width = new GridLength((string.IsNullOrEmpty(_Logo) ? 0 : 34) + (Height < 40d ? 0 : 4));
+ if (PathLogo is not null)
+ {
+ if (_Logo.EndsWithF(".png", true) || _Logo.EndsWithF(".jpg", true) || _Logo.EndsWithF(".webp", true))
+ PathLogo.Margin = new Thickness(4d, 5d, 3d, 5d);
+ else
+ PathLogo.Margin = new Thickness(Height < 40d ? 6 : 8, 8d, Height < 40d ? 4 : 6, 8d);
+ }
+
+ LabTitle.Margin = new Thickness(4d, 0d, 0d, Height < 40d ? 0 : 2);
+ }
+
+ // 勾选状态
+ private bool _Checked;
+
+ public bool Checked
+ {
+ get => _Checked;
+ set => SetChecked(value, false, value != _Checked); // 仅在值发生变化时触发动画 (#4596)
+ }
+
+ ///
+ /// 手动设置 Checked 属性。
+ ///
+ /// 新的 Checked 属性。
+ /// 是否由用户引发。
+ /// 是否执行动画。
+ public void SetChecked(bool value, bool user, bool anime)
+ {
+ try
+ {
+ // 自定义属性基础
+
+ var ChangedEventArgs = new ModBase.RouteEventArgs(user);
+ var RawValue = _Checked;
+ if (Type == CheckType.RadioBox)
+ {
+ if (IsInitialized && !(value == _Checked))
+ {
+ _Checked = value;
+ Changed?.Invoke(this, ChangedEventArgs);
+ if (ChangedEventArgs.Handled)
+ {
+ _Checked = RawValue;
+ return;
+ }
+ }
+
+ _Checked = value;
+ }
+ else
+ {
+ if (value == _Checked)
+ return;
+ _Checked = value;
+ if (IsInitialized)
+ {
+ Changed?.Invoke(this, ChangedEventArgs);
+ if (ChangedEventArgs.Handled)
+ {
+ _Checked = RawValue;
+ return;
+ }
+ }
+ }
+
+ if (value)
+ {
+ var CheckEventArgs = new ModBase.RouteEventArgs(user);
+ Check?.Invoke(this, CheckEventArgs);
+ if (CheckEventArgs.Handled)
+ return;
+ }
+
+ // 保证只有一个单选 ListItem 选中
+
+ if (Type == CheckType.RadioBox)
+ {
+ if (Parent == null)
+ return;
+ var RadioboxList = new List();
+ var CheckedCount = 0;
+ // 收集控件列表与选中个数
+ foreach (var ControlRaw in ((Panel)Parent).Children)
+ {
+ var Control = MyVirtualizingElement.TryInit((FrameworkElement)ControlRaw);
+ if (Control is MyListItem listItem && listItem.Type == CheckType.RadioBox)
+ {
+ RadioboxList.Add(listItem);
+ if (listItem.Checked)
+ CheckedCount += 1;
+ }
+ }
+
+ // 判断选中情况
+ switch (CheckedCount)
+ {
+ case 0:
+ {
+ // 没有任何单选框被选中,选择第一个
+ RadioboxList[0].Checked = true;
+ break;
+ }
+ case var @case when @case > 1:
+ {
+ // 选中项目多于 1 个
+ if (Checked)
+ {
+ // 如果本控件选中,则取消其他所有控件的选中
+ foreach (var Control in RadioboxList)
+ if (Control.Checked && !Control.Equals(this))
+ Control.Checked = false;
+ }
+ else
+ {
+ // 如果本控件未选中,则只保留第一个选中的控件
+ var FirstChecked = false;
+ foreach (var Control in RadioboxList)
+ if (Control.Checked)
+ {
+ if (FirstChecked)
+ Control.Checked = false; // 修改 Checked 会自动触发 Change 事件,所以不用额外触发
+ else
+ FirstChecked = true;
+ }
+ }
+
+ break;
+ }
+ }
+ }
+
+ // 更改动画
+
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0 && anime) // 防止默认属性变更触发动画
+ {
+ var Anim = new List();
+ if (Checked)
+ {
+ // 由无变有
+ if (!(RectCheck == null))
+ {
+ var Delta = 20;
+ Anim.Add(ModAnimation.AaHeight(RectCheck, Delta * 0.4d, 200,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
+ Anim.Add(ModAnimation.AaHeight(RectCheck, Delta * 0.6d, 300,
+ Ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)));
+ Anim.Add(ModAnimation.AaOpacity(RectCheck, 1d - RectCheck.Opacity, 30));
+ RectCheck.VerticalAlignment = VerticalAlignment.Center;
+ RectCheck.Margin = new Thickness(-1, 0d, 0d, 0d);
+ }
+
+ Anim.Add(ModAnimation.AaColor(this, ForegroundProperty,
+ Height < 40d ? "ColorBrush3" : "ColorBrush2", 200));
+ }
+ else
+ {
+ // 由有变无
+ if (!(RectCheck == null))
+ {
+ // Anim.Add(AaWidth(RectCheck, -RectCheck.Width, 120,, New AniEaseInFluent))
+ Anim.Add(ModAnimation.AaHeight(RectCheck, -RectCheck.ActualHeight, 120,
+ Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)));
+ Anim.Add(ModAnimation.AaOpacity(RectCheck, -RectCheck.Opacity, 70, 40));
+ RectCheck.VerticalAlignment = VerticalAlignment.Center;
+ }
+
+ Anim.Add(ModAnimation.AaColor(this, ForegroundProperty, "ColorBrush1", 120));
+ }
+
+ ModAnimation.AniStart(Anim, "MyListItem Checked " + Uuid);
+ }
+ else
+ {
+ // 不使用动画
+ ModAnimation.AniStop("MyListItem Checked " + Uuid);
+ if (Checked)
+ {
+ if (!(RectCheck == null))
+ {
+ RectCheck.Height = double.NaN;
+ RectCheck.Margin = new Thickness(-1, 6d, 0d, 6d);
+ RectCheck.Opacity = 1d;
+ RectCheck.VerticalAlignment = VerticalAlignment.Stretch;
+ }
+
+ SetResourceReference(ForegroundProperty, Height < 40d ? "ColorBrush3" : "ColorBrush2");
+ }
+ else
+ {
+ if (!(RectCheck == null))
+ {
+ RectCheck.Height = 0d;
+ RectCheck.Margin = new Thickness(-1, 0d, 0d, 0d);
+ RectCheck.Opacity = 0d;
+ RectCheck.VerticalAlignment = VerticalAlignment.Center;
+ }
+
+ SetResourceReference(ForegroundProperty, "ColorBrush1");
+ }
+ }
+ }
+
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "设置 Checked 失败");
+ }
+ }
+
+ // 前景色绑定
+ public Brush Foreground
+ {
+ get => (Brush)GetValue(ForegroundProperty);
+ set => SetValue(ForegroundProperty, value);
+ }
+
+ public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register("Foreground",
+ typeof(Brush), typeof(MyListItem), new PropertyMetadata(ModSecret.AppResources["ColorBrush1"]));
+
+ // 菜单与按钮绑定
+ public Action ContentHandler { get; set; }
+
+ #endregion
+
+ #region 点击
+
+ // 触发点击事件
+ private void Button_MouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (!IsMouseDown)
+ return;
+ Click?.Invoke(sender, e);
+ if (e.Handled)
+ return;
+ // 触发自定义事件
+ var dependencyObject = (DependencyObject)sender;
+ if (CustomEventService.GetEventType(dependencyObject) != CustomEvent.EventType.None)
+ {
+ ModMain.RaiseCustomEvent(this);
+ e.Handled = true;
+ }
+
+ if (e.Handled)
+ return;
+ // 实际的单击处理
+ switch (Type)
+ {
+ case CheckType.Clickable:
+ {
+ ModBase.Log("[Control] 按下单击列表项:" + Title);
+ break;
+ }
+ case CheckType.RadioBox:
+ {
+ ModBase.Log("[Control] 按下单选列表项:" + Title);
+ if (!Checked)
+ SetChecked(true, true, true);
+ break;
+ }
+ case CheckType.CheckBox:
+ {
+ ModBase.Log("[Control] 按下复选列表项(" + !Checked + "):" + Title);
+ SetChecked(!Checked, true, true);
+ break;
+ }
+ }
+ }
+
+ // 鼠标点击判定
+ private bool IsMouseDown;
+
+ private void Button_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (IsMouseDirectlyOver && !(Type == CheckType.None))
+ {
+ IsMouseDown = true;
+ if (ButtonStack is not null)
+ ButtonStack.IsHitTestVisible = false;
+ }
+ }
+
+ private void Button_MouseLeave(object sender, object e)
+ {
+ IsMouseDown = false;
+ if (ButtonStack is not null)
+ ButtonStack.IsHitTestVisible = true;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyLoading.xaml b/Plain Craft Launcher 2/Controls/MyLoading.xaml
index f819ef8cb..4215d8d2c 100644
--- a/Plain Craft Launcher 2/Controls/MyLoading.xaml
+++ b/Plain Craft Launcher 2/Controls/MyLoading.xaml
@@ -1,14 +1,17 @@
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="PanBack" mc:Ignorable="d"
+ x:Class="PCL.MyLoading"
+ MinWidth="50" MinHeight="50" Background="{StaticResource ColorBrushTransparent}">
-
-
-
-
+
+
+
+
-
+
@@ -16,22 +19,30 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyLoading.xaml.cs b/Plain Craft Launcher 2/Controls/MyLoading.xaml.cs
new file mode 100644
index 000000000..97ce6b4fc
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyLoading.xaml.cs
@@ -0,0 +1,402 @@
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+using static PCL.MyLoading;
+
+namespace PCL;
+
+public partial class MyLoading
+{
+ public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e);
+
+ public delegate void IsErrorChangedEventHandler(object sender, bool isError);
+
+ public delegate void StateChangedEventHandler(object sender, MyLoadingState newState, MyLoadingState oldState);
+
+ private readonly int Uuid = ModBase.GetUuid();
+
+ public bool AutoRun { get; set; } = true;
+
+ public event IsErrorChangedEventHandler? IsErrorChanged;
+ public event StateChangedEventHandler? StateChanged;
+ public event ClickEventHandler? Click;
+
+ #region 颜色
+
+ public SolidColorBrush Foreground
+ {
+ get => (SolidColorBrush)GetValue(ForegroundProperty);
+ set => SetValue(ForegroundProperty, value);
+ }
+
+ public static readonly DependencyProperty ForegroundProperty =
+ DependencyProperty.Register("Foreground", typeof(SolidColorBrush), typeof(MyLoading));
+
+ public MyLoading()
+ {
+ InitializeComponent();
+ SetResourceReference(ForegroundProperty, "ColorBrush3");
+ IsErrorChanged += (_, _) => RefreshText();
+ Loaded += (_, _) => RefreshText();
+ Loaded += (_, _) => InitState();
+ Loaded += (_, _) => RefreshState();
+ Unloaded += (_, _) => RefreshState();
+ MouseLeftButtonUp += Button_MouseUp;
+ MouseLeftButtonDown += Button_MouseDown;
+ MouseLeave += Button_MouseLeave;
+ MouseLeftButtonUp += Button_MouseLeave;
+ }
+
+ #endregion
+
+ #region 文本
+
+ private bool _ShowProgress { get; set; }
+
+ public bool ShowProgress
+ {
+ get => _ShowProgress;
+ set
+ {
+ if (_ShowProgress == value)
+ return;
+ _ShowProgress = value;
+ RefreshText();
+ }
+ }
+
+ private string _Text = "加载中";
+
+ public string Text
+ {
+ get => _Text;
+ set
+ {
+ _Text = value;
+ RefreshText();
+ }
+ }
+
+ private string _TextError = "加载失败";
+
+ public string TextError
+ {
+ get => _TextError;
+ set
+ {
+ _TextError = value;
+ RefreshText();
+ }
+ }
+
+ ///
+ /// 是否在使用 Loader 时使用 Loader 的错误输出来替换默认的错误文本显示。
+ ///
+ public bool TextErrorInherit { get; set; } = true;
+
+ private void RefreshText()
+ {
+ ModBase.RunInUi(() =>
+ {
+ if (InnerState == MyLoadingState.Error)
+ {
+ if (TextErrorInherit && State.IsLoader)
+ {
+ var Ex = State.Error;
+ if (Ex is null)
+ {
+ LabText.Text = "未知错误";
+ }
+ else
+ {
+ while (Ex.InnerException is not null) Ex = Ex.InnerException;
+ LabText.Text = ModBase.StrTrim(Ex.Message).ToString();
+ if (new[]
+ {
+ "远程主机强迫关闭了", "远程方已关闭传输流", "未能解析此远程名称", "由于目标计算机积极拒绝", "操作已超时", "操作超时", "服务器超时", "连接超时"
+ }.Any(s => LabText.Text.Contains(s))) LabText.Text = "网络环境不佳,请稍后重试,或使用 VPN 以改善网络环境";
+ }
+ }
+ else
+ {
+ LabText.Text = TextError;
+ }
+ }
+ else if (ShowProgress && State.IsLoader)
+ {
+ LabText.Text = Text + " - " + Math.Floor(State.Progress * 100) + "%";
+ }
+ else
+ {
+ LabText.Text = Text;
+ }
+ });
+ }
+
+ #endregion
+
+ #region 状态改变
+
+ // 状态枚举
+ public enum MyLoadingState
+ {
+ Unloaded = -1,
+ Run = 0,
+ Stop = 1,
+ Error = 2
+ }
+
+ // 用于外部改变的公开状态
+ private ILoadingTrigger __State;
+
+ private ILoadingTrigger _State
+ {
+ [MethodImpl(MethodImplOptions.Synchronized)]
+ get => __State;
+
+ [MethodImpl(MethodImplOptions.Synchronized)]
+ set
+ {
+ if (__State != null)
+ {
+ __State.ProgressChanged -= (_, _) => RefreshText();
+ __State.LoadingStateChanged -= (_, _) => RefreshState();
+ }
+
+ __State = value;
+ if (__State != null)
+ {
+ __State.ProgressChanged += (_, _) => RefreshText();
+ __State.LoadingStateChanged += (_, _) => RefreshState();
+ }
+ }
+ }
+
+ public ILoadingTrigger State
+ {
+ get
+ {
+ InitState();
+ return _State;
+ }
+ set
+ {
+ _State = value;
+ RefreshState();
+ }
+ }
+
+ private void InitState()
+ {
+ if (_State is null)
+ {
+ _State = new MyLoadingStateSimulator();
+ if (AutoRun)
+ _State.LoadingState = MyLoadingState.Run;
+ }
+ }
+
+ private void RefreshState()
+ {
+ if (_State.LoadingState == MyLoadingState.Run && !IsLoaded)
+ InnerState = MyLoadingState.Stop;
+ InnerState = _State.LoadingState;
+ OuterState = _State.LoadingState;
+ AniLoop();
+ }
+
+ // 用于引发外部事件的状态
+ private MyLoadingState _OuterState { get; set; } = MyLoadingState.Unloaded;
+
+ private MyLoadingState OuterState
+ {
+ get => _OuterState;
+ set
+ {
+ if (_OuterState == value)
+ return;
+ var OldValue = _OuterState;
+ _OuterState = value;
+ // 引发事件
+ StateChanged?.Invoke(this, value, OldValue);
+ if (OldValue == MyLoadingState.Error != (value == MyLoadingState.Error))
+ IsErrorChanged?.Invoke(this, value == MyLoadingState.Error);
+ }
+ }
+
+
+ // 用于引发内部动画事件的状态
+ private MyLoadingState _InnerState { get; set; } = MyLoadingState.Unloaded;
+
+ private MyLoadingState InnerState
+ {
+ get => _InnerState;
+ set
+ {
+ if (_InnerState == value)
+ return;
+ var OldValue = _InnerState;
+ _InnerState = value;
+ // 引发事件
+ AniLoop();
+ if (OldValue == MyLoadingState.Error != (value == MyLoadingState.Error))
+ ErrorAnimation(this, value == MyLoadingState.Error);
+ }
+ }
+
+ #endregion
+
+ #region 动画
+
+ ///
+ /// 是否需要动画。
+ ///
+ public bool HasAnimation { get; set; } = true;
+
+ ///
+ /// 主动画循环是否正在运行中。
+ ///
+ private bool IsLooping;
+
+ private void AniLoop()
+ {
+ // 这坨循环代码也是老屎坑了,救救.jpg
+ if (!HasAnimation || IsLooping || !(InnerState == MyLoadingState.Run) || ModAnimation.AniSpeed > 10d ||
+ !IsLoaded)
+ return;
+ IsLooping = true;
+ ErrorAnimationWaiting = true;
+ ModAnimation.AniStart(new[]
+ {
+ ModAnimation.AaRotateTransform(PathPickaxe, -20 - ((RotateTransform)PathPickaxe.RenderTransform).Angle, 350,
+ 250, new ModAnimation.AniEaseInBack(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaRotateTransform(PathPickaxe, 50d, 900, Ease: new ModAnimation.AniEaseOutFluent(),
+ After: true),
+ ModAnimation.AaRotateTransform(PathPickaxe, 25d, 900,
+ Ease: new ModAnimation.AniEaseOutElastic(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaCode(() =>
+ {
+ PathLeft.Opacity = 1d;
+ PathLeft.Margin = new Thickness(7d, 41d, 0d, 0d);
+ PathRight.Opacity = 1d;
+ PathRight.Margin = new Thickness(14d, 41d, 0d, 0d);
+ ErrorAnimationWaiting = false;
+ }),
+ ModAnimation.AaOpacity(PathLeft, -1, 100, 50),
+ ModAnimation.AaX(PathLeft, -5, 180, Ease: new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaY(PathLeft, -6, 180, Ease: new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaOpacity(PathRight, -1, 100, 50),
+ ModAnimation.AaX(PathRight, 5d, 180, Ease: new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaY(PathRight, -6, 180, Ease: new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaCode(() =>
+ {
+ IsLooping = false;
+ AniLoop();
+ }, After: true)
+ }, "MyLoader Loop " + Uuid + "/" + ModBase.GetUuid());
+ if (ShowProgress)
+ {
+ }
+ }
+
+ ///
+ /// 镐子是否还没挥下去,要求错误动画等待。
+ ///
+ private bool ErrorAnimationWaiting;
+
+ private void ErrorAnimation(object sender, bool isError)
+ {
+ if (isError)
+ {
+ // 非错误变为错误
+ var Wait = ErrorAnimationWaiting ? 400 : 0;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(PanBack, ForegroundProperty, "ColorBrushRedLight", 300),
+ ModAnimation.AaOpacity(PathError, 1d - PathError.Opacity, 100, 300 + Wait),
+ ModAnimation.AaScaleTransform(PathError, 1d - ((ScaleTransform)PathError.RenderTransform).ScaleX,
+ 400, 300 + Wait, new ModAnimation.AniEaseOutBack())
+ }, "MyLoader Error " + Uuid);
+ }
+ else
+ {
+ // 错误变为非错误
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaOpacity(PathError, -PathError.Opacity, 100),
+ ModAnimation.AaScaleTransform(PathError, 0.5d - ((ScaleTransform)PathError.RenderTransform).ScaleX,
+ 200),
+ ModAnimation.AaColor(PanBack, ForegroundProperty, "ColorBrush3", 300)
+ }, "MyLoader Error " + Uuid);
+ }
+ }
+
+ #endregion
+
+ #region 点击事件
+
+ private void Button_MouseUp(object sender, MouseButtonEventArgs e)
+ {
+ Click?.Invoke(sender, e);
+ }
+
+ private bool IsMouseDown;
+
+ private void Button_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ // 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行)
+ IsMouseDown = true;
+ }
+
+ private void Button_MouseLeave(object sender, object e)
+ {
+ IsMouseDown = false;
+ }
+
+ #endregion
+}
+
+public interface ILoadingTrigger
+{
+ delegate void LoadingStateChangedEventHandler(MyLoadingState NewState, MyLoadingState OldState);
+
+ delegate void ProgressChangedEventHandler(double NewProgress, double OldProgress);
+
+ bool IsLoader { get; }
+
+ double Progress { get; }
+ Exception? Error { get; }
+
+ MyLoadingState LoadingState { get; set; }
+ event LoadingStateChangedEventHandler? LoadingStateChanged;
+ event ProgressChangedEventHandler? ProgressChanged;
+}
+
+public class MyLoadingStateSimulator : ILoadingTrigger
+{
+ private MyLoadingState _LoadingState { get; set; } = MyLoadingState.Unloaded;
+
+ public MyLoadingState LoadingState
+ {
+ get => _LoadingState;
+ set
+ {
+ if (_LoadingState == value)
+ return;
+ var OldState = _LoadingState;
+ _LoadingState = value;
+ LoadingStateChanged?.Invoke(value, OldState);
+ }
+ }
+
+ public bool IsLoader { get; } = false;
+
+ public double Progress => 0;
+ public Exception? Error => null;
+
+ public event ILoadingTrigger.LoadingStateChangedEventHandler? LoadingStateChanged;
+ public event ILoadingTrigger.ProgressChangedEventHandler? ProgressChanged;
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyMenuItem.cs b/Plain Craft Launcher 2/Controls/MyMenuItem.cs
new file mode 100644
index 000000000..9fedff5ce
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyMenuItem.cs
@@ -0,0 +1,94 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Shapes;
+using PCL.Core.App;
+
+namespace PCL;
+
+public class MyMenuItem : MenuItem
+{
+ // 指向动画
+
+ private const int AnimationTimeIn = 100;
+ private const int AnimationTimeOut = 200;
+ private string ColorName;
+
+ // 基础
+
+ public int Uuid = ModBase.GetUuid();
+
+ public MyMenuItem()
+ {
+ Loaded += MyMenuItem_Loaded;
+ MouseEnter += (_, _) => RefreshColor();
+ MouseLeave += (_, _) => RefreshColor();
+ IsEnabledChanged += (_, _) => RefreshColor();
+ }
+
+ private void MyMenuItem_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (Icon is not null)
+ {
+ var IconControl = (Path)GetTemplateChild("Icon");
+ if (IconControl is not null)
+ IconControl.Data = (Geometry)new GeometryConverter().ConvertFromString(Icon.ToString());
+ // 对父级设置透明度
+ }
+
+ ((ContextMenu)Parent).Opacity = Config.Preference.Theme.WindowOpacity / 1000.0 + 0.4;
+ }
+
+ private void RefreshColor()
+ {
+ // 判断当前颜色
+ string BackName;
+ string ForeName;
+ int Time;
+ if (!IsEnabled)
+ {
+ BackName = "ColorBrushTransparent";
+ ForeName = "ColorBrushGray5";
+ Time = AnimationTimeOut;
+ }
+ else if (IsMouseOver)
+ {
+ BackName = "ColorBrush6";
+ ForeName = "ColorBrush2";
+ Time = AnimationTimeIn;
+ }
+ else
+ {
+ BackName = "ColorBrushTransparent";
+ ForeName = "ColorBrush1";
+ Time = AnimationTimeOut;
+ }
+
+ // 重复性验证
+ if ((ColorName ?? "") == (BackName ?? ""))
+ return;
+ ColorName = BackName;
+ // 触发颜色动画
+ if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画
+ {
+ // 有动画
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaColor(this, BackgroundProperty, BackName, Time),
+ ModAnimation.AaColor(this, ForegroundProperty, ForeName, Time)
+ }, "MyMenuItem Color " + Uuid);
+ }
+ else
+ {
+ // 无动画
+ ModAnimation.AniStop("MyMenuItem Color " + Uuid);
+ SetResourceReference(BackgroundProperty, BackName);
+ SetResourceReference(ForegroundProperty, ForeName);
+ }
+ }
+ private void MyMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ ModMain.RaiseCustomEvent(this);
+ }
+}
diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml
index 6721b73ce..f0a783de9 100644
--- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml
+++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml
@@ -1,8 +1,9 @@
-
+
@@ -11,7 +12,8 @@
-
+
@@ -22,18 +24,29 @@
-
+
-
-
+
-
-
-
-
+
+
+
+
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs
new file mode 100644
index 000000000..4ee3b41f3
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs
@@ -0,0 +1,147 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Interop;
+using PCL.Core.UI.Controls;
+
+namespace PCL;
+
+public partial class MyMsgInput
+{
+ private readonly ModMain.MyMsgBoxConverter MyConverter;
+ private readonly int Uuid = ModBase.GetUuid();
+
+ public MyMsgInput(ModMain.MyMsgBoxConverter Converter)
+ {
+ try
+ {
+ InitializeComponent();
+ Btn1.Name = Btn1.Name + ModBase.GetUuid();
+ Btn2.Name = Btn2.Name + ModBase.GetUuid();
+ MyConverter = Converter;
+ LabTitle.Text = Converter.Title;
+ LabText.Text = Converter.Text;
+ PanText.Visibility = string.IsNullOrEmpty(Converter.Text) ? Visibility.Collapsed : Visibility.Visible;
+ TextArea.Text = (string)Converter.Content;
+ TextArea.HintText = Converter.HintText;
+ TextArea.ValidateRules = Converter.ValidateRules;
+ Btn1.Text = Converter.Button1;
+ if (Converter.IsWarn)
+ {
+ Btn1.ColorType = MyButton.ColorState.Red;
+ LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight");
+ }
+
+ Btn2.Text = Converter.Button2;
+ Btn2.Visibility = string.IsNullOrEmpty(Converter.Button2) ? Visibility.Collapsed : Visibility.Visible;
+ ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d);
+ }
+
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "输入弹窗初始化失败", ModBase.LogLevel.Hint);
+ }
+
+ Loaded += Load;
+ }
+
+ private void Load(object sender, EventArgs e)
+ {
+ try
+ {
+ // UI 初始化
+ if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red))
+ Btn1.ColorType = MyButton.ColorState.Highlight;
+ TextArea.Focus();
+ TextArea.SelectionStart = TextArea.Text.Length;
+ // 动画
+ Opacity = 0d;
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, BlurBorder.BackgroundProperty,
+ (MyConverter.IsWarn
+ ? new ModBase.MyColor(140d, 80d, 0d, 0d)
+ : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.FrmMain.PanMsgBackground.Background, 200),
+ "PanMsgBackground Background");
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaOpacity(this, 1d, 120, 60),
+ ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
+ -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
+ -TransformRotate.Angle, 300, 60,
+ new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
+ }, "MyMsgBox " + Uuid);
+ // 记录日志
+ ModBase.Log("[Control] 输入弹窗:" + LabTitle.Text);
+ }
+
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "输入弹窗加载失败", ModBase.LogLevel.Hint);
+ }
+ }
+
+ private void Close()
+ {
+ // 结束线程阻塞
+ MyConverter.WaitFrame.Continue = false;
+ ComponentDispatcher.PopModal();
+ // 动画
+ ModAnimation.AniStart(new[]
+ {
+ ModAnimation.AaCode(() =>
+ {
+ if (!ModMain.WaitingMyMsgBox.Any())
+ ModAnimation.AniStart(ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground,
+ BlurBorder.BackgroundProperty,
+ new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.FrmMain.PanMsgBackground.Background, 200,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
+ }, 30),
+ ModAnimation.AaOpacity(this, -Opacity, 80, 20),
+ ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y,
+ 150, 0, new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
+ 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), After: true)
+ }, "MyMsgBox " + Uuid);
+ }
+
+ public void Btn1_Click(object sender, MouseButtonEventArgs e)
+ {
+ TextArea.Validate(); // #5773
+ if (MyConverter.IsExited || !TextArea.IsValidated)
+ return;
+ MyConverter.IsExited = true;
+ MyConverter.Result = TextArea.Text;
+ Close();
+ }
+
+ public void Btn2_Click(object sender, MouseButtonEventArgs e)
+ {
+ if (MyConverter.IsExited)
+ return;
+ MyConverter.IsExited = true;
+ MyConverter.Result = null;
+ Close();
+ }
+
+ private void TextCaption_ValidateChanged(object sender, EventArgs e)
+ {
+ Btn1.IsEnabled = TextArea.IsValidated;
+ }
+
+ private void Drag(object sender, MouseButtonEventArgs e)
+ {
+ try
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ if (e.GetPosition(ShapeLine).Y <= 2d)
+ ModMain.FrmMain.DragMove();
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml
index 502d01c2a..d34fd682f 100644
--- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml
+++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml
@@ -1,9 +1,10 @@
-
+
@@ -12,7 +13,8 @@
-
+
@@ -22,18 +24,29 @@
-
+
-
+
+ Foreground="{DynamicResource ColorBrush1}" FontWeight="Normal" />
-
-
-
-
+
+
+
+
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs
new file mode 100644
index 000000000..b46a21b67
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs
@@ -0,0 +1,171 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Interop;
+using PCL.Core.UI.Controls;
+
+namespace PCL;
+
+public partial class MyMsgMarkdown
+{
+ private readonly ModMain.MyMsgBoxConverter MyConverter;
+ private readonly int Uuid = ModBase.GetUuid();
+
+ public MyMsgMarkdown(ModMain.MyMsgBoxConverter Converter)
+ {
+ try
+ {
+ InitializeComponent();
+ Btn1.Name = Btn1.Name + ModBase.GetUuid();
+ Btn2.Name = Btn2.Name + ModBase.GetUuid();
+ Btn3.Name = Btn3.Name + ModBase.GetUuid();
+ MyConverter = Converter;
+ LabTitle.Text = Converter.Title;
+ LabCaption.Markdown = Converter.Text;
+ DataContext = this;
+ Btn1.Text = Converter.Button1;
+ if (Converter.IsWarn)
+ {
+ Btn1.ColorType = MyButton.ColorState.Red;
+ LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight");
+ }
+
+ Btn2.Text = Converter.Button2;
+ Btn3.Text = Converter.Button3;
+ Btn2.Visibility = string.IsNullOrEmpty(Converter.Button2) ? Visibility.Collapsed : Visibility.Visible;
+ Btn3.Visibility = string.IsNullOrEmpty(Converter.Button3) ? Visibility.Collapsed : Visibility.Visible;
+ ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d);
+ }
+
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "普通弹窗初始化失败", ModBase.LogLevel.Hint);
+ }
+
+ Loaded += Load;
+ }
+
+ private void Load(object sender, EventArgs e)
+ {
+ try
+ {
+ // UI 初始化
+ if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red))
+ Btn1.ColorType = MyButton.ColorState.Highlight;
+ Btn1.Focus();
+ // 动画
+ Opacity = 0d;
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, BlurBorder.BackgroundProperty,
+ (MyConverter.IsWarn
+ ? new ModBase.MyColor(140d, 80d, 0d, 0d)
+ : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.FrmMain.PanMsgBackground.Background, 200),
+ "PanMsgBackground Background");
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaOpacity(this, 1d, 120, 60),
+ ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
+ -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
+ -TransformRotate.Angle, 300, 60,
+ new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
+ }, "MyMsgBox " + Uuid);
+ // 记录日志
+ ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Markdown);
+ }
+
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "普通弹窗加载失败", ModBase.LogLevel.Hint);
+ }
+ }
+
+ private void Close()
+ {
+ // 结束线程阻塞
+ if (MyConverter.ForceWait || !string.IsNullOrEmpty(MyConverter.Button2))
+ MyConverter.WaitFrame.Continue = false;
+ ComponentDispatcher.PopModal();
+ // 动画
+ ModAnimation.AniStart(new[]
+ {
+ ModAnimation.AaCode(() =>
+ {
+ if (!ModMain.WaitingMyMsgBox.Any())
+ ModAnimation.AniStart(ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground,
+ BlurBorder.BackgroundProperty,
+ new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.FrmMain.PanMsgBackground.Background, 200,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
+ }, 30),
+ ModAnimation.AaOpacity(this, -Opacity, 80, 20),
+ ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y,
+ 150, 0, new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
+ 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), After: true)
+ }, "MyMsgBox " + Uuid);
+ }
+
+ public void Btn1_Click(object sender, MouseButtonEventArgs e)
+ {
+ if (MyConverter.IsExited)
+ return;
+ if (MyConverter.Button1Action is not null)
+ {
+ MyConverter.Button1Action();
+ }
+ else
+ {
+ MyConverter.IsExited = true;
+ MyConverter.Result = 1;
+ Close();
+ }
+ }
+
+ public void Btn2_Click(object sender, MouseButtonEventArgs e)
+ {
+ if (MyConverter.IsExited)
+ return;
+ if (MyConverter.Button2Action is not null)
+ {
+ MyConverter.Button2Action();
+ }
+ else
+ {
+ MyConverter.IsExited = true;
+ MyConverter.Result = 2;
+ Close();
+ }
+ }
+
+ public void Btn3_Click(object sender, MouseButtonEventArgs e)
+ {
+ if (MyConverter.IsExited)
+ return;
+ if (MyConverter.Button3Action is not null)
+ {
+ MyConverter.Button3Action();
+ }
+ else
+ {
+ MyConverter.IsExited = true;
+ MyConverter.Result = 3;
+ Close();
+ }
+ }
+
+ private void Drag(object? sender = null, MouseButtonEventArgs? e = null)
+ {
+ try
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ if (e.GetPosition(ShapeLine).Y <= 2d)
+ ModMain.FrmMain.DragMove();
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml
index cca194dfa..1ba30ccf8 100644
--- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml
+++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml
@@ -1,8 +1,9 @@
-
+
@@ -11,7 +12,8 @@
-
+
@@ -21,15 +23,22 @@
-
+
-
+
-
-
-
+
+
+
diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs
new file mode 100644
index 000000000..7824eacee
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs
@@ -0,0 +1,173 @@
+using System.Collections;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Interop;
+using PCL.Core.UI.Controls;
+
+namespace PCL;
+
+public partial class MyMsgSelect
+{
+ private readonly ModMain.MyMsgBoxConverter MyConverter;
+ private readonly int Uuid = ModBase.GetUuid();
+
+ private int SelectedIndex = -1;
+
+ public MyMsgSelect(ModMain.MyMsgBoxConverter Converter)
+ {
+ try
+ {
+ InitializeComponent();
+ Btn1.Name = Btn1.Name + ModBase.GetUuid();
+ Btn2.Name = Btn2.Name + ModBase.GetUuid();
+ MyConverter = Converter;
+ LabTitle.Text = Converter.Title;
+ Btn1.Text = Converter.Button1;
+ if (Converter.IsWarn)
+ {
+ Btn1.ColorType = MyButton.ColorState.Red;
+ LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight");
+ }
+
+ Btn2.Text = Converter.Button2;
+ Btn2.Visibility = string.IsNullOrEmpty(Converter.Button2) ? Visibility.Collapsed : Visibility.Visible;
+ ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d);
+ // 添加选择控件
+ Btn1.IsEnabled = false;
+ foreach (var rawContent in (IEnumerable)Converter.Content)
+ {
+ // 1. Initialize and get the actual element
+ // Note: We use a new variable because 'foreach' variables are read-only
+ var content = MyVirtualizingElement.TryInit((FrameworkElement)rawContent);
+
+ // 2. Interface casting and event subscription
+ if (content is IMyRadio selection)
+ {
+ PanSelection.Children.Add((UIElement)selection);
+ selection.Check += (sender, e) => OnChecked((IMyRadio)sender, e);
+
+ // 3. Property configuration based on specific type
+ if (selection is MyListItem listItem)
+ {
+ listItem.Type = MyListItem.CheckType.RadioBox;
+ listItem.MinHeight = 24.0;
+ }
+ else if (selection is MyRadioBox radioBox)
+ {
+ radioBox.MinHeight = 24.0;
+ }
+ }
+ }
+ }
+
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "选择弹窗初始化失败", ModBase.LogLevel.Hint);
+ }
+
+ Loaded += Load;
+ Btn1.Click += Btn1_Click;
+ Btn2.Click += Btn2_Click;
+ LabTitle.MouseLeftButtonDown += Drag;
+ PanBorder.MouseLeftButtonDown += Drag;
+ }
+
+ private void Load(object sender, EventArgs e)
+ {
+ try
+ {
+ // UI 初始化
+ if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red))
+ Btn1.ColorType = MyButton.ColorState.Highlight;
+ // 动画
+ Opacity = 0d;
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, BlurBorder.BackgroundProperty,
+ (MyConverter.IsWarn
+ ? new ModBase.MyColor(140d, 80d, 0d, 0d)
+ : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.FrmMain.PanMsgBackground.Background, 200),
+ "PanMsgBackground Background");
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaOpacity(this, 1d, 120, 60),
+ ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
+ -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
+ -TransformRotate.Angle, 300, 60,
+ new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
+ }, "MyMsgBox " + Uuid);
+ // 记录日志
+ ModBase.Log("[Control] 选择弹窗:" + LabTitle.Text);
+ }
+
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "选择弹窗加载失败", ModBase.LogLevel.Hint);
+ }
+ }
+
+ private void Close()
+ {
+ // 结束线程阻塞
+ MyConverter.WaitFrame.Continue = false;
+ ComponentDispatcher.PopModal();
+ // 动画
+ ModAnimation.AniStart(new[]
+ {
+ ModAnimation.AaCode(() =>
+ {
+ if (!ModMain.WaitingMyMsgBox.Any())
+ ModAnimation.AniStart(ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground,
+ BlurBorder.BackgroundProperty,
+ new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.FrmMain.PanMsgBackground.Background, 200,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
+ }, 30),
+ ModAnimation.AaOpacity(this, -Opacity, 80, 20),
+ ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y,
+ 150, 0, new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
+ 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), After: true)
+ }, "MyMsgBox " + Uuid);
+ }
+
+ public void Btn1_Click(object sender, MouseButtonEventArgs e)
+ {
+ if (MyConverter.IsExited || SelectedIndex == -1)
+ return;
+ MyConverter.IsExited = true;
+ MyConverter.Result = SelectedIndex;
+ Close();
+ }
+
+ public void Btn2_Click(object sender, MouseButtonEventArgs e)
+ {
+ if (MyConverter.IsExited)
+ return;
+ MyConverter.IsExited = true;
+ MyConverter.Result = null;
+ Close();
+ }
+
+ private void OnChecked(IMyRadio sender, EventArgs e)
+ {
+ Btn1.IsEnabled = true;
+ SelectedIndex = PanSelection.Children.IndexOf((UIElement)sender);
+ }
+
+ private void Drag(object sender, MouseButtonEventArgs e)
+ {
+ try
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ if (e.GetPosition(ShapeLine).Y <= 2d)
+ ModMain.FrmMain.DragMove();
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml
index 469f70954..ed540acd2 100644
--- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml
+++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml
@@ -1,8 +1,9 @@
-
+
@@ -11,7 +12,8 @@
-
+
@@ -21,18 +23,30 @@
-
+
-
-
+
-
-
-
-
+
+
+
+
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs
new file mode 100644
index 000000000..2cf1bd49b
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs
@@ -0,0 +1,170 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Interop;
+using PCL.Core.UI.Controls;
+
+namespace PCL;
+
+public partial class MyMsgText
+{
+ private readonly ModMain.MyMsgBoxConverter MyConverter;
+ private readonly int Uuid = ModBase.GetUuid();
+
+ public MyMsgText(ModMain.MyMsgBoxConverter Converter)
+ {
+ try
+ {
+ InitializeComponent();
+ Btn1.Name = Btn1.Name + ModBase.GetUuid();
+ Btn2.Name = Btn2.Name + ModBase.GetUuid();
+ Btn3.Name = Btn3.Name + ModBase.GetUuid();
+ MyConverter = Converter;
+ LabTitle.Text = Converter.Title;
+ LabCaption.Text = Converter.Text;
+ Btn1.Text = Converter.Button1;
+ if (Converter.IsWarn)
+ {
+ Btn1.ColorType = MyButton.ColorState.Red;
+ LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight");
+ }
+
+ Btn2.Text = Converter.Button2;
+ Btn3.Text = Converter.Button3;
+ Btn2.Visibility = string.IsNullOrEmpty(Converter.Button2) ? Visibility.Collapsed : Visibility.Visible;
+ Btn3.Visibility = string.IsNullOrEmpty(Converter.Button3) ? Visibility.Collapsed : Visibility.Visible;
+ ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d);
+ }
+
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "普通弹窗初始化失败", ModBase.LogLevel.Hint);
+ }
+
+ Loaded += Load;
+ }
+
+ private void Load(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ // UI 初始化
+ if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red))
+ Btn1.ColorType = MyButton.ColorState.Highlight;
+ Btn1.Focus();
+ // 动画
+ Opacity = 0d;
+ ModAnimation.AniStart(
+ ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, BlurBorder.BackgroundProperty,
+ (MyConverter.IsWarn
+ ? new ModBase.MyColor(140d, 80d, 0d, 0d)
+ : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.FrmMain.PanMsgBackground.Background, 200),
+ "PanMsgBackground Background");
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaOpacity(this, 1d, 120, 60),
+ ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
+ -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
+ -TransformRotate.Angle, 300, 60,
+ new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
+ }, "MyMsgBox " + Uuid);
+ // 记录日志
+ ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Text);
+ }
+
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "普通弹窗加载失败", ModBase.LogLevel.Hint);
+ }
+ }
+
+ private void Close()
+ {
+ // 结束线程阻塞
+ if (MyConverter.ForceWait || !string.IsNullOrEmpty(MyConverter.Button2))
+ MyConverter.WaitFrame.Continue = false;
+ ComponentDispatcher.PopModal();
+ // 动画
+ ModAnimation.AniStart(new[]
+ {
+ ModAnimation.AaCode(() =>
+ {
+ if (!ModMain.WaitingMyMsgBox.Any())
+ ModAnimation.AniStart(ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground,
+ BlurBorder.BackgroundProperty,
+ new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.FrmMain.PanMsgBackground.Background, 200,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
+ }, 30),
+ ModAnimation.AaOpacity(this, -Opacity, 80, 20),
+ ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y,
+ 150, 0, new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
+ 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), After: true)
+ }, "MyMsgBox " + Uuid);
+ }
+
+ public void Btn1_Click(object? sender = null, MouseButtonEventArgs? e = null)
+ {
+ if (MyConverter.IsExited)
+ return;
+ if (MyConverter.Button1Action is not null)
+ {
+ MyConverter.Button1Action();
+ }
+ else
+ {
+ MyConverter.IsExited = true;
+ MyConverter.Result = 1;
+ Close();
+ }
+ }
+
+ public void Btn2_Click(object sender, MouseButtonEventArgs e)
+ {
+ if (MyConverter.IsExited)
+ return;
+ if (MyConverter.Button2Action is not null)
+ {
+ MyConverter.Button2Action();
+ }
+ else
+ {
+ MyConverter.IsExited = true;
+ MyConverter.Result = 2;
+ Close();
+ }
+ }
+
+ public void Btn3_Click(object sender, MouseButtonEventArgs e)
+ {
+ if (MyConverter.IsExited)
+ return;
+ if (MyConverter.Button3Action is not null)
+ {
+ MyConverter.Button3Action();
+ }
+ else
+ {
+ MyConverter.IsExited = true;
+ MyConverter.Result = 3;
+ Close();
+ }
+ }
+
+ private void Drag(object sender, MouseButtonEventArgs e)
+ {
+ try
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ if (e.GetPosition(ShapeLine).Y <= 2d)
+ ModMain.FrmMain.DragMove();
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyPageLeft.cs b/Plain Craft Launcher 2/Controls/MyPageLeft.cs
new file mode 100644
index 000000000..a645e17d1
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyPageLeft.cs
@@ -0,0 +1,165 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace PCL;
+
+public class MyPageLeft : Grid
+{
+ public static DependencyProperty AnimatedControlProperty =
+ DependencyProperty.Register("AnimatedControl", typeof(FrameworkElement), typeof(MyPageLeft));
+
+ private readonly int Uuid = ModBase.GetUuid();
+
+ private bool _animatedControlNullWarned;
+
+ // 执行逐个进入动画的控件
+ public FrameworkElement AnimatedControl
+ {
+ get
+ {
+ var res = GetValue(AnimatedControlProperty);
+ if (res is null && !_animatedControlNullWarned)
+ {
+ _animatedControlNullWarned = true;
+ ModBase.Log($"[MyPageLeft] 获取到 AnimatedControl(来自 {Name}) 的值为 null", ModBase.LogLevel.Debug);
+ }
+
+ return (FrameworkElement)res;
+ }
+ set => SetValue(AnimatedControlProperty, value);
+ }
+
+ public void TriggerShowAnimation()
+ {
+ if (AnimatedControl is null)
+ {
+ // 缩放动画
+ if (!(RenderTransform is ScaleTransform))
+ {
+ RenderTransform = new ScaleTransform(0.96d, 0.96d);
+ RenderTransformOrigin = new Point(0.5d, 0.5d);
+ }
+
+ Opacity = 0d;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX,
+ Ease: new ModAnimation.AniEaseOutBack((ModAnimation.AniEasePower)2)),
+ ModAnimation.AaOpacity(this, 1d, 100)
+ }, "PageLeft PageChange " + Uuid);
+ }
+ else
+ {
+ // 逐个进入动画
+ var AniList = new List();
+ var Id = 0;
+ var Delay = 0;
+ foreach (var ElementRaw in GetAllAnimControls(true))
+ {
+ var Element = MyVirtualizingElement.TryInit(ElementRaw);
+ if (Element.Visibility == Visibility.Collapsed)
+ {
+ // 还原之前的隐藏动画可能导致的改变(#2436)
+ Element.Opacity = 1d;
+ Element.RenderTransform = new TranslateTransform(0d, 0d);
+ if (Element is MyListItem)
+ ((MyListItem)Element).IsMouseOverAnimationEnabled = true;
+ }
+ else
+ {
+ Element.Opacity = 0d;
+ Element.RenderTransform = new TranslateTransform(-25, 0d);
+ if (Element is MyListItem)
+ ((MyListItem)Element).IsMouseOverAnimationEnabled = false;
+ AniList.Add(ModAnimation.AaOpacity(Element, Element is TextBlock ? 0.6d : 1d, 100, Delay,
+ new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
+ AniList.Add(ModAnimation.AaTranslateX(Element, 5d, 200, Delay,
+ new ModAnimation.AniEaseOutFluent()));
+ AniList.Add(ModAnimation.AaTranslateX(Element, 20d, 300, Delay,
+ new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)));
+ if (Element is MyListItem)
+ AniList.Add(ModAnimation.AaCode(() =>
+ {
+ ((MyListItem)Element).IsMouseOverAnimationEnabled = true;
+ ((MyListItem)Element).RefreshColor(this, new EventArgs());
+ }, Delay + 280));
+ Delay += Math.Max(15 - Id, 7) * 2;
+ Id += 1;
+ }
+ }
+
+ ModAnimation.AniStart(AniList, "PageLeft PageChange " + Uuid);
+ }
+ }
+
+ public void TriggerHideAnimation()
+ {
+ if (AnimatedControl is null)
+ {
+ // 缩放动画
+ if (!(RenderTransform is ScaleTransform))
+ {
+ RenderTransform = new ScaleTransform(1d, 1d);
+ RenderTransformOrigin = new Point(0.5d, 0.5d);
+ }
+
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaScaleTransform(this, 0.95d - ((ScaleTransform)RenderTransform).ScaleX, 110,
+ Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaOpacity(this, -Opacity, 80, 30)
+ }, "PageLeft PageChange " + Uuid);
+ }
+ else
+ {
+ // 逐个退出动画
+ var AniList = new List();
+ var Id = 0;
+ var Controls = GetAllAnimControls();
+ foreach (var Element in Controls)
+ {
+ AniList.Add(ModAnimation.AaOpacity(Element, -Element.Opacity, 50,
+ (int)Math.Round(70d / Controls.Count * Id)));
+ AniList.Add(ModAnimation.AaTranslateX(Element, -6, 50, (int)Math.Round(70d / Controls.Count * Id)));
+ Id += 1;
+ }
+
+ ModAnimation.AniStart(AniList, "PageLeft PageChange " + Uuid);
+ }
+ }
+
+ // 遍历获取所有需要生成动画的控件
+ private List GetAllAnimControls(bool IgnoreInvisibility = false)
+ {
+ var AllControls = new List();
+ GetAllAnimControls(AnimatedControl, ref AllControls, IgnoreInvisibility);
+ return AllControls;
+ }
+
+ private void GetAllAnimControls(FrameworkElement Element, ref List AllControls,
+ bool IgnoreInvisibility)
+ {
+ if (!IgnoreInvisibility && Element.Visibility == Visibility.Collapsed)
+ return;
+ if (Element is MyTextButton)
+ AllControls.Add(Element);
+ else if (Element is MyListItem)
+ AllControls.Add(Element);
+ else if (Element is ContentControl)
+ GetAllAnimControls((FrameworkElement)((ContentControl)Element).Content, ref AllControls,
+ IgnoreInvisibility);
+ else if (Element is Panel)
+ foreach (FrameworkElement Element2 in ((Panel)Element).Children)
+ GetAllAnimControls(Element2, ref AllControls, IgnoreInvisibility);
+ else
+ AllControls.Add(Element);
+ }
+}
+
+public interface IRefreshable
+{
+ void Refresh();
+}
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/Controls/MyPageRight.cs b/Plain Craft Launcher 2/Controls/MyPageRight.cs
new file mode 100644
index 000000000..329fa4ca2
--- /dev/null
+++ b/Plain Craft Launcher 2/Controls/MyPageRight.cs
@@ -0,0 +1,715 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Media;
+using static PCL.ModLoader;
+
+namespace PCL;
+
+public class MyPageRight : AdornerDecorator
+{
+ // 当前状态
+ public enum PageStates
+ {
+ Empty, // 默认状态,页面全空
+ LoaderWait, // 加载环初始等待
+ LoaderEnter, // 加载环进入动画
+ LoaderStayForce, // 加载环正常显示(强制等待)
+ LoaderStay, // 加载环正常显示
+ LoaderExit, // 加载环退出动画
+ ContentEnter, // 内容进入动画
+ ContentStay, // 内容正常显示
+ ContentExit, // 刷新导致的全部退出动画,或页面内容退出(子页面更改)导致的全部退出动画
+ PageExit // 切换页面导致的全部退出动画
+ }
+
+ private static readonly DependencyProperty PanScrollProperty =
+ DependencyProperty.Register("PanScroll", typeof(MyScrollViewer), typeof(MyPageRight));
+
+ private PageStates _PageState = PageStates.Empty;
+
+ private bool _panScrollNullWarned;
+
+ public int PageUuid = ModBase.GetUuid();
+
+ // “返回顶部” 按钮检测的滚动区域
+ public MyScrollViewer PanScroll
+ {
+ get
+ {
+ var res = GetValue((DependencyProperty)PanScrollProperty);
+ if (res is null && !_panScrollNullWarned)
+ {
+ _panScrollNullWarned = true;
+ ModBase.Log($"[MyPageRight] 获取到 PanScroll(来自 {Name}) 的值为 null", ModBase.LogLevel.Debug);
+ }
+
+ return (MyScrollViewer)res;
+ }
+ set => SetValue(PanScrollProperty, value);
+ }
+
+ public PageStates PageState
+ {
+ get => _PageState;
+ set
+ {
+ if (_PageState == value)
+ return;
+ _PageState = value;
+ if (ModBase.ModeDebug)
+ ModBase.Log("[UI] 页面状态切换为 " + ModBase.GetStringFromEnum(value));
+ }
+ }
+
+ #region 加载器
+
+ private ModLoader.LoaderBase PageLoader;
+ private Func
-
-
+
+
+ Check="BtnTitleSelect_Click"
+ LogoScale="0.9"
+ Logo="M955 610h-59c-15 0-29 13-29 29v196c0 15-13 29-29 29h-649c-15 0-29-13-29-29v-196c0-15-13-29-29-29h-59c-15 0-29 13-29 29V905c0 43 35 78 78 78h787c43 0 78-35 78-78V640c0-15-13-29-29-29zM492 740c11 11 29 11 41 0l265-265c11-11 11-29 0-41l-41-41c-11-11-29-11-41 0l-110 110c-11 11-33 3-33-13V68C571 53 555 39 541 39h-59c-15 0-29 13-29 29v417c0 17-21 25-33 13l-110-110c-11-11-29-11-41 0L226 433c-11 11-11 29 0 41L492 740z" />
+ Check="BtnTitleSelect_Click"
+ LogoScale="1.1"
+ Logo="M940.4 463.7L773.3 174.2c-17.3-30-49.2-48.4-83.8-48.4H340.2c-34.6 0-66.5 18.5-83.8 48.4L89.2 463.7c-17.3 30-17.3 66.9 0 96.8L256.4 850c17.3 30 49.2 48.4 83.8 48.4h349.2c34.6 0 66.5-18.5 83.8-48.4l167.2-289.5c17.3-29.9 17.3-66.8 0-96.8z m-94.6 96.8L725.9 768.1c-17.3 30-49.2 48.4-83.8 48.4H387.5c-34.6 0-66.5-18.5-83.8-48.4L183.9 560.5c-17.3-30-17.3-66.9 0-96.8l119.8-207.5c17.3-30 49.2-48.4 83.8-48.4h254.6c34.6 0 66.5 18.5 83.8 48.4l119.8 207.5c17.3 30 17.3 66.9 0.1 96.8z M522.3 321.2c-2.5-0.1-5-0.2-7.5-0.2-119.9 0-214 110.3-186.3 235 15.8 70.9 71.5 126.6 142.4 142.4 17.5 3.9 34.7 5.4 51.4 4.7 102.1-3.9 183.6-87.9 183.6-191 0.1-103-81.5-187-183.6-190.9z m68.6 269.1c-18.5 18-43 28.9-68.6 30.7l-6 0.3c-30.2 0.4-58.6-11.4-79.7-33-19.5-20.1-30.7-47-30.9-75-0.3-29.6 11.1-57.4 32-78.3 20.6-20.6 48-32 77.2-32 2.5 0 5 0.1 7.5 0.3 26.7 1.8 51.5 13.2 70.5 32.5 19.6 20 30.8 46.9 31.2 74.9 0.2 30.2-11.5 58.6-33.2 79.6z" />
+ Check="BtnTitleSelect_Click"
+ LogoScale="1"
+ Logo="M623.0016 208.5376c-103.6288-103.6288-269.4144-103.6288-352.256-20.736L415.744 332.8512 332.8 415.7952 187.8016 270.6944c-82.944 82.944-82.944 248.6784 20.736 352.3072 66.56 66.6112 158.9248 88.32 276.8896 64.9728l13.2608-2.7648 198.656 198.656a41.472 41.472 0 0 0 54.7328 3.4304l3.8912-3.4304 127.8976-127.8976a41.472 41.472 0 0 0 3.4304-54.7328l-3.4304-3.8912-198.656-198.656c27.648-124.3648 6.912-221.0816-62.208-290.1504z m-253.2352-9.6256l1.1776-0.4096c64.9728-20.736 150.6304-3.4816 208.0768 54.016 50.6368 50.5344 67.4816 121.7024 48.128 220.16l-2.56 12.4928-7.4752 33.28 208.1792 208.1792-98.6624 98.6112-208.128-208.128-33.28 7.3728c-105.0624 23.3472-180.0192 7.2704-232.704-45.4656-55.04-54.9376-73.216-135.68-56.5248-199.5264l2.9696-9.728L332.8 503.6544 503.7056 332.8 369.7664 198.912z" />
-
-
-
+
+
+
-
-
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
-
+
+
-
-
-
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
+
\ No newline at end of file
diff --git a/Plain Craft Launcher 2/FormMain.xaml.cs b/Plain Craft Launcher 2/FormMain.xaml.cs
new file mode 100644
index 000000000..054854804
--- /dev/null
+++ b/Plain Craft Launcher 2/FormMain.xaml.cs
@@ -0,0 +1,2297 @@
+using System.ComponentModel;
+using System.IO;
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Interop;
+using System.Windows.Media;
+using System.Windows.Media.Effects;
+using PCL.Core.App;
+using PCL.Core.App.IoC;
+using PCL.Core.Logging;
+using PCL.Core.UI;
+using PCL.Core.UI.Theme;
+using PCL.Core.Utils;
+using PCL.Core.Utils.OS;
+using PCL.Core.Utils.Validate;
+using PCL.Network;
+
+namespace PCL;
+
+public partial class FormMain
+{
+ // 愚人节鼠标位置
+ public MouseEventArgs lastMouseArg;
+
+ private void FormMain_MouseMove(object sender, MouseEventArgs e)
+ {
+ lastMouseArg = e;
+ }
+
+ #region 基础
+
+ // 更新日志
+ private void ShowUpdateLog()
+ {
+ ModBase.RunInNewThread(() =>
+ {
+ var ChangelogFile = $"{ModBase.PathTemp}CEUpdateLog.md";
+ string Changelog;
+ if (File.Exists(ChangelogFile))
+ Changelog = ModBase.ReadFile(ChangelogFile);
+ else
+ Changelog = "欢迎使用呀~";
+ if (ModMain.MyMsgBoxMarkdown(Changelog,
+ "PCL CE 已更新至 " + ModBase.VersionBranchName + " " + ModBase.VersionBaseName, "确定", "完整更新日志") ==
+ 2) ModBase.OpenWebsite("https://github.com/PCL-Community/PCL2-CE/releases");
+ }, "UpdateLog Output");
+ }
+
+ // 窗口加载
+ private bool IsWindowLoadFinished;
+ private readonly DragHelper _helper = new();
+
+ public FormMain()
+ {
+ ModBase.ApplicationStartTick = TimeUtils.GetTimeTick();
+ // 刷新主题
+ // ThemeCheckAll(False)
+ // ThemeRefreshColor()
+ ThemeService.ColorModeChanged += (_, _) => ModSecret.ThemeRefresh();
+ ThemeService.ColorThemeChanged += theme => ModSecret.ThemeRefresh((int)theme);
+ // 窗体参数初始化
+ ModMain.FrmMain = this;
+ ModMain.FrmLaunchLeft = new PageLaunchLeft();
+ ModMain.FrmLaunchRight = new PageLaunchRight();
+ // 版本号改变
+ var LastVersion = States.System.LastVersion;
+ if (LastVersion < ModBase.VersionCode)
+ {
+ // 重新询问是否启用遥测数据收集
+ if (LastVersion <= 511)
+ {
+ if (!Config.System.TelemetryConfig.IsDefault() && Config.System.Telemetry)
+ {
+ Config.System.TelemetryConfig.Reset();
+ ModBase.Log("[Start] 遥测策略变更:由旧版本升级到含新版遥测的版本,已重置遥测设置");
+ }
+ }
+ // 触发升级
+ UpgradeSub(LastVersion);
+ }
+ else if (LastVersion > ModBase.VersionCode)
+ // 触发降级
+ DowngradeSub(LastVersion);
+ // 版本隔离设置迁移
+ if (ModBase.Setup.IsUnset("LaunchArgumentIndieV2"))
+ {
+ if (!ModBase.Setup.IsUnset("LaunchArgumentIndie"))
+ {
+ ModBase.Log("[Start] 从老 PCL 迁移版本隔离");
+ Config.Launch.IndieSolutionV2 = Config.Launch.IndieSolutionV1;
+ }
+ else if (!ModBase.Setup.IsUnset("WindowHeight"))
+ {
+ ModBase.Log("[Start] 从老 PCL 升级,但此前未调整版本隔离,使用老的版本隔离默认值");
+ Config.Launch.IndieSolutionV2Config.Reset(Config.Launch.IndieSolutionV1Config.DefaultValue);
+ }
+ else
+ {
+ ModBase.Log("[Start] 全新的 PCL,使用新的版本隔离默认值");
+ Config.Launch.IndieSolutionV2Config.Reset(Config.Launch.IndieSolutionV2Config.DefaultValue);
+ }
+ }
+
+ ModBase.Setup.Load("UiLauncherTheme");
+ // 注册拖拽事件(不能直接加 Handles,否则没用;#6340)
+ AddHandler(DragDrop.DragEnterEvent, new DragEventHandler(HandleDrag), true);
+ AddHandler(DragDrop.DragOverEvent, new DragEventHandler(HandleDrag), true);
+ // 注册 MsgBox 事件
+ MsgBoxWrapper.OnShow += ModMain.MsgBoxWrapper_OnShow;
+ // 注册 Hint 事件
+ HintWrapper.OnShow += ModMain.HintWrapper_OnShow;
+ // 加载 UI
+ InitializeComponent();
+ Opacity = 0d;
+ try
+ {
+ Height = States.UI.WindowHeight;
+ Width = States.UI.WindowWidth;
+ }
+ catch (Exception ex) // 修复 #2019
+ {
+ ModBase.Log(ex, "读取窗口默认大小失败", ModBase.LogLevel.Hint);
+ Height = MinHeight + 100d;
+ Width = MinWidth + 100d;
+ }
+
+ // 管理员权限下文件拖拽
+ if (ProcessInterop.IsAdmin())
+ {
+ ModBase.Log("[Start] PCL 当前正以管理员权限运行");
+ SourceInitialized += (_, _) =>
+ {
+ var windowInterop = new WindowInteropHelper(this);
+ _helper.HwndSource = HwndSource.FromHwnd(windowInterop.Handle);
+ _helper.AddHook();
+ };
+ Closing += (_, _) => _helper.RemoveHook();
+ _helper.DragDrop += (_, _) => FileDrag(_helper.DropFilePaths);
+ }
+
+ if (!(ModMain.FrmLaunchLeft.Parent == null))
+ ModMain.FrmLaunchLeft.SetValue(ContentPresenter.ContentProperty, null);
+ if (!(ModMain.FrmLaunchRight.Parent == null))
+ ModMain.FrmLaunchRight.SetValue(ContentPresenter.ContentProperty, null);
+ PanMainLeft.Child = ModMain.FrmLaunchLeft;
+ PageLeft = ModMain.FrmLaunchLeft;
+ PanMainRight.Child = ModMain.FrmLaunchRight;
+ PageRight = ModMain.FrmLaunchRight;
+ ModMain.FrmLaunchRight.PageState = MyPageRight.PageStates.ContentStay;
+ // 调试模式提醒
+ if (ModBase.ModeDebug)
+ ModMain.Hint("[调试模式] PCL 正以调试模式运行,这可能会导致性能下降,若无必要请不要开启!");
+ // 尽早执行的加载池
+ ModMinecraft.McFolderListLoader
+ .Start(0); // 为了让下载已存在文件检测可以正常运行,必须跑一次;为了让启动按钮尽快可用,需要尽早执行;为了与 PageLaunchLeft 联动,需要为 0 而不是 GetUuid
+
+ ModBase.Log("[Start] 第二阶段加载用时:" + (TimeUtils.GetTimeTick() - ModBase.ApplicationStartTick) + " ms");
+ // 注册生命周期状态事件
+ Lifecycle.When(LifecycleState.WindowCreated, FormMain_Loaded);
+ }
+
+ private void FormMain_Loaded() // (sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+ {
+ FormMain_SizeChanged();
+ ModBase.ApplicationStartTick = TimeUtils.GetTimeTick();
+ ModBase.FrmHandle = new WindowInteropHelper(this).Handle;
+ // 读取设置
+ ModBase.Setup.Load("UiBackgroundOpacity");
+ ModBase.Setup.Load("UiBackgroundBlur");
+ ModBase.Setup.Load("UiLogoType");
+ ModBase.Setup.Load("UiHiddenPageDownload");
+ ModBase.Setup.Load("UiAutoPauseVideo"); // 智能暂停视频背景
+ PageSetupUI.HiddenRefresh();
+ PageSetupUI.BackgroundRefresh(false, true);
+ ModMusic.MusicRefreshPlay(false, true);
+ // 扩展按钮
+ BtnExtraUpdateRestart.ShowCheck = BtnExtraUpdateRestart_ShowCheck;
+ BtnExtraDownload.ShowCheck = BtnExtraDownload_ShowCheck;
+ BtnExtraBack.ShowCheck = BtnExtraBack_ShowCheck;
+ BtnExtraApril.ShowCheck = BtnExtraApril_ShowCheck;
+ BtnExtraShutdown.ShowCheck = BtnExtraShutdown_ShowCheck;
+ BtnExtraLog.ShowCheck = BtnExtraLog_ShowCheck;
+ BtnExtraApril.ShowRefresh();
+ // 初始化尺寸改变
+ if (!Config.Preference.LockWindowSize)
+ AddResizer();
+ else
+ RemoveResizer();
+ // PLC 彩蛋
+ if (RandomUtils.NextInt(1, 1000) == 233)
+ ShapeTitleLogo.Data = (Geometry)new GeometryConverter().ConvertFromString(
+ "M26,29 v-25 h6 a7,7 180 0 1 0,14 h-6 M83,6.5 a10,11.5 180 1 0 0,18 M48,2.5 v24.5 h13.5");
+ // 加载窗口
+
+ ModSecret.ThemeRefresh();
+
+ Lifecycle.CurrentApplication.Resources["BlurSamplingRate"] = Config.Preference.Blur.SamplingRate * 0.01d;
+ Lifecycle.CurrentApplication.Resources["BlurType"] = Config.Preference.Blur.KernelType;
+ if (Config.Preference.Blur.IsEnabled)
+ Lifecycle.CurrentApplication.Resources["BlurRadius"] = Config.Preference.Blur.Radius * 1.0d;
+ else
+ Lifecycle.CurrentApplication.Resources["BlurRadius"] = 0.0d;
+
+ // #If DEBUG Then
+ // MinHeight = 50
+ // MinWidth = 50
+ // #End If
+ Topmost = false;
+ if (ModMain.FrmStart is not null)
+ ModMain.FrmStart.Close(new TimeSpan(0, 0, 0, 0, (int)Math.Round(400d / ModAnimation.AniSpeed)));
+ // 更改窗口
+ // Top = (GetWPFSize(My.Computer.Screen.WorkingArea.Height) - Height) / 2
+ // Left = (GetWPFSize(My.Computer.Screen.WorkingArea.Width) - Width) / 2
+ IsSizeSaveable = true;
+ ShowWindowToTop();
+ var HwndSource = (HwndSource)PresentationSource.FromVisual(this);
+ HwndSource.AddHook(WndProc);
+ ModAnimation.AniStart(new[]
+ {
+ ModAnimation.AaCode(() => ModAnimation.AniControlEnabled -= 1, 50),
+ ModAnimation.AaOpacity(this, Config.Preference.Theme.WindowOpacity / 1000d + 0.4d, 250, 100),
+ ModAnimation.AaDouble(i => TransformPos.Y += (double)i, -TransformPos.Y, 600,
+ 100, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
+ -TransformRotate.Angle, 500, 100, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaCode(() =>
+ {
+ RenderTransform = null;
+ IsWindowLoadFinished = true;
+ ModBase.Log(
+ $"[System] DPI:{ModBase.DPI},系统版本:{Environment.OSVersion.VersionString},PCL 位置:{ModBase.ExePathWithName}");
+ }, After: true)
+ }, "Form Show");
+ // Timer 启动
+ ModAnimation.AniStart();
+ ModMain.TimerMainStart();
+ // 特殊版本提示
+ ModBase.RunInNewThread(() =>
+ {
+ // 特殊版本提示
+ try
+ {
+
+
+#if DEBUG || DEBUGCI
+
+ if (Environment.GetEnvironmentVariable("PCL_DISABLE_DEBUG_HINT") is null)
+ {
+
+#if DEBUG
+ const string hint = """
+ 当前运行的 PCL 社区版为 Debug 版本。
+ 该版本仅适合开发者调试运行,可能会有严重的性能下降以及各种奇怪的网络问题。
+
+ 非开发者用户使用该版本造成的一切问题均不被社区支持,相关 issue 可能会被直接关闭。
+ 除非您是开发者,否则请立即删除该版本,并下载最新稳定版使用。
+ """;
+#else
+ const string hint = """
+ 当前运行的 PCL 社区版为 CI 自动构建版本。
+ 该版本包含最新的漏洞修复、优化和新特性,但性能和稳定性较差,不适合日常使用和制作整合包。
+
+ 除非社区开发者要求或您自己想要这么做,否则请下载最新稳定版使用。
+ """;
+#endif
+
+ ModMain.MyMsgBox(
+ $"{hint}{"\r\n"}{"\r\n"}可以添加 PCL_DISABLE_DEBUG_HINT 环境变量 (任意值) 来隐藏这个提示。",
+ "特殊版本提示", "我清楚我在做什么", "打开最新版下载页并退出", IsWarn: true, Button2Action: () =>
+ {
+ ModBase.OpenWebsite("https://github.com/PCL-Community/PCL2-CE/releases/latest");
+ EndProgram(false);
+ });
+ }
+
+
+#endif
+ // EULA 提示
+ if (!States.System.LauncherEula)
+ switch (ModMain.MyMsgBox("在使用 PCL 前,请同意 PCL 的用户协议与免责声明。", "协议授权", "同意", "拒绝", "查看用户协议与免责声明",
+ Button3Action: () => ModBase.OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW")))
+ {
+ case 1:
+ {
+ States.System.LauncherEula = true;
+ break;
+ }
+ case 2:
+ {
+ EndProgram(false);
+ break;
+ }
+ }
+
+ // 遥测提示
+ if (Config.System.TelemetryConfig.IsDefault())
+ {
+ var selection = ModMain.MyMsgBox(
+ "启用遥测数据收集后,启动器将会收集并上报错误与设备环境信息,这可以帮助开发者修复潜在的问题、更好的进行规划和开发。" + "\r\n" +
+ "若启用此功能,我们将会收集以下信息:" + "\r\n" + "\r\n" + "- 启动器内出现的错误" + "\r\n" + "- 启动器版本信息与识别码" +
+ "\r\n" + "- Windows 系统版本与架构" + "\r\n" + "- 已安装的物理内存大小" +
+ "\r\n" + "- NAT 与 IPv6 支持情况" + "\r\n" + "- 是否使用过官方版 PCL、HMCL 或 BakaXL" +
+ "\r\n" + "\r\n" + "这些数据均不与你关联,我们也绝不会向第三方出售数据。" + "\r\n" +
+ "如果不希望启用遥测,可以选择拒绝。这不会影响其他功能的正常使用,但可能会影响开发者修复潜在 Bug。" + "\r\n" + "你可以随时在启动器设置中调整这项设置。",
+ "启用遥测数据收集", "同意", "拒绝");
+ Config.System.TelemetryConfig.SetValue(selection == 1, forceNewValue: true);
+ }
+ // 启动加载器池
+ try
+ {
+ ModDownload.DlClientListMojangLoader.Start(1); // PCL 会同时根据这里的加载结果决定是否使用官方源进行下载
+ RunCountSub();
+ ModSecret.ServerLoader.Start(1);
+ ModBase.RunInNewThread(ModMain.TryClearTaskTemp, "TryClearTaskTemp", ThreadPriority.BelowNormal);
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "初始化加载池运行失败", ModBase.LogLevel.Feedback);
+ }
+
+ ModSecret.GetSystemInfo();
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "初始弹窗提示运行失败", ModBase.LogLevel.Feedback);
+ }
+ }, "Start Loader", ThreadPriority.BelowNormal);
+
+ ModBase.Log($"[Start] 第三阶段加载用时:{TimeUtils.GetTimeTick() - ModBase.ApplicationStartTick} ms");
+ }
+
+ // 根据打开次数触发的事件
+ private void RunCountSub()
+ {
+ States.System.StartupCount += 1;
+ if (States.System.StartupCount < 99) return;
+ if (ModSecret.ThemeUnlock(6, false))
+ ModMain.MyMsgBox("你已经打开了 99 次 PCL 社区版啦,感谢你长期以来的支持!" + "\r\n" + "隐藏主题 铁杆粉 未解锁!社区版不包含隐藏主题!");
+ }
+
+ // 升级与降级事件
+ private void UpgradeSub(int LastVersionCode)
+ {
+ ModBase.Log("[Start] 版本号从 " + LastVersionCode + " 升高到 " + ModBase.VersionCode);
+ States.System.LastVersion = ModBase.VersionCode;
+ // 检查有记录的最高版本号
+ int LowerVersionCode;
+#if BETA
+ LowerVersionCode = Setup.Get("SystemHighestBetaVersionReg")
+ If LowerVersionCode < VersionCode Then
+ Setup.Set("SystemHighestBetaVersionReg", VersionCode)
+ Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
+ End If
+#else
+ LowerVersionCode = States.System.LastAlphaVersion;
+ if (LowerVersionCode < ModBase.VersionCode)
+ {
+ States.System.LastAlphaVersion = ModBase.VersionCode;
+ ModBase.Log("[Start] 最高版本号从 " + LowerVersionCode + " 升高到 " + ModBase.VersionCode);
+ }
+#endif
+
+ // 被移除的窗口设置选项
+ if ((int)Config.Launch.GameWindowMode == 5)
+ Config.Launch.GameWindowMode = GameWindowSizeMode.Default;
+ // 修改主题设置项名称
+ if (LowerVersionCode <= 207)
+ {
+ var UnlockedTheme = new List { "2" };
+ UnlockedTheme.AddRange(new List(States.UI.ThemeHiddenV1.ToString().Split("|")));
+ UnlockedTheme.AddRange(new List(States.UI.ThemeHiddenV2.ToString().Split("|")));
+ States.UI.ThemeHiddenV2 = UnlockedTheme.Distinct().ToList().Join("|");
+ }
+
+ // 重置欧皇彩
+ if (LastVersionCode <= 115 && States.UI.ThemeHiddenV2.ToString().Split("|").Contains("13"))
+ {
+ var UnlockedTheme = new List(States.UI.ThemeHiddenV2.ToString().Split("|"));
+ UnlockedTheme.Remove("13");
+ States.UI.ThemeHiddenV2 = UnlockedTheme.Join("|");
+ ModMain.MyMsgBox("由于新版 PCL 修改了欧皇彩的解锁方式,你需要重新解锁欧皇彩。" + "\r\n" + "多谢各位的理解啦!", "重新解锁提醒");
+ }
+
+ // 重置滑稽彩
+ if (LastVersionCode <= 152 && States.UI.ThemeHiddenV2.ToString().Split("|").Contains("12"))
+ {
+ var UnlockedTheme = new List(States.UI.ThemeHiddenV2.ToString().Split("|"));
+ UnlockedTheme.Remove("12");
+ States.UI.ThemeHiddenV2 = UnlockedTheme.Join("|");
+ ModMain.MyMsgBox("由于新版 PCL 修改了滑稽彩的解锁方式,你需要重新解锁滑稽彩。" + "\r\n" + "多谢各位的理解啦!", "重新解锁提醒");
+ }
+
+ // 移动自定义皮肤
+ if (LastVersionCode <= 161 && File.Exists(ModBase.ExePath + @"PCL\CustomSkin.png") &&
+ !File.Exists(ModBase.PathAppdata + "CustomSkin.png"))
+ {
+ ModBase.CopyFile(ModBase.ExePath + @"PCL\CustomSkin.png", ModBase.PathAppdata + "CustomSkin.png");
+ ModBase.Log("[Start] 已移动离线自定义皮肤 (162)");
+ }
+
+ if (LastVersionCode <= 263 && File.Exists(ModBase.PathTemp + "CustomSkin.png") &&
+ !File.Exists(ModBase.PathAppdata + "CustomSkin.png"))
+ {
+ ModBase.CopyFile(ModBase.PathTemp + "CustomSkin.png", ModBase.PathAppdata + "CustomSkin.png");
+ ModBase.Log("[Start] 已移动离线自定义皮肤 (264)");
+ }
+
+ // 解除帮助页面的隐藏
+ if (LastVersionCode <= 205)
+ {
+ Config.Preference.Hide.SetupAbout = false;
+ ModBase.Log("[Start] 已解除帮助页面的隐藏");
+ }
+
+ // 迁移旧版用户档案
+ if (LastVersionCode <= 368) ModBase.RunInNewThread(() => ModProfile.MigrateOldProfile());
+ // Mod 命名设置迁移
+ if (!ModBase.Setup.IsUnset("ToolDownloadTranslate") && ModBase.Setup.IsUnset("ToolDownloadTranslateV2"))
+ {
+ Config.Download.Comp.NameFormatV2 += 1;
+ ModBase.Log("[Start] 已从老版本迁移 Mod 命名设置");
+ }
+
+ // 更新后展示社区版提示
+ ModSecret.ShowCEAnnounce();
+ // 输出更新日志
+ if (LastVersionCode <= 0)
+ return;
+ if (LowerVersionCode >= ModBase.VersionCode)
+ return;
+ ShowUpdateLog();
+ }
+
+ private void DowngradeSub(int LastVersionCode)
+ {
+ ModBase.Log("[Start] 版本号从 " + LastVersionCode + " 降低到 " + ModBase.VersionCode);
+ States.System.LastVersion = ModBase.VersionCode;
+ }
+
+ #endregion
+
+ #region 自定义窗口
+
+ private bool CanResize = true;
+
+ // 重写窗口边缘判定以使 DWM 自带的 resizer 行为看起来比较正常
+ private nint _SizeWndProc(nint hWnd, int msg, nint wParam, nint lParam, ref bool handled)
+ {
+ // 窗口活动常量
+ const int WM_NCHITTEST = 0x84;
+ const int HTCLIENT = 1;
+ const int HTLEFT = 10;
+ const int HTRIGHT = 11;
+ const int HTTOP = 12;
+ const int HTTOPLEFT = 13;
+ const int HTTOPRIGHT = 14;
+ const int HTBOTTOM = 15;
+ const int HTBOTTOMLEFT = 16;
+ const int HTBOTTOMRIGHT = 17;
+
+ // WPF 尺寸的 offset
+ const int offsetWpf = 6;
+ const int hitWidthWpf = 5;
+
+ // 过滤非 WM_NCHITTEST 事件
+ if (msg != WM_NCHITTEST)
+ return nint.Zero;
+
+ // 提取鼠标坐标
+ // 没妈的 VB 强转还得检查一下幻想的妈是不是还活着
+ var mouseBytes = BitConverter.GetBytes(lParam.ToInt64());
+ var xMouse = BitConverter.ToInt16(mouseBytes, 0);
+ var yMouse = BitConverter.ToInt16(mouseBytes, 2);
+
+ // 获取窗口参数
+ var windowRect = WindowInterop.GetWindowRectangle(hWnd);
+ var windowBounds = windowRect.ToWindowBounds();
+
+ // 判断鼠标是否在窗口范围内
+ var isInWindow = xMouse >= windowRect.Left && xMouse <= windowRect.Right && yMouse >= windowRect.Top &&
+ yMouse <= windowRect.Bottom;
+
+ // 过滤不在窗口内的请求
+ if (!isInWindow)
+ return nint.Zero;
+
+ // 如果 CanResize 为 False,直接返回 HTCLIENT
+ if (!CanResize)
+ return new nint(HTCLIENT);
+
+ // 真实像素尺寸的 offset
+ var dpi = VisualTreeHelper.GetDpi(this);
+ var offsetPxX = offsetWpf * dpi.DpiScaleX;
+ var offsetPxY = offsetWpf * dpi.DpiScaleY;
+ var hitWidthPxX = hitWidthWpf * dpi.DpiScaleX;
+ var hitWidthPxY = hitWidthWpf * dpi.DpiScaleY;
+
+ // 计算鼠标相对于窗口左上角的物理像素位置
+ var relX = xMouse - windowRect.Left;
+ var relY = yMouse - windowRect.Top;
+ var w = windowBounds.Width;
+ var h = windowBounds.Height;
+
+ // 判定是否命中偏移后的热区
+ var inLeft = relX >= offsetPxX && relX <= offsetPxX + hitWidthPxX;
+ var inRight = relX <= w - offsetPxX && relX >= w - offsetPxX - hitWidthPxX;
+ var inTop = relY >= offsetPxY && relY <= offsetPxY + hitWidthPxY;
+ var inBottom = relY <= h - offsetPxY && relY >= h - offsetPxY - hitWidthPxY;
+
+ handled = true; // 接管该区域的消息
+
+ // 返回结果
+ if (inTop && inLeft)
+ return new nint(HTTOPLEFT);
+ if (inTop && inRight)
+ return new nint(HTTOPRIGHT);
+ if (inBottom && inLeft)
+ return new nint(HTBOTTOMLEFT);
+ if (inBottom && inRight)
+ return new nint(HTBOTTOMRIGHT);
+ if (inLeft)
+ return new nint(HTLEFT);
+ if (inRight)
+ return new nint(HTRIGHT);
+ if (inTop)
+ return new nint(HTTOP);
+ if (inBottom)
+ return new nint(HTBOTTOM);
+
+ // 如果在 0-offset 范围内,返回 HTCLIENT 杀掉默认缩放
+ return new nint(HTCLIENT);
+ }
+
+ protected override void OnSourceInitialized(EventArgs e)
+ {
+ // 硬件加速
+ if (Config.System.DisableHardwareAcceleration)
+ {
+ var hwndSource = PresentationSource.FromVisual(this) as HwndSource;
+ if (hwndSource is not null) hwndSource.CompositionTarget.RenderMode = RenderMode.SoftwareOnly;
+ }
+
+ base.OnSourceInitialized(e);
+
+ // 获取当前窗口句柄
+ var hwnd = new WindowInteropHelper(this).Handle;
+ var source = HwndSource.FromHwnd(hwnd);
+ if (source is not null)
+ {
+ // 渲染层允许 Alpha 通道通过
+ source.CompositionTarget.BackgroundColor = Colors.Transparent;
+ // 魔改窗口边缘判定
+ source.AddHook(_SizeWndProc);
+ }
+
+ // 设置 DWM 窗口框架
+ try
+ {
+ WindowInterop.ExtendFrameIntoClientArea(hwnd, -1);
+ }
+ catch (Exception ex)
+ {
+ LogWrapper.Error("DWM 窗口框架应用失败: " + ex.Message);
+ }
+ }
+
+ // 关闭
+ private void FormMain_Closing(object sender, CancelEventArgs e)
+ {
+ EndProgram(true);
+ e.Cancel = true;
+ }
+
+ ///
+ /// 正常关闭程序。程序将在执行此方法后约 0.3s 退出。
+ ///
+ /// 是否在还有下载任务未完成时发出警告。
+ /// 是否正在更新重启
+ public void EndProgram(bool SendWarning, bool isUpdating = false)
+ {
+ // 发出警告
+ if (SendWarning && ModNet.HasDownloadingTask())
+ {
+ if (ModMain.MyMsgBox("还有下载任务尚未完成,是否确定退出?", "提示", "确定", "取消") == 1)
+ // 强行结束下载任务
+ ModBase.RunInNewThread(() =>
+ {
+ ModBase.Log("[System] 正在强行停止任务");
+ foreach (var Task in ModLoader.LoaderTaskbar.ToList())
+ Task.Abort();
+ }, "强行停止下载任务");
+ else
+ return;
+ }
+
+ // 关闭联机大厅
+ // Await LobbyController.CloseAsync().ConfigureAwait(False)
+ // 存储上次使用的档案编号
+ ModProfile.SaveProfile();
+ // 关闭
+ ModBase.RunInUiWait(() =>
+ {
+ // 清理视频背景
+ VideoBack.Stop();
+ VideoBack.Source = null;
+ VideoBack.Close();
+ IsHitTestVisible = false;
+ if (RenderTransform is null)
+ {
+ var TransformPos = new TranslateTransform(0d, 0d);
+ var TransformRotate = new RotateTransform(0d);
+ var TransformScale = new ScaleTransform(1d, 1d);
+ TransformScale.CenterX = Width / 2d;
+ TransformScale.CenterY = Height / 2d;
+ RenderTransform = new TransformGroup
+ { Children = new TransformCollection([TransformRotate, TransformPos, TransformScale]) };
+ ModAnimation.AniStart(new[]
+ {
+ ModAnimation.AaOpacity(this, -Opacity, 140, 40,
+ new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaDouble(i =>
+ {
+ TransformScale.ScaleX += (double)i;
+ TransformScale.ScaleY += (double)i;
+ }, 0.88d - TransformScale.ScaleX, 180),
+ ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
+ 20d - TransformPos.Y, 180, 0,
+ new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
+ 0.6d - TransformRotate.Angle, 180, 0,
+ new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaCode(() =>
+ {
+ IsHitTestVisible = false;
+ Visibility = Visibility.Collapsed;
+ ShowInTaskbar = false;
+ }, 210),
+ ModAnimation.AaCode(() => EndProgramForce(force: false, isUpdating: isUpdating), 230)
+ }, "Form Close");
+ }
+ else
+ {
+ EndProgramForce(force: false, isUpdating: isUpdating);
+ }
+
+ ModBase.Log("[System] 收到关闭指令");
+ });
+ }
+
+ private static bool IsLogShown;
+
+ public static void EndProgramForce(ModBase.ProcessReturnValues ReturnCode = ModBase.ProcessReturnValues.Success,
+ bool force = true, bool isUpdating = false)
+ {
+ // On Error Resume Next
+ // 关闭联机大厅
+ // Await LobbyController.CloseAsync().ConfigureAwait(False)
+ ModBase.IsProgramEnded = true;
+ ModAnimation.AniControlEnabled += 1;
+ if (ModSecret.IsUpdateWaitingRestart && !isUpdating)
+ ModSecret.UpdateRestart(false, false);
+ if (ReturnCode == ModBase.ProcessReturnValues.Exception)
+ {
+ if (!IsLogShown)
+ {
+ ModBase.FeedbackInfo();
+ ModBase.Log("请在 https://github.com/PCL-Community/PCL2-CE/issues 提交错误报告,以便于社区解决此问题!(这也有可能是原版 PCL 的问题)");
+ IsLogShown = true;
+ ModBase.ShellOnly(LogWrapper.CurrentLogger.CurrentLogFiles.Last());
+ }
+
+ Thread.Sleep(500); // 防止 PCL 在记事本打开前就被掐掉
+ }
+
+ ModBase.Log("[System] 程序已退出,返回值:" + ModBase.GetStringFromEnum(ReturnCode));
+ // If ReturnCode <> ProcessReturnValues.Success Then Environment.Exit(ReturnCode)
+ // Process.GetCurrentProcess.Kill()
+ Lifecycle.Shutdown((int)ReturnCode, force);
+ }
+
+ private void BtnTitleClose_Click(object sender, EventArgs e)
+ {
+ EndProgram(true);
+ }
+
+ // 移动
+ private void FormDragMove(object sender, MouseButtonEventArgs e)
+ {
+ // On Error Resume Next
+ if (((Grid)sender).IsMouseDirectlyOver)
+ DragMove();
+ }
+
+ // 改变大小
+ ///
+ /// 是否可以向注册表储存尺寸改变信息。以此避免初始化时误储存。
+ ///
+ public bool IsSizeSaveable;
+
+ private void FormMain_SizeChanged(object? sender = null, EventArgs? e = null)
+ {
+ if (IsSizeSaveable)
+ {
+ States.UI.WindowHeight = Height;
+ States.UI.WindowWidth = Width;
+ }
+
+ if (PanBack is not null)
+ {
+ RectForm.Rect = new Rect(0d, 0d, PanBack.ActualWidth, PanBack.ActualHeight);
+
+ var formWidth = PanBack.ActualWidth + 0.001d;
+ var formHeight = PanBack.ActualHeight + 0.001d;
+
+ PanForm.Width = formWidth;
+ PanForm.Height = formHeight;
+ PanMain.Width = formWidth;
+
+ if (PanTitle is not null)
+ PanMain.Height = Math.Max(0d, formHeight - PanTitle.ActualHeight);
+ else
+ PanMain.Height = formHeight;
+
+ VideoBack.Width = formWidth;
+ VideoBack.Height = formHeight;
+ }
+
+ if (WindowState == WindowState.Maximized)
+ WindowState = WindowState.Normal; // 修复 #1938
+ }
+
+ // 标题栏改变大小
+ private void PanTitle_SizeChanged(object sender, EventArgs e)
+ {
+ if (PanTitleMain.ColumnDefinitions[0].ActualWidth - 30 <= 0)
+ PanTitleLeft.ColumnDefinitions[0].MaxWidth = 0;
+ else
+ PanTitleLeft.ColumnDefinitions[0].MaxWidth = PanTitleMain.ColumnDefinitions[0].ActualWidth - 30;
+ }
+
+ // 最小化
+ private void BtnTitleMin_Click(object sender, EventArgs e)
+ {
+ WindowState = WindowState.Minimized;
+ }
+
+ //“帮助”
+ private void BtnTitleHelp_Click(object sender, EventArgs e)
+ {
+ ModBase.OpenWebsite("https://www.bilibili.com/video/BV1uT4y1P7CX");
+ }
+
+ #endregion
+
+ #region 窗体事件
+
+ public void AddResizer()
+ {
+ CanResize = true;
+ }
+
+ public void RemoveResizer()
+ {
+ CanResize = false;
+ }
+
+ // 按键事件
+ private void FormMain_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.IsRepeat)
+ return;
+ // 调用弹窗:回车选择第一个,Esc 选择最后一个
+ if (PanMsg.Children.Count > 0)
+ {
+ if (e.Key == Key.Enter)
+ {
+ ((MyMsgInput)PanMsg.Children[0]).Btn1_Click(sender, null);
+ return;
+ }
+
+ if (e.Key == Key.Escape)
+ {
+ var msg = PanMsg.Children[0];
+ Action? escapeAction = msg switch
+ {
+ MyMsgInput input => input.Btn2.Visibility == Visibility.Visible
+ ? () => input.Btn2_Click(sender, null)
+ : () => input.Btn1_Click(sender, null),
+ MyMsgSelect select => select.Btn2.Visibility == Visibility.Visible
+ ? () => select.Btn2_Click(sender, null)
+ : () => select.Btn1_Click(sender, null),
+ MyMsgText text => text.Btn3.Visibility == Visibility.Visible
+ ? () => text.Btn3_Click(sender, null)
+ : text.Btn2.Visibility == Visibility.Visible
+ ? () => text.Btn2_Click(sender, null)
+ : () => text.Btn1_Click(sender, null),
+ MyMsgMarkdown markdown => markdown.Btn3.Visibility == Visibility.Visible
+ ? () => markdown.Btn3_Click(sender, null)
+ : markdown.Btn2.Visibility == Visibility.Visible
+ ? () => markdown.Btn2_Click(sender, null)
+ : () => markdown.Btn1_Click(sender, null),
+ MyMsgLogin login => login.Btn3.Visibility == Visibility.Visible
+ ? () => login.Btn3_Click(sender, null)
+ : () => login.Btn1_Click(sender, null),
+ _ => null
+ };
+ escapeAction?.Invoke();
+ return;
+ }
+ }
+
+ // 按 ESC 返回上一级
+ if (e.Key == Key.Escape)
+ TriggerPageBack();
+ // 更改隐藏实例可见性
+ if (e.Key == Key.F11 && PageCurrent == PageType.InstanceSelect)
+ {
+ ModMain.FrmSelectRight.ShowHidden = !ModMain.FrmSelectRight.ShowHidden;
+ ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected,
+ ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\");
+ return;
+ }
+
+ // 更改功能隐藏可见性
+ if (e.Key == Key.F12)
+ {
+ PageSetupUI.HiddenForceShow = !PageSetupUI.HiddenForceShow;
+ if (PageSetupUI.HiddenForceShow)
+ ModMain.Hint("功能隐藏设置已暂时关闭!", ModMain.HintType.Finish);
+ else
+ ModMain.Hint("功能隐藏设置已重新开启!", ModMain.HintType.Finish);
+ PageSetupUI.HiddenRefresh();
+ return;
+ }
+
+ // 按 F5 刷新页面
+ if (e.Key == Key.F5)
+ {
+ if (PageLeft is IRefreshable)
+ ((IRefreshable)PageLeft).Refresh();
+ if (PageRight is IRefreshable)
+ ((IRefreshable)PageRight).Refresh();
+ return;
+ }
+
+ // 调用启动游戏
+ if (e.Key == Key.Enter && PageCurrent == PageType.Launch)
+ {
+ if (ModMain.IsAprilEnabled && !ModMain.IsAprilGiveup)
+ ModMain.Hint("木大!");
+ else
+ ModMain.FrmLaunchLeft.LaunchButtonClick();
+ }
+
+ // 修复按下 Alt 后误认为弹出系统菜单导致的冻结
+ if (e.SystemKey == Key.LeftAlt || e.SystemKey == Key.RightAlt)
+ e.Handled = true;
+ }
+
+ private void FormMain_MouseDown(object sender, MouseButtonEventArgs e)
+ {
+ // 鼠标侧键返回上一级
+ if (ModMain.FrmMain!.PanMsg.Children.Count > 0 || ModMain.WaitingMyMsgBox.Any())
+ return; // 弹窗中(#5513)
+ if (e.ChangedButton == MouseButton.XButton1 || e.ChangedButton == MouseButton.XButton2)
+ TriggerPageBack();
+ }
+
+ private void TriggerPageBack()
+ {
+ if (PageCurrent == PageType.Download && PageCurrentSub == PageSubType.DownloadInstall &&
+ ModMain.FrmDownloadInstall.IsInSelectPage)
+ ModMain.FrmDownloadInstall.ExitSelectPage();
+ else if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionInstall &&
+ ModMain.FrmInstanceInstall.IsInSelectPage)
+ ModMain.FrmInstanceInstall.ExitSelectPage();
+ else
+ PageBack();
+ }
+
+ // 切回窗口
+ private void FormMain_Activated(object sender, EventArgs e)
+ {
+ try
+ {
+ if (Config.Download.Comp.ReadClipboard)
+ ModComp.CompClipboard.GetClipboardResource();
+ if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionMod)
+ {
+ // Mod 管理自动刷新
+ ModMain.FrmInstanceMod.ReloadCompFileList();
+ }
+ else if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionResourcePack)
+ {
+ // 资源包管理自动刷新
+ if (ModMain.FrmInstanceResourcePack is not null)
+ ModMain.FrmInstanceResourcePack.ReloadCompFileList();
+ }
+ else if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionShader)
+ {
+ // 光影包管理自动刷新
+ if (ModMain.FrmInstanceShader is not null)
+ ModMain.FrmInstanceShader.ReloadCompFileList();
+ }
+ else if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionSchematic)
+ {
+ // 投影原理图管理自动刷新
+ if (ModMain.FrmInstanceSchematic is not null)
+ ModMain.FrmInstanceSchematic.ReloadCompFileList();
+ }
+ else if (PageCurrent == PageType.InstanceSelect)
+ {
+ // 实例选择自动刷新
+ ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected,
+ ModLoader.LoaderFolderRunType.RunOnUpdated, 1, @"versions\");
+ }
+ else if (ModMain.FrmMain.PageRight is PageInstanceSavesDatapack &&
+ ModMain.FrmInstanceSavesDatapack is not null)
+ {
+ // 数据包管理自动刷新
+ ModMain.FrmInstanceSavesDatapack.ReloadDatapackFileList();
+ }
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "切回窗口时出错", ModBase.LogLevel.Feedback);
+ }
+ }
+
+ private IDataObject _HandleDrag_PrevData;
+ private DragDropEffects _HandleDrag_PrevEffects;
+
+ // 文件拖放
+ private void HandleDrag(object sender, DragEventArgs e)
+ {
+ try
+ {
+ if (e.Handled && e.Effects != DragDropEffects.None)
+ return;
+ // 缓存
+ e.Handled = true;
+ if (ReferenceEquals(e.Data, _HandleDrag_PrevData))
+ {
+ e.Effects = _HandleDrag_PrevEffects;
+ return;
+ }
+
+ // 确定拖放效果
+ e.Effects = DragDropEffects.None;
+ if (e.Data.GetDataPresent(DataFormats.Text))
+ {
+ var Str = (string)e.Data.GetData(DataFormats.Text);
+ if (Str.StartsWithF("authlib-injector:yggdrasil-server:"))
+ e.Effects = DragDropEffects.Copy;
+ else if (Str.StartsWithF("file:///")) e.Effects = DragDropEffects.Copy;
+ }
+ else if (e.Data.GetDataPresent(DataFormats.FileDrop))
+ {
+ var Files = (string[])e.Data.GetData(DataFormats.FileDrop);
+ if (Files is not null && Files.Length > 0) e.Effects = DragDropEffects.Link;
+ }
+
+ _HandleDrag_PrevData = e.Data;
+ _HandleDrag_PrevEffects = e.Effects;
+ ModBase.Log("[System] 设置拖放类型:" + ModBase.GetStringFromEnum(e.Effects));
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "处理拖放时出错", ModBase.LogLevel.Feedback);
+ }
+ }
+
+ private void FrmMain_Drop(object sender, DragEventArgs e)
+ {
+ try
+ {
+ if (e.Data.GetDataPresent(DataFormats.Text))
+ {
+ // 获取文本
+ try
+ {
+ var Str = (string)e.Data.GetData(DataFormats.Text);
+ ModBase.Log("[System] 接受文本拖拽:" + Str);
+ if (Str.StartsWithF("authlib-injector:yggdrasil-server:"))
+ {
+ // Authlib 拖拽
+ e.Handled = true;
+ e.Effects = DragDropEffects.Copy;
+ var AuthlibServer =
+ WebUtility.UrlDecode(Str.Substring("authlib-injector:yggdrasil-server:".Length));
+ ModBase.Log("[System] Authlib 拖拽:" + AuthlibServer);
+ if (!new HttpValidator().Validate(AuthlibServer).IsValid)
+ {
+ ModMain.Hint($"输入的 Authlib 验证服务器不符合网址格式({AuthlibServer})!", ModMain.HintType.Critical);
+ return;
+ }
+
+ if (ModMain.MyMsgBox($"是否要创建新的第三方验证档案?{"\r\n"}验证服务器地址:{AuthlibServer}", "创建新的第三方验证档案",
+ "确定", "取消") == 2)
+ return;
+ ModProfile.SelectedProfile = null;
+ ModBase.RunInUi(() =>
+ {
+ PageLoginAuth.DraggedAuthServer = AuthlibServer;
+ ModMain.FrmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Auth);
+ });
+ if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionSetup)
+ // 正在服务器选项页,需要刷新设置项显示
+ ModMain.FrmInstanceSetup.Reload();
+ }
+ else if (Str.StartsWithF("file:///"))
+ {
+ // 文件拖拽(例如从浏览器下载窗口拖入)
+ var FilePath = WebUtility.UrlDecode(Str).Substring("file:///".Length).Replace("/", @"\");
+ e.Handled = true;
+ e.Effects = DragDropEffects.Copy;
+ FileDrag(new List { FilePath });
+ }
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "无法接取文本拖拽事件", ModBase.LogLevel.Developer);
+ }
+ }
+ else if (e.Data.GetDataPresent(DataFormats.FileDrop))
+ {
+ // 获取文件并检查
+ var FilePathRaw = e.Data.GetData(DataFormats.FileDrop);
+ if (FilePathRaw is null) // #2690
+ {
+ ModMain.Hint("请将文件解压后再拖入!", ModMain.HintType.Critical);
+ return;
+ }
+
+ e.Handled = true;
+ e.Effects = DragDropEffects.Link;
+ FileDrag((IEnumerable)FilePathRaw);
+ }
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "接取拖拽事件失败", ModBase.LogLevel.Feedback);
+ }
+ }
+
+ private void FileDrag(IEnumerable FilePathList)
+ {
+ ModBase.RunInNewThread(() =>
+ {
+ var FilePath = FilePathList.First();
+ ModBase.Log("[System] 接受文件拖拽:" + FilePath + (FilePathList.Any() ? $" 等 {FilePathList.Count()} 个文件" : ""),
+ ModBase.LogLevel.Developer);
+ // 基础检查
+ if (Directory.Exists(FilePathList.First()) && !File.Exists(FilePathList.First()))
+ {
+ ModMain.Hint("请拖入一个文件,而非文件夹!", ModMain.HintType.Critical);
+ return;
+ }
+
+ if (!File.Exists(FilePathList.First()))
+ {
+ ModMain.Hint("拖入的文件不存在:" + FilePathList.First(), ModMain.HintType.Critical);
+ return;
+ }
+
+ // 多文件拖拽
+ if (FilePathList.Count() > 1)
+ {
+ // 检查是否为同类型文件
+ var FirstExtension = FilePathList.First().AfterLast(".").ToLower();
+ var AllSameType = FilePathList.All(f => (f.AfterLast(".").ToLower() ?? "") == (FirstExtension ?? ""));
+
+ if (AllSameType &&
+ new[] { "jar", "litemod", "disabled", "old", "litematic", "nbt", "schematic", "schem" }.Contains(
+ FirstExtension))
+ {
+ }
+ // 允许同类型的 Mod 文件或投影文件批量拖拽
+ else
+ {
+ ModMain.Hint("一次请只拖入相同类型的文件!", ModMain.HintType.Critical);
+ return;
+ }
+ }
+
+ // 主页
+ var Extension = FilePath.AfterLast(".").ToLower();
+ if (Extension == "xaml")
+ {
+ ModBase.Log("[System] 文件后缀为 XAML,作为主页加载");
+ if (File.Exists(ModBase.ExePath + @"PCL\Custom.xaml"))
+ if (ModMain.MyMsgBox("已存在一个主页文件,是否要将它覆盖?", "覆盖确认", "覆盖", "取消") == 2)
+ return;
+
+ ModBase.CopyFile(FilePath, ModBase.ExePath + @"PCL\Custom.xaml");
+ ModBase.RunInUi(() =>
+ {
+ Config.Preference.Homepage.Type = 1;
+ ModMain.FrmLaunchRight.ForceRefresh();
+ ModMain.Hint("已加载主页自定义文件!", ModMain.HintType.Finish);
+ });
+ return;
+ }
+
+ // 安装 Mod
+ if (PageInstanceCompResource.InstallMods(FilePathList))
+ return;
+ // 安装投影文件
+ if (new[] { "litematic", "nbt", "schematic", "schem" }.Contains(Extension))
+ {
+ ModBase.Log($"[System] 文件为 {Extension} 格式,尝试作为原理图安装");
+ // 获取当前文件夹路径(如果在资源管理页面)
+ string targetFolderPath = null;
+ if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionSchematic &&
+ ModMain.FrmInstanceSchematic is not null &&
+ ModMain.FrmInstanceSchematic is PageInstanceCompResource)
+ targetFolderPath = ModMain.FrmInstanceSchematic.CurrentFolderPath;
+ PageInstanceCompResource.InstallCompFiles(FilePathList, ModComp.CompType.Schematic, targetFolderPath);
+ return;
+ }
+
+ // 处理资源安装
+ if (PageCurrent == PageType.InstanceSetup && new[] { "zip" }.Any(i => (i ?? "") == (Extension ?? "")))
+ switch (PageCurrentSub)
+ {
+ case PageSubType.VersionWorld:
+ {
+ var DestFolder = PageInstanceLeft.Instance.PathIndie + @"saves\" +
+ ModBase.GetFileNameWithoutExtentionFromPath(FilePath);
+ if (Directory.Exists(DestFolder))
+ {
+ ModMain.Hint("发现同名文件夹,无法粘贴:" + DestFolder, ModMain.HintType.Critical);
+ return;
+ }
+
+ ModBase.ExtractFile(FilePath, DestFolder);
+ ModMain.Hint($"已导入 {ModBase.GetFileNameWithoutExtentionFromPath(FilePath)}",
+ ModMain.HintType.Finish);
+ if (ModMain.FrmInstanceSaves is not null)
+ ModBase.RunInUi(() => ModMain.FrmInstanceSaves.Reload());
+ return;
+ }
+ case PageSubType.VersionResourcePack:
+ {
+ var DestFile = PageInstanceLeft.Instance.PathIndie + @"resourcepacks\" +
+ ModBase.GetFileNameFromPath(FilePath);
+ if (File.Exists(DestFile))
+ {
+ ModMain.Hint("已存在同名文件:" + DestFile, ModMain.HintType.Critical);
+ return;
+ }
+
+ ModBase.CopyFile(FilePath, DestFile);
+ ModMain.Hint($"已导入 {ModBase.GetFileNameFromPath(FilePath)}", ModMain.HintType.Finish);
+ if (ModMain.FrmInstanceResourcePack is not null)
+ ModBase.RunInUi(() => ModMain.FrmInstanceResourcePack.ReloadCompFileList());
+ return;
+ }
+ case PageSubType.VersionShader:
+ {
+ var DestFile = PageInstanceLeft.Instance.PathIndie + @"shaderpacks\" +
+ ModBase.GetFileNameFromPath(FilePath);
+ if (File.Exists(DestFile))
+ {
+ ModMain.Hint("已存在同名文件:" + DestFile, ModMain.HintType.Critical);
+ return;
+ }
+
+ ModBase.CopyFile(FilePath, DestFile);
+ ModMain.Hint($"已导入 {ModBase.GetFileNameFromPath(FilePath)}", ModMain.HintType.Finish);
+ if (ModMain.FrmInstanceShader is not null)
+ ModBase.RunInUi(() => ModMain.FrmInstanceShader.ReloadCompFileList());
+ return;
+ }
+ }
+
+ // 处理投影文件
+ if (PageCurrent == PageType.InstanceSetup &&
+ new[] { "litematic", "nbt", "schematic", "schem" }.Contains(Extension) &&
+ PageCurrentSub == PageSubType.VersionSchematic)
+ {
+ var DestFile = PageInstanceLeft.Instance.PathIndie + @"schematics\" +
+ ModBase.GetFileNameFromPath(FilePath);
+ if (File.Exists(DestFile))
+ {
+ ModMain.Hint("已存在同名文件:" + DestFile, ModMain.HintType.Critical);
+ return;
+ }
+
+ Directory.CreateDirectory(PageInstanceLeft.Instance.PathIndie + @"schematics\");
+ ModBase.CopyFile(FilePath, DestFile);
+ ModMain.Hint($"已导入 {ModBase.GetFileNameFromPath(FilePath)}", ModMain.HintType.Finish);
+ if (ModMain.FrmInstanceSchematic is not null)
+ ModBase.RunInUi(() => ModMain.FrmInstanceSchematic.ReloadCompFileList());
+ return;
+ }
+
+ // 安装整合包
+ if (new[] { "zip", "rar", "mrpack" }.Any(t =>
+ (t ?? "") == (Extension ?? ""))) // 部分压缩包是 zip 格式但后缀为 rar,总之试一试
+ {
+ ModBase.Log("[System] 文件为压缩包,尝试作为整合包安装");
+ try
+ {
+ ModModpack.ModpackInstall(FilePath);
+ return;
+ }
+ catch (ModBase.CancelledException ex)
+ {
+ return; // 用户主动取消
+ }
+ catch (Exception ex)
+ {
+ // 安装失败,继续往后尝试
+ }
+ }
+
+ if (new[] { "zip", "rar" }.Any(t => (t ?? "") == (Extension ?? "")))
+ {
+ ModBase.Log("[System] 文件为压缩包,尝试作为存档分析");
+ try
+ {
+ ModWorld.ReadWorld(FilePath);
+ return;
+ }
+ catch (ModBase.CancelledException ex)
+ {
+ return; // 是存档,但是损坏了
+ }
+ catch (Exception ex)
+ {
+ // 不是存档(或遇到了其他问题),继续往后尝试
+ }
+ }
+
+ // 错误报告分析
+ do
+ {
+ try
+ {
+ ModBase.Log("[System] 尝试进行错误报告分析");
+ var Analyzer = new CrashAnalyzer(ModBase.GetUuid());
+ Analyzer.Import(FilePath);
+ if (!Analyzer.Prepare())
+ break;
+ Analyzer.Analyze();
+ Analyzer.Output(true, new List());
+ return;
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "自主错误报告分析失败", ModBase.LogLevel.Feedback);
+ }
+ } while (false);
+
+ // 未知操作
+ ModMain.Hint("PCL 无法确定应当执行的文件拖拽操作……");
+ }, "文件拖拽");
+ }
+
+ // 接受到 Windows 窗体事件
+ public bool IsSystemTimeChanged;
+
+ private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
+ {
+ if (msg == 30)
+ {
+ var NowDate = DateTime.Now;
+ if (NowDate.Date == ModBase.ApplicationOpenTime.Date)
+ {
+ ModBase.Log("[System] 系统时间微调为:" + NowDate.ToLongDateString() + " " + NowDate.ToLongTimeString());
+ IsSystemTimeChanged = false;
+ }
+ else
+ {
+ ModBase.Log("[System] 系统时间修改为:" + NowDate.ToLongDateString() + " " + NowDate.ToLongTimeString());
+ IsSystemTimeChanged = true;
+ }
+ }
+ else if (msg == 400 * 16 + 2)
+ {
+ ModBase.Log("[System] 收到置顶信息:" + hwnd.ToInt64());
+ if (!IsWindowLoadFinished)
+ {
+ ModBase.Log("[System] 窗口尚未加载完成,忽略置顶请求");
+ return nint.Zero;
+ }
+
+ ShowWindowToTop();
+ handled = true;
+ }
+ else if (msg == 26) // WM_SETTINGCHANGE
+ {
+ if (Marshal.PtrToStringAuto(lParam) == "ImmersiveColorSet")
+ {
+ ModBase.Log($"[System] 系统主题更改,深色模式:{SystemTheme.IsSystemInDarkMode()}");
+ if (Config.Preference.Theme.ColorMode == ColorMode.System &
+ (ModSecret.IsDarkMode != SystemTheme.IsSystemInDarkMode())) ThemeService.RefreshColorMode();
+ }
+ }
+
+ return nint.Zero;
+ }
+
+ // 窗口隐藏与置顶
+ private bool _Hidden;
+
+ public bool Hidden
+ {
+ get => _Hidden;
+ set
+ {
+ if (_Hidden == value)
+ return;
+ _Hidden = value;
+ if (value)
+ {
+ // 隐藏
+ Left -= 10000d;
+ ShowInTaskbar = false;
+ Visibility = Visibility.Hidden;
+ ModBase.Log("[System] 窗口已隐藏,位置:(" + Left + "," + Top + ")");
+ }
+ else
+ {
+ // 取消隐藏
+ if (Left < -2000)
+ Left += 10000d;
+ ShowWindowToTop();
+ }
+ }
+ }
+
+ // 解决龙猫的非通用实现史山
+ protected override void OnActivated(EventArgs e)
+ {
+ base.OnActivated(e);
+ if (Hidden)
+ Hidden = false;
+ }
+
+ ///
+ /// 把当前窗口拖到最前面。
+ ///
+ public void ShowWindowToTop()
+ {
+ ModBase.RunInUi(() =>
+ {
+ // 这一坨乱七八糟的,别改,改了指不定就炸了,自己电脑还复现不出来
+ Visibility = Visibility.Visible;
+ ShowInTaskbar = true;
+ WindowState = WindowState.Normal;
+ Hidden = false;
+ Topmost = true; // 偶尔 SetForegroundWindow 失效
+ Topmost = false;
+ ModMain.SetForegroundWindow(ModBase.FrmHandle);
+ Focus();
+ ModBase.Log($"[System] 窗口已置顶,位置:({Left}, {Top}), {Width} x {Height}");
+ });
+ }
+
+ // 背景视频循环播放
+ private void VideoEnded(object sender, RoutedEventArgs e)
+ {
+ VideoBack.Position = TimeSpan.Zero;
+ VideoBack.Play();
+ }
+
+ // 最小化时暂停背景视频
+ private void WindowStateChanged(object sender, EventArgs e)
+ {
+ switch (WindowState)
+ {
+ case WindowState.Minimized:
+ {
+ ModVideoBack.IsMinimized = true;
+ ModVideoBack.VideoPause();
+ break;
+ }
+ case WindowState.Normal:
+ {
+ ModVideoBack.IsMinimized = false;
+ ModVideoBack.VideoPlay();
+ break;
+ }
+ }
+ }
+
+ #endregion
+
+ #region 切换页面
+
+ // 页面种类与属性
+ // 注意,这一枚举在 “切换页面” EventType 中调用,应视作公开 API 的一部分
+ ///
+ /// 页面种类。
+ ///
+ public enum PageType
+ {
+ ///
+ /// 启动。
+ ///
+ Launch = 0,
+
+ ///
+ /// 下载。
+ ///
+ Download = 1,
+
+ ///
+ /// 联机。
+ ///
+ Tools = 3,
+
+ ///
+ /// 设置。
+ ///
+ Setup = 2,
+
+ ///
+ /// 实例选择。这是一个副页面。
+ ///
+ InstanceSelect = 5,
+
+ ///
+ /// 任务管理。这是一个副页面。
+ ///
+ TaskManager = 6,
+
+ ///
+ /// 实例设置。这是一个副页面。
+ ///
+ InstanceSetup = 7,
+
+ ///
+ /// 资源工程详情。这是一个副页面。
+ ///
+ CompDetail = 8,
+
+ ///
+ /// 帮助详情。这是一个副页面。
+ ///
+ HelpDetail = 9,
+
+ ///
+ /// 游戏实时日志。这是一个副页面。
+ ///
+ GameLog = 10,
+
+ ///
+ /// 存档详细管理,这是一个副页面。
+ ///
+ VersionSaves = 12,
+
+ ///
+ /// 主页市场,这是一个副页面。
+ ///
+ HomePageMarket = 13
+ }
+
+ ///
+ /// 次要页面种类。其数值必须与 StackPanel 中的下标一致。
+ ///
+ public enum PageSubType
+ {
+ Default = 0,
+ DownloadInstall = 1,
+ DownloadMod = 2,
+ DownloadPack = 3,
+ DownloadDataPack = 4,
+ DownloadResourcePack = 5,
+ DownloadShader = 6,
+ DownloadWorld = 7,
+ DownloadCompFavorites = 8,
+ DownloadClient = 9,
+ DownloadOptiFine = 10,
+ DownloadForge = 11,
+ DownloadNeoForge = 12,
+ DownloadCleanroom = 13,
+ DownloadFabric = 14,
+ DownloadQuilt = 15,
+ DownloadLiteLoader = 16,
+ DownloadLabyMod = 17,
+ DownloadLegacyFabric = 18,
+
+ SetupLaunch = 0,
+ SetupUI = 1,
+ SetupGameManage = 2,
+ SetupLink = 3,
+ SetupAbout = 4,
+ SetupLog = 5,
+ SetupFeedback = 6,
+ SetupGameLink = 7,
+ SetupUpdate = 8,
+ SetupJava = 9,
+ SetupLauncherMisc = 10,
+
+ ToolsGameLink = 1,
+ ToolsLauncherHelp = 2,
+ ToolsTest = 3,
+
+ VersionOverall = 0,
+ VersionSetup = 1,
+ VersionExport = 2,
+ VersionWorld = 3,
+ VersionScreenshot = 4,
+ VersionMod = 5,
+ VersionModDisabled = 6,
+ VersionResourcePack = 7,
+ VersionShader = 8,
+ VersionSchematic = 9,
+ VersionInstall = 10,
+ VersionServer = 11,
+ VersionSavesInfo = 0,
+ VersionSavesBackup = 1,
+ VersionSavesDatapack = 2
+ }
+
+ ///
+ /// 获取次级页面的名称。若并非次级页面则返回空字符串,故可以以此判断是否为次级页面。
+ ///
+ private string PageNameGet(PageStackData Stack)
+ {
+ switch (Stack.Page)
+ {
+ case PageType.InstanceSelect:
+ {
+ return "实例选择";
+ }
+ case PageType.TaskManager:
+ {
+ return "任务管理";
+ }
+ case PageType.GameLog:
+ {
+ return "实时日志";
+ }
+ case PageType.InstanceSetup:
+ {
+ return $"实例设置 - {(PageInstanceLeft.Instance is null ? "未知实例" : PageInstanceLeft.Instance.Name)}";
+ }
+ case PageType.CompDetail:
+ {
+ return $"资源下载 - {Stack.Additional.Value.CompProject.TranslatedName}";
+ }
+ case PageType.HelpDetail:
+ {
+ return Stack.Additional.Value.HelpEntry.Title;
+ }
+ case PageType.VersionSaves:
+ {
+ return $"存档管理 - {ModBase.GetFolderNameFromPath(Stack.Additional.Value.SavePath)}";
+ }
+ case PageType.HomePageMarket:
+ {
+ return "主页市场";
+ }
+
+ default:
+ {
+ return "";
+ }
+ }
+ }
+
+ ///
+ /// 刷新次级页面的名称。
+ ///
+ public void PageNameRefresh(PageStackData Type)
+ {
+ LabTitleInner.Text = PageNameGet(Type);
+ }
+
+ ///
+ /// 刷新次级页面的名称。
+ ///
+ public void PageNameRefresh()
+ {
+ PageNameRefresh(PageCurrent);
+ }
+
+ // 页面状态存储
+ ///
+ /// 当前的主页面。
+ ///
+ public PageStackData PageCurrent = PageType.Launch;
+
+ ///
+ /// 上一个主页面。
+ ///
+ public PageStackData PageLast = PageType.Launch;
+
+ ///
+ /// 当前的子页面。
+ ///
+ public PageSubType PageCurrentSub
+ {
+ get
+ {
+ switch (PageCurrent.Page)
+ {
+ case PageType.Download:
+ {
+ if (ModMain.FrmDownloadLeft is null)
+ ModMain.FrmDownloadLeft = new PageDownloadLeft();
+ return ModMain.FrmDownloadLeft.PageID;
+ }
+
+ case PageType.Setup:
+ {
+ if (ModMain.FrmSetupLeft is null)
+ ModMain.FrmSetupLeft = new PageSetupLeft();
+ return ModMain.FrmSetupLeft.PageID;
+ }
+
+ case PageType.InstanceSetup:
+ {
+ if (ModMain.FrmInstanceLeft is null)
+ ModMain.FrmInstanceLeft = new PageInstanceLeft();
+ return ModMain.FrmInstanceLeft.PageID;
+ }
+
+ default:
+ {
+ return 0; // 没有子页面
+ }
+ }
+ }
+ }
+
+ ///
+ /// 上层页面的编号堆栈,用于返回。
+ ///
+ public List PageStack = new();
+
+ public class PageStackData
+ {
+ ///
+ ///
+ /// - CompDetail: (CompProject, ExpandedTitles, TargetVersion, TargetLoader, ResourceType)
+ /// - HelpDetail: (HelpEntry, HelpPage)
+ /// - VersionSaves: SavePath
+ ///
+ ///
+ public (
+ ModComp.CompProject CompProject,
+ List ExpandedTitles,
+ string TargetVersion,
+ ModComp.CompLoaderType TargetLoader,
+ ModComp.CompType ResourceType,
+ ModMain.HelpEntry HelpEntry,
+ FrameworkElement HelpPage,
+ string SavePath
+ )? Additional;
+
+ public PageType Page;
+
+ public override bool Equals(object other)
+ {
+ if (other is null)
+ return false;
+ if (other is PageStackData)
+ {
+ var PageOther = (PageStackData)other;
+ if (Page != PageOther.Page)
+ return false;
+ if (Additional is null) return PageOther.Additional is null;
+
+ return PageOther.Additional is not null && Additional.Equals(PageOther.Additional);
+ }
+
+ if (other is int o)
+ {
+ if ((int)Page == o)
+ return false;
+ return Additional is null;
+ }
+
+ return false;
+ }
+
+ public static bool operator ==(PageStackData left, PageStackData right)
+ {
+ return EqualityComparer.Default.Equals(left, right);
+ }
+
+ public static bool operator !=(PageStackData left, PageStackData right)
+ {
+ return !(left == right);
+ }
+
+ public static implicit operator PageStackData(PageType Value)
+ {
+ return new PageStackData { Page = Value };
+ }
+
+ public static implicit operator PageType(PageStackData Value)
+ {
+ return Value.Page;
+ }
+ }
+
+ public MyPageLeft PageLeft;
+ public MyPageRight PageRight;
+
+ // 引发实际页面切换的入口
+ private bool IsChangingPage;
+
+ ///
+ /// 切换页面,并引起对应选择 UI 的改变。
+ ///
+ public void PageChange(PageStackData Stack, PageSubType SubType = PageSubType.Default)
+ {
+ if (string.IsNullOrEmpty(PageNameGet(Stack)))
+ {
+ // 切换到主页面
+ PageChangeExit();
+ IsChangingPage = true; // 防止下面的勾选直接触发了 PageChangeActual
+ ((MyRadioButton)PanTitleSelect.Children[(int)Stack.Page]).SetChecked(true, true,
+ string.IsNullOrEmpty(PageNameGet(PageCurrent)));
+ IsChangingPage = false;
+ switch (Stack.Page)
+ {
+ case PageType.Download:
+ {
+ if (ModMain.FrmDownloadLeft is null)
+ ModMain.FrmDownloadLeft = new PageDownloadLeft();
+ foreach (var item in ModMain.FrmDownloadLeft.PanItem.Children)
+ if (item is MyListItem listItem &&
+ ModBase.Val(listItem.tag) == (double)SubType)
+ {
+ listItem.SetChecked(true, true, Stack == PageCurrent);
+ break;
+ }
+
+ break;
+ }
+ case PageType.Setup:
+ {
+ if (ModMain.FrmSetupLeft is null)
+ ModMain.FrmSetupLeft = new PageSetupLeft();
+ if (ModMain.FrmSetupLeft.PanItem.Children[(int)SubType] is MyListItem)
+ ((MyListItem)ModMain.FrmSetupLeft.PanItem.Children[(int)SubType]).SetChecked(true, true,
+ Stack == PageCurrent);
+ break;
+ }
+ }
+
+ PageChangeActual(Stack, SubType);
+ }
+ else
+ {
+ // 切换到次页面
+ switch (Stack.Page)
+ {
+ case PageType.InstanceSetup:
+ {
+ if (ModMain.FrmInstanceLeft is null)
+ ModMain.FrmInstanceLeft = new PageInstanceLeft();
+ foreach (var item in ModMain.FrmInstanceLeft.PanItem.Children)
+ if (item is MyListItem listItem &&
+ ModBase.Val(listItem.tag) == (double)SubType)
+ {
+ listItem.SetChecked(true, true, Stack == PageCurrent);
+ break;
+ }
+
+ break;
+ }
+ case PageType.VersionSaves:
+ {
+ if (ModMain.FrmInstanceSavesLeft is null)
+ ModMain.FrmInstanceSavesLeft = new PageInstanceSavesLeft();
+ foreach (var item in ModMain.FrmInstanceSavesLeft.PanItem.Children)
+ if (item is MyListItem listItem &&
+ ModBase.Val(listItem.tag) == (double)SubType)
+ {
+ listItem.SetChecked(true, true, Stack == PageCurrent);
+ break;
+ }
+
+ break;
+ }
+ }
+
+ PageChangeActual(Stack, SubType);
+ }
+ }
+
+ ///
+ /// 通过点击导航栏改变页面。
+ ///
+ private void BtnTitleSelect_Click(MyRadioButton sender, bool raiseByMouse)
+ {
+ if (IsChangingPage)
+ return;
+ var pageType = (PageType)int.Parse(sender.Tag.ToString());
+ PageChangeActual(pageType, PageSubType.Default);
+ }
+
+ private void BtnTitleInner_Click(object sender, EventArgs e)
+ {
+ PageBack();
+ }
+
+ ///
+ /// 通过点击返回按钮或手动触发返回来改变页面。
+ ///
+ public void PageBack()
+ {
+ if (PageStack.Any())
+ PageChangeActual(PageStack[0], PageSubType.Default);
+ else
+ PageChange(PageType.Launch);
+ }
+
+ // 实际处理页面切换
+ ///
+ /// 切换现有页面的实际方法。
+ ///
+ private void PageChangeActual(PageStackData Stack, PageSubType SubType)
+ {
+ if (PageCurrent == Stack && (PageCurrentSub == SubType || (int)SubType == -1))
+ return;
+ ModAnimation.AniControlEnabled += 1;
+ try
+ {
+ #region 子页面处理
+
+ var PageName = PageNameGet(Stack);
+ if (string.IsNullOrEmpty(PageName))
+ {
+ // 即将切换到一个顶级页面
+ PageChangeExit();
+ }
+ // 即将切换到一个子页面
+ else if (PageStack.Any())
+ {
+ // 子页面 → 另一个子页面,更新
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaOpacity(LabTitleInner, -LabTitleInner.Opacity, 130),
+ ModAnimation.AaCode(() => LabTitleInner.Text = PageName, After: true),
+ ModAnimation.AaOpacity(LabTitleInner, 1d, 150, 30)
+ }, "FrmMain Titlebar SubLayer");
+ if (PageStack.Contains(Stack))
+ // 返回到更上层的子页面
+ while (PageStack.Contains(Stack))
+ PageStack.RemoveAt(0);
+ else
+ // 进入更深层的子页面
+ PageStack.Insert(0, PageCurrent);
+ }
+ else
+ {
+ // 主页面 → 子页面,进入
+ PanTitleInner.Visibility = Visibility.Visible;
+ PanTitleMain.IsHitTestVisible = false;
+ PanTitleInner.IsHitTestVisible = true;
+ PageNameRefresh(Stack);
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaOpacity(PanTitleMain, -PanTitleMain.Opacity, 150),
+ ModAnimation.AaX(PanTitleMain, 12d - PanTitleMain.Margin.Left, 150,
+ Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaOpacity(PanTitleInner, 1d - PanTitleInner.Opacity, 150, 200),
+ ModAnimation.AaX(PanTitleInner, -PanTitleInner.Margin.Left, 350, 200,
+ new ModAnimation.AniEaseOutBack()),
+ ModAnimation.AaCode(() => PanTitleMain.Visibility = Visibility.Collapsed, After: true)
+ }, "FrmMain Titlebar FirstLayer");
+ PageStack.Insert(0, PageCurrent);
+ }
+
+ #endregion
+
+ #region 实际更改页面框架 UI
+
+ PageLast = PageCurrent;
+ PageCurrent = Stack;
+ switch (Stack.Page)
+ {
+ case PageType.Launch: // 启动
+ {
+ PageChangeAnim(ModMain.FrmLaunchLeft, ModMain.FrmLaunchRight);
+ break;
+ }
+ case PageType.Download: // 下载
+ {
+ ModMain.FrmDownloadLeft ??= new PageDownloadLeft();
+ SubType = ModMain.FrmDownloadLeft.PageID;
+ // PageGet 方法会在未设置 SubType 时指定默认值,并建立相关页面的实例
+ PageChangeAnim(ModMain.FrmDownloadLeft, (FrameworkElement)ModMain.FrmDownloadLeft.PageGet(SubType));
+ break;
+ }
+ case PageType.Tools: // 联机
+ {
+ ModMain.FrmToolsLeft ??= new PageToolsLeft();
+ SubType = ModMain.FrmToolsLeft.PageID;
+ PageChangeAnim(ModMain.FrmToolsLeft, (FrameworkElement)ModMain.FrmToolsLeft.PageGet(SubType));
+ break;
+ }
+ case PageType.Setup: // 设置
+ {
+ ModMain.FrmSetupLeft ??= new PageSetupLeft();
+ SubType = ModMain.FrmSetupLeft.PageID;
+ PageChangeAnim(ModMain.FrmSetupLeft, (FrameworkElement)ModMain.FrmSetupLeft.PageGet(SubType));
+ break;
+ }
+ case PageType.GameLog: // 实时日志
+ {
+ if (ModMain.FrmLogLeft is null)
+ ModMain.FrmLogLeft = new PageLogLeft();
+ if (ModMain.FrmLogLeft is null)
+ ModMain.FrmLogRight = new PageLogRight();
+ PageChangeAnim(ModMain.FrmLogLeft, ModMain.FrmLogRight);
+ break;
+ }
+ case PageType.InstanceSelect: // 实例选择
+ {
+ if (ModMain.FrmSelectLeft is null)
+ ModMain.FrmSelectLeft = new PageSelectLeft();
+ if (ModMain.FrmSelectRight is null)
+ ModMain.FrmSelectRight = new PageSelectRight();
+ PageChangeAnim(ModMain.FrmSelectLeft, ModMain.FrmSelectRight);
+ break;
+ }
+ case PageType.TaskManager: // 任务管理
+ {
+ if (ModMain.FrmSpeedLeft is null)
+ ModMain.FrmSpeedLeft = new PageSpeedLeft();
+ if (ModMain.FrmSpeedRight is null)
+ ModMain.FrmSpeedRight = new PageSpeedRight();
+ PageChangeAnim(ModMain.FrmSpeedLeft, ModMain.FrmSpeedRight);
+ break;
+ }
+ case PageType.InstanceSetup: // 实例设置
+ {
+ ModMain.FrmInstanceLeft ??= new PageInstanceLeft();
+ SubType = ModMain.FrmInstanceLeft.PageID;
+ PageChangeAnim(ModMain.FrmInstanceLeft, (FrameworkElement)ModMain.FrmInstanceLeft.PageGet(SubType));
+ break;
+ }
+ case PageType.CompDetail: // Mod 信息
+ {
+ if (ModMain.FrmDownloadCompDetail is null)
+ ModMain.FrmDownloadCompDetail = new PageDownloadCompDetail();
+ PageChangeAnim(new MyPageLeft(), ModMain.FrmDownloadCompDetail);
+ break;
+ }
+ case PageType.HelpDetail: // 帮助详情
+ {
+ PageChangeAnim(new MyPageLeft(), Stack.Additional.Value.HelpPage);
+ break;
+ }
+ case PageType.VersionSaves: // 存档管理
+ {
+ if (ModMain.FrmInstanceSavesLeft is null)
+ ModMain.FrmInstanceSavesLeft = new PageInstanceSavesLeft();
+ PageInstanceSavesLeft.CurrentSave = Stack.Additional.Value.SavePath;
+ PageChangeAnim(ModMain.FrmInstanceSavesLeft,
+ (FrameworkElement)ModMain.FrmInstanceSavesLeft.PageGet(SubType));
+ break;
+ }
+ case PageType.HomePageMarket: // 主页市场
+ {
+ ModMain.FrmHomePageMarket = ModMain.FrmHomePageMarket ?? new PageHomePageMarket();
+ PageChangeAnim(new MyPageLeft(), ModMain.FrmHomePageMarket);
+ break;
+ }
+ }
+
+ #endregion
+
+ #region 设置为最新状态
+
+ BtnExtraDownload.ShowRefresh();
+ BtnExtraApril.ShowRefresh();
+
+ #endregion
+
+ ModBase.Log("[Control] 切换主要页面:" + ModBase.GetStringFromEnum(Stack) + ", " + (int)SubType);
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "切换主要页面失败(ID " + (int)PageCurrent.Page + ")", ModBase.LogLevel.Feedback);
+ }
+ finally
+ {
+ ModAnimation.AniControlEnabled -= 1;
+ }
+ }
+
+ private void PageChangeAnim(FrameworkElement TargetLeft, FrameworkElement TargetRight)
+ {
+ ModAnimation.AniStop("FrmMain LeftChange");
+ ModAnimation.AniStop("PageLeft PageChange"); // 停止左边栏变更导致的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter
+ ModAnimation.AniControlEnabled += 1;
+ // 清除新页面关联性
+ if (!(TargetLeft.Parent == null))
+ TargetLeft.SetValue(ContentPresenter.ContentProperty, null);
+ if (!(TargetRight == null) && !(TargetRight.Parent == null))
+ TargetRight.SetValue(ContentPresenter.ContentProperty, null);
+ PageLeft = (MyPageLeft)TargetLeft;
+ PageRight = (MyPageRight)TargetRight;
+ // 触发页面通用动画
+ ((MyPageLeft)PanMainLeft.Child).TriggerHideAnimation();
+ ((MyPageRight)PanMainRight.Child).PageOnExit();
+ ModAnimation.AniControlEnabled -= 1;
+ // 执行动画
+ ModAnimation.AniStart(new[]
+ {
+ ModAnimation.AaCode(() =>
+ {
+ ModAnimation.AniControlEnabled += 1;
+ // 把新页面添加进容器
+ PanMainLeft.Child = PageLeft;
+ PageLeft.Opacity = 0d;
+ PanMainLeft.Background = null;
+ ModAnimation.AniControlEnabled -= 1;
+ ModBase.RunInUi(() => PanMainLeft_Resize(PanMainLeft.ActualWidth), true);
+ }, 110),
+ ModAnimation.AaCode(() =>
+ {
+ // 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
+ PageLeft.Opacity = 1d;
+ PageLeft.TriggerShowAnimation();
+ }, 30, true)
+ }, "FrmMain PageChangeLeft");
+ ModAnimation.AniStart(new[]
+ {
+ ModAnimation.AaCode(() =>
+ {
+ ModAnimation.AniControlEnabled += 1;
+ ((MyPageRight)PanMainRight.Child).PageOnForceExit();
+ // 把新页面添加进容器
+ PanMainRight.Child = PageRight;
+ PageRight.Opacity = 0d;
+ PanMainRight.Background = null;
+ ModAnimation.AniControlEnabled -= 1;
+ ModBase.RunInUi(() => BtnExtraBack.ShowRefresh(), true);
+ }, 110),
+ ModAnimation.AaCode(() =>
+ {
+ // 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
+ PageRight.Opacity = 1d;
+ PageRight.PageOnEnter();
+ }, 30, true)
+ }, "FrmMain PageChangeRight");
+ }
+
+ ///
+ /// 退出子界面。
+ ///
+ private void PageChangeExit()
+ {
+ if (PageStack.Any())
+ {
+ // 子页面 → 主页面,退出
+ PanTitleMain.Visibility = Visibility.Visible;
+ PanTitleMain.IsHitTestVisible = true;
+ PanTitleInner.IsHitTestVisible = false;
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaOpacity(PanTitleInner, -PanTitleInner.Opacity, 150),
+ ModAnimation.AaX(PanTitleInner, -18 - PanTitleInner.Margin.Left, 150,
+ Ease: new ModAnimation.AniEaseInFluent()),
+ ModAnimation.AaOpacity(PanTitleMain, 1d - PanTitleMain.Opacity, 150, 200),
+ ModAnimation.AaX(PanTitleMain, -PanTitleMain.Margin.Left, 350, 200,
+ new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
+ ModAnimation.AaCode(() => PanTitleInner.Visibility = Visibility.Collapsed, After: true)
+ }, "FrmMain Titlebar FirstLayer");
+ PageStack.Clear();
+ }
+ // 主页面 → 主页面,无事发生
+ }
+
+ // 左边栏改变
+ private void PanMainLeft_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (!e.WidthChanged)
+ return;
+ PanMainLeft_Resize(e.NewSize.Width);
+ }
+
+ private void PanMainLeft_Resize(double NewWidth)
+ {
+ var Delta = NewWidth - RectLeftBackground.Width;
+ if (Math.Abs(Delta) > 0.1d && ModAnimation.AniControlEnabled == 0)
+ {
+ if (PanMain.Opacity < 0.1d)
+ PanMainLeft.IsHitTestVisible = false; // 避免左边栏指向背景未能完美覆盖左边栏
+ if (NewWidth > 0d)
+ // 宽度足够,显示
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaWidth(RectLeftBackground, NewWidth - RectLeftBackground.Width, 180,
+ Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)),
+ ModAnimation.AaOpacity(RectLeftShadow, 1d - RectLeftShadow.Opacity, 180),
+ ModAnimation.AaCode(() => PanMainLeft.IsHitTestVisible = true, 150)
+ }, "FrmMain LeftChange", true);
+ else
+ // 宽度不足,隐藏
+ ModAnimation.AniStart(
+ new[]
+ {
+ ModAnimation.AaWidth(RectLeftBackground, -RectLeftBackground.Width, 180,
+ Ease: new ModAnimation.AniEaseOutFluent()),
+ ModAnimation.AaOpacity(RectLeftShadow, -RectLeftShadow.Opacity, 180),
+ ModAnimation.AaCode(() => PanMainLeft.IsHitTestVisible = true, 150)
+ }, "FrmMain LeftChange", true);
+ }
+ else
+ {
+ RectLeftBackground.Width = NewWidth;
+ PanMainLeft.IsHitTestVisible = true;
+ ModAnimation.AniStop("FrmMain LeftChange");
+ }
+ }
+
+ #endregion
+
+ #region 控件拖动
+
+ // 在时钟中调用,使得即使鼠标在窗口外松开,也可以释放控件
+ public void DragTick()
+ {
+ if (ModMain.DragControl is null)
+ return;
+ if (!(Mouse.LeftButton == MouseButtonState.Pressed)) DragStop();
+ }
+
+ // 在鼠标移动时调用,以改变 Slider 位置
+ public void DragDoing()
+ {
+ if (ModMain.DragControl is null)
+ return;
+ if (Mouse.LeftButton == MouseButtonState.Pressed)
+ {
+ ModMain.DragControl.DragDoing();
+ }
+ else
+ DragStop();
+ }
+
+ private void PanBack_MouseMove(object sender, EventArgs e)
+ {
+ DragDoing();
+ }
+
+ public void DragStop()
+ {
+ // 存在其他线程调用的可能性,因此需要确保在 UI 线程运行
+ ModBase.RunInUi(() =>
+ {
+ if (ModMain.DragControl is null)
+ return;
+ var control = ModMain.DragControl;
+ ModMain.DragControl = null;
+ control.DragStop(); // 控件会在该事件中判断 DragControl,所以得放在后面
+ });
+ }
+
+ #endregion
+
+ #region 附加按钮
+
+ // 更新重启
+ private void BtnExtraUpdateRestart_Click(object sender, MouseButtonEventArgs e)
+ {
+ ModSecret.UpdateRestart(true);
+ }
+
+ private bool BtnExtraUpdateRestart_ShowCheck()
+ {
+ return ModSecret.IsUpdateWaitingRestart;
+ }
+
+ // 音乐
+ private void BtnExtraMusic_Click(object sender, MouseButtonEventArgs e)
+ {
+ ModMusic.MusicControlPause();
+ }
+
+ private void BtnExtraMusic_RightClick(object sender, MouseButtonEventArgs e)
+ {
+ ModMusic.MusicControlNext();
+ }
+
+ // 任务管理
+ private void BtnExtraDownload_Click(object sender, MouseButtonEventArgs e)
+ {
+ PageChange(PageType.TaskManager);
+ }
+
+ private bool BtnExtraDownload_ShowCheck()
+ {
+ return ModNet.HasDownloadingTask() && !(PageCurrent == PageType.TaskManager);
+ }
+
+ // 投降
+ public void AprilGiveup()
+ {
+ if (ModMain.IsAprilEnabled && !ModMain.IsAprilGiveup)
+ {
+ ModMain.Hint("=D", ModMain.HintType.Finish);
+ ModMain.IsAprilGiveup = true;
+ ModMain.FrmLaunchLeft.AprilScaleTrans.ScaleX = 1d;
+ ModMain.FrmLaunchLeft.AprilScaleTrans.ScaleY = 1d;
+ BtnExtraApril.ShowRefresh();
+ }
+ }
+
+ private void BtnExtraApril_Click(object sender, MouseButtonEventArgs e)
+ {
+ AprilGiveup();
+ }
+
+ public bool BtnExtraApril_ShowCheck()
+ {
+ return ModMain.IsAprilEnabled && !ModMain.IsAprilGiveup && PageCurrent == PageType.Launch;
+ }
+
+ // 关闭 Minecraft
+ private void BtnExtraShutdown_Click(object sender, MouseButtonEventArgs e)
+ {
+ try
+ {
+ if (ModLaunch.McLaunchLoaderReal is not null)
+ ModLaunch.McLaunchLoaderReal.Abort();
+ foreach (var Watcher in ModWatcher.McWatcherList)
+ Watcher.Kill();
+ ModMain.Hint("已关闭运行中的 Minecraft!", ModMain.HintType.Finish);
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "强制关闭所有 Minecraft 失败", ModBase.LogLevel.Feedback);
+ }
+ }
+
+ public bool BtnExtraShutdown_ShowCheck()
+ {
+ return ModWatcher.HasRunningMinecraft;
+ }
+
+ // 游戏日志
+ private void BtnExtraLog_Click(object sender, MouseButtonEventArgs e)
+ {
+ PageChange(PageType.GameLog);
+ }
+
+ public bool BtnExtraLog_ShowCheck()
+ {
+ if (ModMain.FrmLogLeft is null || ModMain.FrmLogRight is null || PageCurrent == PageType.GameLog)
+ return false;
+ return ModMain.FrmLogLeft.ShownLogs.Count > 0;
+ }
+
+ ///
+ /// 返回顶部。
+ ///
+ public void BackToTop()
+ {
+ var RealScroll = BtnExtraBack_GetRealChild();
+ if (RealScroll is not null)
+ RealScroll.PerformVerticalOffsetDelta(-RealScroll.VerticalOffset);
+ else
+ ModBase.Log("[UI] 无法返回顶部,未找到合适的 RealScroll", ModBase.LogLevel.Hint);
+ }
+
+ private void BtnExtraBack_Click(object sender, MouseButtonEventArgs e)
+ {
+ BackToTop();
+ }
+
+ private bool BtnExtraBack_ShowCheck()
+ {
+ var RealScroll = BtnExtraBack_GetRealChild();
+ return RealScroll is not null && RealScroll.Visibility == Visibility.Visible &&
+ RealScroll.VerticalOffset > Height + (BtnExtraBack.Show ? 0 : 700);
+ }
+
+ private MyScrollViewer? BtnExtraBack_GetRealChild()
+ {
+ if (PanMainRight.Child is null || !(PanMainRight.Child is MyPageRight))
+ return null;
+ return ((MyPageRight)PanMainRight.Child).PanScroll;
+ }
+
+ #endregion
+}
diff --git a/Plain Craft Launcher 2/Modules/Base/ModAnimation.cs b/Plain Craft Launcher 2/Modules/Base/ModAnimation.cs
new file mode 100644
index 000000000..34c691a3b
--- /dev/null
+++ b/Plain Craft Launcher 2/Modules/Base/ModAnimation.cs
@@ -0,0 +1,1505 @@
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using PCL.Core.App;
+using PCL.Core.Utils;
+using PCL.Network;
+
+namespace PCL;
+
+public static partial class ModAnimation
+{
+ private static int AniCount;
+ private static int AniFPSCounter;
+ private static long AniFPSTimer;
+
+ ///
+ /// 当前的动画 FPS。
+ ///
+ public static int AniFPS;
+
+ ///
+ /// 开始动画执行。
+ ///
+ public static void AniStart()
+ {
+ // 初始化计时器
+ AniLastTick = TimeUtils.GetTimeTick();
+ AniFPSTimer = AniLastTick;
+ AniRunning = true; // 标记动画执行开始
+
+ var MinFrameGap = 1000d / (Config.System.AnimationFpsLimit + 1) / 2;
+
+
+ ModBase.RunInNewThread(() =>
+ {
+ try
+ {
+ ModBase.Log("[Animation] 动画线程开始");
+ while (true)
+ {
+ // 两帧之间的间隔时间
+ var DeltaTime =
+ (long)Math.Round(ModBase.MathClamp(TimeUtils.GetTimeTick() - AniLastTick, 0, 100000));
+ if (DeltaTime < MinFrameGap)
+ {
+ // 限制 FPS
+ Thread.Sleep(1);
+ continue;
+ }
+
+ AniLastTick = TimeUtils.GetTimeTick();
+ // 记录 FPS
+ if (ModBase.ModeDebug)
+ {
+ if (ModBase.MathClamp(AniLastTick - AniFPSTimer, 0d, 100000d) >= 500d)
+ {
+ AniFPS = AniFPSCounter;
+ AniFPSCounter = 0;
+ AniFPSTimer = AniLastTick;
+ }
+
+ AniFPSCounter += 2;
+ }
+
+ // 执行动画
+ ModBase.RunInUiWait(() =>
+ {
+ AniCount = 0;
+ AniTimer((int)Math.Round(DeltaTime * AniSpeed));
+ // #If DEBUG Then
+ // FrmMain.Title = "F " & AniFPS & ", A " & AniCount & ", R " & NetManage.FileRemain
+ // #Else
+ // If ModeDebug Then FrmMain.Title = "FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManage.FileRemain
+ // #End If
+ if (RandomUtils.NextInt(0, 64 * (ModBase.ModeDebug ? 5 : 30)) == 0 &&
+ ((AniFPS < 62 && AniFPS > 0) || AniCount > 4 || ModNet.NetManager.FileRemain != 0))
+ ModBase.Log("[Report] FPS " + AniFPS + ", 动画 " + AniCount + ", 下载中 " +
+ ModNet.NetManager.FileRemain + "(" +
+ ModBase.GetString(ModNet.NetManager.Speed) + "/s)");
+ });
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "动画帧执行失败", ModBase.LogLevel.Critical);
+ }
+ }, "Animation", ThreadPriority.AboveNormal);
+ }
+
+ ///
+ /// 动画定时器事件。
+ ///
+ public static void AniTimer(int DeltaTick)
+ {
+ try
+ {
+ if (DeltaTick / AniSpeed > 100d)
+ ModBase.Log("[Animation] 两个动画帧间隔 " + DeltaTick + " ms", ModBase.LogLevel.Developer);
+ var i = -1;
+ // 循环每个动画组
+ while (i + 1 < AniGroups.Count)
+ {
+ i += 1;
+ // 初始化
+ var Entry = AniGroups.Values.ElementAtOrDefault(i);
+ if (Entry.StartTick > AniLastTick)
+ continue; // 跳过本刻之后开始的动画
+ var CanRemoveAfter = true; // 是否应该去除“之后”标记
+ var ii = 0;
+
+ // 循环每个动画
+ while (ii < Entry.Data.Count)
+ {
+ var Anim = Entry.Data[ii];
+ // 执行种类
+ if (!Anim.IsAfter) // 之前
+ {
+ CanRemoveAfter = false; // 取消“之后”标记
+ // 增加执行时间
+ Anim.TimeFinished += DeltaTick;
+ // 执行动画
+ if (Anim.TimeFinished > 0)
+ {
+ Anim = AniRun(Anim);
+ AniCount += 1;
+ }
+
+ // 如果当前动画已执行完毕
+ if (Anim.TimeFinished >= Anim.TimeTotal)
+ {
+ // 如果是去向颜色资源的动画,设置引用
+ if (Anim.TypeMain == AniType.Color &&
+ !string.Equals(((dynamic)Anim.Obj)[2] as string, "", StringComparison.Ordinal))
+ ((dynamic)Anim.Obj)[0]
+ .SetResourceReference(((dynamic)Anim.Obj)[1], ((dynamic)Anim.Obj)[2]);
+ // 删除
+ Entry.Data.RemoveAt(ii);
+ goto NextAni;
+ }
+
+ Entry.Data[ii] = Anim;
+ }
+ else if (CanRemoveAfter) // 之后
+ {
+ // 之后改为之前
+ CanRemoveAfter = false;
+ Anim.IsAfter = false;
+ Entry.Data[ii] = Anim;
+ // 重新循环该动画
+ goto NextAni;
+ }
+ else
+ {
+ // 不能去除该“之后”标记,结束该动画组
+ break;
+ }
+
+ ii += 1;
+ NextAni: ;
+ }
+
+ // 如果当前动画组都执行完毕则删除
+ if (!Entry.Data.Any())
+ {
+ // 为了避免新添加的动画影响顺序,不能 RemoveAt(i)
+ // 为了允许动画在执行中添加同名动画组,不能按名字移除
+ for (int Current = 0, loopTo = AniGroups.Count - 1; Current <= loopTo; Current++)
+ if (AniGroups.ElementAt(Current).Value.Uuid == Entry.Uuid)
+ {
+ AniGroups.Remove(AniGroups.ElementAt(Current).Key, out _);
+ break;
+ }
+
+ i -= 1;
+ }
+ }
+ }
+
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "动画刻执行失败", ModBase.LogLevel.Hint);
+ }
+ }
+
+ ///
+ /// 执行一个动画。
+ ///
+ /// 执行的动画对象。
+ private static AniData AniRun(AniData Ani)
+ {
+ try
+ {
+ switch (Ani.TypeMain)
+ {
+ case AniType.Number:
+ {
+ var Delta = ModBase.MathPercent(0d, (double)Ani.Value,
+ Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, Ani.TimePercent));
+ if (Delta != 0d)
+ switch (Ani.TypeSub)
+ {
+ case AniTypeSub.X:
+ {
+ ModBase.DeltaLeft((FrameworkElement)Ani.Obj, Delta);
+ break;
+ }
+ case AniTypeSub.Y:
+ {
+ ModBase.DeltaTop((FrameworkElement)Ani.Obj, Delta);
+ break;
+ }
+ case AniTypeSub.Opacity:
+ {
+ ((dynamic)Ani.Obj).Opacity = ModBase.MathClamp(
+ Convert.ToDouble(((dynamic)Ani.Obj).Opacity) + Delta, 0d, 1d);
+ break;
+ }
+ case AniTypeSub.Width:
+ {
+ var Obj = (FrameworkElement)Ani.Obj;
+ Obj.Width = Math.Max((double.IsNaN(Obj.Width) ? Obj.ActualWidth : Obj.Width) + Delta,
+ 0d);
+ break;
+ }
+ case AniTypeSub.Height:
+ {
+ var Obj = (FrameworkElement)Ani.Obj;
+ Obj.Height =
+ Math.Max((double.IsNaN(Obj.Height) ? Obj.ActualHeight : Obj.Height) + Delta, 0d);
+ break;
+ }
+ case AniTypeSub.Value:
+ {
+ ((dynamic)Ani.Obj).Value += Delta;
+ break;
+ }
+ case AniTypeSub.Radius:
+ {
+ ((dynamic)Ani.Obj).Radius += Delta;
+ break;
+ }
+ case AniTypeSub.StrokeThickness:
+ {
+ ((dynamic)Ani.Obj).StrokeThickness =
+ Math.Max(Convert.ToDouble(((dynamic)Ani.Obj).StrokeThickness) + Delta, 0);
+ break;
+ }
+ case AniTypeSub.BorderThickness:
+ {
+ ((dynamic)Ani.Obj).BorderThickness =
+ new Thickness(((Thickness)((dynamic)Ani.Obj).BorderThickness).Bottom + Delta);
+ break;
+ }
+ case AniTypeSub.TranslateX:
+ {
+ if (((dynamic)Ani.Obj).RenderTransform == null ||
+ !(((dynamic)Ani.Obj).RenderTransform is TranslateTransform))
+ ((dynamic)Ani.Obj).RenderTransform = new TranslateTransform(0d, 0d);
+ ((TranslateTransform)((dynamic)Ani.Obj).RenderTransform).X += Delta;
+ break;
+ }
+ case AniTypeSub.TranslateY:
+ {
+ if (((dynamic)Ani.Obj).RenderTransform == null ||
+ !(((dynamic)Ani.Obj).RenderTransform is TranslateTransform))
+ ((dynamic)Ani.Obj).RenderTransform = new TranslateTransform(0d, 0d);
+ ((TranslateTransform)((dynamic)Ani.Obj).RenderTransform).Y += Delta;
+ break;
+ }
+ case AniTypeSub.Double:
+ {
+ ((dynamic)Ani.Obj)[0].SetValue(((dynamic)Ani.Obj)[1],
+ Convert.ToDouble(((dynamic)Ani.Obj)[0].GetValue(((dynamic)Ani.Obj)[1])) + Delta);
+ break;
+ }
+ case AniTypeSub.DoubleParam:
+ {
+ ((ParameterizedThreadStart)Ani.Obj)(Delta);
+ break;
+ }
+ case AniTypeSub.GridLengthWidth:
+ {
+ ((dynamic)Ani.Obj).Width =
+ new GridLength(
+ Convert.ToDouble(
+ Math.Max(Convert.ToDouble(((dynamic)Ani.Obj).Width.Value) + Delta, 0)),
+ GridUnitType.Star);
+ break;
+ }
+ }
+
+ break;
+ }
+
+ case AniType.Color:
+ {
+ // 利用 Last 记录了余下的小数值
+ var Delta = ModBase.MathPercent(new ModBase.MyColor(0d, 0d, 0d, 0d), (ModBase.MyColor)Ani.Value,
+ Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, Ani.TimePercent)) +
+ (ModBase.MyColor)Ani.ValueLast;
+ var Obj = (FrameworkElement)((dynamic)Ani.Obj)[0];
+ var Prop = (DependencyProperty)((dynamic)Ani.Obj)[1];
+ var NewColor = new ModBase.MyColor(Obj.GetValue(Prop)) + Delta;
+ Obj.SetValue(Prop, Prop.PropertyType.Name == "Color" ? (Color)NewColor : (SolidColorBrush)NewColor);
+ Ani.ValueLast = NewColor - new ModBase.MyColor(Obj.GetValue(Prop));
+ break;
+ }
+
+ case AniType.Scale:
+ {
+ var Obj = (FrameworkElement)Ani.Obj;
+ var Delta = Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, Ani.TimePercent);
+ Obj.Margin = new Thickness(
+ Obj.Margin.Left +
+ ModBase.MathPercent(0d, Convert.ToDouble(((dynamic)Ani.Value).Left), Delta),
+ Obj.Margin.Top + ModBase.MathPercent(0d, Convert.ToDouble(((dynamic)Ani.Value).Top), Delta),
+ Obj.Margin.Right +
+ ModBase.MathPercent(0d, Convert.ToDouble(((dynamic)Ani.Value).Left), Delta),
+ Obj.Margin.Bottom +
+ ModBase.MathPercent(0d, Convert.ToDouble(((dynamic)Ani.Value).Top), Delta));
+ Obj.Width = Math.Max(
+ Obj.Width + ModBase.MathPercent(0d, Convert.ToDouble(((dynamic)Ani.Value).Width), Delta), 0d);
+ Obj.Height =
+ Math.Max(
+ Obj.Height + ModBase.MathPercent(0d, Convert.ToDouble(((dynamic)Ani.Value).Height), Delta), 0d);
+ break;
+ }
+
+ case AniType.TextAppear:
+ {
+ var hideFlag = (bool)((dynamic)Ani.Value)[1];
+ var textLength = ((dynamic)Ani.Value)[0].ToString().Length;
+ var TextCount = (int)Math.Round(
+ (double)(hideFlag ? textLength : 0) + Math.Round(
+ textLength *
+ (hideFlag ? -1 : 1) *
+ Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, 0d)));
+ var originalText = ((dynamic)Ani.Value)[0].ToString();
+ var NewText = originalText.Substring(0, Math.Min(TextCount, originalText.Length));
+ // 添加乱码
+ if (TextCount < originalText.Length)
+ {
+ var NextText = originalText.Substring(TextCount, 1);
+ if (Convert.ToInt32(Convert.ToChar(NextText)) >= Convert.ToInt32(Convert.ToChar(128)))
+ NewText += Encoding.GetEncoding("GB18030").GetString(new[]
+ {
+ (byte)RandomUtils.NextInt(16 + 160, 87 + 160),
+ (byte)RandomUtils.NextInt(1 + 160, 89 + 160)
+ });
+ else
+ NewText += RandomUtils.PickRandom(
+ @"0123456789./*-+\[]{};':/?,!@#$%^&*()_+-=qwwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
+ .ToCharArray());
+ }
+
+ // 设置文本
+ if (Ani.Obj is TextBlock)
+ ((dynamic)Ani.Obj).Text = NewText;
+ else
+ ((dynamic)Ani.Obj).Context = NewText;
+
+ break;
+ }
+
+ case AniType.Code:
+ {
+ ((ThreadStart)Ani.Value)();
+ break;
+ }
+
+ case AniType.ScaleTransform:
+ {
+ var Obj = (FrameworkElement)Ani.Obj;
+ if (!(Obj.RenderTransform is ScaleTransform))
+ {
+ Obj.RenderTransformOrigin = new Point(0.5d, 0.5d);
+ Obj.RenderTransform = new ScaleTransform(1d, 1d);
+ }
+
+ var Delta = ModBase.MathPercent(0d, (double)Ani.Value,
+ Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, Ani.TimePercent));
+ ((ScaleTransform)Obj.RenderTransform).ScaleX =
+ Math.Max(((ScaleTransform)Obj.RenderTransform).ScaleX + Delta, 0d);
+ ((ScaleTransform)Obj.RenderTransform).ScaleY =
+ Math.Max(((ScaleTransform)Obj.RenderTransform).ScaleY + Delta, 0d);
+ break;
+ }
+
+ case AniType.RotateTransform:
+ {
+ var Obj = (FrameworkElement)Ani.Obj;
+ if (!(Obj.RenderTransform is RotateTransform))
+ {
+ Obj.RenderTransformOrigin = new Point(0.5d, 0.5d);
+ Obj.RenderTransform = new RotateTransform(0d);
+ }
+
+ var Delta = ModBase.MathPercent(0d, (double)Ani.Value,
+ Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, Ani.TimePercent));
+ ((RotateTransform)Obj.RenderTransform).Angle = ((RotateTransform)Obj.RenderTransform).Angle + Delta;
+ break;
+ }
+ }
+
+ Ani.TimePercent = Ani.TimeFinished / (double)Ani.TimeTotal; // 修改执行百分比
+ }
+ catch (Exception ex)
+ {
+ ModBase.Log(ex, "执行动画失败:" + Ani, ModBase.LogLevel.Hint);
+ }
+
+ return Ani;
+ }
+
+ #region 声明
+
+ ///
+ /// 动画速度。最大为 200。
+ ///
+ public static double AniSpeed = 1d;
+
+ ///
+ /// 动画组列表。
+ ///
+ public static ConcurrentDictionary AniGroups = new();
+
+ public class AniGroupEntry
+ {
+ public List Data;
+ public long StartTick;
+ public int Uuid = ModBase.GetUuid();
+ }
+
+ ///
+ /// 上一次记刻的时间。
+ ///
+ private static long AniLastTick;
+
+ ///
+ /// 动画模块是否正在运行。
+ ///
+ public static bool AniRunning;
+
+ private static int _AniControlEnabled;
+ private static readonly object AniControlEnabledLock = new();
+
+ ///
+ /// 控件动画执行是否开启。先 +1,再 -1。
+ ///
+ public static int AniControlEnabled
+ {
+ get => _AniControlEnabled;
+ set
+ {
+ lock (AniControlEnabledLock)
+ {
+ _AniControlEnabled = value;
+ }
+ }
+ }
+
+ #endregion
+
+ #region 类与枚举
+
+ ///
+ /// 单个动画对象。
+ ///
+ ///
+ public struct AniData
+ {
+ ///
+ /// 动画种类。
+ ///
+ ///
+ public AniType TypeMain;
+
+ ///
+ /// 动画副种类。
+ ///
+ ///
+ public AniTypeSub TypeSub;
+
+ ///
+ /// 动画总长度。
+ ///
+ ///
+ public int TimeTotal;
+
+ ///
+ /// 已经执行的动画长度。如果为负数则为延迟。
+ ///
+ ///
+ public int TimeFinished;
+
+ ///
+ /// 已经完成的百分比。
+ ///
+ ///
+ public double TimePercent;
+
+ ///
+ /// 是否为“以后”。
+ ///
+ ///
+ public bool IsAfter;
+
+ ///
+ /// 插值器类型。
+ ///
+ ///
+ public AniEase Ease;
+
+ ///
+ /// 动画对象。
+ ///
+ ///
+ public object Obj;
+
+ ///
+ /// 动画值。
+ ///
+ ///
+ public object Value;
+
+ ///
+ /// 上次执行时的动画值。
+ ///
+ ///
+ public object ValueLast;
+
+ public override string ToString()
+ {
+ return ModBase.GetStringFromEnum(TypeMain) + " | " + TimeFinished + "/" + TimeTotal + "(" +
+ Math.Round(TimePercent * 100d) + "%)" +
+ (Obj is null ? "" : " | " + Obj + "(" + Obj.GetType().Name + ")");
+ }
+ }
+
+ ///
+ /// 动画基础种类。
+ ///
+ public enum AniType
+ {
+ ///
+ /// 单个Double的动画,包括位置、长宽、透明度等。这需要附属类型。
+ ///
+ ///
+ Number,
+
+ ///
+ /// 颜色属性的动画。这需要附属类型。
+ ///
+ ///
+ Color,
+
+ ///
+ /// 缩放控件大小。比起4个DoubleAnimation来说效率更高。
+ ///
+ ///
+ Scale,
+
+ ///
+ /// 文字一个个出现。
+ ///
+ ///
+ TextAppear,
+
+ ///
+ /// 执行代码。
+ ///
+ ///
+ Code,
+
+ ///
+ /// 以 WPF 方式缩放控件。
+ ///
+ ScaleTransform,
+
+ ///
+ /// 以 WPF 方式旋转控件。
+ ///
+ RotateTransform
+ }
+
+ ///
+ /// 动画扩展种类。
+ ///
+ public enum AniTypeSub
+ {
+ X,
+ Y,
+ Width,
+ Height,
+ Opacity,
+ Value,
+ Radius,
+ BorderThickness,
+ StrokeThickness,
+ TranslateX,
+ TranslateY,
+ Double,
+ DoubleParam,
+ GridLengthWidth
+ }
+
+ #endregion
+
+ #region 种类
+
+ // DoubleAnimation
+
+ ///
+ /// 移动X轴的动画。
+ ///
+ /// 动画的对象。
+ /// 进行移动的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaX(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.X,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 移动Y轴的动画。
+ ///
+ /// 动画的对象。
+ /// 进行移动的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaY(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.Y,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 改变宽度的动画。
+ ///
+ /// 动画的对象。
+ /// 宽度改变的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaWidth(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.Width,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 改变高度的动画。
+ ///
+ /// 动画的对象。
+ /// 高度改变的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaHeight(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.Height,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 改变透明度的动画。
+ ///
+ /// 动画的对象。
+ /// 透明度改变的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaOpacity(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.Opacity,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 改变对象的Value属性的动画。
+ ///
+ /// 动画的对象。
+ /// Value属性改变的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaValue(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.Value,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 改变对象的Radius属性的动画。
+ ///
+ /// 动画的对象。
+ /// Radius属性改变的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaRadius(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.Radius,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 改变对象的BorderThickness属性的动画。
+ ///
+ /// 动画的对象。
+ /// BorderThickness属性改变的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaBorderThickness(object Obj, double Value, int Time = 400, int Delay = 0,
+ AniEase Ease = null, bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.BorderThickness,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 改变对象的StrokeThickness属性的动画。
+ ///
+ /// 动画的对象。
+ /// StrokeThickness属性改变的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ public static AniData AaStrokeThickness(object Obj, double Value, int Time = 400, int Delay = 0,
+ AniEase Ease = null, bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.StrokeThickness,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 改变 Width 的 GridLength 属性的动画。必须为 Star。
+ ///
+ /// 动画的对象。
+ /// GridLength.Value 改变的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ public static AniData AaGridLengthWidth(object Obj, double Value, int Time = 400, int Delay = 0,
+ AniEase Ease = null, bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.GridLengthWidth,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ // DoubleAnimation(Obj, Prop, [Res])
+
+ ///
+ /// 改变数字属性的动画。
+ ///
+ /// 动画的对象。
+ /// 动画的依赖属性。
+ /// 改变的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaDouble(object Obj, DependencyProperty Prop, double Value, int Time = 400, int Delay = 0,
+ AniEase Ease = null, bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number, TypeSub = AniTypeSub.Double, TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(), Obj = new[] { Obj, Prop, "" }, Value = Value, IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 获取数字动画值。
+ ///
+ /// 改变的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaDouble(ParameterizedThreadStart Lambda, double Value, int Time = 400, int Delay = 0,
+ AniEase Ease = null, bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number, TypeSub = AniTypeSub.DoubleParam, TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(), Obj = Lambda, Value = Value, IsAfter = After, TimeFinished = -Delay
+ };
+ }
+
+ // ColorAnimation(Obj, Prop, [Res])
+
+ ///
+ /// 改变颜色属性的动画。
+ ///
+ /// 动画的对象。
+ /// 动画的依赖属性。
+ /// 颜色改变的值。以RGB加减法进行计算。不用担心超额。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaColor(FrameworkElement Obj, DependencyProperty Prop, ModBase.MyColor Value, int Time = 400,
+ int Delay = 0, AniEase Ease = null, bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Color, TimeTotal = Time, Ease = Ease ?? new AniEaseLinear(),
+ Obj = new object[] { Obj, Prop, "" }, Value = Value, IsAfter = After, TimeFinished = -Delay,
+ ValueLast = new ModBase.MyColor(0d, 0d, 0d, 0d)
+ };
+ }
+
+ ///
+ /// 改变颜色属性为一个资源的动画。
+ ///
+ /// 动画的对象。
+ /// 动画的依赖属性。
+ /// 要将颜色改变为该资源值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaColor(FrameworkElement Obj, DependencyProperty Prop, string Res, int Time = 400,
+ int Delay = 0, AniEase Ease = null, bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Color, TimeTotal = Time, Ease = Ease ?? new AniEaseLinear(),
+ Obj = new object[] { Obj, Prop, Res },
+ Value = new ModBase.MyColor(System.Windows.Application.Current.FindResource(Res)) -
+ new ModBase.MyColor(Obj.GetValue(Prop)),
+ IsAfter = After, TimeFinished = -Delay, ValueLast = new ModBase.MyColor(0d, 0d, 0d, 0d)
+ };
+ }
+
+ // Scale
+
+ ///
+ /// 缩放控件的动画。
+ ///
+ /// 动画的对象。
+ /// 大小改变的百分比(如-0.6)或值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ /// 大小改变是否为绝对值。若为 True 则为绝对像素,若为 False 则为相对百分比。
+ ///
+ ///
+ public static AniData AaScale(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false, bool Absolute = false)
+ {
+ ModBase.MyRect ChangeRect;
+ if (Absolute)
+ ChangeRect = new ModBase.MyRect(-0.5d * Value, -0.5d * Value, Value, Value);
+ else
+ ChangeRect = new ModBase.MyRect(
+ Convert.ToDouble(-0.5d * ((dynamic)Obj).ActualWidth * Value),
+ Convert.ToDouble(-0.5d * ((dynamic)Obj).ActualHeight * Value),
+ Convert.ToDouble(((dynamic)Obj).ActualWidth * Value),
+ Convert.ToDouble(((dynamic)Obj).ActualHeight * Value));
+ return new AniData
+ {
+ TypeMain = AniType.Scale, TimeTotal = Time, Ease = Ease ?? new AniEaseLinear(), Obj = Obj,
+ Value = ChangeRect, IsAfter = After, TimeFinished = -Delay
+ };
+ }
+
+ // TextAppear
+
+ ///
+ /// 让一段文字一个个字出现或消失的动画。
+ ///
+ /// 动画的对象。必须是Label或TextBlock。
+ /// 是否为一个个字隐藏。默认为False(一个个字出现)。这些字必须已经存在了。
+ /// 是否采用根据文本长度决定时间的方式。
+ /// 动画长度(毫秒)。若TimePerText为True,这代表每个字所占据的时间。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaTextAppear(object Obj, bool Hide = false, bool TimePerText = true, int Time = 70,
+ int Delay = 0, AniEase Ease = null, bool After = false)
+ {
+ // Are we cool yet?
+ return new AniData
+ {
+ TypeMain = AniType.TextAppear, Ease = Ease ?? new AniEaseLinear(),
+ TimeTotal = TimePerText
+ ? Time * (Obj is TextBlock ? ((dynamic)Obj).Text : ((dynamic)Obj).Context.ToString()).ToString().Length
+ : Time,
+ Obj = Obj,
+ Value = new[] { Obj is TextBlock ? ((dynamic)Obj).Text : ((dynamic)Obj).Context.ToString(), Hide },
+ IsAfter = After, TimeFinished = -Delay
+ };
+ }
+
+ // Code
+
+ ///
+ /// 执行代码。
+ ///
+ /// 一个ThreadStart。这将会在执行时在主线程调用。
+ /// 代码延迟执行的时间(毫秒)。
+ /// 是否等到以前的动画完成后才执行。
+ ///
+ ///
+ public static AniData AaCode(ThreadStart Code, int Delay = 0, bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Code,
+ TimeTotal = 1,
+ Value = Code,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ // ScaleTransform
+
+ ///
+ /// 按照 WPF 方式缩放控件的动画。
+ ///
+ /// 动画的对象。它必须已经拥有了单一的 ScaleTransform 值。
+ /// 大小改变的百分比(如-0.6)。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaScaleTransform(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.ScaleTransform, TimeTotal = Time, Ease = Ease ?? new AniEaseLinear(), Obj = Obj,
+ Value = Value, IsAfter = After, TimeFinished = -Delay
+ };
+ }
+
+ // RotateTransform
+
+ ///
+ /// 按照 WPF 方式旋转控件的动画。
+ ///
+ /// 动画的对象。它必须已经拥有了单一的 ScaleTransform 值。
+ /// 大小改变的百分比(如-0.6)。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ ///
+ ///
+ public static AniData AaRotateTransform(object Obj, double Value, int Time = 400, int Delay = 0,
+ AniEase Ease = null, bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.RotateTransform, TimeTotal = Time, Ease = Ease ?? new AniEaseLinear(), Obj = Obj,
+ Value = Value, IsAfter = After, TimeFinished = -Delay
+ };
+ }
+
+ // TranslateTransform
+
+ ///
+ /// 利用 TranslateTransform 移动 X 轴的动画,这不会造成布局更新。
+ ///
+ /// 动画的对象。
+ /// 进行移动的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ public static AniData AaTranslateX(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.TranslateX,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ ///
+ /// 利用 TranslateTransform 移动 Y 轴的动画,这不会造成布局更新。
+ ///
+ /// 动画的对象。
+ /// 进行移动的值。
+ /// 动画长度(毫秒)。
+ /// 动画延迟执行的时间(毫秒)。
+ /// 插值器类型。
+ /// 是否等到以前的动画完成后才继续本动画。
+ public static AniData AaTranslateY(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null,
+ bool After = false)
+ {
+ return new AniData
+ {
+ TypeMain = AniType.Number,
+ TypeSub = AniTypeSub.TranslateY,
+ TimeTotal = Time,
+ Ease = Ease ?? new AniEaseLinear(),
+ Obj = Obj,
+ Value = Value,
+ IsAfter = After,
+ TimeFinished = -Delay
+ };
+ }
+
+ // 特殊
+
+ ///
+ /// 将一个StackPanel中的各个项目依次显示。
+ ///
+ ///
+ public static List AaStack(StackPanel Stack, int Time = 100, int Delay = 25)
+ {
+ List AaStackRet = default;
+ AaStackRet = new List();
+ var AniDelay = 0;
+ foreach (var Item in Stack.Children)
+ {
+ ((dynamic)Item).Opacity = 0;
+ AaStackRet.Add(AaOpacity(Item, 1d, Time, AniDelay));
+ AniDelay += Delay;
+ }
+
+ return AaStackRet;
+ }
+
+ #endregion
+
+ #region 缓动函数
+
+ // 基类
+ public enum AniEasePower
+ {
+ Weak = 2,
+ Middle = 3,
+ Strong = 4,
+ ExtraStrong = 5
+ }
+
+ ///
+ /// 缓动函数基类。
+ ///
+ public abstract class AniEase
+ {
+ ///
+ /// 获取函数值。
+ ///
+ /// 时间百分比。
+ public abstract double GetValue(double t);
+
+ ///
+ /// 获取增量值。
+ ///
+ /// 较大的 X。
+ /// 较小的 X。
+ public virtual double GetDelta(double t1, double t0)
+ {
+ return GetValue(t1) - GetValue(t0);
+ }
+ }
+
+ ///
+ /// 渐入渐出组合。
+ ///
+ public class AniEaseInout : AniEase
+ {
+ private readonly AniEase EaseIn;
+ private readonly double EaseInPercent;
+ private readonly AniEase EaseOut;
+
+ public AniEaseInout(AniEase EaseIn, AniEase EaseOut, double EaseInPercent = 0.5d)
+ {
+ this.EaseIn = EaseIn;
+ this.EaseOut = EaseOut;
+ this.EaseInPercent = EaseInPercent;
+ }
+
+ public override double GetValue(double t)
+ {
+ if (t < EaseInPercent) return EaseInPercent * EaseIn.GetValue(t / EaseInPercent);
+
+ return (1d - EaseInPercent) * EaseOut.GetValue((t - EaseInPercent) / (1d - EaseInPercent)) + EaseInPercent;
+ }
+ }
+
+ // Linear / 线性
+ ///
+ /// 线性,无缓动。
+ ///
+ public class AniEaseLinear : AniEase
+ {
+ public override double GetValue(double t)
+ {
+ return ModBase.MathClamp(t, 0d, 1d);
+ }
+
+ public override double GetDelta(double t1, double t0)
+ {
+ return ModBase.MathClamp(t1, 0d, 1d) - ModBase.MathClamp(t0, 0d, 1d);
+ }
+ }
+
+ // Fluent / 平滑
+ ///
+ /// 平滑开始。
+ ///
+ public class AniEaseInFluent : AniEase
+ {
+ private readonly AniEasePower p;
+
+ public AniEaseInFluent(AniEasePower Power = AniEasePower.Middle)
+ {
+ p = Power;
+ }
+
+ public override double GetValue(double t)
+ {
+ return Math.Pow(ModBase.MathClamp(t, 0d, 1d), (double)p);
+ }
+ }
+
+ ///
+ /// 平滑结束。
+ ///
+ public class AniEaseOutFluent : AniEase
+ {
+ private readonly AniEasePower p;
+
+ public AniEaseOutFluent(AniEasePower Power = AniEasePower.Middle)
+ {
+ p = Power;
+ }
+
+ public override double GetValue(double t)
+ {
+ return 1d - Math.Pow(ModBase.MathClamp(1d - t, 0d, 1d), (double)p);
+ }
+ }
+
+ ///
+ /// 平滑开始与结束。
+ ///
+ public class AniEaseInoutFluent : AniEase
+ {
+ private readonly AniEaseInout Ease;
+
+ public AniEaseInoutFluent(AniEasePower Power = AniEasePower.Middle, double Middle = 0.5d)
+ {
+ Ease = new AniEaseInout(new AniEaseInFluent(Power), new AniEaseOutFluent(Power), Middle);
+ }
+
+ public override double GetValue(double t)
+ {
+ return Ease.GetValue(t);
+ }
+ }
+
+ ///
+ /// 以特定速度开始的平滑结束。
+ ///
+ public class AniEaseOutFluentWithInitial : AniEase
+ {
+ private readonly double alpha; // (初速度 / 平均速度) – 1
+
+ /// 初速度,px/s
+ /// 总时长,s
+ /// 总路程,px
+ public AniEaseOutFluentWithInitial(double InitialPixelPerSecond, double TotalSecond, double TotalDistance)
+ {
+ var v0_norm = InitialPixelPerSecond * TotalSecond / TotalDistance; // 归一化初速度
+ alpha = v0_norm - 1.0d;
+ if (alpha < 0d)
+ alpha = 0d; // 初速度小于平均速度时,退化为线性
+ }
+
+ public override double GetValue(double percent)
+ {
+ var p = ModBase.MathClamp(percent, 0d, 1d);
+ if (alpha == 0d)
+ return p; // 退化到线性
+ return (alpha + 1d) * p / (1d + alpha * p);
+ }
+ }
+
+ // Back / 回弹
+ ///
+ /// 回弹开始。有效时间为 1/3。
+ ///
+ public class AniEaseInBack : AniEase
+ {
+ private readonly double p;
+
+ public AniEaseInBack(AniEasePower Power = AniEasePower.Middle)
+ {
+ p = 3d - (double)Power * 0.5d;
+ }
+
+ public override double GetValue(double t)
+ {
+ t = ModBase.MathClamp(t, 0d, 1d);
+ return Math.Pow(t, p) * Math.Cos(1.5d * Math.PI * (1d - t));
+ }
+ }
+
+ ///
+ /// 回弹结束。有效时间为 1/3。
+ ///
+ public class AniEaseOutBack : AniEase
+ {
+ private readonly double p;
+
+ public AniEaseOutBack(AniEasePower Power = AniEasePower.Middle)
+ {
+ p = 3d - (double)Power * 0.5d;
+ }
+
+ public override double GetValue(double t)
+ {
+ t = ModBase.MathClamp(t, 0d, 1d);
+ return 1d - Math.Pow(1d - t, p) * Math.Cos(1.5d * Math.PI * t);
+ }
+ }
+
+ // Car / 平滑-回弹
+ ///
+ /// 回弹开始,短平滑结束。
+ ///
+ public class AniEaseInCar : AniEase
+ {
+ private readonly AniEaseInout Ease;
+
+ public AniEaseInCar(double Middle = 0.7d, AniEasePower Power = AniEasePower.Middle)
+ {
+ Ease = new AniEaseInout(new AniEaseInBack(Power), new AniEaseOutFluent(Power), Middle);
+ }
+
+ public override double GetValue(double t)
+ {
+ return Ease.GetValue(t);
+ }
+ }
+
+ ///
+ /// 短平滑开始,回弹结束。
+ ///
+ public class AniEaseOutCar : AniEase
+ {
+ private readonly AniEaseInout Ease;
+
+ public AniEaseOutCar(double Middle = 0.3d, AniEasePower Power = AniEasePower.Middle)
+ {
+ Ease = new AniEaseInout(new AniEaseInFluent(Power), new AniEaseOutBack(Power), Middle);
+ }
+
+ public override double GetValue(double t)
+ {
+ return Ease.GetValue(t);
+ }
+ }
+
+ // Elastic / 弹簧
+ ///
+ /// 弹簧开始。约在 60% 到达最小值。
+ ///
+ public class AniEaseInElastic : AniEase
+ {
+ private readonly int p; // 6~9
+
+ public AniEaseInElastic(AniEasePower Power = AniEasePower.Middle)
+ {
+ p = (int)Power + 4;
+ }
+
+ public override double GetValue(double t)
+ {
+ t = ModBase.MathClamp(t, 0d, 1d);
+ return Math.Pow(t, (p - 1) * 0.25d) * Math.Cos((p - 3.5d) * Math.PI * Math.Pow(1d - t, 1.5d));
+ }
+ }
+
+ ///
+ /// 弹簧结束。约在 40% 到达最大值。
+ ///
+ public class AniEaseOutElastic : AniEase
+ {
+ private readonly int p;
+
+ public AniEaseOutElastic(AniEasePower Power = AniEasePower.Middle)
+ {
+ p = (int)Power + 4;
+ }
+
+ public override double GetValue(double t)
+ {
+ t = 1d - ModBase.MathClamp(t, 0d, 1d);
+ return 1d - Math.Pow(t, (p - 1) * 0.25d) * Math.Cos((p - 3.5d) * Math.PI * Math.Pow(1d - t, 1.5d));
+ }
+ }
+
+ #endregion
+
+ #region 接口(开始、中断、检测)
+
+ ///
+ /// 开始一个动画组。
+ ///
+ /// 由 Aa 开头的函数初始化的 AniData 对象集合。
+ /// 动画组的名称。如果重复会直接停止同名动画组。
+ public static void AniStart(IList AniGroup, string Name = "", bool RefreshTime = false)
+ {
+ if (RefreshTime)
+ AniLastTick = TimeUtils.GetTimeTick(); // 避免处理动画时已经造成了极大的延迟,导致动画突然结束
+ // 添加到正在执行的动画组
+ var NewEntry = new AniGroupEntry
+ { Data = ModBase.GetFullList(AniGroup), StartTick = TimeUtils.GetTimeTick() };
+ if (string.IsNullOrEmpty(Name))
+ Name = NewEntry.Uuid.ToString();
+ else
+ AniStop(Name);
+ AniGroups.TryAdd(Name, NewEntry);
+ }
+
+ ///
+ /// 开始一个动画组。
+ ///
+ public static void AniStart(AniData AniGroup, string Name = "", bool RefreshTime = false)
+ {
+ AniStart(new List { AniGroup }, Name, RefreshTime);
+ }
+
+ ///
+ /// 直接停止一个动画组。
+ ///
+ /// 需要停止的动画组的名称。
+ public static void AniStop(string Name)
+ {
+ AniGroups.Remove(Name, out _);
+ }
+
+ ///
+ /// 获取动画是否正在进行中。
+ ///
+ public static bool AniIsRun(string Name)
+ {
+ return AniGroups.ContainsKey(Name);
+ }
+
+ #endregion
+}
diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.cs b/Plain Craft Launcher 2/Modules/Base/ModBase.cs
new file mode 100644
index 000000000..1e42431ad
--- /dev/null
+++ b/Plain Craft Launcher 2/Modules/Base/ModBase.cs
@@ -0,0 +1,4040 @@
+using System.Collections;
+using System.Collections.Concurrent;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.Globalization;
+using System.IO;
+using System.IO.Compression;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Threading;
+using System.Xaml;
+using System.Xml.Linq;
+using Microsoft.VisualBasic;
+using Microsoft.VisualBasic.CompilerServices;
+using Microsoft.Win32;
+using Newtonsoft.Json;
+using PCL.Core.App;
+using PCL.Core.IO;
+using PCL.Core.Logging;
+using PCL.Core.Utils;
+using PCL.Core.Utils.Codecs;
+using PCL.Core.Utils.Hash;
+using PCL.Core.Utils.OS;
+using Brush = System.Windows.Media.Brush;
+using Color = System.Windows.Media.Color;
+using ColorConverter = System.Windows.Media.ColorConverter;
+using FontFamily = System.Windows.Media.FontFamily;
+using Size = System.Windows.Size;
+
+namespace PCL;
+
+public static class ModBase
+{
+ #region 声明
+
+ // 下列版本信息由更新器自动修改
+ public static readonly string VersionBaseName = Basics.VersionName;
+ public static readonly string VersionStandardCode = Basics.Metadata.Version.StandardVersion;
+ public static readonly string UpstreamVersion = Basics.Metadata.Version.UpstreamVersion;
+ public static readonly string CommitHash = Basics.Metadata.Version.Commit;
+ public static readonly string CommitHashShort = Basics.Metadata.Version.CommitDigest;
+ public static readonly int VersionCode = Basics.VersionCode;
+
+#if DEBUG
+ public const string VersionBranchName = "Debug";
+ public const string VersionBranchCode = "100";
+#elif DEBUGCI
+ public const string VersionBranchName = "CI";
+ public const string VersionBranchCode = "50";
+#else
+ public const string VersionBranchName = "Publish";
+ public const string VersionBranchCode = "0";
+#endif
+ ///
+ /// 主窗口句柄。
+ ///
+ public static nint FrmHandle;
+
+ // 龙猫味石山小记: 用最不靠谱的实现写出能跑的代码 (AppDomain.CurrentDomain.SetupInformation.ApplicationBase 获取到的是当前工作目录而不是可执行文件所在目录)
+ ///
+ /// 程序可执行文件所在目录,以“\”结尾。
+ ///
+ public static readonly string ExePath = Conversions.ToString(Basics.ExecutableDirectory.EndsWith(@"\")
+ ? Basics.ExecutableDirectory
+ : Basics.ExecutableDirectory + @"\");
+
+ ///
+ /// 程序可执行文件完整路径。
+ ///
+ public static readonly string ExePathWithName = Basics.ExecutablePath;
+
+ ///
+ /// 程序内嵌图片文件夹路径,以“/”结尾。
+ ///
+ public static readonly string PathImage = "pack://application:,,,/Plain Craft Launcher 2;component/Images/";
+
+ ///
+ /// 当前程序的语言。
+ ///
+ public static string Lang = "zh_CN";
+
+ ///
+ /// 设置对象。
+ ///
+ public static ModSetup Setup = new();
+
+ ///
+ /// 程序的打开计时。
+ ///
+ public static long ApplicationStartTick = TimeUtils.GetTimeTick();
+
+ ///
+ /// 程序打开时的时间。
+ ///
+ public static DateTime ApplicationOpenTime = DateTime.Now;
+
+ ///
+ /// 识别码。
+ ///
+ public static string UniqueAddress = ModSecret.SecretGetUniqueAddress();
+
+ ///
+ /// 程序是否已结束。
+ ///
+ public static bool IsProgramEnded = false;
+
+ ///
+ /// 是否为 32 位系统。
+ ///
+ public static bool Is32BitSystem = !Environment.Is64BitOperatingSystem;
+
+ ///
+ /// 是否为 ARM64 架构。
+ ///
+ public static bool IsArm64System = RuntimeInformation.OSArchitecture == Architecture.Arm64;
+
+ ///
+ /// 是否使用 GBK 编码。
+ ///
+ public static bool IsGBKEncoding = Encoding.Default.CodePage == 936;
+
+ ///
+ /// 系统盘盘符,以 \ 结尾。例如 “C:\”。
+ ///
+ public static string OsDrive =
+ Environment.GetLogicalDrives().Where(p => Directory.Exists(p)).First().ToUpper().First() + @":\"; // #3799
+
+ ///
+ /// 程序的缓存文件夹路径,以 \ 结尾。
+ ///
+ public static string PathTemp = Paths.Temp + @"\";
+
+ ///
+ /// AppData 中的 PCL 文件夹路径,以 \ 结尾。
+ ///
+ public static string PathAppdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\PCL\";
+
+ ///
+ /// AppData 中的 PCLCE 配置文件夹路径,以 \ 结尾。
+ ///
+ public static string PathAppdataConfig = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) +
+ (VersionBranchName == "Debug" ? @"\.pclcedebug\" : @"\.pclce\");
+
+ public static string PathHelpFolder = PathTemp + @"CE\Help\";
+
+ #endregion
+
+ #region 矢量图标
+
+ public class Logo
+ {
+ ///
+ /// 图标按钮,心(空心),1.1x
+ ///
+ public const string IconButtonLikeLine =
+ "M512 896a42.666667 42.666667 0 0 1-30.293333-12.373333l-331.52-331.946667a224.426667 224.426667 0 0 1 0-315.733333 223.573333 223.573333 0 0 1 315.733333 0L512 282.026667l46.08-46.08a223.573333 223.573333 0 0 1 315.733333 0 224.426667 224.426667 0 0 1 0 315.733333l-331.52 331.946667A42.666667 42.666667 0 0 1 512 896zM308.053333 256a136.533333 136.533333 0 0 0-97.28 40.106667 138.24 138.24 0 0 0 0 194.986666L512 792.746667l301.226667-301.653334a138.24 138.24 0 0 0 0-194.986666 141.653333 141.653333 0 0 0-194.56 0l-76.373334 76.8a42.666667 42.666667 0 0 1-60.586666 0L405.333333 296.106667A136.533333 136.533333 0 0 0 308.053333 256z";
+
+ ///
+ /// 图标按钮,心(实心),1.1x
+ ///
+ public const string IconButtonLikeFill =
+ "M700.856 155.543c-74.769 0-144.295 72.696-190.046 127.26-45.737-54.576-115.247-127.26-190.056-127.26-134.79 0-244.443 105.78-244.443 235.799 0 77.57 39.278 131.988 70.845 175.713C238.908 694.053 469.62 852.094 479.39 858.757c9.41 6.414 20.424 9.629 31.401 9.629 11.006 0 21.998-3.215 31.398-9.63 9.782-6.662 240.514-164.703 332.238-291.701 31.587-43.724 70.874-98.143 70.874-175.713-0.001-130.02-109.656-235.8-244.445-235.8z m0 0";
+
+ ///
+ /// 图标按钮,垃圾桶,1.1x
+ ///
+ public const string IconButtonDelete =
+ "M520.192 0C408.43 0 317.44 82.87 313.563 186.734H52.736c-29.038 0-52.663 21.943-52.663 49.079s23.625 49.152 52.663 49.152h58.075v550.473c0 103.35 75.118 187.757 167.717 187.757h472.43c92.599 0 167.716-83.894 167.716-187.757V285.477h52.59c29.038 0 52.59-21.943 52.663-49.08-0.073-27.135-23.625-49.151-52.663-49.151H726.235C723.237 83.017 631.955 0 520.192 0zM404.846 177.957c3.803-50.03 50.176-89.015 107.447-89.015 57.197 0 103.57 38.985 106.788 89.015H404.92zM284.379 933.669c-33.353 0-69.997-39.351-69.997-95.525v-549.01H833.39v549.522c0 56.247-36.645 95.525-69.998 95.525H284.379v-0.512z M357.23 800.695a48.274 48.274 0 0 0 47.616-49.006V471.7a48.274 48.274 0 0 0-47.543-49.08 48.274 48.274 0 0 0-47.69 49.006V751.69c0 27.282 20.846 49.006 47.617 49.006z m166.62 0a48.274 48.274 0 0 0 47.688-49.006V471.7a48.274 48.274 0 0 0-47.689-49.08 48.274 48.274 0 0 0-47.543 49.006V751.69c0 27.282 21.431 49.006 47.543 49.006z m142.92 0a48.274 48.274 0 0 0 47.543-49.006V471.7a48.274 48.274 0 0 0-47.543-49.08 48.274 48.274 0 0 0-47.616 49.006V751.69c0 27.282 20.773 49.006 47.543 49.006z";
+
+ ///
+ /// 图标按钮,禁止,1x
+ ///
+ public const string IconButtonStop =
+ "M508 990.4c-261.6 0-474.4-212-474.4-474.4S246.4 41.6 508 41.6s474.4 212 474.4 474.4S769.6 990.4 508 990.4zM508 136.8c-209.6 0-379.2 169.6-379.2 379.2 0 209.6 169.6 379.2 379.2 379.2s379.2-169.6 379.2-379.2C887.2 306.4 717.6 136.8 508 136.8zM697.6 563.2 318.4 563.2c-26.4 0-47.2-21.6-47.2-47.2 0-26.4 21.6-47.2 47.2-47.2l379.2 0c26.4 0 47.2 21.6 47.2 47.2C744.8 542.4 724 563.2 697.6 563.2z";
+
+ ///
+ /// 图标按钮,勾选,1x
+ ///
+ public const string IconButtonCheck =
+ "M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m0 921.6a409.6 409.6 0 1 1 409.6-409.6 409.6 409.6 0 0 1-409.6 409.6z M716.8 339.968l-256 253.44L328.192 460.8A51.2 51.2 0 0 0 256 532.992l168.448 168.96a51.2 51.2 0 0 0 72.704 0l289.28-289.792A51.2 51.2 0 0 0 716.8 339.968z";
+
+ ///
+ /// 图标按钮,笔,1x
+ ///
+ public const string IconButtonEdit =
+ "M732.64 64.32C688.576 21.216 613.696 21.216 569.6 64.32L120.128 499.52c-17.6 12.896-26.432 30.144-30.848 51.68L32 870.048c0 25.856 8.8 56 26.432 73.248 17.632 17.216 17.632 48.704 88.64 48.704h13.248l326.08-56c22.016-4.32 39.68-12.928 52.864-30.176l449.472-435.2c22.048-21.536 35.264-47.36 35.264-77.536 0-30.176-13.216-56-35.264-77.568l-256.096-251.2zM139.712 903.776l56-326.912 311.04-295.136 267.104 269.44-310.976 295.168-323.168 57.44zM844.576 467.84l-273.984-260.672 61.856-59.84c8.832-8.512 26.528-8.512 39.776 0l234.24 226.496c4.384 4.288 8.832 12.8 8.832 17.088s-4.416 8.544-8.864 12.8l-61.856 64.128z";
+
+ ///
+ /// 图标按钮,齿轮,1.1x
+ ///
+ public const string IconButtonSetup =
+ "M651.946667 1001.813333c-22.186667 0-42.666667-10.24-61.44-27.306666-23.893333-23.893333-49.493333-35.84-75.093334-35.84-29.013333 0-56.32 11.946667-73.386666 30.72v3.413333c-17.066667 17.066667-42.666667 27.306667-66.56 27.306667h-6.826667c-6.826667 0-11.946667-1.706667-15.36-1.706667l-6.826667-1.706667c-64.853333-20.48-121.173333-54.613333-168.96-98.986666-29.013333-23.893333-37.546667-63.146667-25.6-95.573334 8.533333-23.893333 5.12-51.2-10.24-75.093333-15.36-27.306667-34.133333-40.96-59.733333-47.786667h-1.706667l-5.12-1.706666c-35.84-8.533333-61.44-34.133333-66.56-69.973334C1.706667 575.146667 0 537.6 0 512c0-32.426667 3.413333-63.146667 8.533333-93.866667v-6.826666l3.413334-8.533334c10.24-23.893333 23.893333-40.96 44.373333-51.2 5.12-3.413333 11.946667-6.826667 20.48-8.533333 27.306667-8.533333 51.2-25.6 63.146667-44.373333 13.653333-23.893333 17.066667-52.906667 10.24-81.92-11.946667-34.133333 0-71.68 30.72-93.866667 44.373333-37.546667 97.28-68.266667 158.72-93.866667l3.413333-1.706666c44.373333-13.653333 75.093333 3.413333 92.16 20.48 23.893333 23.893333 49.493333 35.84 75.093333 35.84 30.72 0 56.32-10.24 71.68-30.72l3.413334-3.413334c27.306667-27.306667 63.146667-35.84 93.866666-22.186666 63.146667 22.186667 117.76 54.613333 165.546667 97.28 29.013333 23.893333 37.546667 63.146667 25.6 95.573333-8.533333 23.893333-5.12 51.2 10.24 75.093333 15.36 27.306667 34.133333 40.96 59.733333 47.786667h1.706667l5.12 1.706667c35.84 8.533333 61.44 34.133333 66.56 71.68 6.826667 30.72 10.24 63.146667 11.946667 93.866666v3.413334c0 32.426667-3.413333 63.146667-8.533334 93.866666v6.826667l-3.413333 8.533333c-10.24 23.893333-23.893333 40.96-44.373333 51.2-5.12 3.413333-11.946667 6.826667-20.48 8.533334-27.306667 8.533333-51.2 25.6-63.146667 46.08-13.653333 23.893333-17.066667 52.906667-10.24 81.92 11.946667 35.84-1.706667 75.093333-30.72 95.573333-44.373333 35.84-95.573333 66.56-157.013333 92.16-15.36 3.413333-27.306667 3.413333-35.84 3.413333z m3.413333-83.626666z m1.706667 0zM517.12 853.333333c47.786667 0 93.866667 20.48 134.826667 59.733334 1.706667 1.706667 3.413333 1.706667 3.413333 3.413333 52.906667-22.186667 97.28-49.493333 136.533333-80.213333l1.706667-1.706667v-3.413333c-13.653333-52.906667-8.533333-104.106667 17.066667-148.48 23.893333-39.253333 64.853333-69.973333 114.346666-85.333334 1.706667 0 3.413333-1.706667 6.826667-6.826666 5.12-25.6 8.533333-51.2 8.533333-78.506667-1.706667-29.013333-3.413333-56.32-10.24-81.92v-5.12h-1.706666c-51.2-11.946667-90.453333-39.253333-119.466667-87.04-27.306667-44.373333-34.133333-100.693333-17.066667-148.48l-1.706666-1.706667h-3.413334c-39.253333-35.84-85.333333-63.146667-136.533333-80.213333H648.533333s-1.706667 1.706667-3.413333 1.706667c-32.426667 39.253333-80.213333 59.733333-136.533333 59.733333-47.786667 0-93.866667-20.48-134.826667-59.733333l-1.706667-1.706667h-1.706666c-54.613333 22.186667-98.986667 49.493333-136.533334 80.213333l-1.706666 1.706667v3.413333c13.653333 52.906667 8.533333 104.106667-17.066667 148.48-23.893333 39.253333-64.853333 69.973333-114.346667 85.333334-1.706667 0-3.413333 1.706667-6.826666 6.826666-6.826667 25.6-8.533333 51.2-8.533334 78.506667 0 30.72 3.413333 58.026667 6.826667 76.8l1.706667 5.12h1.706666c51.2 11.946667 90.453333 39.253333 119.466667 87.04 27.306667 44.373333 34.133333 100.693333 17.066667 148.48l1.706666 1.706667 1.706667 1.706666c37.546667 35.84 83.626667 63.146667 134.826667 80.213334 1.706667 0 3.413333 0 3.413333 1.706666h1.706667s1.706667 0 5.12-1.706666c34.133333-37.546667 81.92-59.733333 136.533333-59.733334z m-6.826667-146.773333c-110.933333 0-199.68-85.333333-199.68-196.266667 0-109.226667 87.04-196.266667 199.68-196.266666s199.68 85.333333 199.68 196.266666c-1.706667 109.226667-88.746667 196.266667-199.68 196.266667z m0-307.2c-63.146667 0-114.346667 49.493333-114.346666 110.933333 0 63.146667 49.493333 110.933333 114.346666 110.933334 30.72 0 59.733333-11.946667 80.213334-32.426667 20.48-20.48 32.426667-49.493333 32.426666-78.506667 0-63.146667-49.493333-110.933333-112.64-110.933333z";
+
+ ///
+ /// 图标按钮,重置,0.9x
+ ///
+ public const string IconButtonReset =
+ "M667.6817627 313.65283203l-45.28564454 55.76660156L858.06933594 391.27124023 787.61950684 165.93066406l-56.01379395 69.01611328A354.47387695 354.47387695 0 0 0 520.89892578 165.93066406C324.87536621 165.93066406 165.93066406 324.43041992 165.93066406 519.91015625c0 195.52917481 158.94470215 353.97949219 354.96826172 353.97949219a355.06713867 355.06713867 0 0 0 331.73217774-227.66418458 50.52612305 50.52612305 0 0 0-29.21813966-65.25878905 50.77331543 50.77331543 0 0 0-65.50598144 29.16870117A253.61938477 253.61938477 0 0 1 520.94836426 772.78796387c-140.05920411 0-253.61938477-113.21411133-253.61938477-252.87780762 0-139.61425781 113.56018067-252.82836914 253.61938477-252.82836914 53.59130859 0 104.46350098 16.61132813 146.73339843 46.57104492";
+
+ ///
+ /// 图标按钮,刷新,0.85x
+ ///
+ public const string IconButtonRefresh =
+ "M875.52 148.48C783.36 56.32 655.36 0 512 0 291.84 0 107.52 138.24 30.72 332.8l122.88 46.08C204.8 230.4 348.16 128 512 128c107.52 0 199.68 40.96 271.36 112.64L640 384h384V0L875.52 148.48zM512 896c-107.52 0-199.68-40.96-271.36-112.64L384 640H0v384l148.48-148.48C240.64 967.68 368.64 1024 512 1024c220.16 0 404.48-138.24 481.28-332.8L870.4 645.12C819.2 793.6 675.84 896 512 896z";
+
+ ///
+ /// 图标按钮,软盘,1x
+ ///
+ public const string IconButtonSave =
+ "M819.392 0L1024 202.752v652.16a168.96 168.96 0 0 1-168.832 168.768h-104.192a47.296 47.296 0 0 1-10.752 0H283.776a47.232 47.232 0 0 1-10.752 0H168.832A168.96 168.96 0 0 1 0 854.912V168.768A168.96 168.96 0 0 1 168.832 0h650.56z m110.208 854.912V242.112l-149.12-147.776H168.896c-41.088 0-74.432 33.408-74.432 74.432v686.144c0 41.024 33.344 74.432 74.432 74.432h62.4v-190.528c0-33.408 27.136-60.544 60.544-60.544h440.448c33.408 0 60.544 27.136 60.544 60.544v190.528h62.4c41.088 0 74.432-33.408 74.432-74.432z m-604.032 74.432h372.864v-156.736H325.568v156.736z m403.52-596.48a47.168 47.168 0 1 1 0 94.336H287.872a47.168 47.168 0 1 1 0-94.336h441.216z m0-153.728a47.168 47.168 0 1 1 0 94.4H287.872a47.168 47.168 0 1 1 0-94.4h441.216z";
+
+ ///
+ /// 图标按钮,信息,1.05x
+ ///
+ public const string IconButtonInfo =
+ "M512 917.333333c223.861333 0 405.333333-181.472 405.333333-405.333333S735.861333 106.666667 512 106.666667 106.666667 288.138667 106.666667 512s181.472 405.333333 405.333333 405.333333z m0 106.666667C229.226667 1024 0 794.773333 0 512S229.226667 0 512 0s512 229.226667 512 512-229.226667 512-512 512z m-32-597.333333h64a21.333333 21.333333 0 0 1 21.333333 21.333333v320a21.333333 21.333333 0 0 1-21.333333 21.333333h-64a21.333333 21.333333 0 0 1-21.333333-21.333333V448a21.333333 21.333333 0 0 1 21.333333-21.333333z m0-192h64a21.333333 21.333333 0 0 1 21.333333 21.333333v64a21.333333 21.333333 0 0 1-21.333333 21.333333h-64a21.333333 21.333333 0 0 1-21.333333-21.333333v-64a21.333333 21.333333 0 0 1 21.333333-21.333333z";
+
+ ///
+ /// 图标按钮,列表,1x
+ ///
+ public const string IconButtonList =
+ "M384 128h640v128H384zM160 192m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0ZM384 448h640v128H384zM160 512m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0ZM384 768h640v128H384zM160 832m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0Z";
+
+ ///
+ /// 图标按钮,文件夹,1.15x
+ ///
+ public const string IconButtonOpen =
+ "M889.018182 418.909091H884.363636V316.509091a93.090909 93.090909 0 0 0-99.607272-89.832727h-302.545455l-93.090909-76.334546A46.545455 46.545455 0 0 0 358.865455 139.636364H146.152727A93.090909 93.090909 0 0 0 46.545455 229.469091V837.818182a46.545455 46.545455 0 0 0 46.545454 46.545454 46.545455 46.545455 0 0 0 16.756364-3.258181 109.381818 109.381818 0 0 0 25.134545 3.258181h586.472727a85.178182 85.178182 0 0 0 87.04-63.301818l163.374546-302.545454a46.545455 46.545455 0 0 0 5.585454-21.876364A82.385455 82.385455 0 0 0 889.018182 418.909091z m-744.727273-186.181818h198.283636l93.09091 76.334545a46.545455 46.545455 0 0 0 29.323636 10.705455h319.301818a12.101818 12.101818 0 0 1 6.516364 0V418.909091H302.545455a85.178182 85.178182 0 0 0-87.04 63.301818L139.636364 622.778182V232.727273a19.549091 19.549091 0 0 1 6.516363 0z m578.094546 552.029091a27.461818 27.461818 0 0 0-2.792728 6.516363H154.530909l147.083636-272.290909a27.461818 27.461818 0 0 0 2.792728-6.981818h565.061818z";
+
+ ///
+ /// 图标按钮,名片,1.1x
+ ///
+ public const string IconButtonCard =
+ "M834.5 684.1c-31.2-70.4-98.9-120.9-179.1-127.3 63.5-8.5 112.6-63 112.6-128.8 0-71.8-58.2-130-130-130s-130 58.2-130 130c0 65.9 49 120.3 112.6 128.8-80.2 6.4-148 57-179.1 127.3-8.7 19.7 6 42 27.6 42 12.1 0 22.7-7.5 27.7-18.5 24.3-53.9 78.5-91.5 141.3-91.5s117 37.6 141.3 91.5c5 11.1 15.6 18.5 27.7 18.5 21.4 0 36.1-22.3 27.4-42zM567.9 427.9c0-38.6 31.4-70 70-70s70 31.4 70 70-31.4 70-70 70-70-31.4-70-70zM460.3 347.9H216.9c-16.6 0-30 13.4-30 30s13.4 30 30 30h243.3c16.6 0 30-13.4 30-30 0.1-16.5-13.4-30-29.9-30zM367.4 459.6H216.9c-16.6 0-30 13.4-30 30s13.4 30 30 30h150.4c16.6 0 30-13.4 30-30 0.1-16.6-13.4-30-29.9-30zM297.4 571.2H217c-16.6 0-30 13.4-30 30s13.4 30 30 30h80.4c16.6 0 30-13.4 30-30 0-16.5-13.5-30-30-30zM900 236v552H124V236h776m0-60H124c-33.1 0-60 26.9-60 60v552c0 33.1 26.9 60 60 60h776c33.1 0 60-26.9 60-60V236c0-33.1-26.9-60-60-60z";
+
+ ///
+ /// 图标按钮,×,0.85x
+ ///
+ public const string IconButtonCross =
+ "F1 M 26.9166,22.1667L 37.9999,33.25L 49.0832,22.1668L 53.8332,26.9168L 42.7499,38L 53.8332,49.0834L 49.0833,53.8334L 37.9999,42.75L 26.9166,53.8334L 22.1666,49.0833L 33.25,38L 22.1667,26.9167L 26.9166,22.1667 Z";
+
+ ///
+ /// 图标按钮,验证,1.1x
+ ///
+ public const string IconButtonAuth =
+ "M511.488256 95.184408c35.310345 22.516742 95.184408 55.78011 167.34033 84.437781 75.738131 29.681159 148.405797 40.93953 191.392304 45.033483v353.615193c0 73.691154-50.662669 164.781609-136.123938 244.101949C649.65917 901.181409 558.568716 942.12094 512 942.12094c-46.568716 0-137.65917-40.93953-222.096952-119.748126C204.441779 742.54073 153.77911 651.450275 153.77911 577.247376v-353.103448c42.474763-4.093953 116.165917-15.352324 191.904048-45.545227 75.226387-30.192904 133.565217-63.456272 165.805098-83.414293M512 0c-4.093953 0-8.187906 1.535232-11.258371 3.582209l-14.84058 10.234882c-1.023488 0.511744-67.550225 47.592204-170.410794 88.531735-100.813593 39.916042-198.556722 41.963018-199.58021 41.963018l-25.075462 0.511744c-10.746627 0.511744-18.934533 8.187906-18.934533 18.422789v414.000999c0 216.97951 286.064968 446.24088 440.09995 446.24088s440.09995-229.261369 440.09995-445.729136V163.758121c0-10.234883-8.69965-18.422789-18.934533-18.422789l-24.563718-0.511744c-1.023488 0-98.766617-2.046977-199.58021-41.963018-103.372314-40.93953-170.410795-88.01999-170.922538-88.531734L523.258371 3.582209c-3.070465-2.558721-7.164418-3.582209-11.258371-3.582209z M743.308346 410.930535l-260.477761 260.477761c-15.864068 15.864068-41.963018 15.864068-57.827087 0l-144.823588-144.823588c-15.864068-15.864068-15.864068-41.963018 0-57.827087 8.187906-8.187906 18.422789-11.770115 29.169415-11.770115 10.234883 0 20.981509 4.093953 29.169416 11.770115l115.654173 115.654173L685.993003 352.591704c15.864068-15.864068 41.963018-15.864068 57.827087 0 15.352324 16.375812 15.352324 42.474763-0.511744 58.338831z";
+
+ ///
+ /// 图标按钮,第三方
+ ///
+ public const string IconButtonThirdparty =
+ "M865.004 167.069c-10.794-9.687-24.91-15.085-39.579-15.085-1.383 0-2.629 0-4.013 0.139-0.831 0.139-10.102 0.692-24.771 0.692-24.218 0-71.408-1.522-116.107-12.178-57.708-13.7-124.411-77.083-143.785-89.675-9.687-6.227-21.034-9.41-32.244-9.41-11.21 0-22.42 3.182-32.244 9.41-2.353 1.522-72.1 73.484-140.324 89.675-44.699 10.655-92.72 12.178-116.938 12.178-14.53 0-23.941-0.554-24.771-0.692-1.246-0.139-2.629-0.139-3.875-0.139-14.67 0-28.924 5.396-39.717 15.085-11.763 10.655-18.405 25.325-18.405 40.825v140.048c0 517.846 351.089 584.411 366.034 587.040 3.46 0.554 6.782 0.831 10.241 0.831 3.46 0 6.918-0.276 10.241-0.831 14.946-2.629 368.663-69.33 368.663-587.040v-139.911c0.139-15.5-6.642-30.446-18.405-40.962v0zM825.425 348.080c0 476.883-320.783 531.961-320.783 531.961s-318.291-55.078-318.291-531.961v-140.048c0 0 10.933 0.831 28.785 0.831 30.446 0 81.648-2.214 130.777-13.839 80.403-19.098 158.731-97.564 158.731-97.564s81.787 78.466 162.19 97.564c49.129 11.625 99.501 13.839 129.946 13.839 17.714 0 28.785-0.831 28.785-0.831l-0.139 140.048zM463.405 491.173z M349.925 603.958l66.841-15.085c10.102 54.663 40.962 81.925 92.72 81.925 57.43-1.383 87.045-29.476 88.429-84.14 0-50.373-35.289-75.421-105.728-75.421-17.299 0-30.998 0-40.962 0v-51.757c10.102 0 20.757 0 32.382 0 66.149 0 99.916-25.187 101.3-75.421-1.383-45.945-26.571-69.747-75.421-71.132-48.85 0-77.635 26.571-86.215 79.85l-64.766-15.085c18.683-76.252 70.438-114.308 155.27-114.308 87.738 2.906 134.373 40.962 140.187 114.308-1.383 53.279-30.998 87.738-88.429 103.514 63.244 13.008 97.009 49.542 101.3 110.019-4.29 81.925-56.878 124.411-157.486 127.316-87.461 1.246-140.739-36.811-159.422-114.585z";
+
+ ///
+ /// 图标按钮,用户,0.95x
+ ///
+ public const string IconButtonUser =
+ "M660.338 528.065c63.61-46.825 105.131-121.964 105.131-206.83 0-141.7-115.29-256.987-256.997-256.987-141.706 0-256.998 115.288-256.998 256.987 0 85.901 42.52 161.887 107.456 208.562-152.1 59.92-260.185 207.961-260.185 381.077 0 21.276 17.253 38.53 38.53 38.53 21.278 0 38.53-17.254 38.53-38.53 0-183.426 149.232-332.671 332.667-332.671 1.589 0 3.113-0.207 4.694-0.244 0.8 0.056 1.553 0.244 2.362 0.244 183.434 0 332.664 149.245 332.664 332.671 0 21.276 17.255 38.53 38.533 38.53 21.277 0 38.53-17.254 38.53-38.53 0-174.885-110.354-324.13-264.917-382.809z m-331.803-206.83c0-99.22 80.72-179.927 179.935-179.927s179.937 80.708 179.937 179.927c0 99.203-80.721 179.91-179.937 179.91s-179.935-80.708-179.935-179.91z";
+
+ ///
+ /// 图标按钮,盾牌,1x
+ ///
+ public const string IconButtonShield =
+ "M511.488256 95.184408c35.310345 22.516742 95.184408 55.78011 167.34033 84.437781 75.738131 29.681159 148.405797 40.93953 191.392304 45.033483v353.615193c0 73.691154-50.662669 164.781609-136.123938 244.101949C649.65917 901.181409 558.568716 942.12094 512 942.12094c-46.568716 0-137.65917-40.93953-222.096952-119.748126C204.441779 742.54073 153.77911 651.450275 153.77911 577.247376v-353.103448c42.474763-4.093953 116.165917-15.352324 191.904048-45.545227 75.226387-30.192904 133.565217-63.456272 165.805098-83.414293M512 0c-4.093953 0-8.187906 1.535232-11.258371 3.582209l-14.84058 10.234882c-1.023488 0.511744-67.550225 47.592204-170.410794 88.531735-100.813593 39.916042-198.556722 41.963018-199.58021 41.963018l-25.075462 0.511744c-10.746627 0.511744-18.934533 8.187906-18.934533 18.422789v414.000999c0 216.97951 286.064968 446.24088 440.09995 446.24088s440.09995-229.261369 440.09995-445.729136V163.758121c0-10.234883-8.69965-18.422789-18.934533-18.422789l-24.563718-0.511744c-1.023488 0-98.766617-2.046977-199.58021-41.963018-103.372314-40.93953-170.410795-88.01999-170.922538-88.531734L523.258371 3.582209c-3.070465-2.558721-7.164418-3.582209-11.258371-3.582209z M743.308346 410.930535l-260.477761 260.477761c-15.864068 15.864068-41.963018 15.864068-57.827087 0l-144.823588-144.823588c-15.864068-15.864068-15.864068-41.963018 0-57.827087 8.187906-8.187906 18.422789-11.770115 29.169415-11.770115 10.234883 0 20.981509 4.093953 29.169416 11.770115l115.654173 115.654173L685.993003 352.591704c15.864068-15.864068 41.963018-15.864068 57.827087 0 15.352324 16.375812 15.352324 42.474763-0.511744 58.338831z";
+
+ ///
+ /// 图标按钮,离线,0.85x
+ ///
+ public const string IconButtonOffline =
+ "M533.293176 788.841412a60.235294 60.235294 0 1 1 85.202824 85.202823l-42.616471 42.586353c-129.355294 129.385412-339.124706 129.385412-468.510117 0-129.385412-129.385412-129.385412-339.124706 0-468.510117l42.586353-42.616471a60.235294 60.235294 0 1 1 85.202823 85.202824l-42.61647 42.586352a210.823529 210.823529 0 1 0 298.164706 298.164706l42.586352-42.61647z m255.548236-255.548236l42.61647-42.586352a210.823529 210.823529 0 1 0-298.164706-298.164706l-42.586352 42.61647a60.235294 60.235294 0 1 1-85.202824-85.202823l42.616471-42.586353c129.355294-129.385412 339.124706-129.385412 468.510117 0 129.385412 129.385412 129.385412 339.124706 0 468.510117l-42.586353 42.616471a60.235294 60.235294 0 1 1-85.202823-85.202824zM192.542118 192.542118a60.235294 60.235294 0 0 1 85.202823 0l553.712941 553.712941a60.235294 60.235294 0 0 1-85.202823 85.202823L192.542118 277.744941a60.235294 60.235294 0 0 1 0-85.202823z";
+
+ ///
+ /// 图标,服务端,1x
+ ///
+ public const string IconButtonServer =
+ "M224 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V224a64 64 0 0 0-64-64H224z m0 384h576v256H224v-256z m192 96v64h320v-64H416z m-128 0v64h64v-64H288zM224 224h576v256H224V224z m192 96v64h320v-64H416z m-128 0v64h64v-64H288z";
+
+ ///
+ /// 图标按钮,复制
+ ///
+ public const string IconButtonCopy =
+ "M394.666667 106.666667h448a74.666667 74.666667 0 0 1 74.666666 74.666666v448a74.666667 74.666667 0 0 1-74.666666 74.666667H394.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V181.333333a74.666667 74.666667 0 0 1 74.666667-74.666666z m0 64a10.666667 10.666667 0 0 0-10.666667 10.666666v448a10.666667 10.666667 0 0 0 10.666667 10.666667h448a10.666667 10.666667 0 0 0 10.666666-10.666667V181.333333a10.666667 10.666667 0 0 0-10.666666-10.666666H394.666667z m245.333333 597.333333a32 32 0 0 1 64 0v74.666667a74.666667 74.666667 0 0 1-74.666667 74.666666H181.333333a74.666667 74.666667 0 0 1-74.666666-74.666666V394.666667a74.666667 74.666667 0 0 1 74.666666-74.666667h74.666667a32 32 0 0 1 0 64h-74.666667a10.666667 10.666667 0 0 0-10.666666 10.666667v448a10.666667 10.666667 0 0 0 10.666666 10.666666h448a10.666667 10.666667 0 0 0 10.666667-10.666666v-74.666667z";
+
+ ///
+ /// 图标按钮,外链
+ ///
+ public const string IconButtonlink =
+ "M433.230769 74.830769a43.323077 43.323077 0 0 1 0 86.646154l-236.307692 0.157539a35.446154 35.446154 0 0 0-35.446154 35.446153v630.153847a35.446154 35.446154 0 0 0 35.446154 35.446153h630.153846a35.446154 35.446154 0 0 0 35.446154-35.446153V590.769231a43.323077 43.323077 0 1 1 86.646154 0v236.425846a122.092308 122.092308 0 0 1-122.092308 122.092308H196.923077a122.092308 122.092308 0 0 1-122.092308-122.092308v-630.153846a122.092308 122.092308 0 0 1 122.092308-122.092308z m452.923077 0a63.015385 63.015385 0 0 1 63.015385 63.015385V354.461538a43.323077 43.323077 0 0 1-43.323077 43.323077l-4.726154-0.236307A43.323077 43.323077 0 0 1 862.523077 354.461538l-0.039385-131.702153-287.074461 287.15323-90.072616 90.072616a43.323077 43.323077 0 1 1-61.243077-61.243077l90.033231-90.072616 287.113846-287.192615H669.538462a43.323077 43.323077 0 0 1-43.08677-38.596923L626.215385 118.153846A43.323077 43.323077 0 0 1 669.538462 74.830769z";
+
+ ///
+ /// 图标,音符,1x
+ ///
+ public const string IconMusic =
+ "M348.293565 716.53287V254.797913c0-41.672348 28.004174-78.358261 68.919652-90.37913L815.994435 40.826435c62.775652-18.610087 125.907478 26.579478 125.907478 89.933913v539.158261c8.013913 42.25113-8.94887 89.177043-47.014956 127.109565a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c21.014261 1.024 40.514783 5.164522 57.878261 12.065391V233.338435c0-12.109913-10.551652-20.034783-20.569044-20.034783a24.620522 24.620522 0 0 0-5.787826 0.934957L439.785739 338.18713a19.545043 19.545043 0 0 0-14.825739 19.144348v438.984348H423.846957c11.53113 43.987478-5.164522 94.208-45.412174 134.322087a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c20.791652 1.024 40.069565 5.075478 57.299478 11.842783z";
+
+ ///
+ /// 图标,播放,0.8x
+ ///
+ public const string IconPlay =
+ "M803.904 463.936a55.168 55.168 0 0 1 0 96.128l-463.616 264.448C302.848 845.888 256 819.136 256 776.448V247.616c0-42.752 46.848-69.44 84.288-48.064l463.616 264.384z";
+
+ ///
+ /// 图标,创建,0.9x
+ ///
+ public const string IconButtonCreate =
+ "F1 M 4 2 C 2.35499 2 1 3.35499 1 5 v 13 c 0 1.64501 1.35499 3 3 3 h 16 c 1.64501 0 3 -1.35499 3 -3 V 8 C 23 6.35499 21.645 5 20 5 h -7.90039 a 1.0001 1.0001 0 0 0 -0.0098 0 C 11.7487 5.00334 11.4337 4.83568 11.2461 4.55078 a 1.0001 1.0001 0 0 0 -0.0078 -0.00977 L 10.4355 3.34961 C 9.88132 2.50803 8.93736 2.00017 7.92969 2 Z m 0 2 h 3.92969 c 0.337496 5.56e-05 0.650315 0.167354 0.835938 0.449219 a 1.0001 1.0001 0 0 0 0.00586 0.00977 l 0.802734 1.19141 c 0.000794 0.00121 0.00311 0.0007486 0.00391 0.00195 C 10.1385 6.50064 11.0926 7.00997 12.1094 7 H 20 c 0.564129 0 1 0.435871 1 1 v 10 c 0 0.564129 -0.435871 1 -1 1 H 4 C 3.43587 19 3 18.5641 3 18 V 5 C 3 4.43587 3.43587 4 4 4 Z m 5 8 a 1 1 0 0 0 -1 1 a 1 1 0 0 0 1 1 h 6 a 1 1 0 0 0 1 -1 a 1 1 0 0 0 -1 -1 z m 3 -3 a 1 1 0 0 0 -1 1 v 6 a 1 1 0 0 0 1 1 a 1 1 0 0 0 1 -1 V 10 A 1 1 0 0 0 12 9 Z";
+
+ ///
+ /// 图标,分享,1x
+ ///
+ public const string IconButtonShare =
+ "F1 M 14.9062 5.64648 L 8.08594 9.62695 A 1 1 0 0 0 7.72656 10.9941 A 1 1 0 0 0 9.09375 11.3535 L 15.9141 7.37305 A 1 1 0 0 0 16.2734 6.00586 A 1 1 0 0 0 14.9062 5.64648 Z m -5.8125 7 a 1 1 0 0 0 -1.36719 0.359375 a 1 1 0 0 0 0.359375 1.36719 l 6.83008 3.98047 a 1 1 0 0 0 1.36719 -0.359375 a 1 1 0 0 0 -0.359375 -1.36719 z M 18 15 c -2.19729 0 -4 1.80271 -4 4 c 0 2.19729 1.80271 4 4 4 c 2.19729 0 4 -1.80271 4 -4 c 0 -2.19729 -1.80271 -4 -4 -4 z m 0 2 c 1.11641 0 2 0.883586 2 2 c 0 1.11641 -0.883586 2 -2 2 c -1.11641 0 -2 -0.883586 -2 -2 c 0 -1.11641 0.883586 -2 2 -2 z M 6 8 c -2.19729 0 -4 1.80271 -4 4 c 0 2.19729 1.80271 4 4 4 c 2.19729 0 4 -1.80271 4 -4 C 10 9.80271 8.19729 8 6 8 Z m 0 2 c 1.11641 0 2 0.883586 2 2 c 0 1.11641 -0.883586 2 -2 2 C 4.88359 14 4 13.1164 4 12 C 4 10.8836 4.88359 10 6 10 Z M 18 1 c -2.19729 0 -4 1.80271 -4 4 c 0 2.19729 1.80271 4 4 4 c 2.19729 0 4 -1.80271 4 -4 c 0 -2.19729 -1.80271 -4 -4 -4 z m 0 2 c 1.11641 0 2 0.883586 2 2 c 0 1.11641 -0.883586 2 -2 2 c -1.11641 0 -2 -0.883586 -2 -2 c 0 -1.11641 0.883586 -2 2 -2 z";
+
+ ///
+ /// 图标,添加,1x
+ ///
+ public const string IconButtonAdd =
+ "F1 m 12 7 a 1 1 0 0 0 -1 1 v 8 a 1 1 0 0 0 1 1 a 1 1 0 0 0 1 -1 V 8 A 1 1 0 0 0 12 7 Z m -4 4 a 1 1 0 0 0 -1 1 a 1 1 0 0 0 1 1 h 8 a 1 1 0 0 0 1 -1 a 1 1 0 0 0 -1 -1 z M 12 1 C 5.93671 1 1 5.93671 1 12 C 1 18.0633 5.93671 23 12 23 C 18.0633 23 23 18.0633 23 12 C 23 5.93671 18.0633 1 12 1 Z m 0 2 c 4.98241 0 9 4.01759 9 9 c 0 4.98241 -4.01759 9 -9 9 C 7.01759 21 3 16.9824 3 12 C 3 7.01759 7.01759 3 12 3 Z";
+
+ ///
+ /// 图标,开始游戏,1x
+ ///
+ public const string IconPlayGame =
+ "M213.333333 65.386667a85.333333 85.333333 0 0 1 43.904 12.16L859.370667 438.826667a85.333333 85.333333 0 0 1 0 146.346666L257.237333 946.453333A85.333333 85.333333 0 0 1 128 873.28V150.72a85.333333 85.333333 0 0 1 85.333333-85.333333z m0 64a21.333333 21.333333 0 0 0-21.184 18.837333L192 150.72v722.56a21.333333 21.333333 0 0 0 30.101333 19.456l2.197334-1.152L826.453333 530.282667a21.333333 21.333333 0 0 0 2.048-35.178667l-2.048-1.386667L224.298667 132.416A21.333333 21.333333 0 0 0 213.333333 129.386667z";
+
+ public const string IconButtonEnable =
+ "M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m0 921.6a409.6 409.6 0 1 1 409.6-409.6 409.6 409.6 0 0 1-409.6 409.6z M716.8 339.968l-256 253.44L328.192 460.8A51.2 51.2 0 0 0 256 532.992l168.448 168.96a51.2 51.2 0 0 0 72.704 0l289.28-289.792A51.2 51.2 0 0 0 716.8 339.968z";
+
+ public const string IconButtonDisable =
+ "M508 990.4c-261.6 0-474.4-212-474.4-474.4S246.4 41.6 508 41.6s474.4 212 474.4 474.4S769.6 990.4 508 990.4zM508 136.8c-209.6 0-379.2 169.6-379.2 379.2 0 209.6 169.6 379.2 379.2 379.2s379.2-169.6 379.2-379.2C887.2 306.4 717.6 136.8 508 136.8zM697.6 563.2 318.4 563.2c-26.4 0-47.2-21.6-47.2-47.2 0-26.4 21.6-47.2 47.2-47.2l379.2 0c26.4 0 47.2 21.6 47.2 47.2C744.8 542.4 724 563.2 697.6 563.2z";
+ }
+
+ #endregion
+
+ #region 自定义类
+
+ ///
+ /// 支持小数与常见类型隐式转换的颜色。
+ ///
+ public class MyColor
+ {
+ public double A = 255d;
+ public double B;
+ public double G;
+ public double R;
+
+ // 构造函数
+ public MyColor()
+ {
+ }
+
+ public MyColor(Color col)
+ {
+ A = col.A;
+ R = col.R;
+ G = col.G;
+ B = col.B;
+ }
+
+ public MyColor(string HexString)
+ {
+ var StringColor = (Color)ColorConverter.ConvertFromString(HexString);
+ A = StringColor.A;
+ R = StringColor.R;
+ G = StringColor.G;
+ B = StringColor.B;
+ }
+
+ public MyColor(double newA, MyColor col)
+ {
+ A = newA;
+ R = col.R;
+ G = col.G;
+ B = col.B;
+ }
+
+ public MyColor(double newR, double newG, double newB)
+ {
+ A = 255d;
+ R = newR;
+ G = newG;
+ B = newB;
+ }
+
+ public MyColor(double newA, double newR, double newG, double newB)
+ {
+ A = newA;
+ R = newR;
+ G = newG;
+ B = newB;
+ }
+
+ public MyColor(Brush brush)
+ {
+ var Color = ((SolidColorBrush)brush).Color;
+ A = Color.A;
+ R = Color.R;
+ G = Color.G;
+ B = Color.B;
+ }
+
+ public MyColor(SolidColorBrush brush)
+ {
+ var Color = brush.Color;
+ A = Color.A;
+ R = Color.R;
+ G = Color.G;
+ B = Color.B;
+ }
+
+ public MyColor(object obj)
+ {
+ if (obj is null)
+ {
+ A = 255d;
+ R = 255d;
+ G = 255d;
+ B = 255d;
+ }
+ else if (obj is SolidColorBrush)
+ {
+ // 避免反复获取 Color 对象造成性能下降
+ var Color = ((SolidColorBrush)obj).Color;
+ A = Color.A;
+ R = Color.R;
+ G = Color.G;
+ B = Color.B;
+ }
+ else
+ {
+ A = Conversions.ToDouble(((dynamic)obj).A);
+ R = Conversions.ToDouble(((dynamic)obj).R);
+ G = Conversions.ToDouble(((dynamic)obj).G);
+ B = Conversions.ToDouble(((dynamic)obj).B);
+ }
+ }
+
+ // 类型转换
+ public static implicit operator MyColor(string str)
+ {
+ return new MyColor(str);
+ }
+
+ public static implicit operator MyColor(Color col)
+ {
+ return new MyColor(col);
+ }
+
+ public static implicit operator Color(MyColor conv)
+ {
+ return Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G), MathByte(conv.B));
+ }
+
+ public static implicit operator System.Drawing.Color(MyColor conv)
+ {
+ return System.Drawing.Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G),
+ MathByte(conv.B));
+ }
+
+ public static implicit operator MyColor(SolidColorBrush bru)
+ {
+ return new MyColor(bru.Color);
+ }
+
+ public static implicit operator SolidColorBrush(MyColor conv)
+ {
+ return new SolidColorBrush(Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G),
+ MathByte(conv.B)));
+ }
+
+ public static implicit operator MyColor(Brush bru)
+ {
+ return new MyColor(bru);
+ }
+
+ public static implicit operator Brush(MyColor conv)
+ {
+ return new SolidColorBrush(Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G),
+ MathByte(conv.B)));
+ }
+
+ // 颜色运算
+ public static MyColor operator +(MyColor a, MyColor b)
+ {
+ return new MyColor { A = a.A + b.A, B = a.B + b.B, G = a.G + b.G, R = a.R + b.R };
+ }
+
+ public static MyColor operator -(MyColor a, MyColor b)
+ {
+ return new MyColor { A = a.A - b.A, B = a.B - b.B, G = a.G - b.G, R = a.R - b.R };
+ }
+
+ public static MyColor operator *(MyColor a, double b)
+ {
+ return new MyColor { A = a.A * b, B = a.B * b, G = a.G * b, R = a.R * b };
+ }
+
+ public static MyColor operator /(MyColor a, double b)
+ {
+ return new MyColor { A = a.A / b, B = a.B / b, G = a.G / b, R = a.R / b };
+ }
+
+ public static bool operator ==(MyColor a, MyColor b)
+ {
+ if (a == null && b == null)
+ return true;
+ if (a == null || b == null)
+ return false;
+ return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B;
+ }
+
+ public static bool operator !=(MyColor a, MyColor b)
+ {
+ if (a == null && b == null)
+ return false;
+ if (a == null || b == null)
+ return true;
+ return !(a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B);
+ }
+
+ // HSL
+ public double Hue(double v1, double v2, double vH)
+ {
+ if (vH < 0d)
+ vH += 1d;
+ if (vH > 1d)
+ vH -= 1d;
+ if (vH < 0.16667d)
+ return v1 + (v2 - v1) * 6d * vH;
+ if (vH < 0.5d)
+ return v2;
+ if (vH < 0.66667d)
+ return v1 + (v2 - v1) * (4d - vH * 6d);
+ return v1;
+ }
+
+ public MyColor FromHSL(double sH, double sS, double sL)
+ {
+ if (sS == 0d)
+ {
+ R = sL * 2.55d;
+ G = R;
+ B = R;
+ }
+ else
+ {
+ var H = sH / 360d;
+ var S = sS / 100d;
+ var L = sL / 100d;
+ S = L < 0.5d ? S * L + L : S * (1.0d - L) + L;
+ L = 2d * L - S;
+ R = 255d * Hue(L, S, H + 1d / 3d);
+ G = 255d * Hue(L, S, H);
+ B = 255d * Hue(L, S, H - 1d / 3d);
+ }
+
+ A = 255d;
+ return this;
+ }
+
+ public MyColor FromHSL2(double sH, double sS, double sL)
+ {
+ if (sS == 0d)
+ {
+ R = sL * 2.55d;
+ G = R;
+ B = R;
+ }
+ else
+ {
+ // 初始化
+ sH = (sH + 3600000d) % 360d;
+ var cent = new[]
+ {
+ +0.1d, -0.06d, -0.3d, -0.19d, -0.15d, -0.24d, -0.32d, -0.09d, +0.18d, +0.05d, -0.12d, -0.02d, +0.1d,
+ -0.06d
+ }; // 0, 30, 60
+ // 90, 120, 150
+ // 180, 210, 240
+ // 270, 300, 330
+ // 最后两位与前两位一致,加是变亮,减是变暗
+ // 计算色调对应的亮度片区
+ var center = sH / 30.0d;
+ var intCenter = (int)Math.Round(Math.Floor(center)); // 亮度片区编号
+ center = 50d -
+ ((1d - center + intCenter) * cent[intCenter] + (center - intCenter) * cent[intCenter + 1]) *
+ sS;
+ // center = 50 + (cent(intCenter) + (center - intCenter) * (cent(intCenter + 1) - cent(intCenter))) * sS
+ sL = (sL < center ? sL / center : 1d + (sL - center) / (100d - center)) * 50d;
+ FromHSL(sH, sS, sL);
+ }
+
+ A = 255d;
+ return this;
+ }
+
+ public MyColor Alpha(double sA)
+ {
+ A = sA;
+ return this;
+ }
+
+ public override string ToString()
+ {
+ return "(" + A + "," + R + "," + G + "," + B + ")";
+ }
+
+ public override bool Equals(object obj)
+ {
+ return Operators.ConditionalCompareObjectEqual(this, obj, false);
+ }
+ }
+
+ ///
+ /// 支持负数与浮点数的矩形。
+ ///
+ public class MyRect
+ {
+ // 构造函数
+ public MyRect()
+ {
+ }
+
+ public MyRect(double left, double top, double width, double height)
+ {
+ Left = left;
+ Top = top;
+ Width = width;
+ Height = height;
+ }
+
+ // 属性
+ public double Width { get; set; }
+ public double Height { get; set; }
+ public double Left { get; set; }
+ public double Top { get; set; }
+ }
+
+ ///
+ /// 模块加载状态枚举。
+ ///
+ public enum LoadState
+ {
+ Waiting,
+ Loading,
+ Finished,
+ Failed,
+ Aborted
+ }
+
+ ///
+ /// 执行返回值。
+ ///
+ public enum ProcessReturnValues
+ {
+ ///
+ /// 执行成功,或进程被中断。
+ ///
+ Aborted = -1,
+
+ ///
+ /// 执行成功。
+ ///
+ Success = 0,
+
+ ///
+ /// 执行失败。
+ ///
+ Fail = 1,
+
+ ///
+ /// 执行时出现未经处理的异常。
+ ///
+ Exception = 2,
+
+ ///
+ /// 执行超时。
+ ///
+ Timeout = 3,
+
+ ///
+ /// 取消执行。可能是由于不满足执行的前置条件。
+ ///
+ Cancel = 4,
+
+ ///
+ /// 任务成功完成。
+ ///
+ TaskDone = 5
+ }
+
+ ///
+ /// 可以使用 Equals 和等号的 List。
+ ///
+ public class EqualableList : List
+ {
+ public override bool Equals(object obj)
+ {
+ if (obj as List is null)
+ // 类型不同
+ return false;
+
+ // 类型相同
+ var objList = (List)obj;
+ if (objList.Count != Count)
+ return false;
+ for (int i = 0, loopTo = objList.Count - 1; i <= loopTo; i++)
+ if (!objList[i].Equals(this[i]))
+ return false;
+ return true;
+ }
+
+ public static bool operator ==(EqualableList left, EqualableList right)
+ {
+ return EqualityComparer>.Default.Equals(left, right);
+ }
+
+ public static bool operator !=(EqualableList left, EqualableList right)
+ {
+ return !(left == right);
+ }
+ }
+
+ #endregion
+
+ #region 数学
+
+ ///
+ /// 2~65 进制的转换。
+ ///
+ public static string RadixConvert(string Input, int FromRadix, int ToRadix)
+ {
+ const string Digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/+=";
+ // 零与负数的处理
+ if (string.IsNullOrEmpty(Input))
+ return "0";
+ var IsNegative = Input.StartsWithF("-");
+ if (IsNegative)
+ Input = Input.TrimStart('-');
+ // 转换为十进制
+ var RealNum = 0L;
+ var Scale = 1L;
+ foreach (var Digit in Input.Reverse().Select(l => Digits.IndexOfF(Conversions.ToString(l))))
+ {
+ RealNum += Digit * Scale;
+ Scale *= FromRadix;
+ }
+
+ // 转换为指定进制
+ var Result = "";
+ while (RealNum > 0L)
+ {
+ var NewNum = (int)(RealNum % ToRadix);
+ RealNum = (long)Math.Round((RealNum - NewNum) / (double)ToRadix);
+ Result = Digits[NewNum] + Result;
+ }
+
+ // 负数的结束处理与返回
+ return (IsNegative ? "-" : "") + Result;
+ }
+
+ ///
+ /// 计算二阶贝塞尔曲线。
+ ///
+ public static double MathBezier(double x, double x1, double y1, double x2, double y2, double acc = 0.01d)
+ {
+ if (x <= 0d || double.IsNaN(x)) return 0d;
+ if (x >= 1d) return 1d;
+ double a, b;
+ a = x;
+ do
+ {
+ b = 3 * a * ((0.33333333 + x1 - x2) * a * a + (x2 - 2 * x1) * a + x1);
+ a += (x - b) * 0.5;
+ } while (!(Math.Abs(b - x) < acc)); // 精度
+
+ return 3 * a * ((0.33333333 + y1 - y2) * a * a + (y2 - 2 * y1) * a + y1);
+ }
+
+ ///
+ /// 将一个数字限制为 0~255 的 Byte 值。
+ ///
+ public static byte MathByte(double d)
+ {
+ if (d < 0d)
+ d = 0d;
+ if (d > 255d)
+ d = 255d;
+ return (byte)Math.Round(Math.Round(d));
+ }
+
+ ///
+ /// 提供 MyColor 类型支持的 Math.Round。
+ ///
+ public static MyColor MathRound(MyColor col, int w = 0)
+ {
+ return new MyColor
+ { A = Math.Round(col.A, w), R = Math.Round(col.R, w), G = Math.Round(col.G, w), B = Math.Round(col.B, w) };
+ }
+
+ ///
+ /// 获取两数间的百分比。小数点精确到 6 位。
+ ///
+ ///
+ public static double MathPercent(double ValueA, double ValueB, double Percent)
+ {
+ return Math.Round(ValueA * (1d - Percent) + ValueB * Percent, 6); // 解决 Double 计算错误
+ }
+
+ ///
+ /// 获取两颜色间的百分比,根据 RGB 计算。小数点精确到 6 位。
+ ///
+ public static MyColor MathPercent(MyColor ValueA, MyColor ValueB, double Percent)
+ {
+ return MathRound(ValueA * (1d - Percent) + ValueB * Percent, 6); // 解决Double计算错误
+ }
+
+ ///
+ /// 将数值限定在某个范围内。
+ ///
+ public static double MathClamp(double value, double min, double max)
+ {
+ return Math.Max(min, Math.Min(max, value));
+ }
+
+ ///
+ /// 符号函数。
+ ///
+ public static int MathSgn(double Value)
+ {
+ if (Value == 0d) return 0;
+
+ if (Value > 0d) return 1;
+
+ return -1;
+ }
+
+ #endregion
+
+ #region 文件
+
+ // =============================
+ // 注册表
+ // =============================
+
+ ///
+ /// 重命名一个注册表子键。不可用于包含子键的子键。
+ ///
+ public static void RenameReg(RegistryKey parentKey, string subKeyName, string newSubKeyName)
+ {
+ if (parentKey.GetSubKeyNames().Contains(newSubKeyName))
+ parentKey.DeleteSubKeyTree(newSubKeyName, false);
+ var SourceKey = parentKey.OpenSubKey(subKeyName);
+ if (SourceKey == null)
+ return; // 没有目标项
+ var NewKey = parentKey.CreateSubKey(newSubKeyName);
+ if (SourceKey.GetSubKeyNames().Length > 0)
+ throw new NotSupportedException("不支持对包含子键的子键进行重命名:" + SourceKey.GetSubKeyNames()[0] + "。");
+ foreach (var valueName in SourceKey.GetValueNames())
+ {
+ var objValue = SourceKey.GetValue(valueName);
+ var valKind = SourceKey.GetValueKind(valueName);
+ NewKey.SetValue(valueName, objValue, valKind);
+ }
+
+ parentKey.DeleteSubKeyTree(subKeyName, false);
+ }
+
+ ///
+ /// 读取注册表,默认为程序所属。
+ ///
+ public static string ReadReg(string Key, string DefaultValue = "", string Path = "")
+ {
+ string ReadRegRet = default;
+ try
+ {
+ RegistryKey parentKey;
+ RegistryKey softKey;
+ parentKey = Registry.CurrentUser;
+ softKey = parentKey.OpenSubKey(@"Software\" + (string.IsNullOrEmpty(Path) ? ModSecret.RegFolder : Path),
+ true);
+ if (softKey is null)
+ {
+ ReadRegRet = DefaultValue; // 不存在则返回默认值
+ }
+ else
+ {
+ var readValue = new StringBuilder();
+ readValue.AppendLine(softKey.GetValue(Key).ToString());
+ var value = readValue.ToString().Replace("\r\n", ""); // 去除莫名的回车
+ return string.IsNullOrEmpty(value) ? DefaultValue : value;
+ } // 错误则返回默认值
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "读取注册表出错:" + Key, LogLevel.Hint);
+ return DefaultValue;
+ }
+
+ return ReadRegRet;
+ }
+
+ ///
+ /// 写入注册表,默认为程序所属。
+ ///
+ public static void WriteReg(string Key, string Value, bool ShowException = false, string Path = "",
+ bool ThrowException = false)
+ {
+ try
+ {
+ RegistryKey parentKey;
+ RegistryKey softKey;
+ parentKey = Registry.CurrentUser;
+ softKey = parentKey.OpenSubKey(@"Software\" + (string.IsNullOrEmpty(Path) ? ModSecret.RegFolder : Path),
+ true);
+ if (softKey is null)
+ softKey = parentKey.CreateSubKey(@"Software\" +
+ (string.IsNullOrEmpty(Path)
+ ? ModSecret.RegFolder
+ : Path)); // 如果不存在就创建
+ softKey.SetValue(Key, Value);
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "写入注册表出错:" + Key, ThrowException ? LogLevel.Hint : LogLevel.Developer);
+ if (ThrowException)
+ throw;
+ }
+ }
+
+ ///
+ /// 是否存在某个注册表键。
+ ///
+ public static bool HasReg(string Key)
+ {
+ return ReadReg(Key, null) is not null;
+ }
+
+ ///
+ /// 删除注册表键。
+ ///
+ public static void DeleteReg(string Key, bool ThrowException = false)
+ {
+ try
+ {
+ var SubKey = Registry.CurrentUser.OpenSubKey(@"Software\" + ModSecret.RegFolder, true);
+ SubKey?.DeleteValue(Key);
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "删除注册表出错:" + Key, ThrowException ? LogLevel.Hint : LogLevel.Developer);
+ if (ThrowException)
+ throw;
+ }
+ }
+
+ // =============================
+ // ini
+ // =============================
+
+ private static readonly ConcurrentDictionary> IniCache = new();
+
+ ///
+ /// 清除某 ini 文件的运行时缓存。
+ ///
+ /// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。
+ public static void IniClearCache(string FileName)
+ {
+ if (!FileName.Contains(@":\"))
+ FileName = $@"{ExePath}PCL\{FileName}.ini";
+ if (IniCache.ContainsKey(FileName))
+ IniCache.Remove(FileName, out _);
+ }
+
+ ///
+ /// 获取 ini 文件缓存。如果没有,则新读取 ini 文件内容。
+ /// 在文件不存在或读取失败时返回 Nothing。
+ ///
+ /// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。
+ private static ConcurrentDictionary IniGetContent(string FileName)
+ {
+ try
+ {
+ // 还原文件路径
+ if (!FileName.Contains(@":\"))
+ FileName = $@"{ExePath}PCL\{FileName}.ini";
+ // 检索缓存
+ if (IniCache.ContainsKey(FileName))
+ return IniCache[FileName];
+ // 读取文件
+ if (!File.Exists(FileName))
+ return null;
+ var Ini = new ConcurrentDictionary();
+ foreach (var Line in ReadFile(FileName)
+ .Split("\r\n".ToArray(), StringSplitOptions.RemoveEmptyEntries))
+ {
+ var Index = Line.IndexOfF(":");
+ if (Index > 0)
+ Ini[Line.Substring(0, Index)] = Line.Substring(Index + 1); // 可能会有重复键,见 #3616
+ }
+
+ IniCache[FileName] = Ini;
+ return Ini;
+ }
+ catch (Exception ex)
+ {
+ Log(ex, $"生成 ini 文件缓存失败({FileName})", LogLevel.Hint);
+ return null;
+ }
+ }
+
+ ///
+ /// 读取 ini 文件。这可能会使用到缓存。
+ ///
+ /// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。
+ /// 键。
+ /// 没有找到键时返回的默认值。
+ public static string ReadIni(string FileName, string Key, string DefaultValue = "")
+ {
+ var Content = IniGetContent(FileName);
+ if (Content is null || !Content.ContainsKey(Key))
+ return DefaultValue;
+ return Content[Key];
+ }
+
+ ///
+ /// 判断 ini 文件中是否包含某个键。这可能会使用到缓存。
+ ///
+ public static bool HasIniKey(string FileName, string Key)
+ {
+ var Content = IniGetContent(FileName);
+ return Content is not null && Content.ContainsKey(Key);
+ }
+
+ ///
+ /// 从 ini 文件中移除某个键。这会更新缓存。
+ ///
+ public static void DeleteIniKey(string FileName, string Key)
+ {
+ WriteIni(FileName, Key, null);
+ }
+
+ ///
+ /// 写入 ini 文件,这会更新缓存。
+ /// 若 Value 为 Nothing,则删除该键。
+ ///
+ /// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。
+ /// 键。
+ /// 值。
+ ///
+ public static void WriteIni(string FileName, string Key, string Value)
+ {
+ try
+ {
+ // 预处理
+ if (Key.Contains(":"))
+ throw new Exception($"尝试写入 ini 文件 {FileName} 的键名中包含了冒号:{Key}");
+ Key = Key.Replace("\r", "").Replace("\n", "");
+ Value = Value?.Replace("\r", "").Replace("\n", "");
+ // 防止争用
+ lock (WriteIniLock)
+ {
+ // 获取目前文件
+ var Content = IniGetContent(FileName);
+ if (Content is null)
+ Content = new ConcurrentDictionary();
+ // 更新值
+ if (Value is null)
+ {
+ if (!Content.ContainsKey(Key))
+ return; // 无需处理
+ Content.Remove(Key, out _);
+ }
+ else
+ {
+ if (Content.ContainsKey(Key) && (Content[Key] ?? "") == (Value ?? ""))
+ return; // 无需处理
+ Content[Key] = Value;
+ }
+
+ // 写入文件
+ var FileContent = new StringBuilder();
+ foreach (var Pair in Content)
+ {
+ FileContent.Append(Pair.Key);
+ FileContent.Append(":");
+ FileContent.Append(Pair.Value);
+ FileContent.Append("\r\n");
+ }
+
+ if (!FileName.Contains(@":\"))
+ FileName = $@"{ExePath}PCL\{FileName}.ini";
+ WriteFile(FileName, FileContent.ToString());
+ }
+ }
+ catch (Exception ex)
+ {
+ Log(ex, $"写入文件失败({FileName} → {Key}:{Value})", LogLevel.Hint);
+ }
+ }
+
+ private static readonly object WriteIniLock = new();
+
+ // 路径处理
+ ///
+ /// 从文件路径或者 Url 获取不包含文件名的路径,或获取文件夹的父文件夹路径。
+ /// 取决于原路径格式,路径以 / 或 \ 结尾。
+ /// 不包含路径将会抛出异常。
+ ///
+ public static string GetPathFromFullPath(string FilePath)
+ {
+ string GetPathFromFullPathRet = default;
+ if (!(FilePath.Contains(@"\") || FilePath.Contains("/")))
+ throw new Exception("不包含路径:" + FilePath);
+ if (FilePath.EndsWithF(@"\") || FilePath.EndsWithF("/"))
+ {
+ // 是文件夹路径
+ var IsRight = FilePath.EndsWithF(@"\");
+ FilePath = Strings.Left(FilePath, Strings.Len(FilePath) - 1);
+ GetPathFromFullPathRet = Strings.Left(FilePath, FilePath.LastIndexOfAny(new[] { '\\', '/' })) +
+ (IsRight ? @"\" : "/");
+ }
+ else
+ {
+ // 是文件路径
+ GetPathFromFullPathRet = Strings.Left(FilePath, FilePath.LastIndexOfAny(new[] { '\\', '/' }) + 1);
+ if (string.IsNullOrEmpty(GetPathFromFullPathRet))
+ throw new Exception("不包含路径:" + FilePath);
+ }
+
+ return GetPathFromFullPathRet;
+ }
+
+ ///
+ /// 从文件路径或者 Url 获取不包含路径的文件名。不包含文件名将会抛出异常。
+ ///
+ public static string GetFileNameFromPath(string FilePath)
+ {
+ FilePath = FilePath.Replace("/", @"\");
+ if (FilePath.EndsWithF(@"\"))
+ throw new Exception("不包含文件名:" + FilePath);
+ if (FilePath.Contains("?"))
+ FilePath = FilePath.Substring(0, FilePath.IndexOfF("?")); // 去掉网络参数后的 ?
+ if (FilePath.Contains(@"\"))
+ FilePath = FilePath.Substring(FilePath.LastIndexOfF(@"\") + 1);
+ var length = FilePath.Length;
+ if (length == 0)
+ throw new Exception("不包含文件名:" + FilePath);
+ if (length > 250)
+ throw new PathTooLongException("文件名过长:" + FilePath);
+ return FilePath;
+ }
+
+ ///
+ /// 从文件路径或者 Url 获取不包含路径与扩展名的文件名。不包含文件名将会抛出异常。
+ ///
+ public static string GetFileNameWithoutExtentionFromPath(string FilePath)
+ {
+ return Path.GetFileNameWithoutExtension(FilePath);
+ }
+
+ ///
+ /// 从文件夹路径获取文件夹名。
+ ///
+ public static string GetFolderNameFromPath(string FolderPath)
+ {
+ if (FolderPath.EndsWithF(@":\") || FolderPath.EndsWithF(@":\\"))
+ return FolderPath.Substring(0, 1);
+ if (FolderPath.EndsWithF(@"\") || FolderPath.EndsWithF("/"))
+ FolderPath = Strings.Left(FolderPath, FolderPath.Length - 1);
+ return GetFileNameFromPath(FolderPath);
+ }
+
+ // 读取、写入、复制文件
+ ///
+ /// 复制文件。会自动创建文件夹、会覆盖已有的文件。
+ ///
+ public static void CopyFile(string FromPath, string ToPath)
+ {
+ try
+ {
+ // 还原文件路径
+ if (!FromPath.Contains(@":\"))
+ FromPath = ExePath + FromPath;
+ if (!ToPath.Contains(@":\"))
+ ToPath = ExePath + ToPath;
+ // 如果复制同一个文件则跳过
+ if ((FromPath ?? "") == (ToPath ?? ""))
+ return;
+ // 确保目录存在
+ Directory.CreateDirectory(GetPathFromFullPath(ToPath));
+ // 复制文件
+ File.Copy(FromPath, ToPath, true);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception("复制文件出错:" + FromPath + " → " + ToPath, ex);
+ }
+ }
+
+ ///
+ /// 读取文件,如果失败则返回空数组。
+ ///
+ public static byte[] ReadFileBytes(string FilePath, Encoding Encoding = null)
+ {
+ try
+ {
+ // 还原文件路径
+ if (!FilePath.Contains(@":\"))
+ FilePath = ExePath + FilePath;
+ if (File.Exists(FilePath))
+ using (var ReadStream =
+ new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) // 支持读取使用中的文件
+ {
+ using (var ms = new MemoryStream())
+ {
+ ReadStream.CopyTo(ms);
+ return ms.ToArray();
+ }
+ }
+
+ Log("[System] 欲读取的文件不存在,已返回空内容:" + FilePath);
+ return Array.Empty();
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "读取文件出错:" + FilePath);
+ return Array.Empty();
+ }
+ }
+
+ ///
+ /// 读取文件,如果失败则返回空字符串。
+ ///
+ /// 文件完整或相对路径。
+ public static string ReadFile(string FilePath, Encoding Encoding = null)
+ {
+ string ReadFileRet = default;
+ var FileBytes = ReadFileBytes(FilePath);
+ ReadFileRet = Encoding is null ? DecodeBytes(FileBytes) : Encoding.GetString(FileBytes);
+ return ReadFileRet;
+ }
+
+ ///
+ /// 读取流中的所有文本。
+ ///
+ public static string ReadFile(Stream Stream, Encoding Encoding = null)
+ {
+ try
+ {
+ var readedContent = new MemoryStream();
+ Stream.CopyTo(readedContent);
+ var Bts = readedContent.ToArray();
+ return (Encoding ?? EncodingDetector.DetectEncoding(Bts)).GetString(Bts);
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "读取流出错");
+ return "";
+ }
+ }
+
+ ///
+ /// 写入文件。
+ ///
+ /// 文件完整或相对路径。
+ /// 文件内容。
+ /// 是否将文件内容追加到当前文件,而不是覆盖它。
+ public static void WriteFile(string FilePath, string Text, bool Append = false, Encoding? Encoding = null)
+ {
+ // 处理相对路径
+ if (!FilePath.Contains(@":\"))
+ FilePath = ExePath + FilePath;
+ // 确保目录存在
+ Directory.CreateDirectory(GetPathFromFullPath(FilePath));
+ // 写入文件
+ if (Append)
+ // 追加目前文件
+ using (var writer = new StreamWriter(FilePath, true,
+ Encoding ?? EncodingDetector.DetectEncoding(ReadFileBytes(FilePath))))
+ {
+ writer.Write(Text);
+ }
+ else
+ // 直接写入字节
+ File.WriteAllBytes(FilePath,
+ Encoding is null ? new UTF8Encoding(false).GetBytes(Text) : Encoding.GetBytes(Text));
+ }
+
+ ///
+ /// 写入文件。
+ /// 如果 CanThrow 设置为 False,返回是否写入成功。
+ ///
+ /// 文件完整或相对路径。
+ /// 文件内容。
+ /// 是否将文件内容追加到当前文件,而不是覆盖它。
+ public static void WriteFile(string FilePath, byte[] Content, bool Append = false)
+ {
+ // 处理相对路径
+ if (!FilePath.Contains(@":\"))
+ FilePath = ExePath + FilePath;
+ // 确保目录存在
+ Directory.CreateDirectory(GetPathFromFullPath(FilePath));
+ // 写入文件
+ File.WriteAllBytes(FilePath, Content);
+ }
+
+ ///
+ /// 将流写入文件。
+ ///
+ /// 文件完整或相对路径。
+ public static bool WriteFile(string FilePath, Stream Stream)
+ {
+ try
+ {
+ // 还原文件路径
+ if (!FilePath.Contains(@":\"))
+ FilePath = ExePath + FilePath;
+ // 确保目录存在
+ Directory.CreateDirectory(GetPathFromFullPath(FilePath));
+ // 读取流
+ using (var fs = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ fs.SetLength(0L);
+ Stream.CopyTo(fs);
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "保存流出错");
+ return false;
+ }
+ }
+
+ ///
+ /// 解码 Bytes。
+ ///
+ public static string DecodeBytes(byte[] Bytes)
+ {
+ var Length = Bytes.Length;
+ if (Length < 3)
+ return Encoding.UTF8.GetString(Bytes);
+ // 根据 BOM 判断编码
+ if (Bytes[0] >= 0xEF)
+ {
+ // 有 BOM 类型
+ if (Bytes[0] == 0xEF && Bytes[1] == 0xBB) return Encoding.UTF8.GetString(Bytes, 3, Length - 3);
+
+ if (Bytes[0] == 0xFE && Bytes[1] == 0xFF) return Encoding.BigEndianUnicode.GetString(Bytes, 3, Length - 3);
+
+ if (Bytes[0] == 0xFF && Bytes[1] == 0xFE) return Encoding.Unicode.GetString(Bytes, 3, Length - 3);
+
+ return Encoding.GetEncoding("GB18030").GetString(Bytes, 3, Length - 3);
+ }
+
+ // 无 BOM 文件:GB18030(ANSI)或 UTF8
+ var UTF8 = Encoding.UTF8.GetString(Bytes);
+ var ErrorChar = Encoding.UTF8.GetString(new[] { (byte)239, (byte)191, (byte)189 }).ToCharArray()[0];
+ if (UTF8.Contains(ErrorChar)) return Encoding.GetEncoding("GB18030").GetString(Bytes);
+
+ return UTF8;
+ }
+
+ public static object GetHexString(Memory bytes)
+ {
+ var sb = new StringBuilder(bytes.Length * 2);
+ foreach (var c in bytes.Span)
+ sb.Append(c.ToString("x2"));
+
+ return sb.ToString();
+ }
+
+ // 文件校验
+ ///
+ /// 获取文件 MD5,若失败则返回空字符串。
+ ///
+ public static string GetFileMD5(string FilePath)
+ {
+ var Retry = false;
+ Re: ;
+
+ try
+ {
+ // 获取 MD5
+ using (var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ return Conversions.ToString(GetHexString(MD5Provider.Instance.ComputeHash(fs)));
+ }
+ }
+ catch (Exception ex)
+ {
+ if (Retry || ex is FileNotFoundException)
+ {
+ Log(ex, "获取文件 MD5 失败:" + FilePath);
+ return "";
+ }
+
+ Retry = true;
+ Log(ex, "获取文件 MD5 可重试失败:" + FilePath, LogLevel.Normal);
+ Thread.Sleep(RandomUtils.NextInt(200, 500));
+ goto Re;
+ }
+ }
+
+ ///
+ /// 获取文件 SHA512,若失败则返回空字符串。
+ ///
+ public static string GetFileSHA512(string FilePath)
+ {
+ var Retry = false;
+ Re: ;
+
+ try
+ {
+ // '检测该文件是否在下载中,若在下载则放弃检测
+ // If IgnoreOnDownloading AndAlso NetManage.Files.ContainsKey(FilePath) AndAlso NetManage.Files(FilePath).State <= NetState.Merge Then Return ""
+ // 获取 SHA512
+ using (var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ return Conversions.ToString(GetHexString(SHA512Provider.Instance.ComputeHash(fs)));
+ }
+ }
+ catch (Exception ex)
+ {
+ if (Retry || ex is FileNotFoundException)
+ {
+ Log(ex, "获取文件 SHA512 失败:" + FilePath);
+ return "";
+ }
+
+ Retry = true;
+ Log(ex, "获取文件 SHA512 可重试失败:" + FilePath, LogLevel.Normal);
+ Thread.Sleep(RandomUtils.NextInt(200, 500));
+ goto Re;
+ }
+ }
+
+ ///
+ /// 获取文件 SHA256,若失败则返回空字符串。
+ ///
+ public static string GetFileSHA256(string FilePath)
+ {
+ var Retry = false;
+ Re: ;
+
+ try
+ {
+ // '检测该文件是否在下载中,若在下载则放弃检测
+ // If IgnoreOnDownloading AndAlso NetManage.Files.ContainsKey(FilePath) AndAlso NetManage.Files(FilePath).State <= NetState.Merge Then Return ""
+ // 获取 SHA256
+ using (var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ return Conversions.ToString(GetHexString(SHA256Provider.Instance.ComputeHash(fs)));
+ }
+ }
+ catch (Exception ex)
+ {
+ if (Retry || ex is FileNotFoundException)
+ {
+ Log(ex, "获取文件 SHA256 失败:" + FilePath);
+ return "";
+ }
+
+ Retry = true;
+ Log(ex, "获取文件 SHA256 可重试失败:" + FilePath, LogLevel.Normal);
+ Thread.Sleep(RandomUtils.NextInt(200, 500));
+ goto Re;
+ }
+ }
+
+ ///
+ /// 获取文件 SHA1,若失败则返回空字符串。
+ ///
+ public static string GetFileSHA1(string FilePath)
+ {
+ var Retry = false;
+ Re: ;
+
+ try
+ {
+ // 获取 SHA1
+ using (var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ return Conversions.ToString(GetHexString(SHA1Provider.Instance.ComputeHash(fs)));
+ }
+ }
+ catch (Exception ex)
+ {
+ if (Retry || ex is FileNotFoundException)
+ {
+ Log(ex, "获取文件 SHA1 失败:" + FilePath);
+ return "";
+ }
+
+ Retry = true;
+ Log(ex, "获取文件 SHA1 可重试失败:" + FilePath, LogLevel.Normal);
+ Thread.Sleep(RandomUtils.NextInt(200, 500));
+ goto Re;
+ }
+ }
+
+ ///
+ /// 获取流的 SHA1,若失败则返回空字符串。
+ ///
+ public static string GetAuthSHA1(Stream inputStream)
+ {
+ try
+ {
+ return Conversions.ToString(GetHexString(SHA1Provider.Instance.ComputeHash(inputStream)));
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "获取流 SHA1 失败");
+ return "";
+ }
+ }
+
+ ///
+ /// 文件的校验规则。
+ ///
+ public class FileChecker
+ {
+ ///
+ /// 文件的准确大小。
+ /// 不检查则为 -1。
+ ///
+ public long ActualSize = -1;
+
+ ///
+ /// 是否可以使用已经存在的文件。
+ ///
+ public bool CanUseExistsFile = true;
+
+ ///
+ /// 文件的 MD5、SHA1 或 SHA256。会根据输入字符串的长度自动判断种类。
+ /// 不检查则为 Nothing。
+ ///
+ public string Hash;
+
+ ///
+ /// 是否要求为 JSON 文件。
+ /// 即,开头结尾必须为 {} 或 []。
+ ///
+ public bool IsJson;
+
+ ///
+ /// 文件的最小大小。
+ /// 不检查则为 -1。
+ ///
+ public long MinSize = -1;
+
+ public FileChecker(long MinSize = -1, long ActualSize = -1, string Hash = null, bool CanUseExistsFile = true,
+ bool IsJson = false)
+ {
+ this.ActualSize = ActualSize;
+ this.MinSize = MinSize;
+ this.Hash = Hash;
+ this.CanUseExistsFile = CanUseExistsFile;
+ this.IsJson = IsJson;
+ }
+
+ ///
+ /// 检查文件。若成功则返回 Nothing,失败则返回错误的描述文本,描述文本不以句号结尾。不会抛出错误。
+ ///
+ public string Check(string LocalPath)
+ {
+ try
+ {
+ Log($"[Checker] 开始校验文件 {LocalPath}", LogLevel.Developer);
+ var Info = new FileInfo(LocalPath);
+ if (!Info.Exists)
+ return "文件不存在:" + LocalPath;
+ var FileSize = Info.Length;
+ var ErrorMessage = new List();
+ var AllowIgnore = false; // 允许相信哈希正确但是大小不正确
+ if (!string.IsNullOrEmpty(Hash))
+ {
+ if (Hash.Length < 35) // MD5
+ {
+ var ComputedHash = GetFileMD5(LocalPath);
+ if ((Hash.ToLowerInvariant() ?? "") != (ComputedHash ?? ""))
+ ErrorMessage.Add("文件 MD5 应为 " + Hash + ",实际为 " + ComputedHash);
+ }
+ else if (Hash.Length == 64) // SHA256
+ {
+ var ComputedHash = GetFileSHA256(LocalPath);
+ if ((Hash.ToLowerInvariant() ?? "") != (ComputedHash ?? ""))
+ ErrorMessage.Add("文件 SHA256 应为 " + Hash + ",实际为 " + ComputedHash);
+ }
+ else // SHA1 (40)
+ {
+ var ComputedHash = GetFileSHA1(LocalPath);
+ if ((Hash.ToLowerInvariant() ?? "") != (ComputedHash ?? ""))
+ ErrorMessage.Add("文件 SHA1 应为 " + Hash + ",实际为 " + ComputedHash);
+ }
+
+ AllowIgnore = ErrorMessage.Count == 0;
+ }
+
+ if (ActualSize >= 0L && ActualSize != FileSize && !AllowIgnore) // 不允许忽略大小不正确的情况
+ ErrorMessage.Add($"文件大小应为 {ActualSize} B,实际为 {FileSize} B" +
+ (FileSize < 2000L ? ",内容为" + ReadFile(LocalPath) : ""));
+
+ if (MinSize >= 0L && MinSize > FileSize)
+ ErrorMessage.Add($"文件大小应大于 {MinSize} B,实际为 {FileSize} B" +
+ (FileSize < 2000L ? ",内容为:" + ReadFile(LocalPath) : ""));
+
+ if (IsJson)
+ {
+ var Content = ReadFile(LocalPath);
+ if (string.IsNullOrEmpty(Content))
+ throw new Exception("读取到的文件为空");
+ try
+ {
+ GetJson(Content);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception("不是有效的 Json 文件", ex);
+ }
+ }
+
+ if (ErrorMessage.Count != 0)
+ {
+ ErrorMessage.Insert(0, $"实际校验地址:{LocalPath}");
+ return ErrorMessage.Join(";");
+ }
+
+ return null;
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "检查文件出错");
+ return ex.ToString();
+ }
+ }
+ }
+
+ ///
+ /// 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 Jar 以 zip 方式解压。
+ /// 会尝试创建,但不会清空目标文件夹。
+ ///
+ public static void ExtractFile(string CompressFilePath, string DestDirectory, Encoding Encode = null,
+ Action ProgressIncrementHandler = null)
+ {
+ Directory.CreateDirectory(DestDirectory);
+ DestDirectory = Path.GetFullPath(DestDirectory);
+ if (!DestDirectory.EndsWith(Path.DirectorySeparatorChar.ToString()))
+ DestDirectory += Conversions.ToString(Path.DirectorySeparatorChar);
+ if (CompressFilePath.EndsWithF(".gz", true))
+ // 以 gz 方式解压
+ using (var compressedFile = new FileStream(CompressFilePath, FileMode.Open, FileAccess.Read))
+ {
+ using (var decompressStream = new GZipStream(compressedFile, CompressionMode.Decompress))
+ {
+ using (var extractFileStream =
+ new FileStream(
+ Path.Combine(DestDirectory,
+ GetFileNameFromPath(CompressFilePath).ToLower().Replace(".tar", "")
+ .Replace(".gz", "")), FileMode.OpenOrCreate, FileAccess.Write))
+ {
+ decompressStream.CopyTo(extractFileStream);
+ }
+ }
+ }
+ else
+ // 以 zip 方式解压
+ using (var Archive = ZipFile.Open(CompressFilePath, ZipArchiveMode.Read,
+ Encode ?? Encoding.GetEncoding("GB18030")))
+ {
+ var TotalCount = Archive.Entries.Count;
+ foreach (var Entry in Archive.Entries)
+ {
+ if (ProgressIncrementHandler is not null)
+ ProgressIncrementHandler(1d / TotalCount);
+ var DestinationPath = Path.GetFullPath(Path.Combine(DestDirectory, Entry.FullName));
+ if (!DestinationPath.StartsWithF(DestDirectory))
+ throw new Exception(
+ $"解压文件 {Entry.FullName} 错误:解压文件路径 {DestinationPath} 不在目标目录 {DestDirectory} 内");
+ if (DestinationPath.EndsWithF(@"\") || DestinationPath.EndsWithF("/"))
+ {
+ }
+ else
+ {
+ Directory.CreateDirectory(GetPathFromFullPath(DestinationPath));
+ Entry.ExtractToFile(DestinationPath, true);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 删除文件夹,返回删除的文件个数。通过参数选择是否抛出异常。
+ ///
+ public static int DeleteDirectory(string Path, bool IgnoreIssue = false)
+ {
+ if (!Directory.Exists(Path))
+ return 0;
+ var DeletedCount = 0;
+ string[] Files;
+ try
+ {
+ Files = Directory.GetFiles(Path);
+ }
+ catch (DirectoryNotFoundException ex) // #4549
+ {
+ Log(ex, $"疑似为孤立符号链接,尝试直接删除({Path})", LogLevel.Developer);
+ Directory.Delete(Path);
+ return 0;
+ }
+
+ foreach (var FilePath in Files)
+ {
+ var RetriedFile = false;
+ RetryFile: ;
+
+ try
+ {
+ File.Delete(FilePath);
+ DeletedCount += 1;
+ }
+ catch (Exception ex)
+ {
+ if (!RetriedFile)
+ {
+ RetriedFile = true;
+ Log(ex, $"删除文件失败,将在 0.3s 后重试({FilePath})");
+ Thread.Sleep(300);
+ goto RetryFile;
+ }
+
+ if (IgnoreIssue)
+ Log(ex, "删除单个文件可忽略地失败");
+ else
+ throw;
+ }
+ }
+
+ foreach (var str in Directory.GetDirectories(Path))
+ DeleteDirectory(str, IgnoreIssue);
+ var RetriedDir = false;
+ RetryDir: ;
+
+ try
+ {
+ Directory.Delete(Path, true);
+ }
+ catch (Exception ex)
+ {
+ if (!RetriedDir && !RunInUi())
+ {
+ RetriedDir = true;
+ Log(ex, $"删除文件夹失败,将在 0.3s 后重试({Path})");
+ Thread.Sleep(300);
+ goto RetryDir;
+ }
+
+ if (IgnoreIssue)
+ Log(ex, "删除单个文件夹可忽略地失败");
+ else
+ throw;
+ }
+
+ return DeletedCount;
+ }
+
+ ///
+ /// 复制文件夹,失败会抛出异常。
+ ///
+ public static void CopyDirectory(string FromPath, string ToPath, Action ProgressIncrementHandler = null)
+ {
+ FromPath = FromPath.Replace("/", @"\");
+ if (!FromPath.EndsWithF(@"\"))
+ FromPath += @"\";
+ ToPath = ToPath.Replace("/", @"\");
+ if (!ToPath.EndsWithF(@"\"))
+ ToPath += @"\";
+ var AllFiles = EnumerateFiles(FromPath).ToList();
+ var FileCount = AllFiles.Count;
+ foreach (var File in AllFiles)
+ {
+ CopyFile(File.FullName, File.FullName.Replace(FromPath, ToPath));
+ if (ProgressIncrementHandler is not null)
+ ProgressIncrementHandler(1d / FileCount);
+ }
+ }
+
+ ///
+ /// 遍历文件夹中的所有文件。
+ ///
+ public static IEnumerable EnumerateFiles(string Directory)
+ {
+ var Info = new DirectoryInfo(ShortenPath(Directory));
+ if (!Info.Exists)
+ return new List();
+ return Info.EnumerateFiles("*", SearchOption.AllDirectories);
+ }
+
+ ///
+ /// 若路径长度大于指定值,则将长路径转换为短路径。
+ ///
+ public static string ShortenPath(string LongPath, int ShortenThreshold = 247)
+ {
+ if (LongPath.Length <= ShortenThreshold)
+ return LongPath;
+ var ShortPath = new StringBuilder(260);
+ GetShortPathName(LongPath, ShortPath, 260);
+ return ShortPath.ToString();
+ }
+
+ public static void MoveDirectory(string SourceDir, string TargetDir)
+ {
+ if (!Directory.Exists(TargetDir))
+ Directory.CreateDirectory(TargetDir);
+ foreach (var FilePath in Directory.GetFiles(SourceDir))
+ {
+ var FileName = GetFileNameFromPath(FilePath);
+ File.Move(FilePath, Path.Combine(TargetDir, FileName));
+ }
+
+ foreach (var DirPath in Directory.GetDirectories(SourceDir))
+ {
+ var DirName = GetFolderNameFromPath(DirPath);
+ MoveDirectory(DirPath, Path.Combine(TargetDir, DirName));
+ }
+ }
+
+ [DllImport("kernel32", EntryPoint = "GetShortPathNameA")]
+ private static extern int GetShortPathName(string lpszLongPath, StringBuilder lpszShortPath, int cchBuffer);
+
+ public static void CreateSymbolicLink(string LinkPath, string TargetPath, int Flags)
+ {
+ var CMDProcess = new Process();
+ var LinkDPath = ModLaunch.ExtractLinkD();
+ {
+ var withBlock = CMDProcess.StartInfo;
+ withBlock.FileName = LinkDPath;
+ withBlock.Arguments = $"\"{LinkPath}\" \"{TargetPath}\"";
+ withBlock.CreateNoWindow = true;
+ withBlock.UseShellExecute = false;
+ }
+ CMDProcess.Start();
+ while (!CMDProcess.HasExited)
+ {
+ }
+ }
+
+ #endregion
+
+ #region 文本
+
+ public static char vbLQ = Convert.ToChar(8220);
+ public static char vbRQ = Convert.ToChar(8221);
+
+ ///
+ /// 返回一个枚举对应的字符串。
+ ///
+ /// 一个已经实例化的枚举类型。
+ public static string GetStringFromEnum(Enum EnumData)
+ {
+ return Enum.GetName(EnumData.GetType(), EnumData);
+ }
+
+ ///
+ /// 将文件大小转化为适合的文本形式,如“1.28 M”。
+ ///
+ /// 以字节为单位的大小表示。
+ public static string GetString(long FileSize)
+ {
+ return ByteStream.GetReadableLength(FileSize);
+ }
+
+ ///
+ /// 获取 JSON 对象。
+ ///
+ public static object GetJson(string Data)
+ {
+ try
+ {
+ return JsonConvert.DeserializeObject(Data,
+ new JsonSerializerSettings { DateTimeZoneHandling = DateTimeZoneHandling.Local });
+ }
+ catch (Exception ex)
+ {
+ var Length = (Data ?? "").Length;
+ throw new Exception("格式化 JSON 失败:" + (Length > 2000
+ ? Data.Substring(0, 500) + $"...(全长 {Length} 个字符)..." + Strings.Right(Data, 500)
+ : Data));
+ }
+ }
+
+ ///
+ /// 将第一个字符转换为大写,其余字符转换为小写。
+ ///
+ public static string Capitalize(this string word)
+ {
+ if (string.IsNullOrEmpty(word))
+ return word;
+ return word.Substring(0, 1).ToUpperInvariant() + word.Substring(1).ToLowerInvariant();
+ }
+
+ ///
+ /// 将字符串统一至某个长度,过短则以 Code 将其右侧填充,过长则截取靠左的指定长度。
+ ///
+ public static string StrFill(string Str, string Code, byte Length)
+ {
+ if (Str.Length > Length)
+ return Strings.Mid(Str, 1, Length);
+ return Strings.Mid(Str.PadRight(Length, Conversions.ToChar(Code)), Str.Length + 1) + Str;
+ }
+
+ ///
+ /// 将一个小数显示为固定的小数点后位数形式,将向零取整。
+ /// 如 12 保留 2 位则输出 12.00,而 95.678 保留 2 位则输出 95.67。
+ ///
+ public static string StrFillNum(double Num, int Length)
+ {
+ string StrFillNumRet = default;
+ Num = Math.Round(Num, Length, MidpointRounding.AwayFromZero);
+ StrFillNumRet = Num.ToString();
+ if (!StrFillNumRet.Contains("."))
+ return (StrFillNumRet + ".").PadRight(StrFillNumRet.Length + 1 + Length, '0');
+ return StrFillNumRet.PadRight(StrFillNumRet.Split(".")[0].Length + 1 + Length, '0');
+ }
+
+ ///
+ /// 移除字符串首尾的标点符号、回车,以及括号中、冒号后的补充说明内容。
+ ///
+ public static object StrTrim(string Str, bool RemoveQuote = true)
+ {
+ if (RemoveQuote)
+ Str = Str.Split("(")[0].Split(":")[0].Split("(")[0].Split(":")[0];
+ return Str.Trim('.', '。', '!', ' ', '!', '?', '?', Conversions.ToChar("\r"),
+ Conversions.ToChar("\n"));
+ }
+
+ ///
+ /// 连接字符串。
+ ///
+ public static string Join(this IEnumerable List, string Split)
+ {
+ var Builder = new StringBuilder();
+ var IsFirst = true;
+ foreach (var Element in List)
+ {
+ if (IsFirst)
+ IsFirst = false;
+ else
+ Builder.Append(Split);
+ if (Element is not null)
+ Builder.Append(Element);
+ }
+
+ return Builder.ToString();
+ }
+
+ ///
+ /// 分割字符串。
+ ///
+ public static string[] Split(this string FullStr, string SplitStr)
+ {
+ if (SplitStr.Length == 1) return FullStr.Split(SplitStr[0]);
+
+ return FullStr.Split(new[] { SplitStr }, StringSplitOptions.None);
+ }
+
+ ///
+ /// 获取字符串哈希值。
+ ///
+ public static ulong GetHash(string Str)
+ {
+ ulong GetHashRet = default;
+ GetHashRet = 5381UL;
+ for (int i = 0, loopTo = Str.Length - 1; i <= loopTo; i++)
+ GetHashRet = (GetHashRet << 5) ^ GetHashRet ^ (ulong)Strings.AscW(Str[i]);
+ return GetHashRet ^ 0xA98F501BC684032FUL;
+ }
+
+ ///
+ /// 获取字符串 MD5。
+ ///
+ public static string GetStringMD5(string Str)
+ {
+ return Conversions.ToString(GetHexString(MD5Provider.Instance.ComputeHash(Str)));
+ }
+
+ ///
+ /// 检查字符串中的字符是否均为 ASCII 字符。
+ ///
+ public static bool IsASCII(this string Input)
+ {
+ return Input.All(c => Strings.AscW(c) < 128);
+ }
+
+ ///
+ /// 获取在子字符串第一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024。
+ /// 如果未找到子字符串则不裁切。
+ ///
+ public static string BeforeFirst(this string Str, string Text, bool IgnoreCase = false)
+ {
+ var Pos = string.IsNullOrEmpty(Text) ? -1 : Str.IndexOfF(Text, IgnoreCase);
+ if (Pos >= 0) return Str.Substring(0, Pos);
+
+ return Str;
+ }
+
+ ///
+ /// 获取在子字符串最后一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024/11。
+ /// 如果未找到子字符串则不裁切。
+ ///
+ public static string BeforeLast(this string Str, string Text, bool IgnoreCase = false)
+ {
+ var Pos = string.IsNullOrEmpty(Text) ? -1 : Str.LastIndexOfF(Text, IgnoreCase);
+ if (Pos >= 0) return Str.Substring(0, Pos);
+
+ return Str;
+ }
+
+ ///
+ /// 获取在子字符串第一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 11/08。
+ /// 如果未找到子字符串则不裁切。
+ ///
+ public static string AfterFirst(this string Str, string Text, bool IgnoreCase = false)
+ {
+ var Pos = string.IsNullOrEmpty(Text) ? -1 : Str.IndexOfF(Text, IgnoreCase);
+ if (Pos >= 0) return Str.Substring(Pos + Text.Length);
+
+ return Str;
+ }
+
+ ///
+ /// 获取在子字符串最后一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 08。
+ /// 如果未找到子字符串则不裁切。
+ ///
+ public static string AfterLast(this string Str, string Text, bool IgnoreCase = false)
+ {
+ var Pos = string.IsNullOrEmpty(Text) ? -1 : Str.LastIndexOfF(Text, IgnoreCase);
+ if (Pos >= 0) return Str.Substring(Pos + Text.Length);
+
+ return Str;
+ }
+
+ ///
+ /// 获取处于两个子字符串之间的部分,裁切尽可能多的内容。
+ /// 等效于 AfterLast 后接 BeforeFirst。
+ /// 如果未找到子字符串则不裁切。
+ ///
+ public static string Between(this string Str, string After, string Before, bool IgnoreCase = false)
+ {
+ var StartPos = string.IsNullOrEmpty(After) ? -1 : Str.LastIndexOfF(After, IgnoreCase);
+ if (StartPos >= 0)
+ StartPos += After.Length;
+ else
+ StartPos = 0;
+ var EndPos = string.IsNullOrEmpty(Before) ? -1 : Str.IndexOfF(Before, StartPos, IgnoreCase);
+ if (EndPos >= 0) return Str.Substring(StartPos, EndPos - StartPos);
+
+ if (StartPos > 0) return Str.Substring(StartPos);
+
+ return Str;
+ }
+
+ ///
+ /// 高速的 StartsWith。
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool StartsWithF(this string Str, string Prefix, bool IgnoreCase = false)
+ {
+ return Str.StartsWith(Prefix, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
+ }
+
+ ///
+ /// 高速的 EndsWith。
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool EndsWithF(this string Str, string Suffix, bool IgnoreCase = false)
+ {
+ return Str.EndsWith(Suffix, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
+ }
+
+ ///
+ /// 支持可变大小写判断的 Contains。
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool ContainsF(this string Str, string SubStr, bool IgnoreCase = false)
+ {
+ return Str.IndexOf(SubStr, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) >= 0;
+ }
+
+ ///
+ /// 高速的 IndexOf。
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int IndexOfF(this string Str, string SubStr, bool IgnoreCase = false)
+ {
+ return Str.IndexOf(SubStr, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
+ }
+
+ ///
+ /// 高速的 IndexOf。
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int IndexOfF(this string Str, string SubStr, int StartIndex, bool IgnoreCase = false)
+ {
+ return Str.IndexOf(SubStr, StartIndex,
+ IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
+ }
+
+ ///
+ /// 高速的 LastIndexOf。
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int LastIndexOfF(this string Str, string SubStr, bool IgnoreCase = false)
+ {
+ return Str.LastIndexOf(SubStr, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
+ }
+
+ ///
+ /// 高速的 LastIndexOf。
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int LastIndexOfF(this string Str, string SubStr, int StartIndex, bool IgnoreCase = false)
+ {
+ return Str.LastIndexOf(SubStr, StartIndex,
+ IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
+ }
+
+ ///
+ /// 不会报错的 Val。
+ /// 如果输入有误,返回 0。
+ ///
+ public static double Val(object Str)
+ {
+ try
+ {
+ return Str is "&" ? 0d : Conversion.Val(Str);
+ }
+ catch
+ {
+ return 0d;
+ }
+ }
+
+ // 转义
+ ///
+ /// 为字符串进行 XML 转义。
+ ///
+ public static string EscapeXML(string Str)
+ {
+ if (Str.StartsWithF("{"))
+ Str = "{}" + Str; // #4187
+ return Str.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("'", "'")
+ .Replace("\"", """).Replace("\r\n", "
");
+ }
+
+ ///
+ /// 为字符串进行 Like 关键字转义。
+ ///
+ public static string EscapeLikePattern(string input)
+ {
+ var sb = new StringBuilder();
+ foreach (var c in input)
+ switch (c)
+ {
+ case '[':
+ case ']':
+ case '*':
+ case '?':
+ case '#':
+ {
+ sb.Append('[').Append(c).Append(']');
+ break;
+ }
+
+ default:
+ {
+ sb.Append(c);
+ break;
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ // 正则
+ ///
+ /// 搜索字符串中的所有正则匹配项。
+ ///
+ public static List RegexSearch(this string str, string regex, RegexOptions options = RegexOptions.None)
+ {
+ List RegexSearchRet = default;
+ try
+ {
+ RegexSearchRet = new List();
+ var RegexSearchRes = new Regex(regex, options).Matches(str);
+ if (RegexSearchRes is null)
+ return RegexSearchRet;
+ foreach (Match item in RegexSearchRes)
+ RegexSearchRet.Add(item.Value);
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "正则匹配全部项出错");
+ return new List();
+ }
+
+ return RegexSearchRet;
+ }
+
+ ///
+ /// 搜索字符串中的所有正则匹配项。
+ ///
+ /// 要搜索的字符串
+ /// 正则表达式对象
+ /// 所有匹配项的列表
+ public static List RegexSearch(this string str, Regex regex)
+ {
+ try
+ {
+ var result = new List();
+ foreach (Match item in regex.Matches(str))
+ {
+ result.Add(item.Value);
+ }
+ return result;
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "正则匹配全部项出错");
+ return new List();
+ }
+ }
+
+ ///
+ /// 获取字符串中的第一个正则匹配项,若无匹配则返回 Nothing。
+ ///
+ public static string RegexSeek(this string str, string regex, RegexOptions options = RegexOptions.None)
+ {
+ try
+ {
+ var Result = Regex.Match(str, regex, options).Value;
+ return string.IsNullOrEmpty(Result) ? null : Result;
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "正则匹配第一项出错");
+ return null;
+ }
+ }
+
+ ///
+ /// 获取字符串中的第一个正则匹配项,若无匹配则返回 Nothing。
+ ///
+ public static string RegexSeek(this string str, Regex regex, RegexOptions options = RegexOptions.None)
+ {
+ try
+ {
+ var Result = regex.Match(str, (int)options).Value;
+ return string.IsNullOrEmpty(Result) ? null : Result;
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "正则匹配第一项出错");
+ return null;
+ }
+ }
+
+ ///
+ /// 检查字符串是否匹配某正则模式。
+ ///
+ public static bool RegexCheck(this string str, string regex, RegexOptions options = RegexOptions.None)
+ {
+ try
+ {
+ return Regex.IsMatch(str, regex, options);
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "正则检查出错");
+ return false;
+ }
+ }
+
+ ///
+ /// 进行正则替换,会抛出错误。
+ ///
+ public static string RegexReplace(this string AllContents, string SearchRegex, string ReplaceTo,
+ RegexOptions options = RegexOptions.None)
+ {
+ return Regex.Replace(AllContents, SearchRegex, ReplaceTo, options);
+ }
+
+ ///
+ /// 对每个正则匹配分别进行替换,会抛出错误。
+ ///
+ public static string RegexReplaceEach(this string AllContents, string SearchRegex, MatchEvaluator ReplaceTo,
+ RegexOptions options = RegexOptions.None)
+ {
+ return Regex.Replace(AllContents, SearchRegex, ReplaceTo, options);
+ }
+
+ #endregion
+
+ #region 搜索
+
+ ///
+ /// 获取搜索文本的相似度。
+ ///
+ /// 被搜索的长内容。
+ /// 用户输入的搜索文本。
+ private static double SearchSimilarity(string Source, string Query)
+ {
+ var qp = 0;
+ var lenSum = 0d;
+ Source = Source.ToLower().Replace(" ", "");
+ Query = Query.ToLower().Replace(" ", "");
+ var sourceLength = Source.Length;
+ var queryLength = Query.Length; // 用于计算最后因数的长度缓存
+ while (qp < queryLength)
+ {
+ // 对 qp 作为开始位置计算
+ var sp = 0;
+ var lenMax = 0;
+ var spMax = 0;
+ // 查找以 qp 为头的最大子串
+ while (sp < Source.Length)
+ {
+ // 对每个 sp 作为开始位置计算最大子串
+ var len = 0;
+ while (qp + len < queryLength && sp + len < Source.Length && Source[sp + len] == Query[qp + len])
+ len += 1;
+ // 存储 len
+ if (len > lenMax)
+ {
+ lenMax = len;
+ spMax = sp;
+ }
+
+ // 根据结果增加 sp
+ sp += Math.Max(1, len);
+ }
+
+ if (lenMax > 0)
+ {
+ Source = Source.Substring(0, spMax) +
+ (Source.Count() > spMax + lenMax
+ ? Source.Substring(spMax + lenMax)
+ : string.Empty); // 将源中的对应字段替换空
+ // 存储 lenSum
+ var IncWeight = Math.Pow(1.4d, 3 + lenMax) - 3.6d; // 根据长度加成
+ IncWeight *= 1d + 0.3d * Math.Max(0, 3 - Math.Abs(qp - spMax)); // 根据位置加成
+ lenSum += IncWeight;
+ }
+
+ // 根据结果增加 qp
+ qp += Math.Max(1, lenMax);
+ }
+
+ // 计算结果:重复字段量 × 源长度影响比例
+ return lenSum / queryLength * (3d / Math.Pow(sourceLength + 15, 0.5d)) *
+ (queryLength <= 2 ? 3 - queryLength : 1);
+ }
+
+ ///
+ /// 获取多段文本加权后的相似度。
+ ///
+ private static double SearchSimilarityWeighted(List source, string query)
+ {
+ var totalWeight = 0d;
+ var sum = 0d;
+ foreach (var Pair in source)
+ {
+ if (Pair.Aliases.Any())
+ sum += Pair.Aliases.Max(a => SearchSimilarity(a, query)) * Pair.Weight;
+ totalWeight += Pair.Weight;
+ }
+
+ return sum / totalWeight;
+ }
+
+ ///
+ /// 用于搜索的项目。
+ ///
+ public class SearchEntry
+ {
+ ///
+ /// 是否完全匹配。
+ ///
+ public bool AbsoluteRight;
+
+ ///
+ /// 该项目对应的源数据。
+ ///
+ public T Item;
+
+ ///
+ /// 该项目用于搜索的文本源。
+ /// 在搜索时,会对每个文本源单独加权,但单个文本源内的多个别名只取最高的一个的相似度。
+ ///
+ public List SearchSource;
+
+ ///
+ /// 相似度。
+ ///
+ public double Similarity;
+ }
+
+ ///
+ /// 单个用于搜索的文本源。
+ ///
+ public class SearchSource
+ {
+ public string[] Aliases;
+ public double Weight;
+
+ public SearchSource(string[] aliases, double weight = 1)
+ {
+ Aliases = aliases;
+ Weight = weight;
+ }
+
+ public SearchSource(string text, double weight = 1)
+ {
+ Aliases = new[] { text };
+ Weight = weight;
+ }
+ }
+
+ ///
+ /// 进行多段文本加权搜索,获取相似度较高的数项结果。
+ ///
+ /// 返回的最大模糊结果数。
+ /// 返回结果要求的最低相似度。
+ public static List> Search(List> Entries, string Query, int MaxBlurCount = 5,
+ double MinBlurSimilarity = 0.1d)
+ {
+ var ResultList = new List>();
+
+ if (Entries is null || !Entries.Any()) return ResultList;
+
+ // Preprocess query into parts
+ var queryParts = Query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ if (queryParts.Length == 0)
+ {
+ ResultList.AddRange(Entries);
+ return ResultList;
+ }
+
+ // Precompute query parts in lowercase for case-insensitive comparison
+ var queryPartsLower = queryParts.Select(q => q.ToLower()).ToArray();
+
+ // Process each entry to compute similarity and absolute match status
+ foreach (var Entry in Entries)
+ {
+ Entry.Similarity = SearchSimilarityWeighted(Entry.SearchSource, Query);
+
+ // Preprocess search source keys: remove spaces and convert to lowercase
+ var processedSources = Entry.SearchSource.Select(s =>
+ {
+ for (var i = 0; i < s.Aliases.Length; i++)
+ s.Aliases[i] = s.Aliases[i].Replace(" ", "").ToLower();
+ return s.Aliases;
+ }).ToList();
+
+ // Check if all query parts are matched exactly by at least one source
+ var isAbsoluteRight = true;
+ foreach (var qp in queryPartsLower)
+ {
+ var found = false;
+ foreach (var ps in processedSources)
+ if (ps.Any(p => p.Contains(qp)))
+ {
+ found = true;
+ break;
+ }
+
+ if (!found)
+ {
+ isAbsoluteRight = false;
+ break;
+ }
+ }
+
+ Entry.AbsoluteRight = isAbsoluteRight;
+ }
+
+ // Sort by absolute match (descending), then by similarity (descending)
+ var sortedEntries = Entries.OrderByDescending(e => e.AbsoluteRight).ThenByDescending(e => e.Similarity)
+ .ToList();
+
+ // Build the final result list
+ var blurCount = 0;
+ foreach (var Entry in sortedEntries)
+ if (Entry.AbsoluteRight)
+ {
+ ResultList.Add(Entry);
+ }
+ else
+ {
+ if (Entry.Similarity < MinBlurSimilarity || blurCount >= MaxBlurCount) break;
+ ResultList.Add(Entry);
+ blurCount += 1;
+ }
+
+ return ResultList;
+ }
+
+ #endregion
+
+ #region 系统
+
+ public static bool IsUtf8CodePage()
+ {
+ return Encoding.Default.CodePage == 65001;
+ }
+
+ ///
+ /// 线程安全的 List。
+ /// 通过在 For Each 循环中使用一个浅表副本规避多线程操作或移除自身导致的异常。
+ ///
+ public class SafeList : IEnumerable, IDisposable, ICollection
+ {
+ private readonly List _internalList;
+ private readonly ReaderWriterLockSlim _lock = new();
+
+ public SafeList()
+ {
+ _internalList = new List();
+ }
+
+ public SafeList(IEnumerable data)
+ {
+ _internalList = new List(data);
+ }
+
+ public T this[int index]
+ {
+ get => _internalList[index];
+ set => _internalList[index] = value;
+ }
+
+ public void Add(T item)
+ {
+ _lock.EnterWriteLock();
+ try
+ {
+ _internalList.Add(item);
+ }
+ finally
+ {
+ _lock.ExitWriteLock();
+ }
+ }
+
+ public bool Remove(T item)
+ {
+ _lock.EnterWriteLock();
+ try
+ {
+ return _internalList.Remove(item);
+ }
+ finally
+ {
+ _lock.ExitWriteLock();
+ }
+ }
+
+ public void Clear()
+ {
+ _lock.EnterWriteLock();
+ try
+ {
+ _internalList.Clear();
+ }
+ finally
+ {
+ _lock.ExitWriteLock();
+ }
+ }
+
+ public int Count
+ {
+ get
+ {
+ _lock.EnterReadLock();
+ try
+ {
+ return _internalList.Count;
+ }
+ finally
+ {
+ _lock.ExitReadLock();
+ }
+ }
+ }
+
+ public bool IsReadOnly => ((ICollection)_internalList).IsReadOnly;
+
+ public bool Contains(T item)
+ {
+ return ((ICollection)_internalList).Contains(item);
+ }
+
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ ((ICollection)_internalList).CopyTo(array, arrayIndex);
+ }
+
+ public void Dispose()
+ {
+ _lock.Dispose();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return ToList().GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public List ToList()
+ {
+ _lock.EnterReadLock();
+ try
+ {
+ return _internalList.ToList();
+ }
+ finally
+ {
+ _lock.ExitReadLock();
+ }
+ }
+
+ public void RemoveAt(int index)
+ {
+ _lock.EnterWriteLock();
+ try
+ {
+ _internalList.RemoveAt(index);
+ }
+ finally
+ {
+ _lock.ExitWriteLock();
+ }
+ }
+ }
+
+ ///
+ /// 可用于临时存放文件的,不含任何特殊字符的文件夹路径,以“\”结尾。
+ ///
+ public static string PathPure = GetPureASCIIDir();
+
+ private static string GetPureASCIIDir()
+ {
+ if (ExePath.IsASCII()) return ExePath + @"PCL\";
+
+ if (PathAppdata.IsASCII()) return PathAppdata;
+
+ if (PathTemp.IsASCII()) return PathTemp;
+
+ return OsDrive + @"ProgramData\PCL\";
+ }
+
+ ///
+ /// 指示接取到这个异常的函数进行重试。
+ ///
+ public class RestartException : Exception
+ {
+ }
+
+ ///
+ /// 指示用户手动取消了操作,或用户已知晓操作被取消的原因。
+ ///
+ public class CancelledException : Exception
+ {
+ }
+
+ ///
+ /// 判断对象是否为某个泛型类型的实例。
+ ///
+ public static bool IsInstanceOfGenericType(this Type genericType, object obj)
+ {
+ if (obj is null)
+ return false;
+ var t = obj.GetType();
+ while (t is not null)
+ {
+ if (t.IsGenericType && ReferenceEquals(t.GetGenericTypeDefinition(), genericType))
+ return true;
+ t = t.BaseType;
+ }
+
+ return false;
+ }
+
+ private static int Uuid = 1;
+ private static object UuidLock;
+
+ ///
+ /// 获取一个全程序内不会重复的数字(伪 Uuid)。
+ ///
+ public static int GetUuid()
+ {
+ if (UuidLock is null)
+ UuidLock = new object();
+ lock (UuidLock)
+ {
+ Uuid += 1;
+ return Uuid;
+ }
+ }
+
+ ///
+ /// 将元素与 List 的混合体拆分为元素组。
+ ///
+ public static List GetFullList(IList data)
+ {
+ List GetFullListRet = default;
+ GetFullListRet = new List();
+ for (int i = 0, loopTo = data.Count - 1; i <= loopTo; i++)
+ if (data[i] is ICollection)
+ GetFullListRet.AddRange((IEnumerable)data[i]);
+ else
+ GetFullListRet.Add(Conversions.ToGenericParameter(data[i]));
+
+ return GetFullListRet;
+ }
+
+ ///
+ /// 数组去重。
+ ///
+ public static List Distinct(this ICollection Arr, ComparisonBoolean IsEqual)
+ {
+ var ResultArray = new List();
+ for (int i = 0, loopTo = Arr.Count - 1; i <= loopTo; i++)
+ {
+ for (int ii = i + 1, loopTo1 = Arr.Count - 1; ii <= loopTo1; ii++)
+ if (IsEqual(Arr.ElementAtOrDefault(i), Arr.ElementAtOrDefault(ii)))
+ goto NextElement;
+ ResultArray.Add(Arr.ElementAtOrDefault(i));
+ NextElement: ;
+ }
+
+ return ResultArray;
+ }
+
+ ///
+ /// 对集合的每个元素执行指定操作。
+ ///
+ public static IEnumerable ForEach(this IEnumerable Collection, Action Action)
+ {
+ foreach (var Item in Collection)
+ Action(Item);
+ return Collection;
+ }
+
+ ///
+ /// 用于储存 RaiseByMouse 的 EventArgs。
+ ///
+ public sealed class RouteEventArgs : EventArgs
+ {
+ public bool Handled = false;
+ public bool RaiseByMouse;
+
+ public RouteEventArgs(bool RaiseByMouse = false)
+ {
+ this.RaiseByMouse = RaiseByMouse;
+ }
+ }
+
+ ///
+ /// 前台运行文件。
+ ///
+ /// 文件名。可以为“notepad”等缩写。
+ /// 运行参数。
+ public static void ShellOnly(string FileName, string Arguments = "")
+ {
+ try
+ {
+ FileName = ShortenPath(FileName);
+ using (var Program = new Process())
+ {
+ Program.StartInfo.Arguments = Arguments;
+ Program.StartInfo.FileName = FileName;
+ Program.StartInfo.UseShellExecute = true;
+ Log("[System] 执行外部命令:" + FileName + " " + Arguments);
+ Program.Start();
+ }
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "打开文件或程序失败:" + FileName, LogLevel.Msgbox);
+ }
+ }
+
+ ///
+ /// 前台运行文件并返回返回值。
+ ///
+ /// 文件名。可以为“notepad”等缩写。
+ /// 运行参数。
+ /// 等待该程序结束的最长时间(毫秒)。超时会返回 Result.Timeout。
+ public static ProcessReturnValues ShellAndGetExitCode(string FileName, string Arguments = "", int Timeout = 1000000)
+ {
+ try
+ {
+ using (var Program = new Process())
+ {
+ Program.StartInfo.Arguments = Arguments;
+ Program.StartInfo.FileName = FileName;
+ Log("[System] 执行外部命令并等待返回码:" + FileName + " " + Arguments);
+ Program.Start();
+ if (Program.WaitForExit(Timeout)) return (ProcessReturnValues)Program.ExitCode;
+
+ return ProcessReturnValues.Timeout;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log(ex, "执行命令失败:" + FileName, LogLevel.Msgbox);
+ return ProcessReturnValues.Fail;
+ }
+ }
+
+ ///
+ /// 静默运行文件并返回输出流字符串。执行失败会抛出异常。
+ ///
+ /// 文件名。可以为“notepad”等缩写。
+ /// 运行参数。
+ /// 等待该程序结束的最长时间(毫秒)。超时会抛出错误。
+ public static string ShellAndGetOutput(string FileName, string Arguments = "", int Timeout = 1000000,
+ string WorkingDirectory = null)
+ {
+ var Info = new ProcessStartInfo
+ {
+ FileName = FileName,
+ Arguments = Arguments,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ };
+
+ // 设置工作目录(如果提供)
+ if (!string.IsNullOrEmpty(WorkingDirectory)) Info.WorkingDirectory = WorkingDirectory.TrimEnd('\\');
+
+ Log("[System] 执行外部命令并等待返回结果:" + FileName + " " + Arguments);
+
+ using (var Program = new Process { StartInfo = Info })
+ {
+ Program.Start();
+
+ // 异步读取输出和错误流
+ var outputTask = Program.StandardOutput.ReadToEndAsync();
+ var errorTask = Program.StandardError.ReadToEndAsync();
+
+ // 等待进程退出或超时
+ if (Program.WaitForExit(Timeout))
+ {
+ // 确保异步读取完成
+ Task.WaitAll(outputTask, errorTask);
+ }
+ else
+ {
+ // 超时后终止进程
+ Program.Kill();
+ // 仍然尝试获取已输出的内容
+ Task.WaitAll(outputTask, errorTask);
+ }
+
+ // 合并结果并返回
+ return outputTask.Result + errorTask.Result;
+ }
+ }
+
+ ///
+ /// 在新的工作线程中执行代码。
+ ///
+ public static Thread RunInNewThread(Action Action, string Name = null,
+ ThreadPriority Priority = ThreadPriority.Normal)
+ {
+ var th = new Thread(() =>
+ {
+ try
+ {
+ Action();
+ }
+ catch (ThreadInterruptedException ex)
+ {
+ Log(Name + ":线程已中止");
+ }
+ catch (Exception ex)
+ {
+ Log(ex, Name + ":线程执行失败", LogLevel.Feedback);
+ }
+ }) { Name = Name ?? "Runtime New Invoke " + GetUuid() + "#", Priority = Priority };
+ th.Start();
+ return th;
+ }
+
+ ///
+ /// 确保在 UI 线程中执行代码。
+ /// 如果当前并非 UI 线程,则会阻断当前线程,直至 UI 线程执行完毕。
+ /// 为防止线程互锁,请仅在开始加载动画、从 UI 获取输入时使用!
+ ///
+ public static Output RunInUiWait