namespace MeloongCore; /// /// 提供一个追踪任务进度的对象,它所提供的供用户查看的进度必定单调递增。 /// 支持重试、拆分子项进度、记录部分进度被跳过。 /// public class ProgressProvider { /// /// 当原始进度可能被改变时触发。 /// public event Action<(double actual, double skiped)>? ProgressChanged; private event Action? ChildProgressChanged; private void InvokeProgressChanged() { ProgressChanged?.Invoke(GetRaw()); ChildProgressChanged?.Invoke(); } // ===================================== 主项进度 ===================================== private (double actual, double skiped, double splited) progressParts = (0, 0, 0); private double progressSum => progressParts.actual + progressParts.skiped + progressParts.splited; private bool _Set(double value, bool skiped) { value = value.Clamp(0, 1); double delta = value - progressSum; if (delta < 0) { // 等比减少 progressParts = (progressParts.actual * value / progressSum, progressParts.skiped * value / progressSum, progressParts.splited * value / progressSum); } else if (skiped) { progressParts.skiped += delta; } else { progressParts.actual += delta; } return delta != 0; } /// /// 将当前进度设置为指定值。 /// 若指定了 ,这段进度的增量值将被计为跳过:当前观测进度不会增加,后续观测进度将增加地更快。 /// public void Set(double value, bool skiped = false, ChildrenAction action = ChildrenAction.Finished) { bool changed; lock (this) { changed = _Set(value, skiped); changed |= AssumeChildrenAs(action); } if (changed) InvokeProgressChanged(); } /// /// 将当前进度增加指定值。 /// 若指定了 ,这段进度的增量值将被计为跳过:当前观测进度不会增加,后续观测进度将增加地更快。 /// 若指定了 ,当前所有未完成的子项将被视为已完成。 /// public void Add(double value, bool skiped = false, ChildrenAction action = ChildrenAction.Finished) { bool changed; lock (this) { changed = _Set(value + progressSum, skiped); changed |= AssumeChildrenAs(action); } if (changed) InvokeProgressChanged(); } /// 完成当前项的剩余进度。 public void Finish(ChildrenAction action = ChildrenAction.Finished) => Set(1, skiped: false, action: action); /// 跳过当前项的剩余进度。 public void Skip(ChildrenAction action = ChildrenAction.Skiped) => Set(1, skiped: true, action: action); /// /// 重置当前进度,并清除所有子项。 /// public void Reset() { bool changed; lock (this) { changed = progressSum > 0; progressParts = (0, 0, 0); childrens.ForEach(c => c.child.ChildProgressChanged -= InvokeProgressChanged); childrens.Clear(); } if (changed) InvokeProgressChanged(); } // ===================================== 子项进度 ===================================== private readonly List<(double percentage, ProgressProvider child)> childrens = []; /// /// 拆分一个子项:当该子项完成时,进度将从当前值增加至 。 /// public ProgressProvider SplitTo(double value) { lock (this) return SplitBy(value - progressSum).First(); } /// /// 拆分多个子项,每个子项都占据对应指定的进度量。 /// 例如,调用 SplitBy(0.2, 0.3) 将返回两个子项,分别占据总进度的 20%、30%。 /// 若总进度量 > 1,子项的进度量将相对缩小。 /// public List SplitBy(params double[] percentages) { lock (this) { if (percentages.Any(p => p <= 0)) throw new ArgumentException("子项进度必须为正数且"); progressParts.splited += (percentages.Sum() + progressSum).Clamp(0, 1) - progressSum; return percentages.Select(percentage => { var sub = new ProgressProvider(); sub.ChildProgressChanged += InvokeProgressChanged; childrens.Add((percentage, sub)); return sub; }).ToList(); } } public enum ChildrenAction { /// 不做任何处理。 None, /// 将未完成的子项的剩余进度视为已完成。 Finished, /// 将未完成的子项的剩余进度视为已跳过。 Skiped } /// /// 修改所有未完成的子项。 /// /// 是否修改了任意进度不到 1 的子项。 private bool AssumeChildrenAs(ChildrenAction action) { if (action == ChildrenAction.None) return false; lock (this) { bool changed = false; if (childrens.Any(c => c.percentage > 0)) { // 将子项的进度加入主项 var mult = progressParts.splited / childrens.Sum(c => c.percentage); childrens.ForEach(c => { var (subActual, subSkiped) = c.child.GetRaw(); progressParts.actual += mult * c.percentage * (action == ChildrenAction.Finished ? (1 - subSkiped) : subActual); progressParts.skiped += mult * c.percentage * (action == ChildrenAction.Skiped ? (1 - subActual) : subSkiped); c.child.ChildProgressChanged -= InvokeProgressChanged; if (subActual + subSkiped < 0.9999999) changed = true; }); } progressParts.splited = 0; childrens.Clear(); return changed; } } // ===================================== 观测 ===================================== /// /// 获取包含子项进度的实际总进度值。 /// public (double actual, double skiped) GetRaw() { lock (this) { (double actual, double skiped) current = (progressParts.actual, progressParts.skiped); if (childrens.Any(c => c.percentage > 0)) { // 将子项的进度加入主项 var mult = progressParts.splited / childrens.Sum(c => c.percentage); childrens.ForEach(c => { var (subActual, subSkiped) = c.child.GetRaw(); current.actual += mult * c.percentage * subActual; current.skiped += mult * c.percentage * subSkiped; }); } return current; } } public bool IsFinished => GetRaw().actual + GetRaw().skiped > 0.9999999; private (double actual, double skiped) observedProgress = (0, 0); private double incrementProgress = 0; /// /// 获取一个在多次观测之间必定单调递增的进度值,范围为 [0, 1]。 /// public double GetIncreasing() { lock (this) { (double actual, double skiped) current = GetRaw(); if (current == observedProgress) return incrementProgress; // 未改变 if (IsFinished) { // 已完成 incrementProgress = 1; } else if (current.actual >= observedProgress.actual) { // 进度增加 incrementProgress += (1 - incrementProgress) * (current.actual - observedProgress.actual) / (1 - observedProgress.actual - current.skiped); } observedProgress = current; return incrementProgress; } } } /// /// 监听进度变化,并将观测到的进度节流后传给更新方法。 /// public class ProgressObserver : RateLimitedWorker { private readonly ProgressProvider provider; public ProgressObserver(ProgressProvider provider, Action updateAction, double minimalIntervalMs = 70) : base(() => Observe(provider, updateAction), minimalIntervalMs, RateLimitMode.ImmediateThenMerge) { this.provider = provider; provider.ProgressChanged += StartObserver; } public override void Dispose() { provider.ProgressChanged -= StartObserver; base.Dispose(); } private void StartObserver((double actual, double skiped) raw) => Start(); private static void Observe(ProgressProvider provider, Action updateAction) { double progress = provider.GetIncreasing(); // TODO: 添加平缓过渡 updateAction(progress); } }