WPF倒计时用户控制问题

发布于 2025-01-15 04:24:56 字数 7667 浏览 1 评论 0原文

我尝试在此处使用 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);
    }
}

I am trying to use the final revision of this control from Stackexchange here:

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

When I use the code it counts down just a single second and finishes. I am not sure what issue is.

Hopefully someone can help out - thanks.

The code I am using is this:

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);
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

夏有森光若流苏 2025-01-22 04:24:56

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

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

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

  • SecondsRemaining 应该是只读依赖属性。

  • 您应该使用 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;
  }
}

Your control is not properly initialized. You are currently not handling the property changes of the Duration property.

The dependency property values are applied after the control is instantiated (the constructor has returned): the XAML engine creates the element instance and then assigns the resources (e.g. a Style) and local values.
Therefore, your control will currently configure the animation (in the constructor) using the property's default Duration value (which is Duration.Automatic).

  • Generally, you must always assume that control properties are changing, e.g., via data binding or animation. To handle this scenarios you must register a dependency property changed callback - at least for every public property that has a direct impact on the behavior of the control.

  • SecondsRemaining should be a read-only dependency property.

  • You should use a TextBlock instead of a Label to display text.

To fix your issue, you must register a property changed callback for the Duration property to update the DoubleAnimation that depends on the value. Then store the actual DoubleAnimation in a private property, so that you can change its Duration on property changes:

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;
  }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文