23

I'm working on an Unified fitness app for Windows 8.1 and Windows Phone 8.1. Ideally one of the core views would feature a daily progress meter. The problem is that I haven't been able to come up with an actual meter or gauge. What I'd like to have is simply a radial progress bar or something on par with the battery gauges/meters in common battery apps in the Windows Phone store. From what I can tell, WPF/VS 2013 doesn't offer this kind of component out of the box. I know that Telerik and a few other 3rd parties offer something similar, but I'd prefer to use something open source or build it myself.

Does anyone know of newer opensource components that work with .NET 4.5 & WPF or have examples on how I could build my own component?

So far what I have found are similar to this link: Gauges for WPF

But I'm hoping to use something similar to this: enter image description here

etolstoy
  • 1,798
  • 21
  • 33
TheMoonbeam
  • 519
  • 1
  • 5
  • 15

1 Answers1

65

You can build something like that yourself. First of all, you need an Arc:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Shapes;
using System.Windows.Media;
using System.Windows;

...

public class Arc : Shape
{
    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 UIPropertyMetadata(0.0, new PropertyChangedCallback(UpdateArc)));

    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 UIPropertyMetadata(90.0, new PropertyChangedCallback(UpdateArc)));

    //This controls whether or not the progress bar goes clockwise or counterclockwise
    public SweepDirection Direction
    {
        get { return (SweepDirection) GetValue(DirectionProperty); }
        set { SetValue(DirectionProperty, value);}
    }

    public static readonly DependencyProperty DirectionProperty =
        DependencyProperty.Register("Direction", typeof (SweepDirection), typeof (Arc),
            new UIPropertyMetadata(SweepDirection.Clockwise));

    //rotate the start/endpoint of the arc a certain number of degree in the direction
    //ie. if you wanted it to be at 12:00 that would be 270 Clockwise or 90 counterclockwise
    public double OriginRotationDegrees
    {
        get { return (double) GetValue(OriginRotationDegreesProperty); }
        set { SetValue(OriginRotationDegreesProperty, value);}
    }

    public static readonly DependencyProperty OriginRotationDegreesProperty =
        DependencyProperty.Register("OriginRotationDegrees", typeof (double), typeof (Arc),
            new UIPropertyMetadata(270.0, new PropertyChangedCallback(UpdateArc)));

    protected static void UpdateArc(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Arc arc = d as Arc;
        arc.InvalidateVisual();
    }

    protected override Geometry DefiningGeometry
    {
        get { return GetArcGeometry(); }
    }

    protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
    {
        drawingContext.DrawGeometry(null, new Pen(Stroke, StrokeThickness), GetArcGeometry());
    }

    private Geometry GetArcGeometry()
    {
        Point startPoint = PointAtAngle(Math.Min(StartAngle, EndAngle), Direction);
        Point endPoint = PointAtAngle(Math.Max(StartAngle, EndAngle), Direction);

        Size arcSize = new Size(Math.Max(0, (RenderSize.Width - StrokeThickness) / 2),
            Math.Max(0, (RenderSize.Height - StrokeThickness) / 2));
        bool isLargeArc = Math.Abs(EndAngle - StartAngle) > 180;

        StreamGeometry geom = new StreamGeometry();
        using (StreamGeometryContext context = geom.Open())
        {
            context.BeginFigure(startPoint, false, false);
            context.ArcTo(endPoint, arcSize, 0, isLargeArc, Direction, true, false);
        }
        geom.Transform = new TranslateTransform(StrokeThickness / 2, StrokeThickness / 2);
        return geom;
    }

    private Point PointAtAngle(double angle, SweepDirection sweep)
    {
        double translatedAngle = angle + OriginRotationDegrees;
        double radAngle = translatedAngle * (Math.PI / 180);
        double xr = (RenderSize.Width - StrokeThickness) / 2;
        double yr = (RenderSize.Height - StrokeThickness) / 2;

        double x = xr + xr * Math.Cos(radAngle);
        double y = yr * Math.Sin(radAngle);

        if (sweep == SweepDirection.Counterclockwise)
        {
            y = yr - y;
        }
        else
        {
            y = yr + y;
        }

        return new Point(x, y);
    }
}

This arc has an StartAngle and an EndAngle. To convert from a progress of a progressbar to these angles, you need a converter:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

...

public class ProgressToAngleConverter : System.Windows.Data.IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        double progress = (double)values[0];
        System.Windows.Controls.ProgressBar bar = values[1] as System.Windows.Controls.ProgressBar;

        return 359.999 * (progress / (bar.Maximum - bar.Minimum));
    }

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

Ok fine. That was everything you need. Now you can write your XAML. That could be something like that:

<Window x:Class="WPFTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WPFTest"
        Title="MainWindow" Height="525" Width="525">
    <Window.Resources>
        <local:ProgressToAngleConverter x:Key="ProgressConverter"/>
        <Style TargetType="{x:Type ProgressBar}" x:Key="ProgressBarStyle">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ProgressBar}">
                        <Grid>
                            <Ellipse Stroke="Black" Fill="{TemplateBinding Background}"/>
                            <Ellipse Stroke="Black" Margin="40" Fill="White"/>
                            <local:Arc StrokeThickness="30" Stroke="{TemplateBinding BorderBrush}" Margin="5">
                                <local:Arc.StartAngle>
                                    <MultiBinding Converter="{StaticResource ProgressConverter}">
                                        <Binding Path="Minimum" RelativeSource="{RelativeSource TemplatedParent}"/>
                                        <Binding Path="." RelativeSource="{RelativeSource TemplatedParent}"/>
                                    </MultiBinding>
                                </local:Arc.StartAngle>
                                <local:Arc.EndAngle>
                                    <MultiBinding Converter="{StaticResource ProgressConverter}">
                                        <Binding Path="Value" RelativeSource="{RelativeSource TemplatedParent}"/>
                                        <Binding Path="." RelativeSource="{RelativeSource TemplatedParent}"/>
                                    </MultiBinding>
                                </local:Arc.EndAngle>
                            </local:Arc>
                            <TextBlock Text="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat=\{0:0\}}"
                                       Foreground="{TemplateBinding Background}" VerticalAlignment="Center" HorizontalAlignment="Center"
                                       FontSize="72" FontWeight="Bold"/>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Viewbox>
            <ProgressBar Style="{StaticResource ProgressBarStyle}" Width="300" Height="300" 
                         Value="{Binding ElementName=sliderValue, Path=Value}"/>
        </Viewbox>
        <Slider Grid.Row="1" Name="sliderValue" Maximum="100" Value="50" />
    </Grid>
</Window>

Now just take the ProgressBarStyle, modify it and apply it to any progressbar you like.

Finally, you'll get something like this. Have fun!

EDIT: You need the following references (I would recommend you, to just create a new and empty WPF project):

  • WindowsBase
  • PresentationCore
  • PresentationFramework

EDIT: In order to control the rotation direction as well as the position to start the progress at, I added two dependency properties: Direction OriginRotationDegrees

enter image description here

Smolakian
  • 414
  • 3
  • 15
Florian
  • 5,918
  • 3
  • 47
  • 86
  • Thank you for your well detailed answer! For the most part I found it easy to understand and helpful. Unfortunately, I'm having difficulties implementing the solution.I'm unable to "Resolve" or find the Namespace for StreamGeometry, Pen, Point, UIPropertyMetadata, and Media.DrawingContext. DrawingContext not being found in the systems to be caused by System.Windows.Media not being found when trying to use the import. The recommendations I found advised referencing PresentationCore.dll, but this didn't solve the issue for me. Any suggestions? – TheMoonbeam Apr 16 '14 at 03:43
  • I've added the necessary namespaces. – Florian Apr 16 '14 at 08:30
  • thanks for adding the imports. A few of the imports I was using were incorrect, and I was missing a few references: PresentationFramework, PresentationCore, WindowsBase. Everything is working really well, thanks! :) – TheMoonbeam Apr 17 '14 at 00:59
  • 1
    Exactly what I was envisioning, thank you very much! – Alex Hopkins Mar 13 '15 at 13:43
  • Well done! Not hard to implement and easy to customize by playing around with the style. – Mike Fuchs Jul 24 '15 at 11:41
  • For future reference: [Improvement to support RadialGradients](http://stackoverflow.com/questions/37449585/wpf-radial-progress-bar-with-radial-gradient/37461291#37461291) – Manfred Radlwimmer May 26 '16 at 13:11
  • What is the purpose of `arc.InvalidateVisual()` in the `UpdateArc` method? – Aaron Thomas Jul 26 '16 at 19:25
  • how can we change the edges to be round corners ???? – dnxit Aug 12 '17 at 14:55
  • @Florian it was awesome but how its work on click event when i want to count my file and folder in same progress bar in click event – Sanjay Ranavaya Oct 15 '18 at 12:26
  • Can someone explain to me what is being calculated here: `double x = xr + xr * Math.Cos(radAngle);` and the subsequent y calculation? I think it has something to do with unit circles for calculating x,y but I am not sure exactly what is going on. – uhsl_m Apr 25 '19 at 14:14
  • Here's the open-source library for WPF and Core, utilizing the very same methods to the greater extent: https://github.com/panthernet/XamlRadialProgressBar – Alexander Smirnov Mar 05 '20 at 17:25