WPF倒计时用户控件问题

WPF Countdown user control issue

我正在尝试使用 Stackexchange 中此控件的最终版本:

https://codereview.stackexchange.com/questions/197197/countdown-control-with-arc-animation

当我使用代码时,它只倒计时一秒就结束了。我不确定是什么问题。

希望有人能帮忙 - 谢谢。

我使用的代码是这样的:

Arc.cs

public class Arc : Shape
{
    public Point Center
    {
        get => (Point)GetValue(CenterProperty);
        set => SetValue(CenterProperty, value);
    }

    public static readonly DependencyProperty CenterProperty =
        DependencyProperty.Register(nameof(Center), typeof(Point), typeof(Arc),
            new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));

    public double StartAngle
    {
        get => (double)GetValue(StartAngleProperty);
        set => SetValue(StartAngleProperty, value);
    }

    public static readonly DependencyProperty StartAngleProperty =
        DependencyProperty.Register(nameof(StartAngle), typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));

    public double EndAngle
    {
        get => (double)GetValue(EndAngleProperty);
        set => SetValue(EndAngleProperty, value);
    }

    public static readonly DependencyProperty EndAngleProperty =
        DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));

    public double Radius
    {
        get => (double)GetValue(RadiusProperty);
        set => SetValue(RadiusProperty, value);
    }

    public static readonly DependencyProperty RadiusProperty =
        DependencyProperty.Register(nameof(Radius), typeof(double), typeof(Arc),
            new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));

    public bool SmallAngle
    {
        get => (bool)GetValue(SmallAngleProperty);
        set => SetValue(SmallAngleProperty, value);
    }

    public static readonly DependencyProperty SmallAngleProperty =
        DependencyProperty.Register(nameof(SmallAngle), typeof(bool), typeof(Arc),
            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));

    static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));

    protected override Geometry DefiningGeometry
    {
        get
        {
            double startAngleRadians = StartAngle * Math.PI / 180;
            double endAngleRadians = EndAngle * Math.PI / 180;

            double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
            double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;

            if (a1 < a0)
                a1 += Math.PI * 2;

            SweepDirection d = SweepDirection.Counterclockwise;
            bool large;

            if (SmallAngle)
            {
                large = false;
                d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
            }
            else
                large = (Math.Abs(a1 - a0) < Math.PI);

            Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
            Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;

            List<PathSegment> segments = new List<PathSegment>
            {
                new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
            };

            List<PathFigure> figures = new List<PathFigure>
            {
                new PathFigure(p0, segments, true)
                {
                    IsClosed = false
                }
            };

            return new PathGeometry(figures, FillRule.EvenOdd, null);
        }
    }
}

Countdown.xaml

<UserControl x:Class="WpfApp.Countdown"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="450" Loaded="Countdown_Loaded">
    <Viewbox>
        <Grid Width="100" Height="100">
            <Border Background="#222" Margin="5" CornerRadius="50">
                <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
                    <Label Foreground="#fff" Content="{Binding SecondsRemaining}" FontSize="50" Margin="0, -10, 0, 0" />
                    <Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -15, 0, 0" />
                </StackPanel>
            </Border>

            <uc:Arc
                x:Name="Arc"
                Center="50, 50"
                StartAngle="-90"
                EndAngle="-90"
                Stroke="#45d3be"
                StrokeThickness="5"
                Radius="45" />
        </Grid>
    </Viewbox>
</UserControl>

Countdown.xaml.cs

public partial class Countdown : UserControl
{
    public Duration Duration
    {
        get => (Duration)GetValue(DurationProperty);
        set => SetValue(DurationProperty, value);
    }

    public static readonly DependencyProperty DurationProperty =
        DependencyProperty.Register(nameof(Duration), typeof(Duration), typeof(Countdown), new PropertyMetadata(new Duration()));

    public int SecondsRemaining
    {
        get => (int)GetValue(SecondsRemainingProperty);
        set => SetValue(SecondsRemainingProperty, value);
    }

    public static readonly DependencyProperty SecondsRemainingProperty =
        DependencyProperty.Register(nameof(SecondsRemaining), typeof(int), typeof(Countdown), new PropertyMetadata(0));

    public event EventHandler Elapsed;

    private readonly Storyboard _storyboard = new Storyboard();

    public Countdown()
    {
        InitializeComponent();

        DoubleAnimation animation = new DoubleAnimation(-90, 270, Duration);
        Storyboard.SetTarget(animation, Arc);
        Storyboard.SetTargetProperty(animation, new PropertyPath(nameof(Arc.EndAngle)));
        _storyboard.Children.Add(animation);

        DataContext = this;
    }

    private void Countdown_Loaded(object sender, EventArgs e)
    {
        if (IsVisible)
            Start();
    }

    public void Start()
    {
        Stop();

        _storyboard.CurrentTimeInvalidated += Storyboard_CurrentTimeInvalidated;
        _storyboard.Completed += Storyboard_Completed;

        _storyboard.Begin();
    }

    public void Stop()
    {
        _storyboard.CurrentTimeInvalidated -= Storyboard_CurrentTimeInvalidated;
        _storyboard.Completed -= Storyboard_Completed;

        _storyboard.Stop();
    }

    private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e)
    {
        ClockGroup cg = (ClockGroup)sender;
        if (cg.CurrentTime == null) return;
        TimeSpan elapsedTime = cg.CurrentTime.Value;
        SecondsRemaining = (int)Math.Ceiling((Duration.TimeSpan - elapsedTime).TotalSeconds);
    }

    private void Storyboard_Completed(object sender, EventArgs e)
    {
        if (IsVisible)
            Elapsed?.Invoke(this, EventArgs.Empty);
    }
}

您的控件未正确初始化。您当前未处理 Duration 属性.

的 属性 更改

应用依赖项 属性 值 控件实例化后(构造函数已返回):XAML 引擎创建元素实例,然后分配资源(例如样式)和局部值。
因此,您的控件当前将使用 属性 的默认 Duration 值(即 Duration.Automatic)配置动画(在构造函数中)。

  • 通常,您必须始终假设控件属性正在发生变化,例如,通过数据绑定或动画。要处理这种情况,您必须注册一个依赖项 属性 已更改的回调 - 至少对于每个对控件行为有直接影响的 public 属性。

  • SecondsRemaining 应该是 read-only 依赖项 属性.

  • 您应该使用 TextBlock 而不是 Label 来显示文本。

要解决您的问题,您必须为 Duration 属性 注册一个 属性 已更改回调,以更新取决于该值的 DoubleAnimation。然后将实际的 DoubleAnimation 存储在私有 属性 中,以便您可以在 属性 更改时更改其 Duration

public partial class Countdown : UserControl
{
  public Duration Duration
  {
    get => (Duration)GetValue(DurationProperty);
    set => SetValue(DurationProperty, value);
  }

  // Register the property changed callback
  public static readonly DependencyProperty DurationProperty = DependencyProperty.Register(
    nameof(Duration), 
    typeof(Duration), 
    typeof(Countdown), 
    new PropertyMetadata(new Duration(), OnDurationChanged));

  // Store the DoubleAnimation in order to modify the Duration on property changes
  private Timeline Timeline { get; set; }

  public Countdown()
  {
    InitializeComponent();

    // Store the DoubleAnimation in order to modify the Duration on property changes
    this.Timeline = new DoubleAnimation(-90, 270, Duration);
    
    Storyboard.SetTarget(this.Timeline, this.Arc);
    Storyboard.SetTargetProperty(this.Timeline, new PropertyPath(nameof(Arc.EndAngle)));
    _storyboard.Children.Add(this.Timeline);

    DataContext = this;
  }

  // Handle the Duration property changes
  private static void OnDurationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    var this_ = d as Countdown;
    this_.Timeline.Duration = (Duration)e.NewValue;
  }
}