XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace XFEToolBox.WpfCore.Controls;

/// <summary>
/// 同时支持确定进度和循环忙碌状态的环形进度控件。
/// </summary>
public class ProgressRing : RangeBase
{
    private static readonly DependencyProperty RotationAngleProperty = DependencyProperty.Register(
        "RotationAngle", typeof(double), typeof(ProgressRing),
        new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender));

    public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register(
        nameof(IsActive), typeof(bool), typeof(ProgressRing),
        new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender, OnAnimationStateChanged));

    public static readonly DependencyProperty IsIndeterminateProperty = DependencyProperty.Register(
        nameof(IsIndeterminate), typeof(bool), typeof(ProgressRing),
        new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender, OnAnimationStateChanged));

    public static readonly DependencyProperty RingThicknessProperty = DependencyProperty.Register(
        nameof(RingThickness), typeof(double), typeof(ProgressRing),
        new FrameworkPropertyMetadata(3d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
            null, CoerceRingThickness));

    static ProgressRing()
    {
        ValueProperty.OverrideMetadata(
            typeof(ProgressRing),
            new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender));
    }

    public ProgressRing()
    {
        Loaded += (_, _) => UpdateAnimation();
        Unloaded += (_, _) => StopAnimation();
        IsVisibleChanged += (_, _) => UpdateAnimation();
    }

    public bool IsActive
    {
        get => (bool)GetValue(IsActiveProperty);
        set => SetValue(IsActiveProperty, value);
    }

    public bool IsIndeterminate
    {
        get => (bool)GetValue(IsIndeterminateProperty);
        set => SetValue(IsIndeterminateProperty, value);
    }

    public double RingThickness
    {
        get => (double)GetValue(RingThicknessProperty);
        set => SetValue(RingThicknessProperty, value);
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);

        var thickness = Math.Clamp(RingThickness, 0.5, Math.Max(0.5, Math.Min(ActualWidth, ActualHeight) / 2));
        var radius = Math.Max(0, Math.Min(ActualWidth, ActualHeight) / 2 - thickness / 2);
        if (radius <= 0)
            return;

        var center = new Point(ActualWidth / 2, ActualHeight / 2);
        var trackBrush = Background ?? Brushes.Transparent;
        var progressBrush = Foreground ?? Brushes.Transparent;
        var trackPen = CreatePen(trackBrush, thickness);
        var progressPen = CreatePen(progressBrush, thickness);

        drawingContext.DrawEllipse(null, trackPen, center, radius, radius);

        if (IsActive && IsIndeterminate)
        {
            DrawArc(drawingContext, progressPen, center, radius, -90 + (double)GetValue(RotationAngleProperty), 96);
        }
        else
        {
            var range = Maximum - Minimum;
            var progress = range <= 0 ? 0 : Math.Clamp((Value - Minimum) / range, 0, 1);
            if (progress >= 0.9999)
                drawingContext.DrawEllipse(null, progressPen, center, radius, radius);
            else if (progress > 0)
                DrawArc(drawingContext, progressPen, center, radius, -90, progress * 359.9);
        }

    }

    protected override void OnMinimumChanged(double oldMinimum, double newMinimum)
    {
        base.OnMinimumChanged(oldMinimum, newMinimum);
        InvalidateVisual();
    }

    protected override void OnMaximumChanged(double oldMaximum, double newMaximum)
    {
        base.OnMaximumChanged(oldMaximum, newMaximum);
        InvalidateVisual();
    }

    protected override void OnValueChanged(double oldValue, double newValue)
    {
        base.OnValueChanged(oldValue, newValue);
        InvalidateVisual();
    }

    private static object CoerceRingThickness(DependencyObject dependencyObject, object baseValue) =>
        Math.Max(0.5, (double)baseValue);

    private static void OnAnimationStateChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        if (dependencyObject is ProgressRing ring)
            ring.UpdateAnimation();
    }

    private void UpdateAnimation()
    {
        if (!IsLoaded || !IsVisible || !IsActive || !IsIndeterminate)
        {
            StopAnimation();
            return;
        }

        var animation = new DoubleAnimation
        {
            From = 0,
            To = 360,
            Duration = TimeSpan.FromMilliseconds(920),
            RepeatBehavior = RepeatBehavior.Forever
        };
        BeginAnimation(RotationAngleProperty, animation, HandoffBehavior.SnapshotAndReplace);
    }

    private void StopAnimation()
    {
        BeginAnimation(RotationAngleProperty, null);
        SetCurrentValue(RotationAngleProperty, 0d);
    }

    private static Pen CreatePen(Brush brush, double thickness) => new(brush, thickness)
    {
        StartLineCap = PenLineCap.Round,
        EndLineCap = PenLineCap.Round
    };

    private static void DrawArc(
        DrawingContext drawingContext,
        Pen pen,
        Point center,
        double radius,
        double startAngle,
        double sweepAngle)
    {
        var start = PointOnCircle(center, radius, startAngle);
        var end = PointOnCircle(center, radius, startAngle + sweepAngle);
        var geometry = new StreamGeometry();
        using (var context = geometry.Open())
        {
            context.BeginFigure(start, false, false);
            context.ArcTo(
                end,
                new Size(radius, radius),
                0,
                sweepAngle > 180,
                SweepDirection.Clockwise,
                true,
                false);
        }
        geometry.Freeze();
        drawingContext.DrawGeometry(null, pen, geometry);
    }

    private static Point PointOnCircle(Point center, double radius, double angle)
    {
        var radians = angle * Math.PI / 180;
        return new Point(center.X + radius * Math.Cos(radians), center.Y + radius * Math.Sin(radians));
    }
}