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