#1,155 – A Circular Progress Indicator
September 10, 2014 2 Comments
Using the earlier custom pie shape control as a base, we can now create a custom control that serves as a circular progress indicator.
Here’s the code for the circular progress control:
public class CircularProgress : Shape { static CircularProgress() { Brush myGreenBrush = new SolidColorBrush(Color.FromArgb(255, 6, 176, 37)); myGreenBrush.Freeze(); StrokeProperty.OverrideMetadata( typeof(CircularProgress), new FrameworkPropertyMetadata(myGreenBrush)); FillProperty.OverrideMetadata( typeof(CircularProgress), new FrameworkPropertyMetadata(myGreenBrush)); } // Value (0-100) public double Value { get { return (double)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } // DependencyProperty - Value (0 - 100) private static FrameworkPropertyMetadata valueMetadata = new FrameworkPropertyMetadata( 0.0, // Default value FrameworkPropertyMetadataOptions.AffectsRender, null, // Property changed callback new CoerceValueCallback(CoerceValue)); // Coerce value callback public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(CircularProgress), valueMetadata); private static object CoerceValue(DependencyObject depObj, object baseVal) { double val = (double)baseVal; val = Math.Min(val, 99.999); val = Math.Max(val, 0.0); return val; } protected override Geometry DefiningGeometry { get { double startAngle = 90.0; double endAngle = 90.0 - ((Value / 100.0) * 360.0); double maxWidth = Math.Max(0.0, RenderSize.Width - StrokeThickness); double maxHeight = Math.Max(0.0, RenderSize.Height - StrokeThickness); double xStart = maxWidth / 2.0 * Math.Cos(startAngle * Math.PI / 180.0); double yStart = maxHeight / 2.0 * Math.Sin(startAngle * Math.PI / 180.0); double xEnd = maxWidth / 2.0 * Math.Cos(endAngle * Math.PI / 180.0); double yEnd = maxHeight / 2.0 * Math.Sin(endAngle * Math.PI / 180.0); StreamGeometry geom = new StreamGeometry(); using (StreamGeometryContext ctx = geom.Open()) { ctx.BeginFigure( new Point((RenderSize.Width / 2.0) + xStart, (RenderSize.Height / 2.0) - yStart), true, // Filled true); // Closed ctx.ArcTo( new Point((RenderSize.Width / 2.0) + xEnd, (RenderSize.Height / 2.0) - yEnd), new Size(maxWidth / 2.0, maxHeight / 2), 0.0, // rotationAngle (startAngle - endAngle) > 180, // greater than 180 deg? SweepDirection.Clockwise, true, // isStroked false); ctx.LineTo(new Point((RenderSize.Width / 2.0), (RenderSize.Height / 2.0)), true, false); } return geom; } } }
Here’s an example of using this control in XAML. The example also includes a traditional progress bar, so that we can compare them.
<StackPanel> <loc:CircularProgress Height="100" Width="100" Margin="5" Value="{Binding PctComplete}" HorizontalAlignment="Center"/> <ProgressBar x:Name="prog2" Maximum="100" Value="{Binding PctComplete}" Height="25" Margin="10"/> <Button Content="Start Timer" Click="Button_Click" HorizontalAlignment="Center" Padding="12,7"/> </StackPanel>
Finally, here’s the code-behind, including the PctComplete property that we bind to and code that kicks off a timer that updates the property periodically.
public partial class MainWindow : Window, INotifyPropertyChanged { public MainWindow() { this.DataContext = this; InitializeComponent(); } public event PropertyChangedEventHandler PropertyChanged = delegate { }; protected virtual void OnPropertyChanged(string prop) { PropertyChanged(this, new PropertyChangedEventArgs(prop)); } private double pctComplete = 0.0; public double PctComplete { get { return pctComplete; } set { if (pctComplete != value) { pctComplete = value; OnPropertyChanged("PctComplete"); } } } private void Button_Click(object sender, RoutedEventArgs e) { PctComplete = 0.0; DispatcherTimer timer = new DispatcherTimer(); timer.Tick += (s, ea) => { PctComplete += 1.0; if (PctComplete >= 100.0) timer.Stop(); }; timer.Interval = new TimeSpan(0, 0, 0, 0, 30); // 2/sec timer.Start(); } }
Here’s the control in action: