#1,161 – Using Custom Circular Progress Shape in Control Template

In the previous example, we created a custom circular progress shape and then composed it along with a TextBlock to get a labeled circular progress control.

The other approach that we could take is to just create a new control template containing our circular progress shape and a TextBlock and then use that new template as the control template for a standard ProgressBar.

Assuming that we start with the earlier code for CircularProgress, we compose the template and use it as follows:

	<Window.Resources>
        <loc:DoubleToPctConverter x:Key="dblToPct"/>
		<Style x:Key="pbarCircularStyle" TargetType="{x:Type ProgressBar}">
			<Setter Property="Foreground" Value="#01D328"/>
            <Setter Property="Maximum" Value="100"/>
            <Setter Property="Height" Value="100"/>
            <Setter Property="Width" Value="100"/>
            <Setter Property="Template">
				<Setter.Value>
					<ControlTemplate TargetType="{x:Type ProgressBar}">
						<Grid x:Name="TemplateRoot" SnapsToDevicePixels="true">
                            <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
                                       FontSize="32" Foreground="DarkGray"
                                       Text="{TemplateBinding Value, Converter={StaticResource dblToPct}}" />
                            <loc:CircularProgress Stroke="{TemplateBinding Foreground}"
                                                  Value="{TemplateBinding Value}"/>
						</Grid>
					</ControlTemplate>
				</Setter.Value>
			</Setter>
		</Style>
	</Window.Resources>

    <StackPanel>
        <Grid>
			<ProgressBar Style="{DynamicResource pbarCircularStyle}"
						 Value="{Binding PctComplete}"
						 Margin="10"/>
        </Grid>
        <Button Content="Start Timer" Click="Button_Click"
                HorizontalAlignment="Center"
                Padding="12,7"/>
    </StackPanel>

1161-001

For completeness, here’s the source code for our value converter:

    public class DoubleToPctConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            string result = "";
            double d = (double)value;
            result = string.Format("{0}%", d);

            return result;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

#1,160 – Adding a Text Label to the Circular Progress Control

In an earlier post, we created a custom circular progress control, deriving from a Shape element.  Below, we add a text element in the middle of the progress arc to indicate % complete.  We could have also done this by building the text into the control itself.  But the approach shown below is an example of combining the progress control with a user-defined text label.

    <StackPanel>
        <Grid>
            <loc:CircularProgress
                x:Name="circProg"
                Value="{Binding PctComplete}"
                Height="100" Width="100" Margin="5"
                HorizontalAlignment="Center"/>
            <TextBlock
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                FontSize="36" Foreground="DarkGray"
                Text="{Binding ElementName=circProg, Path=Value}" />
        </Grid>
        <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>

1160-001

#1,157 – Overridden Default Property Values Appear in Property Pane

In an earlier example of a custom control, we used the OverrideMetadata method to provide new default values for several dependency properties, as used in our control.  For example, we set the Stroke property to a green brush and StrokeThickness to 10.0.

        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(Brushes.Transparent));

            StrokeThicknessProperty.OverrideMetadata(
                typeof(CircularProgress),
                new FrameworkPropertyMetadata(10.0));
        }

These new values will show up as expected in the property pane within Visual Studio. Below, we can see both the green brush for Stroke and the value of 10.0 for StrokeThickness.

1157-001

#1,156 – Changing Circular Progress Control to Be Only an Arc

An earlier post presented a circular progress control that displayed a pie-shaped filled area indicating progress.

1155-001

We can change the control to draw just the outer part of the circle (an arc) as a progress indicator.  We make the following changes:

  • Do not draw line to origin at end of shape
  • Set isClosed parameter in BeginFigure to false
  • Default Fill property to be transparent
  • Default Stroke to have thickness of 10.0

Here is the updated code:

    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(Brushes.Transparent));

            StrokeThicknessProperty.OverrideMetadata(
                typeof(CircularProgress),
                new FrameworkPropertyMetadata(10.0));
        }

        // 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
                        false);  // 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, true);
                }

                return geom;
            }
        }
    }

Below is a screenshot showing the updated arc-based circular progress indicator.

1156-001

 

#1,155 – A Circular Progress Indicator

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:

1155-001

#1,153 – Handling Custom Dependency Properties that Affect Rendering

In our example of a custom “pie slice” shape, we defined StartAngle and EndAngle properties that dictate where the pie shape starts and ends.  We then define how the shape is rendered by overriding the DefiningGeometry property.

One problem with the earlier code example was that the shape wasn’t automatically re-rendered if the start or end angles were changed.  Since the values of these properties are used in rendering the shape, we want the shape to automatically redraw itself whenever either property changes.

We can force a re-render by specifying the AffectsRender flag when defining the metadata for the StartAngle and EndAngle properties.  Below is updated code for defining these properties.

        // Angle that arc starts at
        public double StartAngle
        {
            get { return (double)GetValue(StartAngleProperty); }
            set { SetValue(StartAngleProperty, value); }
        }

        // DependencyProperty - StartAngle
        private static FrameworkPropertyMetadata startAngleMetadata =
                new FrameworkPropertyMetadata(
                    0.0,     // Default value
                    FrameworkPropertyMetadataOptions.AffectsRender,
                    null,    // Property changed callback
                    new CoerceValueCallback(CoerceAngle));   // Coerce value callback

        public static readonly DependencyProperty StartAngleProperty =
            DependencyProperty.Register("StartAngle", typeof(double), typeof(PieSlice), startAngleMetadata);

        // Angle that arc ends at
        public double EndAngle
        {
            get { return (double)GetValue(EndAngleProperty); }
            set { SetValue(EndAngleProperty, value); }
        }

        // DependencyProperty - EndAngle
        private static FrameworkPropertyMetadata endAngleMetadata =
                new FrameworkPropertyMetadata(
                    90.0,     // Default value
                    FrameworkPropertyMetadataOptions.AffectsRender,
                    null,    // Property changed callback
                    new CoerceValueCallback(CoerceAngle));   // Coerce value callback

        public static readonly DependencyProperty EndAngleProperty =
            DependencyProperty.Register("EndAngle", typeof(double), typeof(PieSlice), endAngleMetadata);