Countdown control with arc animation
Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
11
down vote
favorite
I've implemented countdown timer control with arc animation that looks like this
Implementation notes:
For arc visualization I've created class
Arc
derived fromShape
(code is based on this post).I've created
Countdown
control (derived fromUserControl
). For setting timeout I've addedSeconds
dependency property. I'm using bindingContent="Binding Seconds"
to display seconds. Animation duration is set in code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
because I'm not sure if it's possible to do it in XAML without writing custom converter. I think that creating custom converter is not justified here.
For control's scaling content is wrapped in
Viewbox
ÃÂontrol.For seconds animation I'm using
DispatcherTimer
, nothing special. Is it the best to go here?
Code
Arc.cs
public class Arc : Shape
public Point Center
get => (Point)GetValue(CenterProperty);
set => SetValue(CenterProperty, value);
// Using a DependencyProperty as the backing store for Center. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CenterProperty =
DependencyProperty.Register("Center", typeof(Point), typeof(Arc),
new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));
// Start angle in degrees
public double StartAngle
get => (double)GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);
// Using a DependencyProperty as the backing store for StartAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
// End angle in degrees
public double EndAngle
get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
// Using a DependencyProperty as the backing store for EndAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double Radius
get => (double)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
// Using a DependencyProperty as the backing store for Radius. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register("Radius", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));
public bool SmallAngle
get => (bool)GetValue(SmallAngleProperty);
set => SetValue(SmallAngleProperty, value);
// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("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;
double t = a1;
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="WpfApp3.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="UserControl_Loaded">
<UserControl.Triggers>
<EventTrigger RoutedEvent="UserControl.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Name="Animation"
Storyboard.TargetName="Arc"
Storyboard.TargetProperty="EndAngle"
From="-90"
To="270" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</UserControl.Triggers>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>
<local: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 int Seconds
get => (int)GetValue(SecondsProperty);
set => SetValue(SecondsProperty, value);
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));
private readonly DispatcherTimer _timer = new DispatcherTimer Interval = TimeSpan.FromSeconds(1) ;
public Countdown()
InitializeComponent();
DataContext = this;
private void UserControl_Loaded(object sender, EventArgs e)
Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
if (Seconds > 0)
_timer.Start();
_timer.Tick += Timer_Tick;
private void Timer_Tick(object sender, EventArgs e)
Seconds--;
if (Seconds == 0) _timer.Stop();
Control is placed on Window
like this
<local:Countdown Width="300" Height="300" Seconds="25" />
c# animation wpf timer
add a comment |Â
up vote
11
down vote
favorite
I've implemented countdown timer control with arc animation that looks like this
Implementation notes:
For arc visualization I've created class
Arc
derived fromShape
(code is based on this post).I've created
Countdown
control (derived fromUserControl
). For setting timeout I've addedSeconds
dependency property. I'm using bindingContent="Binding Seconds"
to display seconds. Animation duration is set in code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
because I'm not sure if it's possible to do it in XAML without writing custom converter. I think that creating custom converter is not justified here.
For control's scaling content is wrapped in
Viewbox
ÃÂontrol.For seconds animation I'm using
DispatcherTimer
, nothing special. Is it the best to go here?
Code
Arc.cs
public class Arc : Shape
public Point Center
get => (Point)GetValue(CenterProperty);
set => SetValue(CenterProperty, value);
// Using a DependencyProperty as the backing store for Center. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CenterProperty =
DependencyProperty.Register("Center", typeof(Point), typeof(Arc),
new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));
// Start angle in degrees
public double StartAngle
get => (double)GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);
// Using a DependencyProperty as the backing store for StartAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
// End angle in degrees
public double EndAngle
get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
// Using a DependencyProperty as the backing store for EndAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double Radius
get => (double)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
// Using a DependencyProperty as the backing store for Radius. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register("Radius", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));
public bool SmallAngle
get => (bool)GetValue(SmallAngleProperty);
set => SetValue(SmallAngleProperty, value);
// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("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;
double t = a1;
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="WpfApp3.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="UserControl_Loaded">
<UserControl.Triggers>
<EventTrigger RoutedEvent="UserControl.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Name="Animation"
Storyboard.TargetName="Arc"
Storyboard.TargetProperty="EndAngle"
From="-90"
To="270" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</UserControl.Triggers>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>
<local: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 int Seconds
get => (int)GetValue(SecondsProperty);
set => SetValue(SecondsProperty, value);
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));
private readonly DispatcherTimer _timer = new DispatcherTimer Interval = TimeSpan.FromSeconds(1) ;
public Countdown()
InitializeComponent();
DataContext = this;
private void UserControl_Loaded(object sender, EventArgs e)
Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
if (Seconds > 0)
_timer.Start();
_timer.Tick += Timer_Tick;
private void Timer_Tick(object sender, EventArgs e)
Seconds--;
if (Seconds == 0) _timer.Stop();
Control is placed on Window
like this
<local:Countdown Width="300" Height="300" Seconds="25" />
c# animation wpf timer
add a comment |Â
up vote
11
down vote
favorite
up vote
11
down vote
favorite
I've implemented countdown timer control with arc animation that looks like this
Implementation notes:
For arc visualization I've created class
Arc
derived fromShape
(code is based on this post).I've created
Countdown
control (derived fromUserControl
). For setting timeout I've addedSeconds
dependency property. I'm using bindingContent="Binding Seconds"
to display seconds. Animation duration is set in code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
because I'm not sure if it's possible to do it in XAML without writing custom converter. I think that creating custom converter is not justified here.
For control's scaling content is wrapped in
Viewbox
ÃÂontrol.For seconds animation I'm using
DispatcherTimer
, nothing special. Is it the best to go here?
Code
Arc.cs
public class Arc : Shape
public Point Center
get => (Point)GetValue(CenterProperty);
set => SetValue(CenterProperty, value);
// Using a DependencyProperty as the backing store for Center. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CenterProperty =
DependencyProperty.Register("Center", typeof(Point), typeof(Arc),
new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));
// Start angle in degrees
public double StartAngle
get => (double)GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);
// Using a DependencyProperty as the backing store for StartAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
// End angle in degrees
public double EndAngle
get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
// Using a DependencyProperty as the backing store for EndAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double Radius
get => (double)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
// Using a DependencyProperty as the backing store for Radius. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register("Radius", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));
public bool SmallAngle
get => (bool)GetValue(SmallAngleProperty);
set => SetValue(SmallAngleProperty, value);
// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("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;
double t = a1;
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="WpfApp3.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="UserControl_Loaded">
<UserControl.Triggers>
<EventTrigger RoutedEvent="UserControl.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Name="Animation"
Storyboard.TargetName="Arc"
Storyboard.TargetProperty="EndAngle"
From="-90"
To="270" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</UserControl.Triggers>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>
<local: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 int Seconds
get => (int)GetValue(SecondsProperty);
set => SetValue(SecondsProperty, value);
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));
private readonly DispatcherTimer _timer = new DispatcherTimer Interval = TimeSpan.FromSeconds(1) ;
public Countdown()
InitializeComponent();
DataContext = this;
private void UserControl_Loaded(object sender, EventArgs e)
Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
if (Seconds > 0)
_timer.Start();
_timer.Tick += Timer_Tick;
private void Timer_Tick(object sender, EventArgs e)
Seconds--;
if (Seconds == 0) _timer.Stop();
Control is placed on Window
like this
<local:Countdown Width="300" Height="300" Seconds="25" />
c# animation wpf timer
I've implemented countdown timer control with arc animation that looks like this
Implementation notes:
For arc visualization I've created class
Arc
derived fromShape
(code is based on this post).I've created
Countdown
control (derived fromUserControl
). For setting timeout I've addedSeconds
dependency property. I'm using bindingContent="Binding Seconds"
to display seconds. Animation duration is set in code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
because I'm not sure if it's possible to do it in XAML without writing custom converter. I think that creating custom converter is not justified here.
For control's scaling content is wrapped in
Viewbox
ÃÂontrol.For seconds animation I'm using
DispatcherTimer
, nothing special. Is it the best to go here?
Code
Arc.cs
public class Arc : Shape
public Point Center
get => (Point)GetValue(CenterProperty);
set => SetValue(CenterProperty, value);
// Using a DependencyProperty as the backing store for Center. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CenterProperty =
DependencyProperty.Register("Center", typeof(Point), typeof(Arc),
new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));
// Start angle in degrees
public double StartAngle
get => (double)GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);
// Using a DependencyProperty as the backing store for StartAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
// End angle in degrees
public double EndAngle
get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
// Using a DependencyProperty as the backing store for EndAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double Radius
get => (double)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
// Using a DependencyProperty as the backing store for Radius. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register("Radius", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));
public bool SmallAngle
get => (bool)GetValue(SmallAngleProperty);
set => SetValue(SmallAngleProperty, value);
// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("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;
double t = a1;
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="WpfApp3.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="UserControl_Loaded">
<UserControl.Triggers>
<EventTrigger RoutedEvent="UserControl.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Name="Animation"
Storyboard.TargetName="Arc"
Storyboard.TargetProperty="EndAngle"
From="-90"
To="270" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</UserControl.Triggers>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>
<local: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 int Seconds
get => (int)GetValue(SecondsProperty);
set => SetValue(SecondsProperty, value);
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));
private readonly DispatcherTimer _timer = new DispatcherTimer Interval = TimeSpan.FromSeconds(1) ;
public Countdown()
InitializeComponent();
DataContext = this;
private void UserControl_Loaded(object sender, EventArgs e)
Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
if (Seconds > 0)
_timer.Start();
_timer.Tick += Timer_Tick;
private void Timer_Tick(object sender, EventArgs e)
Seconds--;
if (Seconds == 0) _timer.Stop();
Control is placed on Window
like this
<local:Countdown Width="300" Height="300" Seconds="25" />
c# animation wpf timer
edited Jun 26 at 3:59
200_success
123k14143399
123k14143399
asked Jun 25 at 8:48
Vadim Ovchinnikov
1,0101417
1,0101417
add a comment |Â
add a comment |Â
4 Answers
4
active
oldest
votes
up vote
3
down vote
accepted
I don't like that you are running two timers/timelines in parallel. Instead you could run the animation from code behind. It also gives you the opportunity to trigger it from other places than load:
private void StartAnimation()
double from = -90;
double to = 270;
int seconds = Seconds;
TimeSpan duration = TimeSpan.FromSeconds(Seconds);
DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));
Storyboard.SetTarget(animation, Arc);
Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));
Storyboard storyboard = new Storyboard();
storyboard.CurrentTimeInvalidated += (s, e) =>
int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds);
Seconds = seconds - diff;
;
storyboard.Children.Add(animation);
storyboard.Begin();
private void UserControl_Loaded(object sender, RoutedEventArgs e)
StartAnimation();
//Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
//if (Seconds > 0)
//
// _timer.Tick += Timer_Tick;
// _timer.Start();
//
<UserControl x:Class="WpfApp3.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="800"
Loaded="UserControl_Loaded"
>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>
<local:Arc
x:Name="Arc"
Center="50, 50"
StartAngle="-90"
EndAngle="-90"
Stroke="#45d3be"
StrokeThickness="5"
Radius="45" />
</Grid>
</Viewbox>
</UserControl>
add a comment |Â
up vote
10
down vote
There are a couple of things which could be more consistent.
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));
The dependency property in Countdown.xaml.cs
uses nameof
, but the ones in Arc.cs
don't. IMO they should.
// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));
The dependency properties in Arc.cs
all have a comment, but the constructor doesn't. IMO the constructor is the harder of the two to understand: if someone knows enough WPF to understand that without a comment, they don't need the comments on the dependency properties.
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;
double t = a1;
d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
else
large = (Math.Abs(a1 - a0) < Math.PI);
I could definitely use some comments to explain what's going on here. The last line in particular looks very counterintuitive.
Also there's a dead line: t
isn't used anywhere.
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
Not very localisable...
_timer.Start();
_timer.Tick += Timer_Tick;
It's unlikely to occur in practice, but technically you've got a race condition there. I see only an advantage in switching the order of the two lines.
... For setting timeout I've added Seconds dependency property. I'm using binding
Content="Binding Seconds"
to display seconds. Animation duration is
set in code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
because I'm not sure if it's possible to do it in XAML without writing
custom converter. I think that creating custom converter is not
justified here.
I think you can do it without a custom converter by making the binding property a TimeSpan
(changing the name from Seconds
to something like TimeRemaining
) and using StringFormat
in the binding for the label content. I haven't tested this.
If you do this then you might want to handle non-integer TimeSpan.TotalSeconds
values by rounding to the nearest second in the setter.
Regarding "Not very localisableâ¦" if I would need localization I'll just add something likeContent="x:Static p:Resources.Sec"
. I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
â Vadim Ovchinnikov
Jun 25 at 19:54
I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
â Peter Taylor
Jun 25 at 21:11
add a comment |Â
up vote
7
down vote
1) DispatcherTimer
is good for periodic UI updates, but it should not be used to measure time, because it is just not accurate enough. For precise time measurement you have to use Stopwatch
class inside the timer callback.
2) Seconds
property apparently does two things: it starts as "countdown duration" but after control is loaded it acts as "remaining time". I would use two properties here, so that they can be databound separately when necessary.
3) DataContext = this;
- don't set DataContext
on public re-usable types. Someone (yourself included) can eventually decide to use this class in MVVM environment, but changing DataContext
of your class will break it. Internally, you should use RelativeSource
or ElementName
properties of Binding
class or DependencyProperty
callbacks to do your bidding, while leaving DataContext
empty.
Here is an example of using ElementName
:
<UserControl x:Class="WpfApp3.Countdown"
...
x:Name="this">
...
<!-- Content binds directly to Countdown.Seconds dependency property, DataContext is ignored -->
<Label Content="Binding Seconds, ElementName=this" />
...
</UserControl>
Regarding last point: suppose I want to move my properties from control class to some view model class. When I haveDataContext
assigned I just need to changeDataContext
stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if usingDataContext
is bad idea here. Am I missing something?
â Vadim Ovchinnikov
Jun 27 at 6:52
@VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext:<Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
â Nikita B
Jun 27 at 8:35
add a comment |Â
up vote
3
down vote
Incorporated nearly all recommendations to my solution, but the most radical was accepted answer. Any feedback very welcome!
Also added public Start
and Stop
methods and Elapsed
event.
Results:
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);
Example of usage:
<local:Countdown Width="300" Height="300" Duration="0:0:15" />
add a comment |Â
4 Answers
4
active
oldest
votes
4 Answers
4
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
3
down vote
accepted
I don't like that you are running two timers/timelines in parallel. Instead you could run the animation from code behind. It also gives you the opportunity to trigger it from other places than load:
private void StartAnimation()
double from = -90;
double to = 270;
int seconds = Seconds;
TimeSpan duration = TimeSpan.FromSeconds(Seconds);
DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));
Storyboard.SetTarget(animation, Arc);
Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));
Storyboard storyboard = new Storyboard();
storyboard.CurrentTimeInvalidated += (s, e) =>
int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds);
Seconds = seconds - diff;
;
storyboard.Children.Add(animation);
storyboard.Begin();
private void UserControl_Loaded(object sender, RoutedEventArgs e)
StartAnimation();
//Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
//if (Seconds > 0)
//
// _timer.Tick += Timer_Tick;
// _timer.Start();
//
<UserControl x:Class="WpfApp3.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="800"
Loaded="UserControl_Loaded"
>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>
<local:Arc
x:Name="Arc"
Center="50, 50"
StartAngle="-90"
EndAngle="-90"
Stroke="#45d3be"
StrokeThickness="5"
Radius="45" />
</Grid>
</Viewbox>
</UserControl>
add a comment |Â
up vote
3
down vote
accepted
I don't like that you are running two timers/timelines in parallel. Instead you could run the animation from code behind. It also gives you the opportunity to trigger it from other places than load:
private void StartAnimation()
double from = -90;
double to = 270;
int seconds = Seconds;
TimeSpan duration = TimeSpan.FromSeconds(Seconds);
DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));
Storyboard.SetTarget(animation, Arc);
Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));
Storyboard storyboard = new Storyboard();
storyboard.CurrentTimeInvalidated += (s, e) =>
int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds);
Seconds = seconds - diff;
;
storyboard.Children.Add(animation);
storyboard.Begin();
private void UserControl_Loaded(object sender, RoutedEventArgs e)
StartAnimation();
//Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
//if (Seconds > 0)
//
// _timer.Tick += Timer_Tick;
// _timer.Start();
//
<UserControl x:Class="WpfApp3.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="800"
Loaded="UserControl_Loaded"
>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>
<local:Arc
x:Name="Arc"
Center="50, 50"
StartAngle="-90"
EndAngle="-90"
Stroke="#45d3be"
StrokeThickness="5"
Radius="45" />
</Grid>
</Viewbox>
</UserControl>
add a comment |Â
up vote
3
down vote
accepted
up vote
3
down vote
accepted
I don't like that you are running two timers/timelines in parallel. Instead you could run the animation from code behind. It also gives you the opportunity to trigger it from other places than load:
private void StartAnimation()
double from = -90;
double to = 270;
int seconds = Seconds;
TimeSpan duration = TimeSpan.FromSeconds(Seconds);
DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));
Storyboard.SetTarget(animation, Arc);
Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));
Storyboard storyboard = new Storyboard();
storyboard.CurrentTimeInvalidated += (s, e) =>
int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds);
Seconds = seconds - diff;
;
storyboard.Children.Add(animation);
storyboard.Begin();
private void UserControl_Loaded(object sender, RoutedEventArgs e)
StartAnimation();
//Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
//if (Seconds > 0)
//
// _timer.Tick += Timer_Tick;
// _timer.Start();
//
<UserControl x:Class="WpfApp3.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="800"
Loaded="UserControl_Loaded"
>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>
<local:Arc
x:Name="Arc"
Center="50, 50"
StartAngle="-90"
EndAngle="-90"
Stroke="#45d3be"
StrokeThickness="5"
Radius="45" />
</Grid>
</Viewbox>
</UserControl>
I don't like that you are running two timers/timelines in parallel. Instead you could run the animation from code behind. It also gives you the opportunity to trigger it from other places than load:
private void StartAnimation()
double from = -90;
double to = 270;
int seconds = Seconds;
TimeSpan duration = TimeSpan.FromSeconds(Seconds);
DoubleAnimation animation = new DoubleAnimation(from, to, new Duration(duration));
Storyboard.SetTarget(animation, Arc);
Storyboard.SetTargetProperty(animation, new PropertyPath("EndAngle"));
Storyboard storyboard = new Storyboard();
storyboard.CurrentTimeInvalidated += (s, e) =>
int diff = (int)((s as ClockGroup).CurrentTime.Value.TotalSeconds);
Seconds = seconds - diff;
;
storyboard.Children.Add(animation);
storyboard.Begin();
private void UserControl_Loaded(object sender, RoutedEventArgs e)
StartAnimation();
//Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
//if (Seconds > 0)
//
// _timer.Tick += Timer_Tick;
// _timer.Start();
//
<UserControl x:Class="WpfApp3.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="800"
Loaded="UserControl_Loaded"
>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>
<local:Arc
x:Name="Arc"
Center="50, 50"
StartAngle="-90"
EndAngle="-90"
Stroke="#45d3be"
StrokeThickness="5"
Radius="45" />
</Grid>
</Viewbox>
</UserControl>
edited Jun 26 at 8:06
answered Jun 25 at 14:11
Henrik Hansen
3,7781417
3,7781417
add a comment |Â
add a comment |Â
up vote
10
down vote
There are a couple of things which could be more consistent.
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));
The dependency property in Countdown.xaml.cs
uses nameof
, but the ones in Arc.cs
don't. IMO they should.
// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));
The dependency properties in Arc.cs
all have a comment, but the constructor doesn't. IMO the constructor is the harder of the two to understand: if someone knows enough WPF to understand that without a comment, they don't need the comments on the dependency properties.
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;
double t = a1;
d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
else
large = (Math.Abs(a1 - a0) < Math.PI);
I could definitely use some comments to explain what's going on here. The last line in particular looks very counterintuitive.
Also there's a dead line: t
isn't used anywhere.
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
Not very localisable...
_timer.Start();
_timer.Tick += Timer_Tick;
It's unlikely to occur in practice, but technically you've got a race condition there. I see only an advantage in switching the order of the two lines.
... For setting timeout I've added Seconds dependency property. I'm using binding
Content="Binding Seconds"
to display seconds. Animation duration is
set in code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
because I'm not sure if it's possible to do it in XAML without writing
custom converter. I think that creating custom converter is not
justified here.
I think you can do it without a custom converter by making the binding property a TimeSpan
(changing the name from Seconds
to something like TimeRemaining
) and using StringFormat
in the binding for the label content. I haven't tested this.
If you do this then you might want to handle non-integer TimeSpan.TotalSeconds
values by rounding to the nearest second in the setter.
Regarding "Not very localisableâ¦" if I would need localization I'll just add something likeContent="x:Static p:Resources.Sec"
. I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
â Vadim Ovchinnikov
Jun 25 at 19:54
I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
â Peter Taylor
Jun 25 at 21:11
add a comment |Â
up vote
10
down vote
There are a couple of things which could be more consistent.
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));
The dependency property in Countdown.xaml.cs
uses nameof
, but the ones in Arc.cs
don't. IMO they should.
// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));
The dependency properties in Arc.cs
all have a comment, but the constructor doesn't. IMO the constructor is the harder of the two to understand: if someone knows enough WPF to understand that without a comment, they don't need the comments on the dependency properties.
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;
double t = a1;
d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
else
large = (Math.Abs(a1 - a0) < Math.PI);
I could definitely use some comments to explain what's going on here. The last line in particular looks very counterintuitive.
Also there's a dead line: t
isn't used anywhere.
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
Not very localisable...
_timer.Start();
_timer.Tick += Timer_Tick;
It's unlikely to occur in practice, but technically you've got a race condition there. I see only an advantage in switching the order of the two lines.
... For setting timeout I've added Seconds dependency property. I'm using binding
Content="Binding Seconds"
to display seconds. Animation duration is
set in code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
because I'm not sure if it's possible to do it in XAML without writing
custom converter. I think that creating custom converter is not
justified here.
I think you can do it without a custom converter by making the binding property a TimeSpan
(changing the name from Seconds
to something like TimeRemaining
) and using StringFormat
in the binding for the label content. I haven't tested this.
If you do this then you might want to handle non-integer TimeSpan.TotalSeconds
values by rounding to the nearest second in the setter.
Regarding "Not very localisableâ¦" if I would need localization I'll just add something likeContent="x:Static p:Resources.Sec"
. I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
â Vadim Ovchinnikov
Jun 25 at 19:54
I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
â Peter Taylor
Jun 25 at 21:11
add a comment |Â
up vote
10
down vote
up vote
10
down vote
There are a couple of things which could be more consistent.
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));
The dependency property in Countdown.xaml.cs
uses nameof
, but the ones in Arc.cs
don't. IMO they should.
// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));
The dependency properties in Arc.cs
all have a comment, but the constructor doesn't. IMO the constructor is the harder of the two to understand: if someone knows enough WPF to understand that without a comment, they don't need the comments on the dependency properties.
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;
double t = a1;
d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
else
large = (Math.Abs(a1 - a0) < Math.PI);
I could definitely use some comments to explain what's going on here. The last line in particular looks very counterintuitive.
Also there's a dead line: t
isn't used anywhere.
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
Not very localisable...
_timer.Start();
_timer.Tick += Timer_Tick;
It's unlikely to occur in practice, but technically you've got a race condition there. I see only an advantage in switching the order of the two lines.
... For setting timeout I've added Seconds dependency property. I'm using binding
Content="Binding Seconds"
to display seconds. Animation duration is
set in code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
because I'm not sure if it's possible to do it in XAML without writing
custom converter. I think that creating custom converter is not
justified here.
I think you can do it without a custom converter by making the binding property a TimeSpan
(changing the name from Seconds
to something like TimeRemaining
) and using StringFormat
in the binding for the label content. I haven't tested this.
If you do this then you might want to handle non-integer TimeSpan.TotalSeconds
values by rounding to the nearest second in the setter.
There are a couple of things which could be more consistent.
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));
The dependency property in Countdown.xaml.cs
uses nameof
, but the ones in Arc.cs
don't. IMO they should.
// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));
The dependency properties in Arc.cs
all have a comment, but the constructor doesn't. IMO the constructor is the harder of the two to understand: if someone knows enough WPF to understand that without a comment, they don't need the comments on the dependency properties.
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;
double t = a1;
d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
else
large = (Math.Abs(a1 - a0) < Math.PI);
I could definitely use some comments to explain what's going on here. The last line in particular looks very counterintuitive.
Also there's a dead line: t
isn't used anywhere.
<Label Foreground="#fff" Content="Binding Seconds" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
Not very localisable...
_timer.Start();
_timer.Tick += Timer_Tick;
It's unlikely to occur in practice, but technically you've got a race condition there. I see only an advantage in switching the order of the two lines.
... For setting timeout I've added Seconds dependency property. I'm using binding
Content="Binding Seconds"
to display seconds. Animation duration is
set in code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
because I'm not sure if it's possible to do it in XAML without writing
custom converter. I think that creating custom converter is not
justified here.
I think you can do it without a custom converter by making the binding property a TimeSpan
(changing the name from Seconds
to something like TimeRemaining
) and using StringFormat
in the binding for the label content. I haven't tested this.
If you do this then you might want to handle non-integer TimeSpan.TotalSeconds
values by rounding to the nearest second in the setter.
answered Jun 25 at 10:22
Peter Taylor
14k2454
14k2454
Regarding "Not very localisableâ¦" if I would need localization I'll just add something likeContent="x:Static p:Resources.Sec"
. I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
â Vadim Ovchinnikov
Jun 25 at 19:54
I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
â Peter Taylor
Jun 25 at 21:11
add a comment |Â
Regarding "Not very localisableâ¦" if I would need localization I'll just add something likeContent="x:Static p:Resources.Sec"
. I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.
â Vadim Ovchinnikov
Jun 25 at 19:54
I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
â Peter Taylor
Jun 25 at 21:11
Regarding "Not very localisableâ¦" if I would need localization I'll just add something like
Content="x:Static p:Resources.Sec"
. I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.â Vadim Ovchinnikov
Jun 25 at 19:54
Regarding "Not very localisableâ¦" if I would need localization I'll just add something like
Content="x:Static p:Resources.Sec"
. I'm using Resx-files based localization, don't know if it's the best option, but didn't find good alternatives, what do you think? But I assume adding this piece of code to the question would be redundant.â Vadim Ovchinnikov
Jun 25 at 19:54
I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
â Peter Taylor
Jun 25 at 21:11
I haven't found any good standard l10n approach for WPF. I ended up doing a DIY approach with a custom converter and various ugly hacks. If you've thought about the issue and made a decision that YAGNI then that's fair enough.
â Peter Taylor
Jun 25 at 21:11
add a comment |Â
up vote
7
down vote
1) DispatcherTimer
is good for periodic UI updates, but it should not be used to measure time, because it is just not accurate enough. For precise time measurement you have to use Stopwatch
class inside the timer callback.
2) Seconds
property apparently does two things: it starts as "countdown duration" but after control is loaded it acts as "remaining time". I would use two properties here, so that they can be databound separately when necessary.
3) DataContext = this;
- don't set DataContext
on public re-usable types. Someone (yourself included) can eventually decide to use this class in MVVM environment, but changing DataContext
of your class will break it. Internally, you should use RelativeSource
or ElementName
properties of Binding
class or DependencyProperty
callbacks to do your bidding, while leaving DataContext
empty.
Here is an example of using ElementName
:
<UserControl x:Class="WpfApp3.Countdown"
...
x:Name="this">
...
<!-- Content binds directly to Countdown.Seconds dependency property, DataContext is ignored -->
<Label Content="Binding Seconds, ElementName=this" />
...
</UserControl>
Regarding last point: suppose I want to move my properties from control class to some view model class. When I haveDataContext
assigned I just need to changeDataContext
stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if usingDataContext
is bad idea here. Am I missing something?
â Vadim Ovchinnikov
Jun 27 at 6:52
@VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext:<Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
â Nikita B
Jun 27 at 8:35
add a comment |Â
up vote
7
down vote
1) DispatcherTimer
is good for periodic UI updates, but it should not be used to measure time, because it is just not accurate enough. For precise time measurement you have to use Stopwatch
class inside the timer callback.
2) Seconds
property apparently does two things: it starts as "countdown duration" but after control is loaded it acts as "remaining time". I would use two properties here, so that they can be databound separately when necessary.
3) DataContext = this;
- don't set DataContext
on public re-usable types. Someone (yourself included) can eventually decide to use this class in MVVM environment, but changing DataContext
of your class will break it. Internally, you should use RelativeSource
or ElementName
properties of Binding
class or DependencyProperty
callbacks to do your bidding, while leaving DataContext
empty.
Here is an example of using ElementName
:
<UserControl x:Class="WpfApp3.Countdown"
...
x:Name="this">
...
<!-- Content binds directly to Countdown.Seconds dependency property, DataContext is ignored -->
<Label Content="Binding Seconds, ElementName=this" />
...
</UserControl>
Regarding last point: suppose I want to move my properties from control class to some view model class. When I haveDataContext
assigned I just need to changeDataContext
stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if usingDataContext
is bad idea here. Am I missing something?
â Vadim Ovchinnikov
Jun 27 at 6:52
@VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext:<Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
â Nikita B
Jun 27 at 8:35
add a comment |Â
up vote
7
down vote
up vote
7
down vote
1) DispatcherTimer
is good for periodic UI updates, but it should not be used to measure time, because it is just not accurate enough. For precise time measurement you have to use Stopwatch
class inside the timer callback.
2) Seconds
property apparently does two things: it starts as "countdown duration" but after control is loaded it acts as "remaining time". I would use two properties here, so that they can be databound separately when necessary.
3) DataContext = this;
- don't set DataContext
on public re-usable types. Someone (yourself included) can eventually decide to use this class in MVVM environment, but changing DataContext
of your class will break it. Internally, you should use RelativeSource
or ElementName
properties of Binding
class or DependencyProperty
callbacks to do your bidding, while leaving DataContext
empty.
Here is an example of using ElementName
:
<UserControl x:Class="WpfApp3.Countdown"
...
x:Name="this">
...
<!-- Content binds directly to Countdown.Seconds dependency property, DataContext is ignored -->
<Label Content="Binding Seconds, ElementName=this" />
...
</UserControl>
1) DispatcherTimer
is good for periodic UI updates, but it should not be used to measure time, because it is just not accurate enough. For precise time measurement you have to use Stopwatch
class inside the timer callback.
2) Seconds
property apparently does two things: it starts as "countdown duration" but after control is loaded it acts as "remaining time". I would use two properties here, so that they can be databound separately when necessary.
3) DataContext = this;
- don't set DataContext
on public re-usable types. Someone (yourself included) can eventually decide to use this class in MVVM environment, but changing DataContext
of your class will break it. Internally, you should use RelativeSource
or ElementName
properties of Binding
class or DependencyProperty
callbacks to do your bidding, while leaving DataContext
empty.
Here is an example of using ElementName
:
<UserControl x:Class="WpfApp3.Countdown"
...
x:Name="this">
...
<!-- Content binds directly to Countdown.Seconds dependency property, DataContext is ignored -->
<Label Content="Binding Seconds, ElementName=this" />
...
</UserControl>
edited Jun 25 at 12:55
answered Jun 25 at 11:28
Nikita B
12.3k11551
12.3k11551
Regarding last point: suppose I want to move my properties from control class to some view model class. When I haveDataContext
assigned I just need to changeDataContext
stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if usingDataContext
is bad idea here. Am I missing something?
â Vadim Ovchinnikov
Jun 27 at 6:52
@VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext:<Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
â Nikita B
Jun 27 at 8:35
add a comment |Â
Regarding last point: suppose I want to move my properties from control class to some view model class. When I haveDataContext
assigned I just need to changeDataContext
stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if usingDataContext
is bad idea here. Am I missing something?
â Vadim Ovchinnikov
Jun 27 at 6:52
@VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext:<Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
â Nikita B
Jun 27 at 8:35
Regarding last point: suppose I want to move my properties from control class to some view model class. When I have
DataContext
assigned I just need to change DataContext
stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if using DataContext
is bad idea here. Am I missing something?â Vadim Ovchinnikov
Jun 27 at 6:52
Regarding last point: suppose I want to move my properties from control class to some view model class. When I have
DataContext
assigned I just need to change DataContext
stored reference, but when I don't have one I need to rewrite all my bindings. So I'm not sure if using DataContext
is bad idea here. Am I missing something?â Vadim Ovchinnikov
Jun 27 at 6:52
@VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext:
<Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
â Nikita B
Jun 27 at 8:35
@VadimOvchinnikov, I'm not sure I follow. Why do you want to move the properties? Just leave them where they are. For MVVM to work, you need to have a property on each side: one property on your view, and another property on your viewmodel. Then you can bind them by setting viewmodel as datacontext:
<Countdown Duration=Binding DurationPropertyOfViewModel DataContext=Binding ViewModel/>
â Nikita B
Jun 27 at 8:35
add a comment |Â
up vote
3
down vote
Incorporated nearly all recommendations to my solution, but the most radical was accepted answer. Any feedback very welcome!
Also added public Start
and Stop
methods and Elapsed
event.
Results:
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);
Example of usage:
<local:Countdown Width="300" Height="300" Duration="0:0:15" />
add a comment |Â
up vote
3
down vote
Incorporated nearly all recommendations to my solution, but the most radical was accepted answer. Any feedback very welcome!
Also added public Start
and Stop
methods and Elapsed
event.
Results:
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);
Example of usage:
<local:Countdown Width="300" Height="300" Duration="0:0:15" />
add a comment |Â
up vote
3
down vote
up vote
3
down vote
Incorporated nearly all recommendations to my solution, but the most radical was accepted answer. Any feedback very welcome!
Also added public Start
and Stop
methods and Elapsed
event.
Results:
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);
Example of usage:
<local:Countdown Width="300" Height="300" Duration="0:0:15" />
Incorporated nearly all recommendations to my solution, but the most radical was accepted answer. Any feedback very welcome!
Also added public Start
and Stop
methods and Elapsed
event.
Results:
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);
Example of usage:
<local:Countdown Width="300" Height="300" Duration="0:0:15" />
edited Jul 16 at 17:14
answered Jun 25 at 21:25
Vadim Ovchinnikov
1,0101417
1,0101417
add a comment |Â
add a comment |Â
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f197197%2fcountdown-control-with-arc-animation%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password