Home > Net >  How to create a Marquee for Circular Progress Bar and fill only the gray area of bar using C# and WP
How to create a Marquee for Circular Progress Bar and fill only the gray area of bar using C# and WP

Time:11-06

I have created a custom circular progress bar, as shown below:

enter image description here

Using the following code:

<UserControl x:Class="WpfApp1.SpinnerProgressBar"
             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:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
            <Viewbox>
            <Canvas Width="100" Height="100">
                <!-- Base Spinner -->
            <Path Stroke="LightGray" StrokeThickness="3" Fill="LightGray"
                      Data="M 0 100 a 100,100 0 1 1 200,0 
                                    a 100,100 0 1 1 -200,0 
                            M 30 100 a 70,70 0 1 1 140,0
                                     a 70,70 0 1 1 -140,0" RenderTransformOrigin="0.5,0.5" />

            <!-- Loader Spinner "M 0 100 a 100,100 0 0 1 100,-100 v 30 a 70,70 0 0 0 -70,70" -->

            <Path Fill="Gold" Data="M 0 100 a 100,100 0 0 1 100,-100 v 30 
                                              a 70,70 0 0 0 -70,70" RenderTransformOrigin="1,1" >
                <Path.RenderTransform>
                    <TransformGroup>
                        <ScaleTransform/>
                        <SkewTransform/>
                        <RotateTransform Angle="0"/>
                        <TranslateTransform/>
                    </TransformGroup>
                </Path.RenderTransform>
            </Path>
        </Canvas>
    </Viewbox>

</UserControl>

with Mimicing Marquee behaviour, I would just need to do the binding to <RotateTransform Angle="0"/> (If I am not mistaken). However, I am stuck on the part of how to create a full progressbar when there is a case say like successful connection to the database and with marquee when still trying to connect. How can I do the full progressbar part? If there is a tutorial/video that would explain how to do it I would appreciate it.

CodePudding user response:

Doing the rotate animation for Marquee effect is pretty easy, just need to animate the RenderTransform.RotateTranform property.

You can achieve it like so. Here's declaration of the Storyboard, with some extra effect for the rotation, and example trigger by IsMouseOver on selected Path.

<UserControl.Resources>
    <Storyboard x:Key="SpinStoryboardSpinning" RepeatBehavior="Forever">
        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Path.RenderTransform).(RotateTransform.Angle)">
            <EasingDoubleKeyFrame KeyTime="0:0:0" Value="90"/>
            <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="180"/>
            <EasingDoubleKeyFrame KeyTime="0:0:1" Value="450">
                <EasingDoubleKeyFrame.EasingFunction>
                    <QuadraticEase EasingMode="EaseInOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>
</UserControl.Resources>

<Path Fill="Gold"
      Data="M 0 100 a 100,100 0 0 1 100,-100 v 30 a 70,70 0 0 0 -70,70"
      RenderTransformOrigin="1,1">
    <Path.RenderTransform>
            <RotateTransform Angle="90"/>
    </Path.RenderTransform>
    <Path.Style>
        <Style TargetType="{x:Type Path}">
            <Style.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Trigger.EnterActions>
                        <BeginStoryboard Storyboard="{StaticResource SpinStoryboardSpinning}"/>
                    </Trigger.EnterActions>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Path.Style>
</Path>

But in order to create a full progress bar, it's more complext task.

First of all you need to have control over the created Storyboards, ie stop and start them when needed. Also for the ProgressBar itself, I'd recommend to start by deriving from the ProgressBar Control and style it as you wish.

Good start would be to create first, standard, but styled ProgressBar based on this example from MSDN https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/progressbar-styles-and-templates?view=netframeworkdesktop-4.8

CodePudding user response:

Example of a round ProgressBar.

Custom Control and Converter:

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Markup;
using System.Windows.Media;
using static System.Math;

namespace Core2023.RoundProgressBar
{
    public class RoundProgressBar : RangeBase
    {
        static RoundProgressBar()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(RoundProgressBar), new FrameworkPropertyMetadata(typeof(RoundProgressBar)));
        }
    }

    [ValueConversion(typeof(ProgressBar), typeof(Geometry))]
    public class ProgressBarToGeometryConverter : IMultiValueConverter
    {
        private static readonly double valPI = 2 * PI;
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            double min = (double)values[0];
            double max = (double)values[1];
            double value = (double)values[2];

            double angle = (value - min) / (max - min);
            angle *= valPI;
            if (!double.IsNormal(angle))
                return DependencyProperty.UnsetValue;

            double cos = Cos(angle);
            double sin = Sin(angle);
            double x1 = 100 * cos   100;
            double y1 = 100 * sin   100;
            double x2 = 70 * cos   100;
            double y2 = 70 * sin   100;


            string data;
            if (angle < PI)
            {
                data = @$"
M200,100
A100,100 0 0 1 {x1.ToString(culture)},{y1.ToString(culture)}
L{x2.ToString(culture)},{y2.ToString(culture)}
A070,070 0 0 0 170,100
z";
            }
            else
            {
                data = @$"
M200,100
A100,100 0 1 1 {x1.ToString(culture)},{y1.ToString(culture)}
L{x2.ToString(culture)},{y2.ToString(culture)}
A070,070 0 1 0 170,100
z";
            }
            return Geometry.Parse(data);
        }

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

        private ProgressBarToGeometryConverter() { }
        public static ProgressBarToGeometryConverter Instance { get; } = new();
    }

    [MarkupExtensionReturnType(typeof(ProgressBarToGeometryConverter))]
    public class ProgressBarToGeometryExtension : MarkupExtension
    {
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return  ProgressBarToGeometryConverter.Instance;
        }
    }
}

Theme Generic.xaml:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Core2023"
    xmlns:rpb="clr-namespace:Core2023.RoundProgressBar">


    <Style TargetType="{x:Type rpb:RoundProgressBar}">
        <Setter Property="Background" Value="LightGray"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type rpb:RoundProgressBar}">
                    <ControlTemplate.Resources>
                        <Geometry x:Key="round">
                            M 0 100 a 100,100 0 1 1 200,0
                            a 100,100 0 1 1 -200,0
                            M 30 100 a 70,70 0 1 1 140,0
                            a 70,70 0 1 1 -140,0</Geometry>
                    </ControlTemplate.Resources>
                    <Grid>
                        <!-- Base Spinner -->
                        <Path Fill="{TemplateBinding Background}"
                              Data="{StaticResource round}"/>
                        <Path Stroke="{TemplateBinding BorderBrush}" StrokeThickness="3"
                              Data="{StaticResource round}" Panel.ZIndex="2"/>

                        <!-- Loader Spinner "M 0 100 a 100,100 0 0 1 100,-100 v 30 a 70,70 0 0 0 -70,70" -->

                        <Path Fill="Gold">
                            <Path.Data>
                                <MultiBinding Converter="{rpb:ProgressBarToGeometry}">
                                    <Binding RelativeSource="{RelativeSource AncestorType=rpb:RoundProgressBar}" Path="Minimum"/>
                                    <Binding RelativeSource="{RelativeSource AncestorType=rpb:RoundProgressBar}" Path="Maximum"/>
                                    <Binding RelativeSource="{RelativeSource AncestorType=rpb:RoundProgressBar}" Path="Value"/>
                                </MultiBinding>
                            </Path.Data>
                        </Path>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Window with an example of use and animation of the Value property:

<Window x:Class="Core2023.RoundProgressBar.RoundProgressBarWindow"
        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:Core2023.RoundProgressBar"
        mc:Ignorable="d"
        Title="RoundProgressBarWindow" Height="450" Width="800">
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Duration="0:0:5"
                                     To="10"
                                     Storyboard.TargetName="rpb"
                                     Storyboard.TargetProperty="Value"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
    <Grid>
        <local:RoundProgressBar x:Name="rpb"
                                Maximum="10" Minimum="-10" Value="-10"
                                BorderBrush="Aqua"/>
    </Grid>
</Window>

The Value property can be bound to the ViewModel or set in any other way. Progress will be displayed correctly.

P.S. This is an improved version of the converter using StreamGeometry.

    [ValueConversion(typeof(ProgressBar), typeof(Geometry))]
    public class ProgressBarToGeometryConverter : IMultiValueConverter
    {
        private static readonly double valPI = 2 * PI;
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            double min = (double)values[0];
            double max = (double)values[1];
            double value = (double)values[2];

            double angle = (value - min) / (max - min);
            angle *= valPI;
            if (!double.IsNormal(angle))
                return DependencyProperty.UnsetValue;

            double cos = Cos(angle);
            double sin = Sin(angle);
            double x1 = 100 * cos   100;
            double y1 = 100 * sin   100;
            double x2 = 70 * cos   100;
            double y2 = 70 * sin   100;

            StreamGeometry sg = new();
            sg.FillRule = FillRule.EvenOdd;

            using StreamGeometryContext sgc = sg.Open();
            if (value >= max)
            {
                sgc.BeginFigure(new Point(200, 100), true, true);
                sgc.ArcTo(new Point(0, 100), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
                sgc.ArcTo(new Point(200, 100), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
                sgc.LineTo(new Point(170, 100), true, false);
                sgc.ArcTo(new Point(30, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
                sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
            }
            else
            if (angle < PI)
            {
                sgc.BeginFigure(new Point(200, 100), true, true);
                sgc.ArcTo(new Point(x1, y1), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
                sgc.LineTo(new Point(x2, y2), true, false);
                sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
            }
            else
            {
                sgc.BeginFigure(new Point(200, 100), true, true);
                sgc.ArcTo(new Point(x1, y1), new Size(100, 100), 0, true, SweepDirection.Clockwise, true, false);
                sgc.LineTo(new Point(x2, y2), true, false);
                sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, true, SweepDirection.Counterclockwise, true, false);
            }
            return sg;
        }

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

        private ProgressBarToGeometryConverter() { }
        public static ProgressBarToGeometryConverter Instance { get; } = new();
    }
  • Related