WPF Countdown user control issue - c#

I am trying to use the final revision of this control from Stackexchange here:
https://codereview.stackexchange.com/questions/197197/countdown-control-with-arc-animation
When I use the code it counts down just a single second and finishes. I am not sure what issue is.
Hopefully someone can help out - thanks.
The code I am using is this:
Arc.cs
public class Arc : Shape
{
public Point Center
{
get => (Point)GetValue(CenterProperty);
set => SetValue(CenterProperty, value);
}
public static readonly DependencyProperty CenterProperty =
DependencyProperty.Register(nameof(Center), typeof(Point), typeof(Arc),
new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));
public double StartAngle
{
get => (double)GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);
}
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register(nameof(StartAngle), typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double EndAngle
{
get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
}
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double Radius
{
get => (double)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register(nameof(Radius), typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));
public bool SmallAngle
{
get => (bool)GetValue(SmallAngleProperty);
set => SetValue(SmallAngleProperty, value);
}
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register(nameof(SmallAngle), typeof(bool), typeof(Arc),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));
protected override Geometry DefiningGeometry
{
get
{
double startAngleRadians = StartAngle * Math.PI / 180;
double endAngleRadians = EndAngle * Math.PI / 180;
double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;
if (a1 < a0)
a1 += Math.PI * 2;
SweepDirection d = SweepDirection.Counterclockwise;
bool large;
if (SmallAngle)
{
large = false;
d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
}
else
large = (Math.Abs(a1 - a0) < Math.PI);
Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;
List<PathSegment> segments = new List<PathSegment>
{
new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
};
List<PathFigure> figures = new List<PathFigure>
{
new PathFigure(p0, segments, true)
{
IsClosed = false
}
};
return new PathGeometry(figures, FillRule.EvenOdd, null);
}
}
}
Countdown.xaml
<UserControl x:Class="WpfApp.Countdown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="450" Loaded="Countdown_Loaded">
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="{Binding SecondsRemaining}" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -15, 0, 0" />
</StackPanel>
</Border>
<uc:Arc
x:Name="Arc"
Center="50, 50"
StartAngle="-90"
EndAngle="-90"
Stroke="#45d3be"
StrokeThickness="5"
Radius="45" />
</Grid>
</Viewbox>
</UserControl>
Countdown.xaml.cs
public partial class Countdown : UserControl
{
public Duration Duration
{
get => (Duration)GetValue(DurationProperty);
set => SetValue(DurationProperty, value);
}
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register(nameof(Duration), typeof(Duration), typeof(Countdown), new PropertyMetadata(new Duration()));
public int SecondsRemaining
{
get => (int)GetValue(SecondsRemainingProperty);
set => SetValue(SecondsRemainingProperty, value);
}
public static readonly DependencyProperty SecondsRemainingProperty =
DependencyProperty.Register(nameof(SecondsRemaining), typeof(int), typeof(Countdown), new PropertyMetadata(0));
public event EventHandler Elapsed;
private readonly Storyboard _storyboard = new Storyboard();
public Countdown()
{
InitializeComponent();
DoubleAnimation animation = new DoubleAnimation(-90, 270, Duration);
Storyboard.SetTarget(animation, Arc);
Storyboard.SetTargetProperty(animation, new PropertyPath(nameof(Arc.EndAngle)));
_storyboard.Children.Add(animation);
DataContext = this;
}
private void Countdown_Loaded(object sender, EventArgs e)
{
if (IsVisible)
Start();
}
public void Start()
{
Stop();
_storyboard.CurrentTimeInvalidated += Storyboard_CurrentTimeInvalidated;
_storyboard.Completed += Storyboard_Completed;
_storyboard.Begin();
}
public void Stop()
{
_storyboard.CurrentTimeInvalidated -= Storyboard_CurrentTimeInvalidated;
_storyboard.Completed -= Storyboard_Completed;
_storyboard.Stop();
}
private void Storyboard_CurrentTimeInvalidated(object sender, EventArgs e)
{
ClockGroup cg = (ClockGroup)sender;
if (cg.CurrentTime == null) return;
TimeSpan elapsedTime = cg.CurrentTime.Value;
SecondsRemaining = (int)Math.Ceiling((Duration.TimeSpan - elapsedTime).TotalSeconds);
}
private void Storyboard_Completed(object sender, EventArgs e)
{
if (IsVisible)
Elapsed?.Invoke(this, EventArgs.Empty);
}
}

Your control is not properly initialized. You are currently not handling the property changes of the Duration property.
The dependency property values are applied after the control is instantiated (the constructor has returned): the XAML engine creates the element instance and then assigns the resources (e.g. a Style) and local values.
Therefore, your control will currently configure the animation (in the constructor) using the property's default Duration value (which is Duration.Automatic).
Generally, you must always assume that control properties are changing, e.g., via data binding or animation. To handle this scenarios you must register a dependency property changed callback - at least for every public property that has a direct impact on the behavior of the control.
SecondsRemaining should be a read-only dependency property.
You should use a TextBlock instead of a Label to display text.
To fix your issue, you must register a property changed callback for the Duration property to update the DoubleAnimation that depends on the value. Then store the actual DoubleAnimation in a private property, so that you can change its Duration on property changes:
public partial class Countdown : UserControl
{
public Duration Duration
{
get => (Duration)GetValue(DurationProperty);
set => SetValue(DurationProperty, value);
}
// Register the property changed callback
public static readonly DependencyProperty DurationProperty = DependencyProperty.Register(
nameof(Duration),
typeof(Duration),
typeof(Countdown),
new PropertyMetadata(new Duration(), OnDurationChanged));
// Store the DoubleAnimation in order to modify the Duration on property changes
private Timeline Timeline { get; set; }
public Countdown()
{
InitializeComponent();
// Store the DoubleAnimation in order to modify the Duration on property changes
this.Timeline = new DoubleAnimation(-90, 270, Duration);
Storyboard.SetTarget(this.Timeline, this.Arc);
Storyboard.SetTargetProperty(this.Timeline, new PropertyPath(nameof(Arc.EndAngle)));
_storyboard.Children.Add(this.Timeline);
DataContext = this;
}
// Handle the Duration property changes
private static void OnDurationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var this_ = d as Countdown;
this_.Timeline.Duration = (Duration)e.NewValue;
}
}

Related

Animating textbox text bound to a double property on property value change

I'd like to animate the transition from the old to the new value as seen in many software, i.e. when the value of the bound property changes, I'd like to increase or decrease the text of the text box by a specific offset until it reaches the new value. As an example:
Initial value: 26.0%
New value: 43.5%
Animation:
26.5% -> 30.0% -> 30.5% ....... -> 43.4
Is it possible to do this with the standard equipment of .Net or do you need a custom control?
Thanks in advance for any help.
I'm afraid I don't know if .NET has a custom control. But you can do it very easily with special supervision.
Step One : Create a timer
Step Two : Run timer1 object in form load event [timerName.Start();]
Step Three : Create a global variable to control the second. The local variable
Step Four : Check textbox contents by performing a second check on the tick event of the Timer object
private void Form1_Load(object sender, EventArgs e)
{
timer1.Start();
}
private int elapsed;
private void timer1_Tick(object sender, EventArgs e)
{
elapsed = elapsed + 1;
if (elapsed == 15)
{
textBox1.Text = "%" + "next value 1";
}
if (elapsed == 30)
{
textBox1.Text = "%" + "next value 2";
}
if (elapsed == 45)
{
textBox1.Text = "%" + "next value 3";
}
}
I hope I could help. If I can't help you, please write it down. Good coding!
An example of an auxiliary proxy for a discrete animation of a Double number:
using System;
using System.Windows;
namespace Proxy
{
public partial class DeltaNumberAnimator : Freezable
{
protected override Freezable CreateInstanceCore()
{
throw new NotImplementedException();
}
}
}
using System;
using System.Windows;
namespace Proxy
{
public partial class DeltaNumberAnimator
{
/// <summary>Source of number</summary>
public double Source
{
get => (double)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="Source"/>.</summary>
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(nameof(Source), typeof(double), typeof(DeltaNumberAnimator), new PropertyMetadata(0d, (d, e) => ((DeltaNumberAnimator)d).SourceChanged(e)));
/// <summary>Number with animated change</summary>
public double Number
{
get => (double)GetValue(NumberProperty);
private set => SetValue(NumberPropertyKey, value);
}
private static readonly DependencyPropertyKey NumberPropertyKey =
DependencyProperty.RegisterReadOnly(nameof(Number), typeof(double), typeof(DeltaNumberAnimator), new PropertyMetadata(0d, NumberChanged));
/// <summary><see cref="DependencyProperty"/> for property <see cref="Number"/>.</summary>
public static readonly DependencyProperty NumberProperty = NumberPropertyKey.DependencyProperty;
/// <summary>Sets the delta value for a discrete change in the <see cref="Number"/> property.</summary>
public double Delta
{
get => (double)GetValue(DeltaProperty);
set => SetValue(DeltaProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="Delta"/>.</summary>
public static readonly DependencyProperty DeltaProperty =
DependencyProperty.Register(nameof(Delta), typeof(double), typeof(DeltaNumberAnimator), new PropertyMetadata(0.01, DeltaChanged, CoerceDelta));
/// <summary>Number increment interval. </summary>
public TimeSpan Interval
{
get => (TimeSpan)GetValue(IntervalProperty);
set => SetValue(IntervalProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="Interval"/>.</summary>
public static readonly DependencyProperty IntervalProperty =
DependencyProperty.Register(nameof(Interval), typeof(TimeSpan), typeof(DeltaNumberAnimator), new PropertyMetadata(TimeSpan.Zero, IntervalChanged));
/// <summary>Animation in progress.</summary>
public bool IsAnimation
{
get => (bool)GetValue(IsAnimationProperty);
private set => SetValue(IsAnimationPropertyKey, value);
}
public static readonly DependencyPropertyKey IsAnimationPropertyKey =
DependencyProperty.RegisterReadOnly(nameof(IsAnimation), typeof(bool), typeof(DeltaNumberAnimator), new PropertyMetadata(false));
/// <summary><see cref="DependencyProperty"/> for property <see cref="IsAnimation"/>.</summary>
public static readonly DependencyProperty IsAnimationProperty = IsAnimationPropertyKey.DependencyProperty;
}
}
using System;
using System.Windows;
using System.Windows.Threading;
namespace Proxy
{
public partial class DeltaNumberAnimator
{
private readonly DispatcherTimer timer = new DispatcherTimer();
private double source;
private double delta;
private double number;
public DeltaNumberAnimator()
{
timer.Tick += OnTick;
}
private void OnTick(object sender, EventArgs e)
{
if (NextStep())
{
timer.Stop();
IsAnimation = false;
}
}
private static void IntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((DeltaNumberAnimator)d).timer.Interval = (TimeSpan)e.NewValue;
}
private void SourceChanged(DependencyPropertyChangedEventArgs e)
{
source = (double)e.NewValue;
if (source == number)
{
timer.Stop();
IsAnimation = false;
return;
}
if (timer.Interval == TimeSpan.Zero ||
delta == 0d)
{
Number = source;
timer.Stop();
IsAnimation = false;
return;
}
if (!NextStep())
{
timer.Start();
IsAnimation = true;
}
}
// Changing Number by Delta towards Source.
// Returns true if the Source value is reached.
private bool NextStep()
{
if (number < source)
{
double next = number + delta;
if (next >= source)
{
Number = source;
return true;
}
else
{
Number = next;
return false;
}
}
else
{
double next = number - delta;
if (next <= source)
{
Number = source;
return true;
}
else
{
Number = next;
return false;
}
}
}
private static object CoerceDelta(DependencyObject d, object baseValue)
{
double num = (double)baseValue;
if (num < double.Epsilon && num > -double.Epsilon)
return 0d;
if (num > 0)
return baseValue;
return -num;
}
private static void DeltaChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((DeltaNumberAnimator)d).delta = (double)e.NewValue;
}
private static void NumberChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((DeltaNumberAnimator)d).number = (double)e.NewValue;
}
}
}
Usage example:
namespace DeltaNumber
{
public class ViewModel
{
public double NumberProperty { get; set; }
}
}
<Window x:Class="DeltaNumber.DeltaNumberTestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DeltaNumber" xmlns:proxy="clr-namespace:Proxy;assembly=Common"
mc:Ignorable="d"
Title="DeltaNumberTestWindow" Height="450" Width="800">
<FrameworkElement.DataContext>
<local:ViewModel/>
</FrameworkElement.DataContext>
<UniformGrid Columns="1">
<FrameworkElement.Resources>
<proxy:DeltaNumberAnimator x:Key="deltaAnimator"
Source="{Binding NumberProperty}"
Delta="1"
Interval="0:0:1"/>
</FrameworkElement.Resources>
<TextBlock Text="{Binding Number, Source={StaticResource deltaAnimator}}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAnimation, Source={StaticResource deltaAnimator}}"
Value="True">
<Setter Property="Background" Value="LightGreen"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBox Text="{Binding NumberProperty}"/>
<TextBox Text="Any text. Used only to lose focus in an binded TextBox."/>
</UniformGrid>
</Window>

HelixToolkit.WPF Additional DependencyProperty not working

I am creating an application based on one of the example applications in the HelixToolkit (SurfacePlot) found in the ExampleBrwoser.
The SurfacePlotVisual3D class has three dependency properties, I tried to add another one simply by copying/renaming an existing one, but it does not work.
XAML
<h:HelixViewport3D ZoomExtentsWhenLoaded="True" ShowCoordinateSystem="True">
<local:SurfacePlotVisual3D CurrentX="{Binding CurrentX}" Points="{Binding Data}" ColorValues="{Binding ColorValues}" SurfaceBrush="{Binding SurfaceBrush}" />
</h:HelixViewport3D>
The "CurrentX" property is the one I tried to add.
SurfacePlotVisual3D.cs
public class SurfacePlotVisual3D : ModelVisual3D
{
public static readonly DependencyProperty CurrentXProperty =
DependencyProperty.Register("CurrentX", typeof(double), typeof(SurfacePlotVisual3D),
new UIPropertyMetadata(ModelChanged));
public static readonly DependencyProperty PointsProperty =
DependencyProperty.Register("Points", typeof(Point3D[,]), typeof(SurfacePlotVisual3D),
new UIPropertyMetadata(null, ModelChanged));
public static readonly DependencyProperty ColorValuesProperty =
DependencyProperty.Register("ColorValues", typeof(double[,]), typeof(SurfacePlotVisual3D),
new UIPropertyMetadata(null, ModelChanged));
public static readonly DependencyProperty SurfaceBrushProperty =
DependencyProperty.Register("SurfaceBrush", typeof(Brush), typeof(SurfacePlotVisual3D),
new UIPropertyMetadata(null, ModelChanged));
...
...
public Brush SurfaceBrush
{
get { return (Brush)GetValue(SurfaceBrushProperty); }
set { SetValue(SurfaceBrushProperty, value); }
}
public double CurrentX
{
get { return (double)GetValue(CurrentXProperty); }
set { SetValue(CurrentXProperty, value); }
}
private static void ModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((SurfacePlotVisual3D)d).UpdateModel();
}
MainWindow.cs
public partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.DataContext = new MainViewModel();
}
}
MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
...
...
public Func<double, double, double> Function { get; set; }
public Point3D[,] Data { get; set; }
public double[,] ColorValues { get; set; }
public double CurrentX { get; set; }
public MainViewModel()
{
CurrentX = 6;
MinX = 0;
MaxX = 3;
MinY = -2;
MaxY = 2;
Rows = 100;
Columns = 50;
Function = (x, y) => Math.Pow(Math.E, (-1) * Math.Pow(y, 2)) * 3*Math.Sin(2*x) + 2;
ColorCoding = ColorCoding.ByGradientX;
UpdateModel();
}
...
...
}
I can change "Data", "ColorValues" and "Function" in the MainViewModel and the effect is immediately visible in the plot, but the CurrentX property wont do anything.
As the comment of Anton suggested the missing default value was the problem.
public static readonly DependencyProperty CurrentXProperty =
DependencyProperty.Register("CurrentX", typeof(double), typeof(SurfacePlotVisual3D),
new UIPropertyMetadata((double)0, ModelChanged));
This fixed the problem. The (double)0 cast is needed because without it you will get an exception because "object" is not "double" (in my case). So thanks a lot Anton, made my day!

Custom control does not exist in the namespace?

I've been interested in WPF bindings in c# and tried to bind a control to the mouse position, but after getting a frustrating "The name "[control]" does not exist in the namespace "clr-namespace:[namespace]" every time I pasted the xaml code into the editor I decided it wasn't worth the time to investigate the quirk.
Now I am attempting to simply implement a binded Arc drawing example from Stack Overflow and am getting the same eror again. (All the code for a short runnable example can be found there)
So I've combed through all the Stack Overflow solution to this issue (it actually seems rather widespread) with what appear to be sporadic and inexpiable workarounds and fixes.
This question says that
If nothing else is possible, comment the lines which use the namespace, rebuild, and then build the full project again.
I also tried rebuilding the project, reopening Visual Studio. Nothing
helped. I finally commented xaml, rebuilt the project, uncommented
xaml and it finally worked! Strange issue.
This one said to set the project to release mode and another answer-er on the same question said to define the assembly: (xmlns:Local="clr-namespace:MusicPlayer.Controls;assembly=MusicPlayer") which did not work for me either.
This person recommended changing the build target platform (x86 - x64)
I've tried pretty much all of these solution to no avail. ReSharper seems to know that Arc class exists in the namespace assigned to Local, but Visual Studio does not.
<Local:Arc Center="{Binding Path=PreviousMousePositionPixels}"
Stroke="White"
StrokeDashArray="4 4"
SnapsToDevicePixels="True"
StartAngle="0"
EndAngle="{Binding Path=DeltaAngle}"
SmallAngle="True"
Radius="40"/>
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;
public sealed class Arc : Shape
{
public Point Center
{
get { return (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(0, 0), FrameworkPropertyMetadataOptions.AffectsRender));
public double StartAngle
{
get { return (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));
public double EndAngle
{
get { return (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(Math.PI/2.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double Radius
{
get { return (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 { return (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
{
var a0 = StartAngle < 0 ? StartAngle + 2 * Math.PI : StartAngle;
var a1 = EndAngle < 0 ? EndAngle + 2 * Math.PI : EndAngle;
if (a1<a0)
{
a1 += Math.PI * 2;
}
SweepDirection d = SweepDirection.Counterclockwise;
bool large;
if (SmallAngle)
{
large = false;
double t = a1;
if ((a1-a0)>Math.PI)
{
d = SweepDirection.Counterclockwise;
}
else
{
d = 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>(1);
segments.Add(new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true));
List<PathFigure> figures = new List<PathFigure>(1);
PathFigure pf = new PathFigure(p0, segments, true);
pf.IsClosed = false;
figures.Add(pf);
Geometry g = new PathGeometry(figures, FillRule.EvenOdd, null);
return g;
}
}
}
Define Arc inside a namespace as suggested by RobCroll. Check below code, I have included it in the Arc_Learning namespace which is referred in XAML as local.
<Window x:Class="Arc_Learning.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Arc_Learning"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:Arc
Stroke="White"
StrokeDashArray="4 4"
SnapsToDevicePixels="True"
StartAngle="0"
EndAngle="{Binding Path=DeltaAngle}"
SmallAngle="True"
Radius="40"/>
</Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Arc_Learning
{
public sealed class Arc : Shape
{
public Point Center
{
get { return (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(0, 0), FrameworkPropertyMetadataOptions.AffectsRender));
public double StartAngle
{
get { return (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));
public double EndAngle
{
get { return (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(Math.PI / 2.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double Radius
{
get { return (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 { return (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
{
var a0 = StartAngle < 0 ? StartAngle + 2 * Math.PI : StartAngle;
var a1 = EndAngle < 0 ? EndAngle + 2 * Math.PI : EndAngle;
if (a1 < a0)
{
a1 += Math.PI * 2;
}
SweepDirection d = SweepDirection.Counterclockwise;
bool large;
if (SmallAngle)
{
large = false;
double t = a1;
if ((a1 - a0) > Math.PI)
{
d = SweepDirection.Counterclockwise;
}
else
{
d = 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>(1);
segments.Add(new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true));
List<PathFigure> figures = new List<PathFigure>(1);
PathFigure pf = new PathFigure(p0, segments, true);
pf.IsClosed = false;
figures.Add(pf);
Geometry g = new PathGeometry(figures, FillRule.EvenOdd, null);
return g;
}
}
}
}
try to add local reference to your window as follow:
in the opening tag of your window add this reference
xmlns:local="clr-namespace:CustomControlTest"
in code behind C# Code you missing the namespace definition
add the missing namespace line before the class definition so your code should be like this:
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;
namespace CustomControlTest
{
public sealed class Arc : Shape
{
public Point Center
{
get { return (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(0, 0), FrameworkPropertyMetadataOptions.AffectsRender));
public double StartAngle
{
get { return (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));
public double EndAngle
{
get { return (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(Math.PI/2.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double Radius
{
get { return (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 { return (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
{
var a0 = StartAngle < 0 ? StartAngle + 2 * Math.PI : StartAngle;
var a1 = EndAngle < 0 ? EndAngle + 2 * Math.PI : EndAngle;
if (a1<a0)
{
a1 += Math.PI * 2;
}
SweepDirection d = SweepDirection.Counterclockwise;
bool large;
if (SmallAngle)
{
large = false;
double t = a1;
if ((a1-a0)>Math.PI)
{
d = SweepDirection.Counterclockwise;
}
else
{
d = 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>(1);
segments.Add(new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true));
List<PathFigure> figures = new List<PathFigure>(1);
PathFigure pf = new PathFigure(p0, segments, true);
pf.IsClosed = false;
figures.Add(pf);
Geometry g = new PathGeometry(figures, FillRule.EvenOdd, null);
return g;
}
}
}
}
add this namespace CustomControlTest and then it contains the class

Create custom Shape Control in UWP (Universal Windows Apps), Windows 10

I want to create a custom Shape control, that paints different shapes like Polygon, Ellipse, Rectangle, etc, depending on some custom properties.
I was able to create a custom template control ColorShape like this:
<Style TargetType="local:CustomShape">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:CustomShape">
<ContentControl x:Name="shapeParent">
</ContentControl>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And then, override the OnTemplateChanged method, and insert a corresponding Shape control inside the shapeParent ContentControl
But what I'd like is to actually extend the Shape, so I can treat all the shapes, framewok and custom, in the same way.
In WPF we were able to extend the Shape and override the property DefiningGeometry.
In UWP it doesn´t exist any DefiningGeometry property to override.
How is it possible to create a custom Shape control and define the corresponding Geometry?
The only way I found to create custom shapes in UWP is to extend the Path class and set its Data property.
Updating the Data property to account for changes in other dependency properties (like Width) must not be done in layouting relevant sections, like the LayoutUpdated event or the ArrangeOverride method.
Setting Data leads to another layout run, so setting it in anything that is called during that would lead to an exception:
Layout cycle detected. Layout could not complete
The way I use is to register handler for property changed events and update Data in them.
I have written a blog post that explains it in a bit more detail.
This is the example I used:
public class CandlestickShape : Path
{
public double StartValue
{
get { return Convert.ToDouble(GetValue(StartValueProperty)); }
set { SetValue(StartValueProperty, value); }
}
public static readonly DependencyProperty StartValueProperty =
DependencyProperty.Register("StartValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));
public double EndValue
{
get { return Convert.ToDouble(GetValue(EndValueProperty)); }
set { SetValue(EndValueProperty, value); }
}
public static readonly DependencyProperty EndValueProperty =
DependencyProperty.Register("EndValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));
public double MinValue
{
get { return Convert.ToDouble(GetValue(MinValueProperty)); }
set { SetValue(MinValueProperty, value); }
}
public static readonly DependencyProperty MinValueProperty =
DependencyProperty.Register("MinValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));
public double MaxValue
{
get { return Convert.ToDouble(GetValue(MaxValueProperty)); }
set { SetValue(MaxValueProperty, value); }
}
public static readonly DependencyProperty MaxValueProperty =
DependencyProperty.Register("MaxValue", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));
/// <summary>
/// Defines how many Pixel should be drawn for one Point
/// </summary>
public double PixelPerPoint
{
get { return Convert.ToDouble(GetValue(PointsPerPixelProperty)); }
set { SetValue(PointsPerPixelProperty, value); }
}
public static readonly DependencyProperty PointsPerPixelProperty =
DependencyProperty.Register("PixelPerPoint", typeof(double), typeof(CandlestickShape), new PropertyMetadata(0));
public CandlestickShape()
{
this.RegisterPropertyChangedCallback(CandlestickShape.WidthProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
this.RegisterPropertyChangedCallback(CandlestickShape.StartValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
this.RegisterPropertyChangedCallback(CandlestickShape.EndValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
this.RegisterPropertyChangedCallback(CandlestickShape.MinValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
this.RegisterPropertyChangedCallback(CandlestickShape.MaxValueProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
this.RegisterPropertyChangedCallback(CandlestickShape.PointsPerPixelProperty, new DependencyPropertyChangedCallback(RenderAffectingPropertyChanged));
}
private void RenderAffectingPropertyChanged(DependencyObject o, DependencyProperty e)
{
(o as CandlestickShape)?.SetRenderData();
}
private void SetRenderData()
{
var maxBorderValue = Math.Max(this.StartValue, this.EndValue);
var minBorderValue = Math.Min(this.StartValue, this.EndValue);
double topLineLength = (this.MaxValue - maxBorderValue) * this.PixelPerPoint;
double bottomLineLength = (minBorderValue - this.MinValue) * this.PixelPerPoint;
double bodyLength = (this.EndValue - this.StartValue) * this.PixelPerPoint;
var fillColor = new SolidColorBrush(Colors.Green);
if (bodyLength < 0)
fillColor = new SolidColorBrush(Colors.Red);
bodyLength = Math.Abs(bodyLength);
var bodyGeometry = new RectangleGeometry
{
Rect = new Rect(new Point(0, topLineLength), new Point(this.Width, topLineLength + bodyLength)),
};
var topLineGeometry = new LineGeometry
{
StartPoint = new Point(this.Width / 2, 0),
EndPoint = new Point(this.Width / 2, topLineLength)
};
var bottomLineGeometry = new LineGeometry
{
StartPoint = new Point(this.Width / 2, topLineLength + bodyLength),
EndPoint = new Point(this.Width / 2, topLineLength + bodyLength + bottomLineLength)
};
this.Data = new GeometryGroup
{
Children = new GeometryCollection
{
bodyGeometry,
topLineGeometry,
bottomLineGeometry
}
};
this.Fill = fillColor;
this.Stroke = new SolidColorBrush(Colors.Black);
}
protected override Size ArrangeOverride(Size finalSize)
{
double height = (MaxValue - MinValue) * PixelPerPoint;
return new Size(this.Width, height);
}
protected override Size MeasureOverride(Size availableSize)
{
double height = (MaxValue - MinValue) * PixelPerPoint;
return new Size(this.Width, height);
}
}

Combining LineGeometry with EllipseGeometry (in code, not XAML)

I'm trying to create a custom shape with WPF. For starters I was just trying to create a simple line, that has a circle at each end (I know there are LineCaps, but that's not what I'm looking for).
I've looked into some tutorials and the easiest way to do that, seems to use a CombinedGeometry. However I can't get it to work properly. Here is my code that creates the geometry object:
protected override Geometry DefiningGeometry
{
get
{
Point ellipseCenter1 = new Point(X1 - this.CapDiameter / 2, Y1 - this.CapDiameter / 2);
Point ellipseCenter2 = new Point(X2 - this.CapDiameter / 2, Y2 - this.CapDiameter / 2);
var ellipse1 = new EllipseGeometry(ellipseCenter1, CapDiameter, CapDiameter);
var ellipse2 = new EllipseGeometry(ellipseCenter2, CapDiameter, CapDiameter);
var line = new LineGeometry(this.StartPoint, this.EndPoint);
var combined1 = new CombinedGeometry(GeometryCombineMode.Union, ellipse1, line);
var combined2 = new CombinedGeometry(GeometryCombineMode.Union, combined1, ellipse2);
// Freeze the geometry for performance benefits
combined2.Freeze();
return combined2;
}
}
However, for some reason, this doesn't draw the line. It draws both circles, but not the line. Obviously I'm missing something here, could someone point out what? :)
Just in case it matters, here is the rest of the class:
#region Dependency Properties
public static readonly DependencyProperty X1Property =
DependencyProperty.Register(
"X1",
typeof(double),
typeof(CappedLine),
new FrameworkPropertyMetadata(
0.0,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure
)
);
public static readonly DependencyProperty Y1Property =
DependencyProperty.Register(
"Y1",
typeof(double),
typeof(CappedLine),
new FrameworkPropertyMetadata(
0.0,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure
)
);
public static readonly DependencyProperty X2Property =
DependencyProperty.Register(
"X2",
typeof(double),
typeof(CappedLine),
new FrameworkPropertyMetadata(
0.0,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure
)
);
public static readonly DependencyProperty Y2Property =
DependencyProperty.Register(
"Y2",
typeof(double),
typeof(CappedLine),
new FrameworkPropertyMetadata(
0.0,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure
)
);
public static readonly DependencyProperty CapDiameterProperty =
DependencyProperty.Register(
"CapDiameter",
typeof(double),
typeof(CappedLine),
new FrameworkPropertyMetadata(
0.0,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure
)
);
#endregion
#region CLR Properties
public double X1
{
get { return (double)base.GetValue(X1Property); }
set { base.SetValue(X1Property, value); }
}
public double Y1
{
get { return (double)base.GetValue(Y1Property); }
set { base.SetValue(Y1Property, value); }
}
public double X2
{
get { return (double)base.GetValue(X2Property); }
set { base.SetValue(X2Property, value); }
}
public double Y2
{
get { return (double)base.GetValue(Y2Property); }
set { base.SetValue(Y2Property, value); }
}
public Point StartPoint
{
get { return (new Point(X1, Y1)); }
}
public Point EndPoint
{
get { return (new Point(X2, Y2)); }
}
public double CapDiameter
{
get { return (double)base.GetValue(CapDiameterProperty); }
set { base.SetValue(CapDiameterProperty, value); }
}
#endregion
And here is the XAML code I used to test it:
<Window x:Class="HG.WPF.Shapes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shapes="clr-namespace:Tomers.WPF.Shapes"
Title="WPF Custom Shapes" Height="300" Width="300" ResizeMode="NoResize">
<Canvas Background="Black">
<shapes:CappedLine X1="30" Y1="30" X2="200" Y2="200" CapDiameter="5" Fill="White" Stroke="White" StrokeThickness="2"></shapes:CappedLine>
</Canvas>
</Window>
Your LineGeometry objects will vanish if you try to Union them. From MSDN:
The GeometryCombineMode property specifies how the two geometries will
be combined. Note that CombinedGeometry combines the area specified by
two geometries, so geometries that do not have area (such as
LineGeometry) disappear when combined.
You can use GeometryGroup:
GeometryGroup combined = new GeometryGroup();
combined.Children.Add(ellipse1);
combined.Children.Add(ellipse2);
combined.Children.Add(line);
or you can use CombinedGeometry on the ellipse objects first, and then group that together with the line using GeometryGroup.

Categories