初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,1517 @@
|
||||
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;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
public static partial class ModAnimation
|
||||
{
|
||||
private static int aniCount;
|
||||
private static int aniFPSCounter;
|
||||
private static long aniFPSTimer;
|
||||
|
||||
/// <summary>
|
||||
/// 当前的动画 FPS。
|
||||
/// </summary>
|
||||
public static int aniFPS;
|
||||
|
||||
/// <summary>
|
||||
/// 开始动画执行。
|
||||
/// </summary>
|
||||
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,
|
||||
userSummary: Lang.Text("Application.Animation.Error.OperationFailed"));
|
||||
}
|
||||
}, "Animation", ThreadPriority.AboveNormal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 动画定时器事件。
|
||||
/// </summary>
|
||||
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,
|
||||
userSummary: Lang.Text("Application.Animation.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行一个动画。
|
||||
/// </summary>
|
||||
/// <param name="ani">执行的动画对象。</param>
|
||||
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 is 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 is 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,
|
||||
userSummary: Lang.Text("Application.Animation.Error.OperationFailed"));
|
||||
}
|
||||
|
||||
return ani;
|
||||
}
|
||||
|
||||
#region 声明
|
||||
|
||||
/// <summary>
|
||||
/// 动画速度。最大为 200。
|
||||
/// </summary>
|
||||
public static double aniSpeed = 1d;
|
||||
|
||||
/// <summary>
|
||||
/// 动画组列表。
|
||||
/// </summary>
|
||||
public static ConcurrentDictionary<string, AniGroupEntry> aniGroups = new();
|
||||
|
||||
public class AniGroupEntry
|
||||
{
|
||||
public List<AniData> data;
|
||||
public long startTick;
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上一次记刻的时间。
|
||||
/// </summary>
|
||||
private static long aniLastTick;
|
||||
|
||||
/// <summary>
|
||||
/// 动画模块是否正在运行。
|
||||
/// </summary>
|
||||
public static bool aniRunning;
|
||||
|
||||
private static readonly object aniControlEnabledLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 控件动画执行是否开启。先 +1,再 -1。
|
||||
/// </summary>
|
||||
public static int AniControlEnabled
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
lock (aniControlEnabledLock)
|
||||
{
|
||||
field = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 类与枚举
|
||||
|
||||
/// <summary>
|
||||
/// 单个动画对象。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public struct AniData
|
||||
{
|
||||
/// <summary>
|
||||
/// 动画种类。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public AniType typeMain;
|
||||
|
||||
/// <summary>
|
||||
/// 动画副种类。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public AniTypeSub typeSub;
|
||||
|
||||
/// <summary>
|
||||
/// 动画总长度。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public int timeTotal;
|
||||
|
||||
/// <summary>
|
||||
/// 已经执行的动画长度。如果为负数则为延迟。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public int timeFinished;
|
||||
|
||||
/// <summary>
|
||||
/// 已经完成的百分比。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public double timePercent;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为“以后”。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public bool isAfter;
|
||||
|
||||
/// <summary>
|
||||
/// 插值器类型。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public AniEase ease;
|
||||
|
||||
/// <summary>
|
||||
/// 动画对象。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public object obj;
|
||||
|
||||
/// <summary>
|
||||
/// 动画值。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public object value;
|
||||
|
||||
/// <summary>
|
||||
/// 上次执行时的动画值。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public object valueLast;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return ModBase.GetStringFromEnum(typeMain) + " | " + timeFinished + "/" + timeTotal + "(" +
|
||||
Math.Round(timePercent * 100d) + "%)" +
|
||||
(obj is null ? "" : " | " + obj + "(" + obj.GetType().Name + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 动画基础种类。
|
||||
/// </summary>
|
||||
public enum AniType
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个Double的动画,包括位置、长宽、透明度等。这需要附属类型。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
Number,
|
||||
|
||||
/// <summary>
|
||||
/// 颜色属性的动画。这需要附属类型。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
Color,
|
||||
|
||||
/// <summary>
|
||||
/// 缩放控件大小。比起4个DoubleAnimation来说效率更高。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
Scale,
|
||||
|
||||
/// <summary>
|
||||
/// 文字一个个出现。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
TextAppear,
|
||||
|
||||
/// <summary>
|
||||
/// 执行代码。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
Code,
|
||||
|
||||
/// <summary>
|
||||
/// 以 WPF 方式缩放控件。
|
||||
/// </summary>
|
||||
ScaleTransform,
|
||||
|
||||
/// <summary>
|
||||
/// 以 WPF 方式旋转控件。
|
||||
/// </summary>
|
||||
RotateTransform
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 动画扩展种类。
|
||||
/// </summary>
|
||||
public enum AniTypeSub
|
||||
{
|
||||
X,
|
||||
Y,
|
||||
Width,
|
||||
Height,
|
||||
Opacity,
|
||||
Value,
|
||||
Radius,
|
||||
BorderThickness,
|
||||
StrokeThickness,
|
||||
TranslateX,
|
||||
TranslateY,
|
||||
Double,
|
||||
DoubleParam,
|
||||
GridLengthWidth
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 种类
|
||||
|
||||
// DoubleAnimation
|
||||
|
||||
/// <summary>
|
||||
/// 移动X轴的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">进行移动的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移动Y轴的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">进行移动的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 改变宽度的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">宽度改变的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 改变高度的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">高度改变的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 改变透明度的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">透明度改变的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 改变对象的Value属性的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">Value属性改变的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 改变对象的Radius属性的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">Radius属性改变的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 改变对象的BorderThickness属性的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">BorderThickness属性改变的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 改变对象的StrokeThickness属性的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">StrokeThickness属性改变的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 改变 Width 的 GridLength 属性的动画。必须为 Star。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">GridLength.Value 改变的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
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])
|
||||
|
||||
/// <summary>
|
||||
/// 改变数字属性的动画。
|
||||
/// </summary>
|
||||
/// <param name="Obj">动画的对象。</param>
|
||||
/// <param name="Prop">动画的依赖属性。</param>
|
||||
/// <param name="value">改变的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数字动画值。
|
||||
/// </summary>
|
||||
/// <param name="value">改变的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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])
|
||||
|
||||
/// <summary>
|
||||
/// 改变颜色属性的动画。
|
||||
/// </summary>
|
||||
/// <param name="Obj">动画的对象。</param>
|
||||
/// <param name="Prop">动画的依赖属性。</param>
|
||||
/// <param name="value">颜色改变的值。以RGB加减法进行计算。不用担心超额。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 改变颜色属性为一个资源的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="prop">动画的依赖属性。</param>
|
||||
/// <param name="res">要将颜色改变为该资源值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 缩放控件的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">大小改变的百分比(如-0.6)或值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <param name="absolute">大小改变是否为绝对值。若为 True 则为绝对像素,若为 False 则为相对百分比。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 让一段文字一个个字出现或消失的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。必须是Label或TextBlock。</param>
|
||||
/// <param name="hide">是否为一个个字隐藏。默认为False(一个个字出现)。这些字必须已经存在了。</param>
|
||||
/// <param name="timePerText">是否采用根据文本长度决定时间的方式。</param>
|
||||
/// <param name="time">动画长度(毫秒)。若TimePerText为True,这代表每个字所占据的时间。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 执行代码。
|
||||
/// </summary>
|
||||
/// <param name="Code">一个ThreadStart。这将会在执行时在主线程调用。</param>
|
||||
/// <param name="delay">代码延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才执行。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 按照 WPF 方式缩放控件的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。它必须已经拥有了单一的 ScaleTransform 值。</param>
|
||||
/// <param name="value">大小改变的百分比(如-0.6)。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 按照 WPF 方式旋转控件的动画。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。它必须已经拥有了单一的 ScaleTransform 值。</param>
|
||||
/// <param name="value">大小改变的百分比(如-0.6)。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks></remarks>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 利用 TranslateTransform 移动 X 轴的动画,这不会造成布局更新。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">进行移动的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 利用 TranslateTransform 移动 Y 轴的动画,这不会造成布局更新。
|
||||
/// </summary>
|
||||
/// <param name="obj">动画的对象。</param>
|
||||
/// <param name="value">进行移动的值。</param>
|
||||
/// <param name="time">动画长度(毫秒)。</param>
|
||||
/// <param name="delay">动画延迟执行的时间(毫秒)。</param>
|
||||
/// <param name="ease">插值器类型。</param>
|
||||
/// <param name="after">是否等到以前的动画完成后才继续本动画。</param>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// 特殊
|
||||
|
||||
/// <summary>
|
||||
/// 将一个StackPanel中的各个项目依次显示。
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public static List<AniData> AaStack(StackPanel stack, int time = 100, int delay = 25)
|
||||
{
|
||||
List<AniData> aaStackRet = default;
|
||||
aaStackRet = new List<AniData>();
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 缓动函数基类。
|
||||
/// </summary>
|
||||
public abstract class AniEase
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取函数值。
|
||||
/// </summary>
|
||||
/// <param name="t">时间百分比。</param>
|
||||
public abstract double GetValue(double t);
|
||||
|
||||
/// <summary>
|
||||
/// 获取增量值。
|
||||
/// </summary>
|
||||
/// <param name="t1">较大的 X。</param>
|
||||
/// <param name="t0">较小的 X。</param>
|
||||
public virtual double GetDelta(double t1, double t0)
|
||||
{
|
||||
return GetValue(t1) - GetValue(t0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 渐入渐出组合。
|
||||
/// </summary>
|
||||
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 / 线性
|
||||
/// <summary>
|
||||
/// 线性,无缓动。
|
||||
/// </summary>
|
||||
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 / 平滑
|
||||
/// <summary>
|
||||
/// 平滑开始。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平滑结束。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平滑开始与结束。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以特定速度开始的平滑结束。
|
||||
/// </summary>
|
||||
public class AniEaseOutFluentWithInitial : AniEase
|
||||
{
|
||||
private readonly double alpha; // (初速度 / 平均速度) – 1
|
||||
|
||||
/// <param name="initialPixelPerSecond">初速度,px/s</param>
|
||||
/// <param name="totalSecond">总时长,s</param>
|
||||
/// <param name="totalDistance">总路程,px</param>
|
||||
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 / 回弹
|
||||
/// <summary>
|
||||
/// 回弹开始。有效时间为 1/3。
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 回弹结束。有效时间为 1/3。
|
||||
/// </summary>
|
||||
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 / 平滑-回弹
|
||||
/// <summary>
|
||||
/// 回弹开始,短平滑结束。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 短平滑开始,回弹结束。
|
||||
/// </summary>
|
||||
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 / 弹簧
|
||||
/// <summary>
|
||||
/// 弹簧开始。约在 60% 到达最小值。
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹簧结束。约在 40% 到达最大值。
|
||||
/// </summary>
|
||||
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 接口(开始、中断、检测)
|
||||
|
||||
/// <summary>
|
||||
/// 开始一个动画组。
|
||||
/// </summary>
|
||||
/// <param name="aniGroup">由 Aa 开头的函数初始化的 AniData 对象集合。</param>
|
||||
/// <param name="name">动画组的名称。如果重复会直接停止同名动画组。</param>
|
||||
public static void AniStart(IList aniGroup, string name = "", bool refreshTime = false)
|
||||
{
|
||||
if (refreshTime)
|
||||
aniLastTick = TimeUtils.GetTimeTick(); // 避免处理动画时已经造成了极大的延迟,导致动画突然结束
|
||||
// 添加到正在执行的动画组
|
||||
var newEntry = new AniGroupEntry
|
||||
{ data = ModBase.GetFullList<AniData>(aniGroup), startTick = TimeUtils.GetTimeTick() };
|
||||
if (string.IsNullOrEmpty(name))
|
||||
name = newEntry.Uuid.ToString();
|
||||
else
|
||||
AniStop(name);
|
||||
aniGroups.TryAdd(name, newEntry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始一个动画组。
|
||||
/// </summary>
|
||||
public static void AniStart(AniData aniGroup, string name = "", bool refreshTime = false)
|
||||
{
|
||||
AniStart(new List<AniData> { aniGroup }, name, refreshTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接停止一个动画组。
|
||||
/// </summary>
|
||||
/// <param name="name">需要停止的动画组的名称。</param>
|
||||
public static void AniStop(string name)
|
||||
{
|
||||
aniGroups.Remove(name, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取动画是否正在进行中。
|
||||
/// </summary>
|
||||
public static bool AniIsRun(string name)
|
||||
{
|
||||
return aniGroups.ContainsKey(name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,3759 @@
|
||||
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 PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
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 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
|
||||
/// <summary>
|
||||
/// 主窗口句柄。
|
||||
/// </summary>
|
||||
public static nint frmHandle;
|
||||
|
||||
// 龙猫味石山小记: 用最不靠谱的实现写出能跑的代码 (AppDomain.CurrentDomain.SetupInformation.ApplicationBase 获取到的是当前工作目录而不是可执行文件所在目录)
|
||||
/// <summary>
|
||||
/// 程序可执行文件所在目录,以“\”结尾。
|
||||
/// </summary>
|
||||
public static readonly string exePath = (Basics.ExecutableDirectory.EndsWith(@"\")
|
||||
? Basics.ExecutableDirectory
|
||||
: Basics.ExecutableDirectory + @"\");
|
||||
|
||||
/// <summary>
|
||||
/// 程序内嵌图片文件夹路径,以“/”结尾。
|
||||
/// </summary>
|
||||
public static readonly string pathImage = "pack://application:,,,/Plain Craft Launcher 2;component/Images/";
|
||||
|
||||
/// <summary>
|
||||
/// 当前程序的语言。
|
||||
/// </summary>
|
||||
public static string currentLang = "zh_CN";
|
||||
|
||||
/// <summary>
|
||||
/// 设置对象。
|
||||
/// </summary>
|
||||
public static ModSetup setup = new();
|
||||
|
||||
/// <summary>
|
||||
/// 程序的打开计时。
|
||||
/// </summary>
|
||||
public static long applicationStartTick = TimeUtils.GetTimeTick();
|
||||
|
||||
/// <summary>
|
||||
/// 程序打开时的时间。
|
||||
/// </summary>
|
||||
public static DateTime applicationOpenTime = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 程序是否已结束。
|
||||
/// </summary>
|
||||
public static bool isProgramEnded = false;
|
||||
|
||||
/// <summary>
|
||||
/// 程序的缓存文件夹路径,以 \ 结尾。
|
||||
/// </summary>
|
||||
public static string pathTemp = Paths.Temp + @"\";
|
||||
|
||||
/// <summary>
|
||||
/// AppData 中的 PCL 文件夹路径,以 \ 结尾。
|
||||
/// </summary>
|
||||
public static string pathAppdata = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PCL") + @"\";
|
||||
|
||||
/// <summary>
|
||||
/// AppData 中的 PCLCE 配置文件夹路径,以 \ 结尾。
|
||||
/// </summary>
|
||||
public static string pathAppdataConfig = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) +
|
||||
(versionBranchName == "Debug" ? @"\.pclcedebug\" : @"\.pclce\");
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region 自定义类
|
||||
|
||||
/// <summary>
|
||||
/// 支持小数与常见类型隐式转换的颜色。
|
||||
/// </summary>
|
||||
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 = Convert.ToDouble(((dynamic)obj).A);
|
||||
r = Convert.ToDouble(((dynamic)obj).R);
|
||||
g = Convert.ToDouble(((dynamic)obj).G);
|
||||
b = Convert.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 is null && b is null)
|
||||
return true;
|
||||
if (a is null || b is 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 is null && b is null)
|
||||
return false;
|
||||
if (a is null || b is 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 obj is MyColor other && a == other.a && r == other.r && g == other.g && b == other.b;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 支持负数与浮点数的矩形。
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模块加载状态枚举。
|
||||
/// </summary>
|
||||
public enum LoadState
|
||||
{
|
||||
Waiting,
|
||||
Loading,
|
||||
Finished,
|
||||
Failed,
|
||||
Aborted
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行返回值。
|
||||
/// </summary>
|
||||
public enum ProcessReturnValues
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行成功,或进程被中断。
|
||||
/// </summary>
|
||||
Aborted = -1,
|
||||
|
||||
/// <summary>
|
||||
/// 执行成功。
|
||||
/// </summary>
|
||||
Success = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 执行失败。
|
||||
/// </summary>
|
||||
Fail = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 执行时出现未经处理的异常。
|
||||
/// </summary>
|
||||
Exception = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 执行超时。
|
||||
/// </summary>
|
||||
Timeout = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 取消执行。可能是由于不满足执行的前置条件。
|
||||
/// </summary>
|
||||
Cancel = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 任务成功完成。
|
||||
/// </summary>
|
||||
TaskDone = 5
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可以使用 Equals 和等号的 List。
|
||||
/// </summary>
|
||||
public class EqualableList<T> : List<T>
|
||||
{
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj as List<T> is null)
|
||||
// 类型不同
|
||||
return false;
|
||||
|
||||
// 类型相同
|
||||
var objList = (List<T>)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<T> left, EqualableList<T> right)
|
||||
{
|
||||
return EqualityComparer<EqualableList<T>>.Default.Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(EqualableList<T> left, EqualableList<T> right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 数学
|
||||
|
||||
/// <summary>
|
||||
/// 2~65 进制的转换。
|
||||
/// </summary>
|
||||
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(l.ToString())))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算二阶贝塞尔曲线。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将一个数字限制为 0~255 的 Byte 值。
|
||||
/// </summary>
|
||||
public static byte MathByte(double d)
|
||||
{
|
||||
if (d < 0d)
|
||||
d = 0d;
|
||||
if (d > 255d)
|
||||
d = 255d;
|
||||
return (byte)Math.Round(Math.Round(d));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供 MyColor 类型支持的 Math.Round。
|
||||
/// </summary>
|
||||
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) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取两数间的百分比。小数点精确到 6 位。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static double MathPercent(double valueA, double valueB, double percent)
|
||||
{
|
||||
return Math.Round(valueA * (1d - percent) + valueB * percent, 6); // 解决 Double 计算错误
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取两颜色间的百分比,根据 RGB 计算。小数点精确到 6 位。
|
||||
/// </summary>
|
||||
public static MyColor MathPercent(MyColor valueA, MyColor valueB, double percent)
|
||||
{
|
||||
return MathRound(valueA * (1d - percent) + valueB * percent, 6); // 解决Double计算错误
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将数值限定在某个范围内。
|
||||
/// </summary>
|
||||
public static double MathClamp(double value, double min, double max)
|
||||
{
|
||||
return Math.Max(min, Math.Min(max, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 符号函数。
|
||||
/// </summary>
|
||||
public static int MathSgn(double value)
|
||||
{
|
||||
if (value == 0d) return 0;
|
||||
|
||||
if (value > 0d) return 1;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 文件
|
||||
|
||||
// =============================
|
||||
// ini
|
||||
// =============================
|
||||
|
||||
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> iniCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// 清除某 ini 文件的运行时缓存。
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。</param>
|
||||
public static void IniClearCache(string fileName)
|
||||
{
|
||||
if (!fileName.Contains(@":\"))
|
||||
fileName = $@"{exePath}PCL\{fileName}.ini";
|
||||
if (iniCache.ContainsKey(fileName))
|
||||
iniCache.Remove(fileName, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 ini 文件缓存。如果没有,则新读取 ini 文件内容。
|
||||
/// 在文件不存在或读取失败时返回 Nothing。
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。</param>
|
||||
private static ConcurrentDictionary<string, string> 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<string, string>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 ini 文件。这可能会使用到缓存。
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。</param>
|
||||
/// <param name="key">键。</param>
|
||||
/// <param name="defaultValue">没有找到键时返回的默认值。</param>
|
||||
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];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断 ini 文件中是否包含某个键。这可能会使用到缓存。
|
||||
/// </summary>
|
||||
public static bool HasIniKey(string fileName, string key)
|
||||
{
|
||||
var content = IniGetContent(fileName);
|
||||
return content is not null && content.ContainsKey(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 ini 文件中移除某个键。这会更新缓存。
|
||||
/// </summary>
|
||||
public static void DeleteIniKey(string fileName, string key)
|
||||
{
|
||||
WriteIni(fileName, key, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入 ini 文件,这会更新缓存。
|
||||
/// 若 Value 为 Nothing,则删除该键。
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。</param>
|
||||
/// <param name="key">键。</param>
|
||||
/// <param name="value">值。</param>
|
||||
/// <remarks></remarks>
|
||||
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<string, string>();
|
||||
// 更新值
|
||||
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();
|
||||
|
||||
// 路径处理
|
||||
/// <summary>
|
||||
/// 从文件路径或者 Url 获取不包含文件名的路径,或获取文件夹的父文件夹路径。
|
||||
/// 取决于原路径格式,路径以 / 或 \ 结尾。
|
||||
/// 不包含路径将会抛出异常。
|
||||
/// </summary>
|
||||
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 = filePath.Substring(0, filePath.Length - 1);
|
||||
getPathFromFullPathRet = filePath.Substring(0, filePath.LastIndexOfAny(new[] { '\\', '/' })) +
|
||||
(isRight ? @"\" : "/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 是文件路径
|
||||
getPathFromFullPathRet = filePath.Substring(0, filePath.LastIndexOfAny(new[] { '\\', '/' }) + 1);
|
||||
if (string.IsNullOrEmpty(getPathFromFullPathRet))
|
||||
throw new Exception("不包含路径:" + filePath);
|
||||
}
|
||||
|
||||
return getPathFromFullPathRet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件路径或者 Url 获取不包含路径的文件名。不包含文件名将会抛出异常。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件路径或者 Url 获取不包含路径与扩展名的文件名。不包含文件名将会抛出异常。
|
||||
/// </summary>
|
||||
public static string GetFileNameWithoutExtentionFromPath(string filePath)
|
||||
{
|
||||
return Path.GetFileNameWithoutExtension(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件夹路径获取文件夹名。
|
||||
/// </summary>
|
||||
public static string GetFolderNameFromPath(string folderPath)
|
||||
{
|
||||
if (folderPath.EndsWithF(@":\") || folderPath.EndsWithF(@":\\"))
|
||||
return folderPath.Substring(0, 1);
|
||||
if (folderPath.EndsWithF(@"\") || folderPath.EndsWithF("/"))
|
||||
folderPath = folderPath.Substring(0, folderPath.Length - 1);
|
||||
return GetFileNameFromPath(folderPath);
|
||||
}
|
||||
|
||||
// 读取、写入、复制文件
|
||||
/// <summary>
|
||||
/// 复制文件。会自动创建文件夹、会覆盖已有的文件。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取文件,如果失败则返回空数组。
|
||||
/// </summary>
|
||||
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.Read))
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
readStream.CopyTo(ms);
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
Log("[System] 欲读取的文件不存在,已返回空内容:" + filePath);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex, "读取文件出错:" + filePath);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取文件,如果失败则返回空字符串。
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件完整或相对路径。</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取流中的所有文本。
|
||||
/// </summary>
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入文件。
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件完整或相对路径。</param>
|
||||
/// <param name="text">文件内容。</param>
|
||||
/// <param name="append">是否将文件内容追加到当前文件,而不是覆盖它。</param>
|
||||
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
|
||||
{
|
||||
// 直接写入字节
|
||||
var bytes = encoding is null ? new UTF8Encoding(false).GetBytes(text) : encoding.GetBytes(text);
|
||||
var tempPath = filePath + ".pcltmp." + Guid.NewGuid().ToString("N");
|
||||
File.WriteAllBytes(tempPath, bytes);
|
||||
File.Move(tempPath, filePath, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入文件。
|
||||
/// 如果 CanThrow 设置为 False,返回是否写入成功。
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件完整或相对路径。</param>
|
||||
/// <param name="content">文件内容。</param>
|
||||
/// <param name="append">是否将文件内容追加到当前文件,而不是覆盖它。</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将流写入文件。
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件完整或相对路径。</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解码 Bytes。
|
||||
/// </summary>
|
||||
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<byte> bytes)
|
||||
{
|
||||
var sb = new StringBuilder(bytes.Length * 2);
|
||||
foreach (var c in bytes.Span)
|
||||
sb.Append(c.ToString("x2"));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// 文件校验
|
||||
/// <summary>
|
||||
/// 获取文件 MD5,若失败则返回空字符串。
|
||||
/// </summary>
|
||||
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 (string)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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件 SHA512,若失败则返回空字符串。
|
||||
/// </summary>
|
||||
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 (string)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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件 SHA256,若失败则返回空字符串。
|
||||
/// </summary>
|
||||
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 (string)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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件 SHA1,若失败则返回空字符串。
|
||||
/// </summary>
|
||||
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 (string)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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取流的 SHA1,若失败则返回空字符串。
|
||||
/// </summary>
|
||||
public static string GetAuthSHA1(Stream inputStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (string)GetHexString(SHA1Provider.Instance.ComputeHash(inputStream));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex, "获取流 SHA1 失败");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文件的校验规则。
|
||||
/// </summary>
|
||||
public class FileChecker
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件的准确大小。
|
||||
/// 不检查则为 -1。
|
||||
/// </summary>
|
||||
public long actualSize = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 是否可以使用已经存在的文件。
|
||||
/// </summary>
|
||||
public bool canUseExistsFile = true;
|
||||
|
||||
/// <summary>
|
||||
/// 文件的 MD5、SHA1 或 SHA256。会根据输入字符串的长度自动判断种类。
|
||||
/// 不检查则为 Nothing。
|
||||
/// </summary>
|
||||
public string hash;
|
||||
|
||||
/// <summary>
|
||||
/// 是否要求为 JSON 文件。
|
||||
/// 即,开头结尾必须为 {} 或 []。
|
||||
/// </summary>
|
||||
public bool isJson;
|
||||
|
||||
/// <summary>
|
||||
/// 文件的最小大小。
|
||||
/// 不检查则为 -1。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查文件。若成功则返回 Nothing,失败则返回错误的描述文本,描述文本不以句号结尾。不会抛出错误。
|
||||
/// </summary>
|
||||
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<string>();
|
||||
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(Lang.Text("Common.Error.InvalidJson"), ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.Count != 0)
|
||||
{
|
||||
errorMessage.Insert(0, $"实际校验地址:{localPath}");
|
||||
return errorMessage.Join(";");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex, "检查文件出错");
|
||||
return ex.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待文件就绪可读,在指定超时时间内轮询检查文件是否存在且内容非空。
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径。</param>
|
||||
/// <param name="timeoutMs">超时时间(毫秒)。</param>
|
||||
public static void WaitForFileReady(string filePath, int timeoutMs = 2000)
|
||||
{
|
||||
WaitForFileReady(filePath, timeoutMs, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待文件就绪可读,在指定超时时间内轮询检查文件是否存在且内容非空。
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径。</param>
|
||||
/// <param name="timeoutMs">超时时间(毫秒)。</param>
|
||||
/// <param name="requireJson">是否要求文件为合法 JSON。</param>
|
||||
public static void WaitForFileReady(string filePath, int timeoutMs, bool requireJson)
|
||||
{
|
||||
filePath = filePath.Contains(@":\") ? filePath : exePath + filePath;
|
||||
var start = Environment.TickCount;
|
||||
long lastSize = -1;
|
||||
while (Environment.TickCount - start < timeoutMs)
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = new FileInfo(filePath);
|
||||
var size = info.Length;
|
||||
if (size <= 0)
|
||||
continue;
|
||||
if (!requireJson)
|
||||
{
|
||||
if (size == lastSize)
|
||||
return;
|
||||
lastSize = size;
|
||||
}
|
||||
else
|
||||
{
|
||||
var content = ReadFile(filePath);
|
||||
if (!string.IsNullOrEmpty(content) && content.Trim().StartsWith("{"))
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 Jar 以 zip 方式解压。
|
||||
/// 会尝试创建,但不会清空目标文件夹。
|
||||
/// </summary>
|
||||
public static void ExtractFile(string compressFilePath, string destDirectory, Encoding encode = null,
|
||||
Action<double> progressIncrementHandler = null)
|
||||
{
|
||||
Directory.CreateDirectory(destDirectory);
|
||||
destDirectory = Path.GetFullPath(destDirectory);
|
||||
if (!destDirectory.EndsWith(Path.DirectorySeparatorChar.ToString()))
|
||||
destDirectory += Path.DirectorySeparatorChar.ToString();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件夹,返回删除的文件个数。通过参数选择是否抛出异常。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复制文件夹,失败会抛出异常。
|
||||
/// </summary>
|
||||
public static void CopyDirectory(string fromPath, string toPath, Action<double> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 遍历文件夹中的所有文件。
|
||||
/// </summary>
|
||||
public static IEnumerable<FileInfo> EnumerateFiles(string directory)
|
||||
{
|
||||
var info = new DirectoryInfo(ShortenPath(directory));
|
||||
if (!info.Exists)
|
||||
return new List<FileInfo>();
|
||||
return info.EnumerateFiles("*", SearchOption.AllDirectories);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 若路径长度大于指定值,则将长路径转换为短路径。
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// 返回一个枚举对应的字符串。
|
||||
/// </summary>
|
||||
/// <param name="enumData">一个已经实例化的枚举类型。</param>
|
||||
public static string GetStringFromEnum(Enum enumData)
|
||||
{
|
||||
return Enum.GetName(enumData.GetType(), enumData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将文件大小转化为适合的文本形式,如“1.28 M”。
|
||||
/// </summary>
|
||||
/// <param name="fileSize">以字节为单位的大小表示。</param>
|
||||
public static string GetString(long fileSize)
|
||||
{
|
||||
return ByteStream.GetReadableLength(fileSize, provider: Lang.Culture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 JSON 对象。
|
||||
/// </summary>
|
||||
public static JsonNode GetJson(string data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonCompat.ParseNode(data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dataText = data ?? "";
|
||||
var length = dataText.Length;
|
||||
throw new Exception("格式化 JSON 失败:" + (length > 2000
|
||||
? dataText.Substring(0, 500) + $"...(全长 {length} 个字符)..." + dataText.Substring(length - 500)
|
||||
: dataText), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将第一个字符转换为大写,其余字符转换为小写。
|
||||
/// </summary>
|
||||
public static string Capitalize(this string word)
|
||||
{
|
||||
if (string.IsNullOrEmpty(word))
|
||||
return word;
|
||||
return word.Substring(0, 1).ToUpperInvariant() + word.Substring(1).ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将字符串统一至某个长度,过短则以 Code 将其右侧填充,过长则截取靠左的指定长度。
|
||||
/// </summary>
|
||||
public static string StrFill(string str, string code, byte length)
|
||||
{
|
||||
if (str.Length > length)
|
||||
return str.Substring(0, length);
|
||||
return str.PadRight(length, code[0]).Substring(str.Length) + str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将一个小数显示为固定的小数点后位数形式,将向零取整。
|
||||
/// 如 12 保留 2 位则输出 12.00,而 95.678 保留 2 位则输出 95.67。
|
||||
/// </summary>
|
||||
public static string StrFillNum(double num, int length)
|
||||
{
|
||||
return Lang.Number(num, $"F{length}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除字符串首尾的标点符号、回车,以及括号中、冒号后的补充说明内容。
|
||||
/// </summary>
|
||||
public static object StrTrim(string str, bool removeQuote = true)
|
||||
{
|
||||
if (removeQuote)
|
||||
str = str.Split("(")[0].Split(":")[0].Split("(")[0].Split(":")[0];
|
||||
return str.Trim('.', '。', '!', ' ', '!', '?', '?', '\r',
|
||||
'\n');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接字符串。
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分割字符串。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串哈希值。
|
||||
/// </summary>
|
||||
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 ^ str[i];
|
||||
return getHashRet ^ 0xA98F501BC684032FUL;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串 MD5。
|
||||
/// </summary>
|
||||
public static string GetStringMD5(string str)
|
||||
{
|
||||
return (string)GetHexString(MD5Provider.Instance.ComputeHash(str));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查字符串中的字符是否均为 ASCII 字符。
|
||||
/// </summary>
|
||||
public static bool IsASCII(this string input)
|
||||
{
|
||||
return input.All(c => c < 128);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取在子字符串第一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024。
|
||||
/// 如果未找到子字符串则不裁切。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取在子字符串最后一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024/11。
|
||||
/// 如果未找到子字符串则不裁切。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取在子字符串第一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 11/08。
|
||||
/// 如果未找到子字符串则不裁切。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取在子字符串最后一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 08。
|
||||
/// 如果未找到子字符串则不裁切。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取处于两个子字符串之间的部分,裁切尽可能多的内容。
|
||||
/// 等效于 AfterLast 后接 BeforeFirst。
|
||||
/// 如果未找到子字符串则不裁切。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高速的 StartsWith。
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool StartsWithF(this string str, string prefix, bool ignoreCase = false)
|
||||
{
|
||||
return str.StartsWith(prefix, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高速的 EndsWith。
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool EndsWithF(this string str, string suffix, bool ignoreCase = false)
|
||||
{
|
||||
return str.EndsWith(suffix, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 支持可变大小写判断的 Contains。
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高速的 IndexOf。
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int IndexOfF(this string str, string subStr, bool ignoreCase = false)
|
||||
{
|
||||
return str.IndexOf(subStr, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高速的 IndexOf。
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高速的 LastIndexOf。
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int LastIndexOfF(this string str, string subStr, bool ignoreCase = false)
|
||||
{
|
||||
return str.LastIndexOf(subStr, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高速的 LastIndexOf。
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 不会报错的 Val。
|
||||
/// 如果输入有误,返回 0。
|
||||
/// </summary>
|
||||
public static double Val(object str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return str is "&" ? 0d : Conversion.Val(str);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
}
|
||||
|
||||
// 转义
|
||||
/// <summary>
|
||||
/// 为字符串进行 XML 转义。
|
||||
/// </summary>
|
||||
public static string EscapeXML(string str)
|
||||
{
|
||||
if (str.StartsWithF("{"))
|
||||
str = "{}" + str; // #4187
|
||||
return str.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("'", "'")
|
||||
.Replace("\"", """).Replace("\r\n", "
");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为字符串进行 Like 关键字转义。
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
// 正则
|
||||
/// <summary>
|
||||
/// 搜索字符串中的所有正则匹配项。
|
||||
/// </summary>
|
||||
public static List<string> RegexSearch(this string str, string regex, RegexOptions options = RegexOptions.None)
|
||||
{
|
||||
List<string> regexSearchRet = default;
|
||||
try
|
||||
{
|
||||
regexSearchRet = new List<string>();
|
||||
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<string>();
|
||||
}
|
||||
|
||||
return regexSearchRet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 搜索字符串中的所有正则匹配项。
|
||||
/// </summary>
|
||||
/// <param name="str">要搜索的字符串</param>
|
||||
/// <param name="regex">正则表达式对象</param>
|
||||
/// <returns>所有匹配项的列表</returns>
|
||||
public static List<string> RegexSearch(this string str, Regex regex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = new List<string>();
|
||||
foreach (Match item in regex.Matches(str))
|
||||
{
|
||||
result.Add(item.Value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex, "正则匹配全部项出错");
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串中的第一个正则匹配项,若无匹配则返回 Nothing。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串中的第一个正则匹配项,若无匹配则返回 Nothing。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查字符串是否匹配某正则模式。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进行正则替换,会抛出错误。
|
||||
/// </summary>
|
||||
public static string RegexReplace(this string allContents, string searchRegex, string replaceTo,
|
||||
RegexOptions options = RegexOptions.None)
|
||||
{
|
||||
return Regex.Replace(allContents, searchRegex, replaceTo, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对每个正则匹配分别进行替换,会抛出错误。
|
||||
/// </summary>
|
||||
public static string RegexReplaceEach(this string allContents, string searchRegex, MatchEvaluator replaceTo,
|
||||
RegexOptions options = RegexOptions.None)
|
||||
{
|
||||
return Regex.Replace(allContents, searchRegex, replaceTo, options);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 搜索
|
||||
|
||||
/// <summary>
|
||||
/// 获取搜索文本的相似度。
|
||||
/// </summary>
|
||||
/// <param name="source">被搜索的长内容。</param>
|
||||
/// <param name="query">用户输入的搜索文本。</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取多段文本加权后的相似度。
|
||||
/// </summary>
|
||||
private static double SearchSimilarityWeighted(List<SearchSource> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于搜索的项目。
|
||||
/// </summary>
|
||||
public class SearchEntry<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否完全匹配。
|
||||
/// </summary>
|
||||
public bool absoluteRight;
|
||||
|
||||
/// <summary>
|
||||
/// 该项目对应的源数据。
|
||||
/// </summary>
|
||||
public T item;
|
||||
|
||||
/// <summary>
|
||||
/// 该项目用于搜索的文本源。
|
||||
/// 在搜索时,会对每个文本源单独加权,但单个文本源内的多个别名只取最高的一个的相似度。
|
||||
/// </summary>
|
||||
public List<SearchSource> searchSource;
|
||||
|
||||
/// <summary>
|
||||
/// 相似度。
|
||||
/// </summary>
|
||||
public double similarity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个用于搜索的文本源。
|
||||
/// </summary>
|
||||
public class SearchSource
|
||||
{
|
||||
public string[] aliases;
|
||||
public double weight;
|
||||
|
||||
public SearchSource(string[] aliases, double weight = 1)
|
||||
{
|
||||
this.aliases = aliases;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
public SearchSource(string text, double weight = 1)
|
||||
{
|
||||
aliases = new[] { text };
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地搜索返回的最大模糊结果数。
|
||||
/// </summary>
|
||||
public const int MaxLocalSearchDepth = 25;
|
||||
|
||||
/// <summary>
|
||||
/// 进行多段文本加权搜索,获取相似度较高的数项结果。
|
||||
/// </summary>
|
||||
/// <param name="maxBlurCount">返回的最大模糊结果数。</param>
|
||||
/// <param name="minBlurSimilarity">返回结果要求的最低相似度。</param>
|
||||
public static List<SearchEntry<T>> Search<T>(List<SearchEntry<T>> entries, string query, int maxBlurCount = 5,
|
||||
double minBlurSimilarity = 0.1d)
|
||||
{
|
||||
var resultList = new List<SearchEntry<T>>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 线程安全的 List。
|
||||
/// 通过在 For Each 循环中使用一个浅表副本规避多线程操作或移除自身导致的异常。
|
||||
/// </summary>
|
||||
public class SafeList<T> : IEnumerable<T>, IDisposable, ICollection<T>
|
||||
{
|
||||
private readonly List<T> _internalList;
|
||||
private readonly ReaderWriterLockSlim _lock = new();
|
||||
|
||||
public SafeList()
|
||||
{
|
||||
_internalList = new List<T>();
|
||||
}
|
||||
|
||||
public SafeList(IEnumerable<T> data)
|
||||
{
|
||||
_internalList = new List<T>(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<T>)_internalList).IsReadOnly;
|
||||
|
||||
public bool Contains(T item)
|
||||
{
|
||||
return ((ICollection<T>)_internalList).Contains(item);
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
((ICollection<T>)_internalList).CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_lock.Dispose();
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return ToList().GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public List<T> ToList()
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return _internalList.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_internalList.RemoveAt(index);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可用于临时存放文件的,不含任何特殊字符的文件夹路径,以“\”结尾。
|
||||
/// </summary>
|
||||
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 Path.Combine(SystemPaths.DriveLetter, "ProgramData", "PCL");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指示接取到这个异常的函数进行重试。
|
||||
/// </summary>
|
||||
public class RestartException : Exception
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指示用户手动取消了操作,或用户已知晓操作被取消的原因。
|
||||
/// </summary>
|
||||
public class CancelledException : Exception
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断对象是否为某个泛型类型的实例。
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个全程序内不会重复的数字(伪 Uuid)。
|
||||
/// </summary>
|
||||
public static int GetUuid()
|
||||
{
|
||||
if (uuidLock is null)
|
||||
uuidLock = new object();
|
||||
lock (uuidLock)
|
||||
{
|
||||
uuid += 1;
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将元素与 List 的混合体拆分为元素组。
|
||||
/// </summary>
|
||||
public static List<T> GetFullList<T>(IList data)
|
||||
{
|
||||
List<T> getFullListRet = default;
|
||||
getFullListRet = new List<T>();
|
||||
for (int i = 0, loopTo = data.Count - 1; i <= loopTo; i++)
|
||||
if (data[i] is ICollection)
|
||||
getFullListRet.AddRange((IEnumerable<T>)data[i]);
|
||||
else
|
||||
getFullListRet.Add((T)data[i]);
|
||||
|
||||
return getFullListRet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数组去重。
|
||||
/// </summary>
|
||||
public static List<T> Distinct<T>(this ICollection<T> arr, ComparisonBoolean<T> isEqual)
|
||||
{
|
||||
var resultArray = new List<T>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对集合的每个元素执行指定操作。
|
||||
/// </summary>
|
||||
public static IEnumerable<T> ForEach<T>(this IEnumerable<T> collection, Action<T> action)
|
||||
{
|
||||
foreach (var item in collection)
|
||||
action(item);
|
||||
return collection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于储存 RaiseByMouse 的 EventArgs。
|
||||
/// </summary>
|
||||
public sealed class RouteEventArgs : EventArgs
|
||||
{
|
||||
public bool handled = false;
|
||||
public bool raiseByMouse;
|
||||
|
||||
public RouteEventArgs(bool raiseByMouse = false)
|
||||
{
|
||||
this.raiseByMouse = raiseByMouse;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 前台运行文件。
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名。可以为“notepad”等缩写。</param>
|
||||
/// <param name="arguments">运行参数。</param>
|
||||
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,
|
||||
userSummary: Lang.Text("SystemDialog.File.OpenFailed.Message", fileName));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 前台运行文件并返回返回值。
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名。可以为“notepad”等缩写。</param>
|
||||
/// <param name="arguments">运行参数。</param>
|
||||
/// <param name="timeout">等待该程序结束的最长时间(毫秒)。超时会返回 Result.Timeout。</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 静默运行文件并返回输出流字符串。执行失败会抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名。可以为“notepad”等缩写。</param>
|
||||
/// <param name="arguments">运行参数。</param>
|
||||
/// <param name="timeout">等待该程序结束的最长时间(毫秒)。超时会抛出错误。</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在新的工作线程中执行代码。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保在 UI 线程中执行代码。
|
||||
/// 如果当前并非 UI 线程,则会阻断当前线程,直至 UI 线程执行完毕。
|
||||
/// 为防止线程互锁,请仅在开始加载动画、从 UI 获取输入时使用!
|
||||
/// </summary>
|
||||
public static Output RunInUiWait<Output>(Func<Output> action)
|
||||
{
|
||||
if (RunInUi()) return action();
|
||||
|
||||
return System.Windows.Application.Current.Dispatcher.Invoke(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保在 UI 线程中执行代码。
|
||||
/// 如果当前并非 UI 线程,则会阻断当前线程,直至 UI 线程执行完毕。
|
||||
/// 为防止线程互锁,请仅在开始加载动画、从 UI 获取输入时使用!
|
||||
/// </summary>
|
||||
public static void RunInUiWait(Action action)
|
||||
{
|
||||
if (System.Windows.Application.Current is null)
|
||||
return;
|
||||
if (RunInUi())
|
||||
action();
|
||||
else
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保在 UI 线程中执行代码,代码按触发顺序执行。
|
||||
/// 如果当前并非 UI 线程,也不阻断当前线程的执行。
|
||||
/// </summary>
|
||||
public static void RunInUi(Action action, bool forceWaitUntilLoaded = false)
|
||||
{
|
||||
if (System.Windows.Application.Current is null)
|
||||
return;
|
||||
if (RunInUi())
|
||||
action();
|
||||
else
|
||||
System.Windows.Application.Current.Dispatcher.InvokeAsync(action,
|
||||
forceWaitUntilLoaded ? DispatcherPriority.Loaded : DispatcherPriority.Normal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保在工作线程中执行代码。
|
||||
/// </summary>
|
||||
public static void RunInThread(Action action)
|
||||
{
|
||||
if (RunInUi())
|
||||
RunInNewThread(action, "Runtime Invoke " + GetUuid() + "#");
|
||||
else
|
||||
action();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用优化的归并排序算法进行稳定排序。
|
||||
/// </summary>
|
||||
/// <param name="sortRule">传入两个对象,若第一个对象应该排在前面,则返回 True。</param>
|
||||
public static List<T> Sort<T>(this IList<T> list, ComparisonBoolean<T> sortRule)
|
||||
{
|
||||
// 创建原列表的副本以避免修改原始列表
|
||||
var tempList = new List<T>(list);
|
||||
if (tempList.Count <= 1)
|
||||
return tempList;
|
||||
|
||||
// 使用归并排序核心算法
|
||||
MergeSort_Sort(ref tempList, 0, tempList.Count - 1, sortRule);
|
||||
return tempList;
|
||||
}
|
||||
|
||||
private static void MergeSort_Sort<T>(ref List<T> array, int left, int right, ComparisonBoolean<T> comparator)
|
||||
{
|
||||
if (left >= right)
|
||||
return;
|
||||
|
||||
var mid = (left + right) / 2;
|
||||
MergeSort_Sort(ref array, left, mid, comparator);
|
||||
MergeSort_Sort(ref array, mid + 1, right, comparator);
|
||||
MergeSort_Merge(ref array, left, mid, right, comparator);
|
||||
}
|
||||
|
||||
private static void MergeSort_Merge<T>(ref List<T> array, int left, int mid, int right,
|
||||
ComparisonBoolean<T> comparator)
|
||||
{
|
||||
var leftArray = new List<T>();
|
||||
var rightArray = new List<T>();
|
||||
|
||||
for (int i = left, loopTo = mid; i <= loopTo; i++)
|
||||
leftArray.Add(array[i]);
|
||||
|
||||
for (int j = mid + 1, loopTo1 = right; j <= loopTo1; j++)
|
||||
rightArray.Add(array[j]);
|
||||
|
||||
var leftPtr = 0;
|
||||
var rightPtr = 0;
|
||||
var current = left;
|
||||
|
||||
while (leftPtr < leftArray.Count && rightPtr < rightArray.Count)
|
||||
{
|
||||
// 保持稳定性的关键比较逻辑:当相等时优先取左数组元素
|
||||
if (comparator(leftArray[leftPtr], rightArray[rightPtr]))
|
||||
{
|
||||
array[current] = leftArray[leftPtr];
|
||||
leftPtr += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
array[current] = rightArray[rightPtr];
|
||||
rightPtr += 1;
|
||||
}
|
||||
|
||||
current += 1;
|
||||
}
|
||||
|
||||
while (leftPtr < leftArray.Count)
|
||||
{
|
||||
array[current] = leftArray[leftPtr];
|
||||
leftPtr += 1;
|
||||
current += 1;
|
||||
}
|
||||
|
||||
while (rightPtr < rightArray.Count)
|
||||
{
|
||||
array[current] = rightArray[rightPtr];
|
||||
rightPtr += 1;
|
||||
current += 1;
|
||||
}
|
||||
}
|
||||
|
||||
public delegate bool ComparisonBoolean<T>(T left, T right);
|
||||
|
||||
/// <summary>
|
||||
/// 返回列表的浅表副本。
|
||||
/// </summary>
|
||||
public static IList<T> Clone<T>(this IList<T> list)
|
||||
{
|
||||
return new List<T>(list);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从字典中获取某项,如果该项不存在,则返回默认值。
|
||||
/// </summary>
|
||||
public static TValue GetOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key,
|
||||
TValue defaultValue = default)
|
||||
{
|
||||
if (dict.ContainsKey(key)) return dict[key];
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将某项添加到以列表作为值的字典中。
|
||||
/// </summary>
|
||||
public static void AddToList<TKey, TValue>(this Dictionary<TKey, List<TValue>> dict, TKey key, TValue value)
|
||||
{
|
||||
if (dict.ContainsKey(key))
|
||||
dict[key].Add(value);
|
||||
else
|
||||
dict.Add(key, new List<TValue> { value });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取程序启动参数。
|
||||
/// </summary>
|
||||
/// <param name="name">参数名。</param>
|
||||
/// <param name="defaultValue">默认值。</param>
|
||||
public static object GetProgramArgument(string name, object defaultValue = null)
|
||||
{
|
||||
var allArguments = Interaction.Command().Split(" ");
|
||||
for (int i = 0, loopTo = allArguments.Length - 1; i <= loopTo; i++)
|
||||
if ((allArguments[i] ?? "") == ("-" + name ?? ""))
|
||||
{
|
||||
if (allArguments.Length == i + 1 || allArguments[i + 1].StartsWithF("-"))
|
||||
return true;
|
||||
return allArguments[i + 1];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开网页。
|
||||
/// </summary>
|
||||
public static void OpenWebsite(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!url.StartsWithF("http", true) && !url.StartsWithF("minecraft://", true))
|
||||
throw new Exception(url + " 不是一个有效的网址,它必须以 http 开头!");
|
||||
Log("[System] 正在打开网页:" + url);
|
||||
var psi = new ProcessStartInfo(url)
|
||||
{
|
||||
UseShellExecute = true,
|
||||
};
|
||||
Process.Start(psi);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex, "无法打开网页(" + url + ")");
|
||||
ClipboardSet(url, false);
|
||||
var message = ExceptionDetails.Compose(
|
||||
Lang.Text("SystemDialog.Browser.OpenFailed.Message", url),
|
||||
ex);
|
||||
ModMain.MyMsgBox(
|
||||
message,
|
||||
Lang.Text("SystemDialog.Browser.OpenFailed.Title"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开 explorer。
|
||||
/// 若不以 \ 结尾,则将视作文件路径,打开并选中此文件。
|
||||
/// </summary>
|
||||
public static void OpenExplorer(string location)
|
||||
{
|
||||
try
|
||||
{
|
||||
location = ShortenPath(location.Replace("/", @"\").Trim(' ', '"'));
|
||||
Log("[System] 正在打开资源管理器:" + location);
|
||||
if (location.EndsWithF(@"\"))
|
||||
ShellOnly(location);
|
||||
else
|
||||
ShellOnly("explorer", $"/select,\"{location}\"");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(
|
||||
ex,
|
||||
"打开资源管理器失败,请尝试关闭安全软件(如 360 安全卫士)",
|
||||
LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("SystemDialog.Folder.OpenFailed.Message", location));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置剪贴板。将在另一线程运行,且不会抛出异常。
|
||||
/// </summary>
|
||||
public static void ClipboardSet(string text, bool showSuccessHint = true)
|
||||
{
|
||||
RunInThread(() =>
|
||||
{
|
||||
var success = false;
|
||||
|
||||
for (var attempt = 0; attempt <= 5; attempt++)
|
||||
try
|
||||
{
|
||||
RunInUi(() => Clipboard.SetText(text));
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) when (attempt < 5)
|
||||
{
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
catch (Exception finalEx)
|
||||
{
|
||||
Log(
|
||||
finalEx,
|
||||
"剪贴板被占用,文本复制失败",
|
||||
LogLevel.Hint,
|
||||
userSummary: Lang.Text("Common.Hint.CopyFailed"));
|
||||
}
|
||||
|
||||
if (success && showSuccessHint)
|
||||
RunInUi(() => HintService.Hint(Lang.Text("Common.Hint.Copied"), HintType.Success));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从剪切板粘贴文件或文件夹
|
||||
/// </summary>
|
||||
/// <param name="dest">目标文件夹</param>
|
||||
/// <param name="copyFile">是否粘贴文件</param>
|
||||
/// <param name="copyDir">是否粘贴文件夹</param>
|
||||
/// <returns>总共粘贴的数量</returns>
|
||||
public static int PasteFileFromClipboard(string dest, bool copyFile = true, bool copyDir = true)
|
||||
{
|
||||
Log("[System] 从剪贴板粘贴文件到:" + dest);
|
||||
try
|
||||
{
|
||||
var files = Clipboard.GetFileDropList();
|
||||
if (files.Count.Equals(0))
|
||||
{
|
||||
Log("[System] 剪贴板内无文件可粘贴");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var copiedFiles = 0;
|
||||
var copiedFolders = 0;
|
||||
foreach (var i in files)
|
||||
{
|
||||
if (copyFile && File.Exists(i)) // 文件
|
||||
try
|
||||
{
|
||||
var thisDest = dest + GetFileNameFromPath(i);
|
||||
if (File.Exists(thisDest))
|
||||
{
|
||||
Log("[System] 已存在同名文件:" + thisDest);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Copy(i, thisDest);
|
||||
copiedFiles += 1;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex, "[System] 复制文件时出错");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (copyDir && Directory.Exists(i)) // 文件夹
|
||||
try
|
||||
{
|
||||
var thisDest = dest + GetFolderNameFromPath(i);
|
||||
if (Directory.Exists(thisDest))
|
||||
{
|
||||
Log("[System] 已存在同名文件夹:" + thisDest);
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyDirectory(i, thisDest);
|
||||
copiedFolders += 1;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex, "[System] 复制文件时出错");
|
||||
}
|
||||
}
|
||||
|
||||
HintService.Hint(Lang.Text("Common.Hint.FilesPasted", copiedFiles, copiedFolders));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex, "[System] 从剪切板粘贴文件失败", LogLevel.Hint);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取程序打包资源的输入流。该资源必须声明为 <c>Resource</c> 类型,否则将会报错,<c>Images</c>
|
||||
/// 和 <c>Resources</c> 目录已默认声明该类型。
|
||||
/// </summary>
|
||||
public static Stream GetResourceStream(string path)
|
||||
{
|
||||
var resourceInfo =
|
||||
System.Windows.Application.GetResourceStream(new Uri($"pack://application:,,,/{path}", UriKind.Absolute));
|
||||
return resourceInfo?.Stream;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否拥有某一文件夹的 I/O 权限。如果文件夹不存在,会返回 False。
|
||||
/// </summary>
|
||||
public static bool CheckPermission(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return false;
|
||||
if (!path.EndsWithF(@"\"))
|
||||
path += @"\";
|
||||
if (path.EndsWithF(@":\System Volume Information\") || path.EndsWithF(@":\$RECYCLE.BIN\"))
|
||||
return false;
|
||||
if (!Directory.Exists(path))
|
||||
return false;
|
||||
var fileName = "CheckPermission" + GetUuid();
|
||||
if (File.Exists(path + fileName))
|
||||
File.Delete(path + fileName);
|
||||
File.Create(path + fileName).Dispose();
|
||||
File.Delete(path + fileName);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex, "没有对文件夹 " + path + " 的权限,请尝试以管理员权限运行 PCL");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否拥有某一文件夹的 I/O 权限。如果出错,则抛出异常。
|
||||
/// </summary>
|
||||
public static void CheckPermissionWithException(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentNullException("文件夹名不能为空!");
|
||||
if (!path.EndsWithF(@"\"))
|
||||
path += @"\";
|
||||
if (!Directory.Exists(path))
|
||||
throw new DirectoryNotFoundException("文件夹不存在!");
|
||||
if (File.Exists(path + "CheckPermission"))
|
||||
File.Delete(path + "CheckPermission");
|
||||
File.Create(path + "CheckPermission").Dispose();
|
||||
File.Delete(path + "CheckPermission");
|
||||
}
|
||||
|
||||
#region UI
|
||||
|
||||
public static void SetLaunchFont(string fontName = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
LocalizationFontService.ApplyLaunchFont(fontName, LocalizationService.CurrentLanguage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log(ex, "设置字体失败", LogLevel.Hint);
|
||||
}
|
||||
}
|
||||
|
||||
// 边距改变
|
||||
/// <summary>
|
||||
/// 相对增减控件的左边距。
|
||||
/// </summary>
|
||||
public static void DeltaLeft(FrameworkElement control, double newValue)
|
||||
{
|
||||
// 安全性检查
|
||||
DebugAssert(!double.IsNaN(newValue));
|
||||
DebugAssert(!double.IsInfinity(newValue));
|
||||
|
||||
if (control is Window)
|
||||
// 窗口改变
|
||||
((Window)control).Left += newValue;
|
||||
else
|
||||
// 根据 HorizontalAlignment 改变数值
|
||||
switch (control.HorizontalAlignment)
|
||||
{
|
||||
case HorizontalAlignment.Left:
|
||||
case HorizontalAlignment.Stretch:
|
||||
{
|
||||
control.Margin = new Thickness(control.Margin.Left + newValue, control.Margin.Top,
|
||||
control.Margin.Right, control.Margin.Bottom);
|
||||
break;
|
||||
}
|
||||
case HorizontalAlignment.Right:
|
||||
{
|
||||
// control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, CType(control.Parent, Object).ActualWidth - control.ActualWidth - newValue, control.Margin.Bottom)
|
||||
control.Margin = new Thickness(control.Margin.Left, control.Margin.Top,
|
||||
control.Margin.Right - newValue, control.Margin.Bottom);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DebugAssert(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置控件的左边距。(仅针对置左控件)
|
||||
/// </summary>
|
||||
public static void SetLeft(FrameworkElement control, double newValue)
|
||||
{
|
||||
DebugAssert(control.HorizontalAlignment == HorizontalAlignment.Left);
|
||||
control.Margin = new Thickness(newValue, control.Margin.Top, control.Margin.Right, control.Margin.Bottom);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 相对增减控件的上边距。
|
||||
/// </summary>
|
||||
public static void DeltaTop(FrameworkElement control, double newValue)
|
||||
{
|
||||
// 安全性检查
|
||||
DebugAssert(!double.IsNaN(newValue));
|
||||
DebugAssert(!double.IsInfinity(newValue));
|
||||
|
||||
if (control is Window)
|
||||
// 窗口改变
|
||||
((Window)control).Top += newValue;
|
||||
else
|
||||
// 根据 VerticalAlignment 改变数值
|
||||
switch (control.VerticalAlignment)
|
||||
{
|
||||
case VerticalAlignment.Top:
|
||||
{
|
||||
control.Margin = new Thickness(control.Margin.Left, control.Margin.Top + newValue,
|
||||
control.Margin.Right, control.Margin.Bottom);
|
||||
break;
|
||||
}
|
||||
case VerticalAlignment.Bottom:
|
||||
{
|
||||
// control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, CType(control.Parent, Object).ActualWidth - control.ActualWidth - newValue, control.Margin.Bottom)
|
||||
control.Margin = new Thickness(control.Margin.Left, control.Margin.Top, control.Margin.Right,
|
||||
control.Margin.Bottom - newValue);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
DebugAssert(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置控件的顶边距。(仅针对置上控件)
|
||||
/// </summary>
|
||||
public static void SetTop(FrameworkElement control, double newValue)
|
||||
{
|
||||
DebugAssert(control.VerticalAlignment == VerticalAlignment.Top);
|
||||
control.Margin = new Thickness(control.Margin.Left, newValue, control.Margin.Right, control.Margin.Bottom);
|
||||
}
|
||||
|
||||
// DPI 转换
|
||||
public static readonly int dpi = (int)Math.Round(Graphics.FromHwnd(nint.Zero).DpiX);
|
||||
|
||||
/// <summary>
|
||||
/// 将经过 DPI 缩放的 WPF 尺寸转化为实际的像素尺寸。
|
||||
/// </summary>
|
||||
public static double GetPixelSize(double wPFSize)
|
||||
{
|
||||
return wPFSize / 96d * dpi;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将实际的像素尺寸转化为经过 DPI 缩放的 WPF 尺寸。
|
||||
/// </summary>
|
||||
public static double GetWPFSize(double pixelSize)
|
||||
{
|
||||
return pixelSize * 96d / dpi;
|
||||
}
|
||||
|
||||
// UI 截图
|
||||
/// <summary>
|
||||
/// 将某个控件的呈现转换为图片。
|
||||
/// </summary>
|
||||
public static ImageBrush ControlBrush(FrameworkElement uI)
|
||||
{
|
||||
var width = uI.ActualWidth;
|
||||
var height = uI.ActualHeight;
|
||||
if (width < 1d || height < 1d)
|
||||
return new ImageBrush();
|
||||
var bmp = new RenderTargetBitmap((int)Math.Round(GetPixelSize(width)), (int)Math.Round(GetPixelSize(height)),
|
||||
dpi, dpi, PixelFormats.Pbgra32);
|
||||
bmp.Render(uI);
|
||||
return new ImageBrush(bmp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将某个控件的模拟呈现转换为图片。
|
||||
/// </summary>
|
||||
public static ImageBrush ControlBrush(FrameworkElement uI, double width, double height, double left = 0d,
|
||||
double top = 0d)
|
||||
{
|
||||
uI.Measure(new Size(width, height));
|
||||
uI.Arrange(new Rect(0d, 0d, width, height));
|
||||
var bmp = new RenderTargetBitmap((int)Math.Round(GetPixelSize(width)), (int)Math.Round(GetPixelSize(height)),
|
||||
dpi, dpi, PixelFormats.Default);
|
||||
bmp.Render(uI);
|
||||
if (left != 0d || top != 0d)
|
||||
uI.Arrange(new Rect(left, top, width, height));
|
||||
return new ImageBrush(bmp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 UI 内容固定为图片并进行 Clear。
|
||||
/// </summary>
|
||||
public static void ControlFreeze(Panel uI)
|
||||
{
|
||||
uI.Background = ControlBrush(uI);
|
||||
uI.Children.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 UI 内容固定为图片并进行 Clear。
|
||||
/// </summary>
|
||||
public static void ControlFreeze(Border uI)
|
||||
{
|
||||
uI.Background = ControlBrush(uI);
|
||||
uI.Child = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 XElement 转换为对应 UI 对象(不返回 XAML 清理结果)。
|
||||
/// </summary>
|
||||
public static object GetObjectFromXML(XElement str)
|
||||
{
|
||||
return GetObjectFromXML(str.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 XML 转换为对应 UI 对象。
|
||||
/// </summary>
|
||||
public static object GetObjectFromXML(string str)
|
||||
{
|
||||
return GetObjectFromXML(str, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 XML 转换为对应 UI 对象,并输出 XAML 清理结果。
|
||||
/// </summary>
|
||||
public static object GetObjectFromXML(string str, out XamlEventSanitizer.SanitizeResult sanitizeResult)
|
||||
{
|
||||
str = str. // 兼容旧版自定义事件写法
|
||||
Replace("EventType=\"", "local:CustomEventService.EventType=\"").
|
||||
Replace("EventData=\"", "local:CustomEventService.EventData=\"").
|
||||
Replace("Property=\"EventType\"", "Property=\"local:CustomEventService.EventType\"").
|
||||
Replace("Property=\"EventData\"", "Property=\"local:CustomEventService.EventData\"");
|
||||
// 修复因上述替换导致重复前缀的情况:local:CustomEventService.local:CustomEventService.EventType
|
||||
str = str.Replace("local:CustomEventService.local:CustomEventService.", "local:CustomEventService.");
|
||||
|
||||
sanitizeResult = XamlEventSanitizer.Sanitize(str);
|
||||
str = sanitizeResult.SanitizedXaml;
|
||||
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(str)))
|
||||
{
|
||||
// 类型检查
|
||||
using (var reader = new XamlXmlReader(stream))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
foreach (var blackListType in new[]
|
||||
{
|
||||
typeof(WebBrowser), typeof(Frame), typeof(MediaElement), typeof(ObjectDataProvider),
|
||||
typeof(XamlReader), typeof(Window), typeof(XmlDataProvider)
|
||||
})
|
||||
{
|
||||
if (reader.Type is not null && blackListType.IsAssignableFrom(reader.Type.UnderlyingType))
|
||||
throw new UnauthorizedAccessException($"不允许使用 {blackListType.Name} 类型。");
|
||||
if (reader.Value is not null && Equals(reader.Value, blackListType.Name))
|
||||
throw new UnauthorizedAccessException($"不允许使用 {blackListType.Name} 值。");
|
||||
}
|
||||
|
||||
foreach (var blackListMember in new[] { "Code", "FactoryMethod", "Static" })
|
||||
if (reader.Member is not null && (reader.Member.Name ?? "") == (blackListMember ?? ""))
|
||||
throw new UnauthorizedAccessException($"不允许使用 {blackListMember} 成员。");
|
||||
}
|
||||
}
|
||||
|
||||
// 实际的加载
|
||||
stream.Position = 0L;
|
||||
using (var writer = new StreamWriter(stream))
|
||||
{
|
||||
writer.Write(str);
|
||||
writer.Flush();
|
||||
stream.Position = 0L;
|
||||
return System.Windows.Markup.XamlReader.Load(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly int uiThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
/// <summary>
|
||||
/// 当前线程是否为主线程。
|
||||
/// </summary>
|
||||
public static bool RunInUi()
|
||||
{
|
||||
return Thread.CurrentThread.ManagedThreadId == uiThreadId;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Debug
|
||||
|
||||
public static bool modeDebug = false;
|
||||
|
||||
// Log
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// 不提示,只记录日志。
|
||||
/// </summary>
|
||||
Normal = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 只提示开发者。
|
||||
/// </summary>
|
||||
Developer = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 只提示开发者与调试模式用户。
|
||||
/// </summary>
|
||||
Debug = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 弹出提示所有用户。
|
||||
/// </summary>
|
||||
Hint = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 弹窗,不要求反馈。
|
||||
/// </summary>
|
||||
Msgbox = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 弹窗,要求反馈。
|
||||
/// </summary>
|
||||
Feedback = 5,
|
||||
|
||||
/// <summary>
|
||||
/// 弹出 Windows 原生弹窗,要求反馈。在无法保证 WPF 窗口能正常运行时使用此级别。
|
||||
/// 在第二次触发后会直接结束程序。
|
||||
/// </summary>
|
||||
Critical = 6
|
||||
}
|
||||
|
||||
private static bool isCriticalErrorTriggered;
|
||||
|
||||
/// <summary>
|
||||
/// 输出 Log。
|
||||
/// </summary>
|
||||
/// <param name="title">如果要求弹窗,指定弹窗的标题。</param>
|
||||
public static void Log(
|
||||
string text,
|
||||
LogLevel level = LogLevel.Normal,
|
||||
string? title = null,
|
||||
string? userSummary = null)
|
||||
{
|
||||
// On Error Resume Next
|
||||
// 放在最后会导致无法显示极端错误下的弹窗(如无法写入日志文件)
|
||||
// 处理错误会导致再次调用 Log() 导致无限循环
|
||||
|
||||
// 输出日志
|
||||
if (new[] { LogLevel.Msgbox, LogLevel.Hint }.Contains(level))
|
||||
LogWrapper.Warn(text);
|
||||
else if (LogLevel.Feedback == level)
|
||||
LogWrapper.Error(text);
|
||||
else if (LogLevel.Critical == level)
|
||||
LogWrapper.Fatal(text);
|
||||
else if (LogLevel.Debug == level)
|
||||
LogWrapper.Debug(text);
|
||||
else if (LogLevel.Developer == level)
|
||||
LogWrapper.Trace(text);
|
||||
else
|
||||
LogWrapper.Info(text);
|
||||
|
||||
if (isProgramEnded || level == LogLevel.Normal)
|
||||
return;
|
||||
|
||||
var userDetails = text.RegexReplace(@"\[[^\]]+?\] ", "");
|
||||
var userMessage = string.IsNullOrWhiteSpace(userSummary)
|
||||
? Lang.Text("SystemDialog.Error.UserVisible.Message", userDetails)
|
||||
: userSummary;
|
||||
var dialogTitle = _GetUserDialogTitle(title);
|
||||
|
||||
switch (level)
|
||||
{
|
||||
case LogLevel.Developer:
|
||||
break;
|
||||
|
||||
case LogLevel.Debug:
|
||||
if (modeDebug)
|
||||
HintService.Hint("[调试模式] " + text, HintType.Info, false);
|
||||
break;
|
||||
|
||||
case LogLevel.Hint:
|
||||
HintService.Hint(userMessage, HintType.Error, false);
|
||||
break;
|
||||
|
||||
case LogLevel.Msgbox:
|
||||
ModMain.MyMsgBox(userMessage, dialogTitle, isWarn: true);
|
||||
break;
|
||||
|
||||
case LogLevel.Feedback:
|
||||
_ShowFeedbackPrompt(userMessage, dialogTitle, false);
|
||||
break;
|
||||
|
||||
case LogLevel.Critical:
|
||||
_ShowFeedbackPrompt(userMessage, dialogTitle, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输出错误信息。
|
||||
/// </summary>
|
||||
/// <param name="desc">错误描述,仅用于日志和错误详情。</param>
|
||||
/// <param name="userSummary">可选的本地化用户摘要;不会写入日志。</param>
|
||||
public static void Log(
|
||||
Exception ex,
|
||||
string desc,
|
||||
LogLevel level = LogLevel.Debug,
|
||||
string? title = null,
|
||||
string? userSummary = null)
|
||||
{
|
||||
// On Error Resume Next
|
||||
if (ex is ThreadInterruptedException)
|
||||
return;
|
||||
|
||||
// 输出日志
|
||||
if (new[] { LogLevel.Msgbox, LogLevel.Hint }.Contains(level))
|
||||
LogWrapper.Warn(ex, desc);
|
||||
else if (LogLevel.Feedback == level)
|
||||
LogWrapper.Error(ex, desc);
|
||||
else if (LogLevel.Critical == level)
|
||||
LogWrapper.Fatal(ex, desc);
|
||||
else if (LogLevel.Debug == level)
|
||||
LogWrapper.Debug($"{desc}:{ex}");
|
||||
else if (LogLevel.Developer == level)
|
||||
LogWrapper.Trace($"{desc}:{ex}");
|
||||
else
|
||||
LogWrapper.Error(ex, desc);
|
||||
|
||||
if (isProgramEnded)
|
||||
return;
|
||||
|
||||
var userMessage = _GetUserExceptionMessage(desc, ex, userSummary);
|
||||
var dialogTitle = _GetUserDialogTitle(title);
|
||||
|
||||
switch (level)
|
||||
{
|
||||
case LogLevel.Normal or LogLevel.Developer:
|
||||
break;
|
||||
|
||||
case LogLevel.Debug:
|
||||
if (modeDebug)
|
||||
HintService.Hint("[调试模式] " + desc + ":" + ex, HintType.Info, false);
|
||||
break;
|
||||
|
||||
case LogLevel.Hint:
|
||||
HintService.Hint(userMessage, HintType.Error, false);
|
||||
break;
|
||||
|
||||
case LogLevel.Msgbox:
|
||||
ModMain.MyMsgBox(userMessage, dialogTitle, isWarn: true);
|
||||
break;
|
||||
|
||||
case LogLevel.Feedback:
|
||||
_ShowFeedbackPrompt(userMessage, dialogTitle, false);
|
||||
break;
|
||||
|
||||
case LogLevel.Critical:
|
||||
_ShowFeedbackPrompt(userMessage, dialogTitle, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string _GetUserDialogTitle(string? title)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(title)
|
||||
? Lang.Text("SystemDialog.Error.Title")
|
||||
: title;
|
||||
}
|
||||
|
||||
private static string _GetUserExceptionMessage(
|
||||
string desc,
|
||||
Exception ex,
|
||||
string? userSummary)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(userSummary))
|
||||
return ExceptionDetails.Compose(userSummary, ex);
|
||||
|
||||
return ex.GetType() == typeof(Win32Exception)
|
||||
? Lang.Text("SystemDialog.Error.OperationFailed.RuntimeMessage", desc, ex.ToString())
|
||||
: Lang.Text("SystemDialog.Error.OperationFailed.Message", desc, ex.ToString());
|
||||
}
|
||||
|
||||
private static void _ShowFeedbackPrompt(
|
||||
string userMessage,
|
||||
string title,
|
||||
bool isCritical)
|
||||
{
|
||||
if (isCritical && isCriticalErrorTriggered)
|
||||
{
|
||||
FormMain.EndProgramForce(ProcessReturnValues.Exception);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCritical)
|
||||
isCriticalErrorTriggered = true;
|
||||
|
||||
if (CanFeedback(false))
|
||||
{
|
||||
var message = Lang.Text("Setup.Feedback.ErrorPrompt.Submit.Message", userMessage);
|
||||
var shouldSend = isCritical
|
||||
? Interaction.MsgBox(
|
||||
message,
|
||||
(MsgBoxStyle)((int)MsgBoxStyle.Critical + (int)MsgBoxStyle.YesNo),
|
||||
title) == MsgBoxResult.Yes
|
||||
: ModMain.MyMsgBox(
|
||||
message,
|
||||
title,
|
||||
Lang.Text("Setup.Feedback.ErrorPrompt.Submit.Action"),
|
||||
Lang.Text("Common.Action.Cancel"),
|
||||
isWarn: true) == 1;
|
||||
|
||||
if (shouldSend)
|
||||
Feedback(false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
var updateMessage = Lang.Text("Setup.Feedback.ErrorPrompt.Update.Message", userMessage);
|
||||
if (isCritical)
|
||||
Interaction.MsgBox(updateMessage, MsgBoxStyle.Critical, title);
|
||||
else
|
||||
ModMain.MyMsgBox(updateMessage, title, isWarn: true);
|
||||
}
|
||||
|
||||
public static string Base64Decode(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return "";
|
||||
var decodedBytes = Convert.FromBase64String(text);
|
||||
return Encoding.UTF8.GetString(decodedBytes);
|
||||
}
|
||||
|
||||
public static string Base64Encode(string text)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
public static string Base64Encode(byte[] bytes)
|
||||
{
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
// 反馈
|
||||
public static void Feedback(bool showMsgbox = true, bool forceOpenLog = false)
|
||||
{
|
||||
// On Error Resume Next
|
||||
FeedbackInfo();
|
||||
var currentDate = DateTime.Now.ToString("yyyy-M-dd", CultureInfo.InvariantCulture);
|
||||
|
||||
if (forceOpenLog || (showMsgbox &&
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Setup.Feedback.Reminder.Message", currentDate),
|
||||
Lang.Text("Setup.Feedback.Reminder.Title"),
|
||||
Lang.Text("Common.Action.OpenFolder"),
|
||||
Lang.Text("Setup.Feedback.Reminder.NotNeeded")) ==
|
||||
1)) OpenExplorer(exePath + @"PCL\Log\");
|
||||
OpenWebsite("https://github.com/PCL-Community/PCL2-CE/issues/");
|
||||
}
|
||||
|
||||
public static bool CanFeedback(bool showHint)
|
||||
{
|
||||
var stat = UpdateManager.GetVersionStatus();
|
||||
if (stat == UpdateEnums.VersionStatus.Latest) return true;
|
||||
|
||||
if (!showHint) return false;
|
||||
|
||||
if (ModMain.MyMsgBox(
|
||||
stat == UpdateEnums.VersionStatus.NotLatest
|
||||
? Lang.Text("Setup.Feedback.Unavailable.NotLatest.Message")
|
||||
: Lang.Text("Setup.Feedback.Unavailable.CheckFailed.Message"),
|
||||
Lang.Text("Setup.Feedback.Unavailable.Title"),
|
||||
stat == UpdateEnums.VersionStatus.NotLatest
|
||||
? Lang.Text("Setup.Feedback.Unavailable.NotLatest.Action")
|
||||
: Lang.Text("Setup.Feedback.Unavailable.CheckFailed.Action"),
|
||||
Lang.Text("Common.Action.Cancel")) == 1)
|
||||
ModMain.frmMain.PageChange(FormMain.PageType.Setup, FormMain.PageSubType.SetupUpdate);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在日志中输出系统诊断信息。
|
||||
/// </summary>
|
||||
public static void FeedbackInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get system memory info
|
||||
var phyRam = KernelInterop.GetPhysicalMemoryBytes();
|
||||
|
||||
// Calculate memory and DPI scale
|
||||
var availableMb = phyRam.Available / 1024 / 1024;
|
||||
var totalMb = phyRam.Total / 1024 / 1024;
|
||||
var dpiScale = Math.Round(dpi / 96.0, 2);
|
||||
|
||||
// Build diagnostic information string
|
||||
var info = $"""
|
||||
[System] Diagnostic Information:
|
||||
OS: {RuntimeInformation.OSDescription} (32-bit: {SystemInfo.Is32BitSystem})
|
||||
Memory: {availableMb} MiB / {totalMb} MiB
|
||||
DPI: {dpi} ({dpiScale * 100}%)
|
||||
MC Folder: {ModFolder.mcFolderSelected ?? "Nothing"}
|
||||
Executable Path: {exePath}
|
||||
""";
|
||||
|
||||
LogWrapper.Info(info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Basic fail-safe to replace "On Error Resume Next"
|
||||
LogWrapper.Error(ex, "Failed to collect feedback information");
|
||||
}
|
||||
}
|
||||
|
||||
// 断言
|
||||
public static void DebugAssert(bool exp)
|
||||
{
|
||||
if (!exp)
|
||||
throw new Exception("断言命中");
|
||||
}
|
||||
|
||||
// 获取当前的堆栈信息
|
||||
public static string GetStackTrace()
|
||||
{
|
||||
var stack = new StackTrace();
|
||||
return stack.GetFrames().Skip(1).Select(f => f.GetMethod())
|
||||
.Select(f => f.Name + "(" + f.GetParameters().Select(p => p.ToString()).ToList().Join(", ") + ") - " +
|
||||
f.Module).ToList().Join("\r\n")
|
||||
.Replace("\r\n" + "\r\n", "\r\n");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region WPF
|
||||
|
||||
/// <summary>
|
||||
/// 对数据绑定进行加法运算,使用参数决定加数。
|
||||
/// </summary>
|
||||
public class AdditionConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is null)
|
||||
return 0;
|
||||
double before;
|
||||
if (!double.TryParse(value.ToString(), out before))
|
||||
return 0;
|
||||
var scale = 1d;
|
||||
if (parameter is not null)
|
||||
double.TryParse(parameter.ToString(), out scale);
|
||||
return before + scale;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is null)
|
||||
return Binding.DoNothing;
|
||||
double before;
|
||||
if (!double.TryParse(value.ToString(), out before))
|
||||
return Binding.DoNothing;
|
||||
var scale = 1d;
|
||||
if (parameter is not null)
|
||||
double.TryParse(parameter.ToString(), out scale);
|
||||
if (scale == 0d)
|
||||
return Binding.DoNothing;
|
||||
return before - scale;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对数据绑定进行乘法运算,使用参数决定乘数。
|
||||
/// </summary>
|
||||
public class MultiplicationConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is null)
|
||||
return 0;
|
||||
double before;
|
||||
if (!double.TryParse(value.ToString(), out before))
|
||||
return 0;
|
||||
var scale = 1d;
|
||||
if (parameter is not null)
|
||||
double.TryParse(parameter.ToString(), out scale);
|
||||
return before * scale;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is null)
|
||||
return Binding.DoNothing;
|
||||
double before;
|
||||
if (!double.TryParse(value.ToString(), out before))
|
||||
return Binding.DoNothing;
|
||||
var scale = 1d;
|
||||
if (parameter is not null)
|
||||
double.TryParse(parameter.ToString(), out scale);
|
||||
if (scale == 0d)
|
||||
return Binding.DoNothing;
|
||||
return before / scale;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将取反的 Boolean 绑定到 Visibility。
|
||||
/// </summary>
|
||||
public class InverseBooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is null)
|
||||
return Visibility.Visible;
|
||||
bool boolValue;
|
||||
return bool.TryParse(value.ToString(), out boolValue)
|
||||
? boolValue ? Visibility.Collapsed : Visibility.Visible
|
||||
: Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is null)
|
||||
return false;
|
||||
return value is Visibility
|
||||
? (Visibility)value != Visibility.Visible
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Boolean 取反。
|
||||
/// </summary>
|
||||
public class InverseBooleanConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is null)
|
||||
return false;
|
||||
bool boolValue;
|
||||
return bool.TryParse(value.ToString(), out boolValue) ? !boolValue : false;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is null) return false;
|
||||
|
||||
if (bool.TryParse(value.ToString(), out var result)) return !result;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1,1030 @@
|
||||
using Microsoft.VisualBasic.CompilerServices;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Utils;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Windows.Shell;
|
||||
using PCL.Network;
|
||||
using PCL.Network.Loaders;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
public static class ModLoader
|
||||
{
|
||||
public enum LoaderFolderRunType
|
||||
{
|
||||
RunOnUpdated,
|
||||
ForceRun,
|
||||
UpdateOnly
|
||||
}
|
||||
|
||||
// 任务栏进度条
|
||||
public static ModBase.SafeList<LoaderBase> loaderTaskbar = new();
|
||||
public static double loaderTaskbarProgress; // 平滑后的进度
|
||||
private static TaskbarItemProgressState loaderTaskbarProgressLast = TaskbarItemProgressState.None;
|
||||
|
||||
// 文件夹刷新类委托
|
||||
private static readonly Dictionary<LoaderBase, LoaderFolderDictionaryEntry> loaderFolderDictionary = new();
|
||||
|
||||
public static void LoaderTaskbarAdd<T>(LoaderCombo<T> loader)
|
||||
{
|
||||
if (ModMain.frmSpeedLeft is not null)
|
||||
ModMain.frmSpeedLeft.TaskRemove(loader);
|
||||
loaderTaskbar.Add(loader);
|
||||
ModBase.Log($"[Taskbar] {loader.name} 已加入任务列表");
|
||||
}
|
||||
|
||||
public static void LoaderTaskbarProgressRefresh()
|
||||
{
|
||||
try
|
||||
{
|
||||
TaskbarItemProgressState newState;
|
||||
var newProgress = LoaderTaskbarProgressGet();
|
||||
// 若单个任务已中止,或全部任务已完成,则刷新并移除
|
||||
foreach (var Task in loaderTaskbar)
|
||||
if (loaderTaskbar.All(l => l.State != ModBase.LoadState.Loading) ||
|
||||
Task.State == ModBase.LoadState.Waiting || Task.State == ModBase.LoadState.Aborted)
|
||||
{
|
||||
ModMain.frmSpeedLeft?.TaskRefresh(Task);
|
||||
loaderTaskbar.Remove(Task);
|
||||
ModBase.Log($"[Taskbar] {Task.name} 已移出任务列表");
|
||||
}
|
||||
|
||||
// 更新平滑后的进度
|
||||
if (newProgress <= 0d || newProgress >= 1d || loaderTaskbarProgress > newProgress)
|
||||
loaderTaskbarProgress = newProgress;
|
||||
else
|
||||
loaderTaskbarProgress = loaderTaskbarProgress * 0.9d + newProgress * 0.1d;
|
||||
ModBase.RunInUi(() => ModMain.frmMain.BtnExtraDownload.Progress = loaderTaskbarProgress);
|
||||
// 更新任务栏信息
|
||||
if (!loaderTaskbar.Any() || loaderTaskbarProgress == 1d)
|
||||
{
|
||||
newState = TaskbarItemProgressState.None;
|
||||
}
|
||||
else if (loaderTaskbarProgress < 0.015d)
|
||||
{
|
||||
newState = TaskbarItemProgressState.Indeterminate;
|
||||
}
|
||||
else
|
||||
{
|
||||
newState = TaskbarItemProgressState.Normal;
|
||||
ModMain.frmMain.TaskbarItemInfo.ProgressValue = loaderTaskbarProgress;
|
||||
}
|
||||
|
||||
if (loaderTaskbarProgressLast != newState)
|
||||
{
|
||||
loaderTaskbarProgressLast = newState;
|
||||
ModMain.frmMain.TaskbarItemInfo.ProgressState = newState;
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"刷新任务栏进度显示失败",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Application.Loader.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
public static double LoaderTaskbarProgressGet()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!loaderTaskbar.Any())
|
||||
return 1d;
|
||||
|
||||
return ModBase.MathClamp(
|
||||
loaderTaskbar.Select(l => l.Progress).Average(),
|
||||
0,
|
||||
1
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"获取任务栏进度出错",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Application.Loader.Error.OperationFailed"));
|
||||
return 0.5d;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行以文件夹检测作为输入的加载器。加载器需以文件夹路径为输入值。
|
||||
/// 返回是否执行了加载器。
|
||||
/// </summary>
|
||||
/// <param name="extraPath">用于检查文件夹修改的额外路径。该路径不会传入加载器。</param>
|
||||
/// <param name="loaderInput">如果不想要文件夹路径为输入值,则传入期望数据</param>
|
||||
public static bool LoaderFolderRun(LoaderBase loader, string folderPath, LoaderFolderRunType type, int maxDepth = 0,
|
||||
string extraPath = "", bool waitForExit = false, object loaderInput = null)
|
||||
{
|
||||
DirectoryInfo folderInfo;
|
||||
var value = new LoaderFolderDictionaryEntry { folderPath = folderPath + extraPath, lastCheckTime = default };
|
||||
try
|
||||
{
|
||||
// 获取数据
|
||||
folderInfo = new DirectoryInfo(folderPath + extraPath);
|
||||
value.lastCheckTime = folderInfo.Exists ? GetActualLastWriteTimeUtc(folderInfo, maxDepth) : null;
|
||||
// 如果已经检查过,则跳过
|
||||
if (type == LoaderFolderRunType.RunOnUpdated && loaderFolderDictionary.ContainsKey(loader))
|
||||
{
|
||||
if (folderInfo.Exists)
|
||||
{
|
||||
if (loaderFolderDictionary[loader].lastCheckTime is not null &&
|
||||
value.Equals(loaderFolderDictionary[loader]))
|
||||
return false;
|
||||
}
|
||||
else if (loaderFolderDictionary[loader].lastCheckTime is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "文件夹加载器启动检测出错");
|
||||
}
|
||||
|
||||
// 写入检查数据
|
||||
loaderFolderDictionary[loader] = value;
|
||||
// 开始检查
|
||||
if (type == LoaderFolderRunType.UpdateOnly)
|
||||
return false;
|
||||
if (waitForExit)
|
||||
loader.WaitForExit(loaderInput ?? folderPath, isForceRestart: true);
|
||||
else
|
||||
loader.Start(loaderInput ?? folderPath, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static DateTime GetActualLastWriteTimeUtc(DirectoryInfo folderInfo, int maxDepth)
|
||||
{
|
||||
var time = folderInfo.LastWriteTimeUtc;
|
||||
if (maxDepth > 0)
|
||||
foreach (var Folder in folderInfo.EnumerateDirectories())
|
||||
{
|
||||
var folderTime = GetActualLastWriteTimeUtc(Folder, maxDepth - 1);
|
||||
if (folderTime > time)
|
||||
time = folderTime;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
// 各类加载器
|
||||
/// <summary>
|
||||
/// 加载器的统一基类。
|
||||
/// </summary>
|
||||
public abstract class LoaderBase : ILoadingTrigger
|
||||
{
|
||||
public delegate void OnStateChangedThreadEventHandler(LoaderBase loader, ModBase.LoadState newState,
|
||||
ModBase.LoadState oldState);
|
||||
|
||||
public delegate void OnStateChangedUiEventHandler(LoaderBase loader, ModBase.LoadState newState,
|
||||
ModBase.LoadState oldState);
|
||||
|
||||
public delegate void PreviewFinishEventHandler(LoaderBase loader);
|
||||
|
||||
// 等待结束
|
||||
public const string waitForExitTimeoutMessage = "等待加载器执行超时。";
|
||||
|
||||
/// <summary>
|
||||
/// 用于状态改变检测的同步锁。
|
||||
/// </summary>
|
||||
public readonly object lockState = new();
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 使用 LoaderCombo 加载时,该任务是否会阻碍后续任务的进行。
|
||||
/// </summary>
|
||||
public bool block = true;
|
||||
|
||||
public bool hasOnStateChangedThread;
|
||||
|
||||
/// <summary>
|
||||
/// 当前加载器是否由 IsForceRestart 强制调起。
|
||||
/// 这个属性自身不会干任何事,而是提供给加载器执行的函数,使得加载器调用另一个加载器时,可以继承强制重启属性。
|
||||
/// </summary>
|
||||
public bool isForceRestarting;
|
||||
|
||||
/// <summary>
|
||||
/// 加载器的名称。
|
||||
/// </summary>
|
||||
public string name;
|
||||
|
||||
/// <summary>
|
||||
/// 父加载器。
|
||||
/// </summary>
|
||||
public LoaderBase parent;
|
||||
|
||||
/// <summary>
|
||||
/// 该加载器是否显示在列表中。
|
||||
/// </summary>
|
||||
public bool show = true;
|
||||
|
||||
// 基础属性
|
||||
/// <summary>
|
||||
/// 加载器的标识编号。
|
||||
/// </summary>
|
||||
public int Uuid = ModBase.GetUuid();
|
||||
|
||||
public LoaderBase()
|
||||
{
|
||||
name = "未命名任务 " + Uuid + "#";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最上级的加载器。
|
||||
/// </summary>
|
||||
public LoaderBase RealParent
|
||||
{
|
||||
get
|
||||
{
|
||||
LoaderBase realParentRet = default;
|
||||
try
|
||||
{
|
||||
realParentRet = parent;
|
||||
while (realParentRet is not null && realParentRet.parent is not null)
|
||||
realParentRet = realParentRet.parent;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"获取父加载器失败(" + name + ")",
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Application.Loader.Error.OperationFailed"));
|
||||
return null;
|
||||
}
|
||||
|
||||
return realParentRet;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 简易的在 UI 线程添加触发事件的方式。主要用于在新建 Loader 时直接使用 With 绑定事件,以及进行老代码兼容。
|
||||
/// </summary>
|
||||
public Action<LoaderBase> OnStateChanged
|
||||
{
|
||||
set { OnStateChangedUi += (loader, newState, oldState) => value(loader); }
|
||||
}
|
||||
|
||||
// 状态监控
|
||||
/// <summary>
|
||||
/// 加载器的状态。
|
||||
/// </summary>
|
||||
public ModBase.LoadState State
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
var oldState = field;
|
||||
if (value == ModBase.LoadState.Finished && Config.Debug.AddRandomDelay)
|
||||
Thread.Sleep(RandomUtils.NextInt(100, 2000));
|
||||
field = value;
|
||||
ModBase.Log("[Loader] 加载器 " + name + " 状态改变:" + ModBase.GetStringFromEnum(value));
|
||||
// 实现 ILoadingTrigger 接口与 OnStateChanged 回调
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case ModBase.LoadState.Loading:
|
||||
{
|
||||
LoadingState = MyLoading.MyLoadingState.Run;
|
||||
break;
|
||||
}
|
||||
case ModBase.LoadState.Failed:
|
||||
{
|
||||
LoadingState = MyLoading.MyLoadingState.Error;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
LoadingState = MyLoading.MyLoadingState.Stop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
OnStateChangedUi?.Invoke(this, value, oldState);
|
||||
});
|
||||
if (hasOnStateChangedThread)
|
||||
ModBase.RunInThread(() => OnStateChangedThread?.Invoke(this, value, oldState));
|
||||
}
|
||||
} = ModBase.LoadState.Waiting;
|
||||
|
||||
/// <summary>
|
||||
/// 若加载器出错,可提供给外部参考的异常。
|
||||
/// </summary>
|
||||
public Exception Error { get; set; }
|
||||
|
||||
// 进度监控
|
||||
/// <summary>
|
||||
/// 加载器的执行进度,为 0 至 1 的小数。
|
||||
/// </summary>
|
||||
public virtual double Progress
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case ModBase.LoadState.Waiting:
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
case ModBase.LoadState.Loading:
|
||||
{
|
||||
return field == -1 ? 0.02d : field;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return 1d;
|
||||
}
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
var oldValue = field;
|
||||
field = value;
|
||||
ProgressChanged?.Invoke(value, oldValue);
|
||||
}
|
||||
} = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 计算总进度时的权重。它应该为预计时间(秒)。
|
||||
/// </summary>
|
||||
public double ProgressWeight { get; set; } = 1d;
|
||||
|
||||
public bool IsLoader { get; } = true;
|
||||
|
||||
public MyLoading.MyLoadingState LoadingState
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
return;
|
||||
var oldState = field;
|
||||
field = value;
|
||||
LoadingStateChanged?.Invoke(value, oldState);
|
||||
}
|
||||
} = MyLoading.MyLoadingState.Stop;
|
||||
|
||||
public event ILoadingTrigger.LoadingStateChangedEventHandler? LoadingStateChanged;
|
||||
public event ILoadingTrigger.ProgressChangedEventHandler? ProgressChanged;
|
||||
|
||||
public virtual void InitParent(LoaderBase parent)
|
||||
{
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
// 事件
|
||||
|
||||
/// <summary>
|
||||
/// 当状态改变时,在工作线程触发代码。在添加事件后,必须将 HasOnStateChangedThread 设为 True。
|
||||
/// </summary>
|
||||
public event OnStateChangedThreadEventHandler? OnStateChangedThread;
|
||||
|
||||
/// <summary>
|
||||
/// 当状态改变时,在 UI 线程触发代码。
|
||||
/// </summary>
|
||||
public event OnStateChangedUiEventHandler? OnStateChangedUi;
|
||||
|
||||
/// <summary>
|
||||
/// 在加载器目标事件执行完成,加载器状态即将变为 Finish 时调用。可以视为扩展加载器目标事件。
|
||||
/// </summary>
|
||||
public event PreviewFinishEventHandler? PreviewFinish;
|
||||
|
||||
protected void RaisePreviewFinish()
|
||||
{
|
||||
PreviewFinish?.Invoke(this);
|
||||
}
|
||||
|
||||
// 状态变化
|
||||
public abstract void Start(object? input = null, bool isForceRestart = false);
|
||||
public abstract void Abort();
|
||||
|
||||
/// <summary>
|
||||
/// 无限期地等待加载器完成,直到结束或抛出异常。若加载器尚未开始,则会开始执行。
|
||||
/// </summary>
|
||||
public void WaitForExit(object input = null, LoaderBase loaderToSyncProgress = null,
|
||||
bool isForceRestart = false)
|
||||
{
|
||||
Start(input, isForceRestart);
|
||||
while (State == ModBase.LoadState.Loading)
|
||||
{
|
||||
if (loaderToSyncProgress is not null)
|
||||
loaderToSyncProgress.Progress = Progress;
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
if (State == ModBase.LoadState.Finished)
|
||||
{
|
||||
}
|
||||
else if (State == ModBase.LoadState.Aborted)
|
||||
{
|
||||
throw new ThreadInterruptedException("加载器执行已中断。");
|
||||
}
|
||||
else if (Error is null)
|
||||
{
|
||||
throw new Exception("未知错误!");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(Error.Message, Error);
|
||||
} // 保留调用堆栈,同时不影响信息输出与单元测试
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待加载器完成,直到结束、抛出异常或超时。若加载器尚未开始,则会开始执行。
|
||||
/// </summary>
|
||||
/// <param name="timeout">等待的超时时间,以毫秒为单位。</param>
|
||||
/// <param name="timeoutMessage">若执行超时,将会抛出的异常信息。</param>
|
||||
public void WaitForExitTime(int timeout, object input = null, string timeoutMessage = waitForExitTimeoutMessage,
|
||||
object loaderToSyncProgress = null, bool isForceRestart = false)
|
||||
{
|
||||
Start(input, isForceRestart);
|
||||
while (State == ModBase.LoadState.Loading)
|
||||
{
|
||||
if (loaderToSyncProgress is not null)
|
||||
((dynamic)loaderToSyncProgress).Progress = Progress;
|
||||
Thread.Sleep(10);
|
||||
timeout -= 10;
|
||||
if (timeout < 0)
|
||||
throw new TimeoutException(timeoutMessage);
|
||||
}
|
||||
|
||||
if (State == ModBase.LoadState.Finished)
|
||||
{
|
||||
}
|
||||
else if (State == ModBase.LoadState.Aborted)
|
||||
{
|
||||
throw new ThreadInterruptedException("加载器执行已中断。");
|
||||
}
|
||||
else if (Error is null)
|
||||
{
|
||||
throw new Exception("未知错误!");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw Error;
|
||||
}
|
||||
}
|
||||
|
||||
// 相同重载
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var @base = obj as LoaderBase;
|
||||
return @base is not null && Uuid == @base.Uuid;
|
||||
}
|
||||
}
|
||||
|
||||
// 说实话,我真的觉得 C# 应该学学 VB 的那种近乎 Java 泛型擦除的兼容性,省掉一堆麻烦
|
||||
public abstract class LoaderTask : LoaderBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 上次完成加载时的时间。
|
||||
/// </summary>
|
||||
public long lastFinishedTime;
|
||||
|
||||
/// <summary>
|
||||
/// 最后一次运行加载器的线程。可能为 Nothing,或线程已结束。
|
||||
/// </summary>
|
||||
public Task? lastRunningTask;
|
||||
|
||||
/// <summary>
|
||||
/// 在输入相同时使用原有结果的超时,单位为毫秒。
|
||||
/// </summary>
|
||||
public int reloadTimeout = -1;
|
||||
|
||||
// 状态指示
|
||||
/// <summary>
|
||||
/// 当前执行线程是否应当中断。只应用在加载器的工作线程中判断,不可跨线程调用。
|
||||
/// </summary>
|
||||
public bool IsAborted => IsAbortedWithThread(Task.CurrentId ?? -1);
|
||||
|
||||
/// <summary>
|
||||
/// 当前执行线程是否应当中断。需要手动提供加载器线程,用于需要跨线程检查的情况。
|
||||
/// </summary>
|
||||
public bool IsAbortedWithThread(int compareTaskId)
|
||||
{
|
||||
return lastRunningTask is null || compareTaskId != lastRunningTask.Id ||
|
||||
State == ModBase.LoadState.Aborted;
|
||||
}
|
||||
|
||||
public abstract bool ShouldStart(ref object? input, bool isForceRestart = false, bool ignoreReloadTimeout = false);
|
||||
|
||||
// 装箱!装箱!装箱圣地!
|
||||
public abstract object? StartGetInputNoType(object? input = null, Func<object>? inputDelegate = null);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于异步执行并监控单一函数的加载器。
|
||||
/// </summary>
|
||||
public class LoaderTask<InputType, OutputType> : LoaderTask
|
||||
{
|
||||
// 输入输出
|
||||
public InputType input;
|
||||
protected internal Func<InputType?>? inputDelegate;
|
||||
|
||||
// 执行事件
|
||||
protected internal Action<LoaderTask<InputType, OutputType>> loadDelegate;
|
||||
public OutputType output = default;
|
||||
|
||||
private CancellationTokenSource? cancelToken;
|
||||
|
||||
// 线程设定
|
||||
protected internal ThreadPriority threadPriority;
|
||||
|
||||
public LoaderTask(string name, Action<LoaderTask<InputType, OutputType>> loadDelegate,
|
||||
Func<InputType?>? inputDelegate = null, ThreadPriority priority = ThreadPriority.Normal)
|
||||
{
|
||||
this.name = name;
|
||||
this.loadDelegate = loadDelegate;
|
||||
this.inputDelegate = inputDelegate;
|
||||
}
|
||||
|
||||
// 获取输入
|
||||
public InputType? StartGetInput(InputType? input = default, Func<InputType?>? inputDelegate = null) // InputDelegate 参数存在匿名调用
|
||||
{
|
||||
inputDelegate ??= this.inputDelegate;
|
||||
// 按照龙猫的逻辑,此处将 input 与默认值直接进行等价比较,若相等则认为 input 未传入具体值,而调用 inputDelegate 获取
|
||||
// 这种逻辑未考虑值类型恰好传入 default 值 (如 double 传了 0.0) 的情况,这是一个陷阱,可能会产生 undefined behavior
|
||||
if (EqualityComparer<InputType>.Default.Equals(input, default) && inputDelegate is not null)
|
||||
ModBase.RunInUiWait(() => input = inputDelegate());
|
||||
return input;
|
||||
}
|
||||
|
||||
public override object? StartGetInputNoType(object? input = null, Func<object?>? inputDelegate = null)
|
||||
{
|
||||
return StartGetInput(input is null ? default : (InputType?)input, inputDelegate is null ? null : () => (InputType?)inputDelegate());
|
||||
}
|
||||
|
||||
// 代码执行
|
||||
public override bool ShouldStart(ref object? input, bool isForceRestart = false, bool ignoreReloadTimeout = false)
|
||||
{
|
||||
// 获取输入
|
||||
try
|
||||
{
|
||||
input = StartGetInput(Conversions.ToGenericParameter<InputType>(input));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"加载输入获取失败(" + name + ")",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Application.Loader.Error.OperationFailed"));
|
||||
Error = ex;
|
||||
lock (lockState)
|
||||
{
|
||||
State = ModBase.LoadState.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
// 检验输入以确定情况
|
||||
if (isForceRestart)
|
||||
return true; // 强制要求重启
|
||||
if (input is null != this.input is null || (input is not null && !input.Equals(this.input)))
|
||||
return true; // 输入不同
|
||||
if ((State == ModBase.LoadState.Loading || State == ModBase.LoadState.Finished) && (ignoreReloadTimeout ||
|
||||
reloadTimeout == -1 || lastFinishedTime == 0L ||
|
||||
TimeUtils.GetTimeTick() - lastFinishedTime < reloadTimeout)) // 正在加载或已结束
|
||||
// 没有超时
|
||||
return false; // 则不重试
|
||||
|
||||
return true;
|
||||
// 需要开始
|
||||
}
|
||||
|
||||
public override void Start(object input = null, bool isForceRestart = false)
|
||||
{
|
||||
// 确认是否开始加载
|
||||
if (ShouldStart(ref input, isForceRestart))
|
||||
{
|
||||
// 输入不同或失败,开始加载
|
||||
if (State == ModBase.LoadState.Loading)
|
||||
TriggerThreadAbort();
|
||||
this.input = Conversions.ToGenericParameter<InputType>(input);
|
||||
lock (lockState)
|
||||
{
|
||||
State = ModBase.LoadState.Loading;
|
||||
Progress = -1;
|
||||
}
|
||||
}
|
||||
else return;
|
||||
|
||||
// 如果线程是因为判断到 IsAborted 而提前中止,则代表已有新线程被重启,此时不应当改为 Aborted
|
||||
// 如果线程是在没有 IsAborted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Aborted
|
||||
lastRunningTask = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
isForceRestarting = isForceRestart;
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log(
|
||||
$"[Loader] 加载线程 {name} ({Task.CurrentId}) 已{(isForceRestarting ? "强制" : "")}启动");
|
||||
loadDelegate(this);
|
||||
if (IsAborted)
|
||||
{
|
||||
ModBase.Log(
|
||||
$"[Loader] 加载线程 {name} ({Task.CurrentId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{(lastRunningTask is null ? -1 : lastRunningTask.Id)})",
|
||||
ModBase.LogLevel.Developer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log($"[Loader] 加载线程 {name} ({Task.CurrentId}) 已完成");
|
||||
RaisePreviewFinish();
|
||||
State = ModBase.LoadState.Finished;
|
||||
lastFinishedTime = TimeUtils.GetTimeTick();
|
||||
}
|
||||
catch (ModBase.CancelledException ex)
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log(ex,
|
||||
$"加载线程 {name} ({Task.CurrentId}) 已触发取消中断,已完成 {Math.Round(Progress * 100d)}%");
|
||||
if (!IsAborted) State = ModBase.LoadState.Aborted;
|
||||
}
|
||||
catch (ThreadInterruptedException ex)
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
ModBase.Log(ex,
|
||||
$"加载线程 {name} ({Task.CurrentId}) 已触发线程中断,已完成 {Math.Round(Progress * 100d)}%");
|
||||
if (!IsAborted) State = ModBase.LoadState.Aborted;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (IsAborted) return;
|
||||
ModBase.Log(ex,
|
||||
$"加载线程 {name} ({Task.CurrentId}) 出错,已完成 {Math.Round(Progress * 100d)}%",
|
||||
ModBase.LogLevel.Developer);
|
||||
Error = ex;
|
||||
State = ModBase.LoadState.Failed;
|
||||
}
|
||||
}, (cancelToken ??= new CancellationTokenSource()).Token); // 未中断,本次输出有效
|
||||
// LastRunningTask.Start(); // 不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted
|
||||
}
|
||||
|
||||
public override void Abort()
|
||||
{
|
||||
if (State != ModBase.LoadState.Loading)
|
||||
return;
|
||||
lock (lockState)
|
||||
{
|
||||
State = ModBase.LoadState.Aborted;
|
||||
}
|
||||
|
||||
TriggerThreadAbort();
|
||||
}
|
||||
|
||||
private void TriggerThreadAbort()
|
||||
{
|
||||
if (lastRunningTask is null) return;
|
||||
if (ModBase.modeDebug) ModBase.Log($"[Loader] 加载线程 {name} ({lastRunningTask.Id}) 已中断");
|
||||
if (!lastRunningTask.IsCompleted) cancelToken?.Cancel();
|
||||
lastRunningTask = null;
|
||||
cancelToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 支持多个加载器连续运作的复合加载器。
|
||||
/// </summary>
|
||||
public class LoaderCombo : LoaderBase
|
||||
{
|
||||
public object? input;
|
||||
|
||||
public List<LoaderBase> loaders = new();
|
||||
|
||||
public LoaderCombo(string name, IEnumerable<LoaderBase> loaders)
|
||||
{
|
||||
this.loaders.Clear();
|
||||
foreach (var Loader in loaders)
|
||||
if (Loader is not null)
|
||||
{
|
||||
this.loaders.Add(Loader);
|
||||
Loader.OnStateChangedThread += SubTaskStateChanged;
|
||||
Loader.hasOnStateChangedThread = true;
|
||||
}
|
||||
|
||||
InitParent(null);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public override double Progress
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case ModBase.LoadState.Waiting:
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
case ModBase.LoadState.Loading:
|
||||
{
|
||||
var total = 0d;
|
||||
var finished = 0d;
|
||||
foreach (var Loader in loaders)
|
||||
{
|
||||
total += Loader.ProgressWeight;
|
||||
finished += Loader.ProgressWeight * Loader.Progress;
|
||||
}
|
||||
|
||||
if (total == 0d)
|
||||
return 0d;
|
||||
return finished / total;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return 1d;
|
||||
}
|
||||
}
|
||||
}
|
||||
set => throw new Exception("多重加载器不支持设置进度");
|
||||
}
|
||||
|
||||
public override void InitParent(LoaderBase parent)
|
||||
{
|
||||
this.parent = parent;
|
||||
foreach (var Loader in loaders)
|
||||
Loader.InitParent(this);
|
||||
}
|
||||
|
||||
public override void Start(object input = null, bool isForceRestart = false)
|
||||
{
|
||||
isForceRestarting = isForceRestart;
|
||||
lock (lockState)
|
||||
{
|
||||
if (State == ModBase.LoadState.Loading) return;
|
||||
|
||||
State = ModBase.LoadState.Loading;
|
||||
}
|
||||
|
||||
// 启动加载
|
||||
this.input = input;
|
||||
if (isForceRestart)
|
||||
foreach (var Loader in loaders)
|
||||
Loader.State = ModBase.LoadState.Waiting;
|
||||
ModBase.RunInThread(Update);
|
||||
}
|
||||
|
||||
public override void Abort()
|
||||
{
|
||||
lock (lockState)
|
||||
{
|
||||
if (State != ModBase.LoadState.Loading && State != ModBase.LoadState.Waiting)
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var Loader in loaders) Loader.Abort();
|
||||
|
||||
lock (lockState)
|
||||
{
|
||||
if (State == ModBase.LoadState.Loading || State == ModBase.LoadState.Waiting)
|
||||
State = ModBase.LoadState.Aborted;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 子任务状态变更。
|
||||
/// </summary>
|
||||
private void SubTaskStateChanged(LoaderBase loader, ModBase.LoadState newState, ModBase.LoadState oldState)
|
||||
{
|
||||
switch (newState)
|
||||
{
|
||||
case ModBase.LoadState.Loading:
|
||||
{
|
||||
break;
|
||||
}
|
||||
// 开始,啥都不干
|
||||
case ModBase.LoadState.Waiting:
|
||||
{
|
||||
break;
|
||||
}
|
||||
// 子加载器可能由于外部输入改变而暂时变为 Waiting,之后会立即重新启动
|
||||
// 所以啥都不干就行
|
||||
case ModBase.LoadState.Finished:
|
||||
{
|
||||
// 正常结束,触发刷新
|
||||
Update();
|
||||
break;
|
||||
}
|
||||
case ModBase.LoadState.Aborted:
|
||||
{
|
||||
// 被中断,这个任务也中断
|
||||
Abort();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
// 完蛋,出错了
|
||||
lock (lockState)
|
||||
{
|
||||
if (State >= ModBase.LoadState.Finished)
|
||||
return;
|
||||
Error = new Exception(loader.name + "失败", loader.Error);
|
||||
State = loader.State;
|
||||
}
|
||||
|
||||
foreach (var currentLoader in loaders)
|
||||
{
|
||||
loader = currentLoader;
|
||||
loader.Abort();
|
||||
}
|
||||
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发一次更新,以启动新加载器或完成。
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (State == ModBase.LoadState.Finished
|
||||
|| State == ModBase.LoadState.Failed
|
||||
|| State == ModBase.LoadState.Aborted)
|
||||
return;
|
||||
|
||||
var isFinished = true;
|
||||
var blocked = false;
|
||||
object input = this.input;
|
||||
|
||||
foreach (var loader in loaders)
|
||||
switch (loader.State)
|
||||
{
|
||||
case ModBase.LoadState.Finished:
|
||||
{
|
||||
if (loader is LoaderTask task)
|
||||
{
|
||||
var genericArg = task.GetType().GenericTypeArguments.FirstOrDefault();
|
||||
var shouldInput = input is not null && genericArg == input.GetType()
|
||||
? input
|
||||
: null;
|
||||
|
||||
if (task.ShouldStart(ref shouldInput, false, true))
|
||||
{
|
||||
ModBase.Log("[Loader] 由于输入条件变更,重启已完成的加载器 " + loader.name);
|
||||
goto Restart;
|
||||
}
|
||||
|
||||
input = ((dynamic)loader).output; // 何意味啊,没法匹配 LoaderTask<,>
|
||||
}
|
||||
|
||||
if (loader.block && !isFinished)
|
||||
blocked = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ModBase.LoadState.Loading:
|
||||
{
|
||||
if (loader is LoaderTask task)
|
||||
{
|
||||
var genericArg = loader.GetType().GenericTypeArguments.FirstOrDefault();
|
||||
var shouldInput = input is not null && genericArg == input.GetType()
|
||||
? input
|
||||
: null;
|
||||
if (task.ShouldStart(ref shouldInput, false, true))
|
||||
{
|
||||
ModBase.Log($"[Loader] 由于输入条件变更,重启进行中的加载器 {loader.name}",
|
||||
ModBase.LogLevel.Developer);
|
||||
goto Restart;
|
||||
}
|
||||
}
|
||||
|
||||
isFinished = false;
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
Restart:
|
||||
|
||||
isFinished = false;
|
||||
|
||||
if (blocked)
|
||||
continue;
|
||||
|
||||
if (input is not null)
|
||||
{
|
||||
switch (loader)
|
||||
{
|
||||
case LoaderTask:
|
||||
case LoaderCombo:
|
||||
var genericArg = loader.GetType().GenericTypeArguments.FirstOrDefault();
|
||||
|
||||
loader.Start(
|
||||
genericArg == input.GetType() ? input : null,
|
||||
isForceRestarting);
|
||||
break;
|
||||
case not null:
|
||||
loader.Start(
|
||||
input is List<DownloadFile> ? input : null,
|
||||
isForceRestarting);
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"未知的加载器类型({loader?.GetType()})");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
loader.Start(isForceRestart: isForceRestarting);
|
||||
}
|
||||
|
||||
if (loader.block)
|
||||
blocked = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (isFinished)
|
||||
{
|
||||
RaisePreviewFinish();
|
||||
State = ModBase.LoadState.Finished;
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获得最底层的,应被显示给用户的加载器列表,并追加于 List。
|
||||
/// </summary>
|
||||
public static void GetLoaderList(LoaderCombo loader, ref List<LoaderBase> list, bool requireShow = true)
|
||||
{
|
||||
foreach (var SubLoader in loader.loaders)
|
||||
{
|
||||
if (SubLoader.show || !requireShow)
|
||||
list.Add(SubLoader);
|
||||
if (SubLoader is LoaderCombo combo)
|
||||
GetLoaderList(combo, ref list);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获得最底层的,应被显示给用户的加载器列表,并追加于 List。
|
||||
/// </summary>
|
||||
public void GetLoaderList(ref List<LoaderBase> list, bool requireShow = true)
|
||||
{
|
||||
GetLoaderList(this, ref list, requireShow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获得最底层的,应被显示给用户的加载器列表。
|
||||
/// </summary>
|
||||
public List<LoaderBase> GetLoaderList(bool requireShow = true)
|
||||
{
|
||||
var list = new List<LoaderBase>();
|
||||
GetLoaderList(ref list, requireShow);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 支持多个加载器连续运作的复合加载器(泛型版本)。
|
||||
/// </summary>
|
||||
public class LoaderCombo<InputType> : LoaderCombo
|
||||
{
|
||||
public new InputType typedInput;
|
||||
|
||||
public LoaderCombo(string name, IEnumerable<LoaderBase> loaders) : base(name, loaders) { }
|
||||
|
||||
public override void Start(object input = null, bool isForceRestart = false)
|
||||
{
|
||||
this.typedInput = Conversions.ToGenericParameter<InputType>(input);
|
||||
base.Start(this.typedInput, isForceRestart);
|
||||
}
|
||||
}
|
||||
|
||||
private struct LoaderFolderDictionaryEntry
|
||||
{
|
||||
public DateTime? lastCheckTime;
|
||||
public string folderPath;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is not LoaderFolderDictionaryEntry)
|
||||
return false;
|
||||
var entry = (LoaderFolderDictionaryEntry)obj;
|
||||
return EqualityComparer<DateTime?>.Default.Equals(lastCheckTime, entry.lastCheckTime) &&
|
||||
(folderPath ?? "") == (entry.folderPath ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
using System.Net;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Effects;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Configuration;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
using PCL.Core.UI.Theme;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using PCL.Network;
|
||||
|
||||
using PCL.Core.App.Localization;
|
||||
namespace PCL;
|
||||
|
||||
public class ModSetup
|
||||
{
|
||||
public ModSetup()
|
||||
{
|
||||
// === Hide Group ===
|
||||
ConfigService.RegisterObserver(Config.Preference.Hide,
|
||||
new ConfigObserver(ConfigEvent.Changed, _ => PageSetupUI.HiddenRefresh()));
|
||||
|
||||
// === Launch ===
|
||||
Config.Launch.MemoryAllocationModeConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => LaunchRamType((int)e.Value!)));
|
||||
States.Game.SelectedFolderConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => LaunchFolderSelect((string)(e.Value ?? ""))));
|
||||
States.Game.SelectedInstanceConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => LaunchInstanceSelect((string)(e.Value ?? ""))));
|
||||
|
||||
// === Tool ===
|
||||
Config.Download.ThreadLimitConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => ToolDownloadThread((int)e.Value!)));
|
||||
Config.Download.SpeedLimitConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => ToolDownloadSpeed((int)e.Value!)));
|
||||
|
||||
// === UI - Launcher ===
|
||||
Config.Preference.Theme.WindowOpacityConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiLauncherTransparent((int)e.Value!)));
|
||||
Config.Preference.Theme.ThemeSelectedConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiLauncherTheme((int)e.Value!)));
|
||||
Config.Preference.Background.BackgroundColorfulConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiBackgroundColorful((bool)e.Value!)));
|
||||
Config.Preference.LockWindowSizeConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiLockWindowSize((bool)e.Value!)));
|
||||
|
||||
// UI - Video Background
|
||||
Config.Preference.Background.AutoPauseVideoConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiAutoPauseVideo((bool)e.Value!)));
|
||||
|
||||
// UI - Background Image
|
||||
Config.Preference.Background.WallpaperOpacityConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiBackgroundOpacity((int)e.Value!)));
|
||||
Config.Preference.Background.WallpaperBlurRadiusConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiBackgroundBlur((int)e.Value!)));
|
||||
Config.Preference.Background.WallpaperSuitModeConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiBackgroundSuit((int)e.Value!)));
|
||||
|
||||
// UI - Font
|
||||
Config.Preference.FontConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiFont((string)(e.Value ?? ""))));
|
||||
|
||||
// UI - Homepage
|
||||
Config.Preference.Homepage.TypeConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiCustomType((int)e.Value!)));
|
||||
|
||||
// UI - Blur
|
||||
Config.Preference.Blur.IsEnabledConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiBlur((bool)e.Value!)));
|
||||
Config.Preference.Blur.RadiusConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiBlurValue((int)e.Value!)));
|
||||
Config.Preference.Blur.SamplingRateConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiBlurSamplingRate((int)e.Value!)));
|
||||
Config.Preference.Blur.KernelTypeConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiBlurType((int)e.Value!)));
|
||||
|
||||
// UI - Title Bar
|
||||
Config.Preference.WindowTitleTypeConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiLogoType((int)e.Value!)));
|
||||
Config.Preference.WindowTitleCustomTextConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiLogoText((string)(e.Value ?? ""))));
|
||||
Config.Preference.TopBarLeftAlignConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => UiLogoLeft((bool)e.Value!)));
|
||||
|
||||
// === System ===
|
||||
Config.Debug.EnabledConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => SystemDebugMode((bool)e.Value!)));
|
||||
Config.Debug.AnimationSpeedConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => SystemDebugAnim((int)e.Value!)));
|
||||
Config.Network.HttpProxy.CustomAddressConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => SystemHttpProxy((string)(e.Value ?? ""))));
|
||||
Config.Network.HttpProxy.TypeConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => SystemHttpProxyType((int)e.Value!)));
|
||||
Config.Network.HttpProxy.CustomUsernameConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => SystemHttpProxyCustomUsername((string)(e.Value ?? ""))));
|
||||
Config.Network.HttpProxy.CustomPasswordConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => SystemHttpProxyCustomPassword((string)(e.Value ?? ""))));
|
||||
|
||||
// === Version ===
|
||||
Config.Instance.MemorySolutionConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => VersionRamType((int)e.Value!)));
|
||||
Config.InstanceAuth.LoginRequirementSolutionConfig.Observe(new ConfigObserver(ConfigEvent.Changed,
|
||||
e => VersionServerLogin((int)e.Value!)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主动应用所有当前配置值。
|
||||
/// </summary>
|
||||
public static void ApplyAll()
|
||||
{
|
||||
// Launch
|
||||
LaunchRamType(Config.Launch.MemoryAllocationMode);
|
||||
|
||||
// Tool
|
||||
ToolDownloadThread(Config.Download.ThreadLimit);
|
||||
ToolDownloadSpeed(Config.Download.SpeedLimit);
|
||||
|
||||
// UI - Launcher
|
||||
UiLauncherTransparent(Config.Preference.Theme.WindowOpacity);
|
||||
UiLauncherTheme(Config.Preference.Theme.ThemeSelected);
|
||||
UiBackgroundColorful(Config.Preference.Background.BackgroundColorful);
|
||||
UiLockWindowSize(Config.Preference.LockWindowSize);
|
||||
|
||||
// UI - Video Background
|
||||
UiAutoPauseVideo(Config.Preference.Background.AutoPauseVideo);
|
||||
|
||||
// UI - Background Image
|
||||
UiBackgroundOpacity(Config.Preference.Background.WallpaperOpacity);
|
||||
UiBackgroundBlur(Config.Preference.Background.WallpaperBlurRadius);
|
||||
UiBackgroundSuit(Config.Preference.Background.WallpaperSuitMode);
|
||||
|
||||
// UI - Font
|
||||
UiFont(Config.Preference.Font);
|
||||
|
||||
// UI - Homepage
|
||||
UiCustomType(Config.Preference.Homepage.Type);
|
||||
|
||||
// UI - Blur
|
||||
if (Config.Preference.Blur.IsEnabled)
|
||||
{
|
||||
UiBlurValue(Config.Preference.Blur.Radius);
|
||||
UiBlurSamplingRate(Config.Preference.Blur.SamplingRate);
|
||||
UiBlurType(Config.Preference.Blur.KernelType);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiBlurValue(0);
|
||||
}
|
||||
|
||||
UiBlur(Config.Preference.Blur.IsEnabled);
|
||||
|
||||
// UI - Title Bar
|
||||
UiLogoType((int)Config.Preference.WindowTitleType);
|
||||
UiLogoText(Config.Preference.WindowTitleCustomText);
|
||||
UiLogoLeft(Config.Preference.TopBarLeftAlign);
|
||||
|
||||
// UI - Hide
|
||||
PageSetupUI.HiddenRefresh();
|
||||
|
||||
// System
|
||||
SystemDebugMode(Config.Debug.Enabled);
|
||||
SystemDebugAnim(Config.Debug.AnimationSpeed);
|
||||
SystemHttpProxy(Config.Network.HttpProxy.CustomAddress);
|
||||
SystemHttpProxyType(Config.Network.HttpProxy.Type);
|
||||
SystemHttpProxyCustomUsername(Config.Network.HttpProxy.CustomUsername);
|
||||
SystemHttpProxyCustomPassword(Config.Network.HttpProxy.CustomPassword);
|
||||
}
|
||||
|
||||
#region Launch
|
||||
|
||||
// 切换选择
|
||||
public static void LaunchInstanceSelect(string value)
|
||||
{
|
||||
ModBase.Log("[Setup] 当前选择的 Minecraft 版本:" + value);
|
||||
ModBase.WriteIni(ModFolder.mcFolderSelected + "PCL.ini", "Version", value);
|
||||
}
|
||||
|
||||
public static void LaunchFolderSelect(string value)
|
||||
{
|
||||
ModBase.Log("[Setup] 当前选择的 Minecraft 文件夹:" + value.Replace("$", ModBase.exePath));
|
||||
ModFolder.mcFolderSelected = value.Replace("$", ModBase.exePath);
|
||||
}
|
||||
|
||||
// 游戏内存
|
||||
public static void LaunchRamType(int type)
|
||||
{
|
||||
if (ModMain.frmSetupLaunch is null)
|
||||
return;
|
||||
ModMain.frmSetupLaunch.RamType(type);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tool
|
||||
|
||||
public static void ToolDownloadThread(int value)
|
||||
{
|
||||
ModNet.NetTaskThreadLimit = value + 1;
|
||||
}
|
||||
|
||||
public static void ToolDownloadSpeed(int value)
|
||||
{
|
||||
ModNet.NetTaskSpeedLimitHigh = value switch
|
||||
{
|
||||
<= 14 => (long)Math.Round((value + 1) * 0.1d * 1024d * 1024d),
|
||||
<= 31 => (long)Math.Round((value - 11) * 0.5d * 1024d * 1024d),
|
||||
<= 41 => (value - 21) * 1024 * 1024L,
|
||||
_ => -1
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI
|
||||
|
||||
// 启动器
|
||||
public static void UiLauncherTransparent(int value)
|
||||
{
|
||||
ModMain.frmMain.Opacity = value / 1000d + 0.4d;
|
||||
}
|
||||
|
||||
public static void UiLauncherTheme(int value)
|
||||
{
|
||||
ThemeManager.ThemeRefresh(value);
|
||||
}
|
||||
|
||||
public static void UiBackgroundColorful(bool value)
|
||||
{
|
||||
ThemeManager.ThemeRefresh();
|
||||
}
|
||||
|
||||
public static void UiLockWindowSize(bool value)
|
||||
{
|
||||
if (value)
|
||||
ModMain.frmMain.RemoveResizer();
|
||||
else
|
||||
ModMain.frmMain.AddResizer();
|
||||
}
|
||||
|
||||
// 视频背景
|
||||
public static void UiAutoPauseVideo(bool value)
|
||||
{
|
||||
if (!value)
|
||||
{
|
||||
ModVideoBack.ForcePlay = true;
|
||||
ModVideoBack.VideoPlay();
|
||||
}
|
||||
else
|
||||
{
|
||||
ModVideoBack.ForcePlay = false;
|
||||
if (ModVideoBack.IsGaming)
|
||||
ModVideoBack.VideoPause();
|
||||
}
|
||||
}
|
||||
|
||||
// 背景图片
|
||||
public static void UiBackgroundOpacity(int value)
|
||||
{
|
||||
ModMain.frmMain.ImgBack.Opacity = value / 1000d;
|
||||
}
|
||||
|
||||
public static void UiBackgroundBlur(int value)
|
||||
{
|
||||
ModMain.frmMain.ImgBack.Effect = value == 0 ? null : new BlurEffect { Radius = value + 1 };
|
||||
ModMain.frmMain.ImgBack.Margin = new Thickness(-(value + 1) / 1.8d);
|
||||
}
|
||||
|
||||
public static void UiBackgroundSuit(int value)
|
||||
{
|
||||
if (ModMain.frmMain.ImgBack.Background is null)
|
||||
return;
|
||||
var width = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Width;
|
||||
var height = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Height;
|
||||
if (value == 0)
|
||||
{
|
||||
// 智能:当图片较小时平铺,较大时适应
|
||||
if (width < ModMain.frmMain.PanMain.ActualWidth / 2d && height < ModMain.frmMain.PanMain.ActualHeight / 2d)
|
||||
value = 4; // 平铺
|
||||
else
|
||||
value = 2; // 适应
|
||||
}
|
||||
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).TileMode = TileMode.None;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).Viewport = new Rect(0d, 0d, 1d, 1d);
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).ViewportUnits = BrushMappingMode.RelativeToBoundingBox;
|
||||
switch (value)
|
||||
{
|
||||
case 1: // 居中
|
||||
{
|
||||
ModMain.frmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Center;
|
||||
ModMain.frmMain.ImgBack.VerticalAlignment = VerticalAlignment.Center;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).Stretch = Stretch.None;
|
||||
ModMain.frmMain.ImgBack.Width = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Width;
|
||||
ModMain.frmMain.ImgBack.Height = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Height;
|
||||
break;
|
||||
}
|
||||
case 2: // 适应
|
||||
{
|
||||
ModMain.frmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
ModMain.frmMain.ImgBack.VerticalAlignment = VerticalAlignment.Stretch;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).Stretch = Stretch.UniformToFill;
|
||||
ModMain.frmMain.ImgBack.Width = double.NaN;
|
||||
ModMain.frmMain.ImgBack.Height = double.NaN;
|
||||
break;
|
||||
}
|
||||
case 3: // 拉伸
|
||||
{
|
||||
ModMain.frmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
ModMain.frmMain.ImgBack.VerticalAlignment = VerticalAlignment.Stretch;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).Stretch = Stretch.Fill;
|
||||
ModMain.frmMain.ImgBack.Width = double.NaN;
|
||||
ModMain.frmMain.ImgBack.Height = double.NaN;
|
||||
break;
|
||||
}
|
||||
case 4: // 平铺
|
||||
{
|
||||
ModMain.frmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
ModMain.frmMain.ImgBack.VerticalAlignment = VerticalAlignment.Stretch;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).Stretch = Stretch.None;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).TileMode = TileMode.Tile;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).Viewport = new Rect(0d, 0d,
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Width,
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Height);
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).ViewportUnits = BrushMappingMode.Absolute;
|
||||
ModMain.frmMain.ImgBack.Width = double.NaN;
|
||||
ModMain.frmMain.ImgBack.Height = double.NaN;
|
||||
break;
|
||||
}
|
||||
case 5: // 左上
|
||||
{
|
||||
ModMain.frmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Left;
|
||||
ModMain.frmMain.ImgBack.VerticalAlignment = VerticalAlignment.Top;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).Stretch = Stretch.None;
|
||||
ModMain.frmMain.ImgBack.Width = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Width;
|
||||
ModMain.frmMain.ImgBack.Height = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Height;
|
||||
break;
|
||||
}
|
||||
case 6: // 右上
|
||||
{
|
||||
ModMain.frmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Right;
|
||||
ModMain.frmMain.ImgBack.VerticalAlignment = VerticalAlignment.Top;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).Stretch = Stretch.None;
|
||||
ModMain.frmMain.ImgBack.Width = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Width;
|
||||
ModMain.frmMain.ImgBack.Height = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Height;
|
||||
break;
|
||||
}
|
||||
case 7: // 左下
|
||||
{
|
||||
ModMain.frmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Left;
|
||||
ModMain.frmMain.ImgBack.VerticalAlignment = VerticalAlignment.Bottom;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).Stretch = Stretch.None;
|
||||
ModMain.frmMain.ImgBack.Width = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Width;
|
||||
ModMain.frmMain.ImgBack.Height = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Height;
|
||||
break;
|
||||
}
|
||||
case 8: // 右下
|
||||
{
|
||||
ModMain.frmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Right;
|
||||
ModMain.frmMain.ImgBack.VerticalAlignment = VerticalAlignment.Bottom;
|
||||
((ImageBrush)ModMain.frmMain.ImgBack.Background).Stretch = Stretch.None;
|
||||
ModMain.frmMain.ImgBack.Width = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Width;
|
||||
ModMain.frmMain.ImgBack.Height = ((ImageBrush)ModMain.frmMain.ImgBack.Background).ImageSource.Height;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 字体
|
||||
public static void UiFont(string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
ModBase.SetLaunchFont(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"字体加载失败",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Setup.Error.OperationFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
// 主页
|
||||
public static void UiCustomType(int value)
|
||||
{
|
||||
if (ModMain.frmSetupUI is null)
|
||||
return;
|
||||
switch (value)
|
||||
{
|
||||
case 0: // 无
|
||||
{
|
||||
ModMain.frmSetupUI.PanCustomPreset.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanCustomNet.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.HintCustom.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.HintCustomWarn.Visibility = Visibility.Collapsed;
|
||||
break;
|
||||
}
|
||||
case 1: // 本地
|
||||
{
|
||||
ModMain.frmSetupUI.PanCustomPreset.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanCustomLocal.Visibility = Visibility.Visible;
|
||||
ModMain.frmSetupUI.PanCustomNet.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.HintCustom.Visibility = Visibility.Visible;
|
||||
ModMain.frmSetupUI.HintCustomWarn.Visibility =
|
||||
States.Hint.UntrustedHomepage ? Visibility.Collapsed : Visibility.Visible;
|
||||
ModMain.frmSetupUI.HintCustom.Text =
|
||||
Lang.Text("Setup.Ui.Homepage.LocalFile.Hint");
|
||||
CustomEventService.SetEventType(ModMain.frmSetupUI.HintCustom, EventType.None);
|
||||
break;
|
||||
}
|
||||
case 2: // 联网
|
||||
{
|
||||
ModMain.frmSetupUI.PanCustomPreset.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanCustomNet.Visibility = Visibility.Visible;
|
||||
ModMain.frmSetupUI.HintCustom.Visibility = Visibility.Visible;
|
||||
ModMain.frmSetupUI.HintCustomWarn.Visibility =
|
||||
States.Hint.UntrustedHomepage ? Visibility.Collapsed : Visibility.Visible;
|
||||
ModMain.frmSetupUI.HintCustom.Text =
|
||||
Lang.Text("Setup.Ui.Homepage.NetUpdate.Hint");
|
||||
CustomEventService.SetEventType(ModMain.frmSetupUI.HintCustom, EventType.OpenUrl);
|
||||
CustomEventService.SetEventData(ModMain.frmSetupUI.HintCustom,
|
||||
"https://github.com/PCL-Community/PCL-CE/discussions");
|
||||
break;
|
||||
}
|
||||
case 3: // 预设
|
||||
{
|
||||
ModMain.frmSetupUI.PanCustomPreset.Visibility = Visibility.Visible;
|
||||
ModMain.frmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanCustomNet.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.HintCustom.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.HintCustomWarn.Visibility = Visibility.Collapsed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ModMain.frmSetupUI.CardCustom.TriggerForceResize();
|
||||
}
|
||||
|
||||
// 高级材质
|
||||
public static void UiBlur(bool value)
|
||||
{
|
||||
if (ModMain.frmSetupUI is null)
|
||||
return;
|
||||
|
||||
ModMain.frmSetupUI.PanBlurValue.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
|
||||
UiBlurValue(value ? Config.Preference.Blur.Radius : 0);
|
||||
}
|
||||
|
||||
public static void UiBlurValue(int value)
|
||||
{
|
||||
System.Windows.Application.Current.Resources["BlurRadius"] = value * 1.0d;
|
||||
}
|
||||
|
||||
public static void UiBlurSamplingRate(int value)
|
||||
{
|
||||
System.Windows.Application.Current.Resources["BlurSamplingRate"] = value * 0.01d;
|
||||
}
|
||||
|
||||
public static void UiBlurType(int value)
|
||||
{
|
||||
System.Windows.Application.Current.Resources["BlurType"] = (KernelType)value;
|
||||
}
|
||||
|
||||
// 顶部栏
|
||||
public static void UiLogoType(int value)
|
||||
{
|
||||
if (ThemeService.CurrentTheme == ColorTheme.HmclBlue) value = 4;
|
||||
switch (value)
|
||||
{
|
||||
case 0: // 无
|
||||
{
|
||||
ModMain.frmMain.ShapeTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.BtnTitleHelp.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.ShapeHMCLTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.LabTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.ImageTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.CELogo.Visibility = Visibility.Collapsed;
|
||||
if (ModMain.frmSetupUI is not null)
|
||||
{
|
||||
ModMain.frmSetupUI.CheckLogoLeft.Visibility = Visibility.Visible;
|
||||
ModMain.frmSetupUI.PanLogoText.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanLogoChange.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 1: // 默认
|
||||
{
|
||||
ModMain.frmMain.ShapeTitleLogo.Visibility = Visibility.Visible;
|
||||
ModMain.frmMain.BtnTitleHelp.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.ShapeHMCLTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.LabTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.ImageTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.CELogo.Visibility = Visibility.Visible;
|
||||
if (ModMain.frmSetupUI is not null)
|
||||
{
|
||||
ModMain.frmSetupUI.CheckLogoLeft.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanLogoText.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanLogoChange.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 2: // 文本
|
||||
{
|
||||
ModMain.frmMain.ShapeTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.BtnTitleHelp.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.ShapeHMCLTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.LabTitleLogo.Visibility = Visibility.Visible;
|
||||
ModMain.frmMain.ImageTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.CELogo.Visibility = Visibility.Visible;
|
||||
if (ModMain.frmSetupUI is not null)
|
||||
{
|
||||
ModMain.frmSetupUI.CheckLogoLeft.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanLogoText.Visibility = Visibility.Visible;
|
||||
ModMain.frmSetupUI.PanLogoChange.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
_ = Config.Preference.WindowTitleCustomText;
|
||||
break;
|
||||
}
|
||||
case 3: // 图片
|
||||
{
|
||||
ModMain.frmMain.ShapeTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.BtnTitleHelp.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.ShapeHMCLTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.LabTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.ImageTitleLogo.Visibility = Visibility.Visible;
|
||||
ModMain.frmMain.CELogo.Visibility = Visibility.Visible;
|
||||
if (ModMain.frmSetupUI is not null)
|
||||
{
|
||||
ModMain.frmSetupUI.CheckLogoLeft.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanLogoText.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanLogoChange.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ModMain.frmMain.ImageTitleLogo.Source = ModBase.exePath + @"PCL\Logo.png";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModMain.frmMain.ImageTitleLogo.Source = null;
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"显示标题栏图片失败",
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Setup.Error.OperationFailed"));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 4: //HMCL (愚人节)
|
||||
ModMain.frmMain.ShapeTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.ShapeHMCLTitleLogo.Visibility = Visibility.Visible;
|
||||
ModMain.frmMain.LabTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.ImageTitleLogo.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmMain.BtnTitleHelp.Visibility = Visibility.Visible;
|
||||
if (ModMain.frmSetupUI is not null)
|
||||
{
|
||||
ModMain.frmSetupUI.CheckLogoLeft.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanLogoText.Visibility = Visibility.Collapsed;
|
||||
ModMain.frmSetupUI.PanLogoChange.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
_ = Config.Preference.TopBarLeftAlign;
|
||||
if (ModMain.frmSetupUI is not null)
|
||||
ModMain.frmSetupUI.CardLogo.TriggerForceResize();
|
||||
}
|
||||
|
||||
public static void UiLogoText(string value)
|
||||
{
|
||||
ModMain.frmMain.LabTitleLogo.Text = value;
|
||||
}
|
||||
|
||||
public static void UiLogoLeft(bool value)
|
||||
{
|
||||
ModMain.frmMain.PanTitleMain.ColumnDefinitions[0].Width = new GridLength(
|
||||
value && Config.Preference.WindowTitleType == LauncherTitleType.None ? 0 : 1,
|
||||
GridUnitType.Star);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region System
|
||||
|
||||
// 调试选项
|
||||
public static void SystemDebugMode(bool value)
|
||||
{
|
||||
ModBase.modeDebug = value;
|
||||
}
|
||||
|
||||
public static void SystemDebugAnim(int value)
|
||||
{
|
||||
ModAnimation.aniSpeed = value >= 30
|
||||
? 200d
|
||||
: ModBase.MathClamp(value * 0.1d + 0.1d, 0.1d, 3d);
|
||||
}
|
||||
|
||||
public static void SystemHttpProxy(string value)
|
||||
{
|
||||
if (value.IsNullOrWhiteSpace()) return;
|
||||
try
|
||||
{
|
||||
HttpProxyManager.Instance.CustomProxyAddress = new Uri(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "HTTP 代理应用出错");
|
||||
}
|
||||
}
|
||||
|
||||
public static void SystemHttpProxyType(int value)
|
||||
{
|
||||
var mode = (HttpProxyManager.ProxyMode)value;
|
||||
HttpProxyManager.Instance.Mode = Enum.IsDefined(mode)
|
||||
? mode
|
||||
: HttpProxyManager.Instance.Mode;
|
||||
}
|
||||
|
||||
public static void SystemHttpProxyCustomUsername(string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
var password = Config.Network.HttpProxy.CustomPassword;
|
||||
HttpProxyManager.Instance.Credentials = new NetworkCredential(value, password);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpProxyManager.Instance.Credentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SystemHttpProxyCustomPassword(string value)
|
||||
{
|
||||
var username = Config.Network.HttpProxy.CustomUsername;
|
||||
HttpProxyManager.Instance.Credentials = !string.IsNullOrEmpty(username)
|
||||
? new NetworkCredential(username, value)
|
||||
: null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Version
|
||||
|
||||
// 游戏内存
|
||||
public static void VersionRamType(int type)
|
||||
{
|
||||
if (ModMain.frmInstanceSetup is null)
|
||||
return;
|
||||
ModMain.frmInstanceSetup.RamType(type);
|
||||
}
|
||||
|
||||
// 服务器
|
||||
public static void VersionServerLogin(int type)
|
||||
{
|
||||
if (ModMain.frmInstanceSetup is null)
|
||||
return;
|
||||
// 为第三方登录清空缓存以更新描述
|
||||
ModBase.WriteIni(ModFolder.mcFolderSelected + "PCL.ini", "InstanceCache", "");
|
||||
if (PageInstanceLeft.McInstance is null)
|
||||
return;
|
||||
PageInstanceLeft.McInstance = new McInstance(PageInstanceLeft.McInstance.Name).Load();
|
||||
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
|
||||
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using PCL.Core.UI.Media;
|
||||
using PixelFormat = System.Drawing.Imaging.PixelFormat;
|
||||
|
||||
// 一个万能的自动图片类型转换工具类
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class MyBitmap
|
||||
{
|
||||
// 使用缓存
|
||||
private readonly ConcurrentDictionary<string, MyBitmap> _Cache = new();
|
||||
|
||||
/// <summary>
|
||||
/// 存储的图片
|
||||
/// </summary>
|
||||
public Bitmap pic;
|
||||
|
||||
// 构造函数
|
||||
public MyBitmap()
|
||||
{
|
||||
}
|
||||
|
||||
public MyBitmap(string filePathOrResourceName)
|
||||
{
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
filePathOrResourceName =
|
||||
filePathOrResourceName.Replace("pack://application:,,,/images/", ModBase.pathImage);
|
||||
if (filePathOrResourceName.StartsWithF(ModBase.pathImage))
|
||||
{
|
||||
if (_Cache.ContainsKey(filePathOrResourceName))
|
||||
{
|
||||
pic = _Cache[filePathOrResourceName].pic;
|
||||
}
|
||||
else
|
||||
{
|
||||
pic = new MyBitmap(
|
||||
(ImageSource)new ImageSourceConverter().ConvertFromString(filePathOrResourceName));
|
||||
_Cache.TryAdd(filePathOrResourceName, pic);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 使用这种自己接管 FileStream 的方法加载才能解除文件占用
|
||||
using (var picStream = new FileStream(filePathOrResourceName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
if (picStream.Length > 2L && picStream.ReadByte() == 82 && picStream.ReadByte() == 73)
|
||||
{
|
||||
picStream.Seek(0L, SeekOrigin.Begin);
|
||||
// 调用 WIC 转换,需要系统内置 WebP 组件,专治各种精简系统
|
||||
using (var ms = picStream.FromWebpToPng())
|
||||
{
|
||||
pic = new Bitmap(ms);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pic = new Bitmap(picStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
pic = (Bitmap)System.Windows.Application.Current.TryFindResource(filePathOrResourceName);
|
||||
if (pic is null)
|
||||
{
|
||||
pic = new Bitmap(1, 1);
|
||||
if (ex is ArgumentException) throw new Exception($"图片格式不支持,或图片文件损坏({filePathOrResourceName})", ex);
|
||||
|
||||
throw new Exception($"加载 MyBitmap 意外失败({filePathOrResourceName})", ex);
|
||||
}
|
||||
|
||||
ModBase.Log(ex, $"指定类型有误的 MyBitmap 加载({filePathOrResourceName})", ModBase.LogLevel.Developer);
|
||||
break;
|
||||
}
|
||||
} while (false);
|
||||
}
|
||||
|
||||
public MyBitmap(ImageSource image)
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
var encoder = new PngBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create((BitmapSource)image));
|
||||
encoder.Save(ms);
|
||||
pic = new Bitmap(ms);
|
||||
}
|
||||
}
|
||||
|
||||
public MyBitmap(Image image)
|
||||
{
|
||||
pic = (Bitmap)image;
|
||||
}
|
||||
|
||||
public MyBitmap(Bitmap image)
|
||||
{
|
||||
pic = image;
|
||||
}
|
||||
|
||||
public MyBitmap(ImageBrush image)
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
var encoder = new BmpBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create((BitmapSource)image.ImageSource));
|
||||
encoder.Save(ms);
|
||||
pic = new Bitmap(ms);
|
||||
}
|
||||
}
|
||||
|
||||
// 自动类型转换
|
||||
// 支持的类:Image,ImageSource,Bitmap,ImageBrush,BitmapSource
|
||||
public static implicit operator MyBitmap(Image image)
|
||||
{
|
||||
if (image is null)
|
||||
return null;
|
||||
return new MyBitmap(image);
|
||||
}
|
||||
|
||||
public static implicit operator Image(MyBitmap image)
|
||||
{
|
||||
if (image is null)
|
||||
return null;
|
||||
return image.pic;
|
||||
}
|
||||
|
||||
public static implicit operator MyBitmap(ImageSource image)
|
||||
{
|
||||
if (image is null)
|
||||
return null;
|
||||
return new MyBitmap(image);
|
||||
}
|
||||
|
||||
public static implicit operator ImageSource(MyBitmap image)
|
||||
{
|
||||
if (image is null)
|
||||
return null;
|
||||
var bitmapPic = image.pic;
|
||||
var rect = new Rectangle(0, 0, bitmapPic.Width, bitmapPic.Height);
|
||||
var bitmapData = bitmapPic.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
|
||||
try
|
||||
{
|
||||
var result = BitmapSource.Create(bitmapPic.Width, bitmapPic.Height, bitmapPic.HorizontalResolution,
|
||||
bitmapPic.VerticalResolution, PixelFormats.Bgra32, null, bitmapData.Scan0, rect.Width * rect.Height * 4,
|
||||
bitmapData.Stride);
|
||||
result.Freeze();
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
bitmapPic.UnlockBits(bitmapData);
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator MyBitmap(Bitmap image)
|
||||
{
|
||||
if (image is null)
|
||||
return null;
|
||||
return new MyBitmap(image);
|
||||
}
|
||||
|
||||
public static implicit operator Bitmap(MyBitmap image)
|
||||
{
|
||||
if (image is null)
|
||||
return null;
|
||||
return image.pic;
|
||||
}
|
||||
|
||||
public static implicit operator MyBitmap(ImageBrush image)
|
||||
{
|
||||
if (image is null)
|
||||
return null;
|
||||
return new MyBitmap(image);
|
||||
}
|
||||
|
||||
public static implicit operator ImageBrush(MyBitmap image)
|
||||
{
|
||||
if (image is null)
|
||||
return null;
|
||||
return new ImageBrush(new MyBitmap(image.pic));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取裁切的图片,这个方法不会导致原对象改变且会返回一个新的对象。
|
||||
/// </summary>
|
||||
public MyBitmap Clip(int x, int y, int width, int height)
|
||||
{
|
||||
var bmp = new Bitmap(width, height, pic.PixelFormat);
|
||||
bmp.SetResolution(pic.HorizontalResolution, pic.VerticalResolution);
|
||||
using (var g = Graphics.FromImage(bmp))
|
||||
{
|
||||
g.InterpolationMode = InterpolationMode.NearestNeighbor;
|
||||
g.TranslateTransform(-x, -y);
|
||||
g.DrawImage(pic, new Rectangle(0, 0, pic.Width, pic.Height));
|
||||
}
|
||||
|
||||
return bmp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取旋转或翻转后的图片,这个方法不会导致原对象改变且会返回一个新的对象。
|
||||
/// </summary>
|
||||
public MyBitmap RotateFlip(RotateFlipType type)
|
||||
{
|
||||
var bmp = new Bitmap(pic);
|
||||
bmp.SetResolution(pic.HorizontalResolution, pic.VerticalResolution);
|
||||
bmp.RotateFlip(type);
|
||||
return bmp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将图像保存到文件。
|
||||
/// </summary>
|
||||
public void Save(string filePath)
|
||||
{
|
||||
BitmapEncoder encoder = new PngBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create((BitmapSource)this));
|
||||
using (var fileStream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
encoder.Save(fileStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user