WPF: Progress dot bar with panel below for each progress point

503 Views Asked by At

I have to make a control which shows progress. Each progress point has some UI controls to display and interact with user . Like for example at 3rd stage we have some data which user will interact with.

What is my plan is to make (or customize) an Existing UIControl.

Can somebody help me how to achieve the target, In explaining in detail with to implement this idea ? (Any other idea is also appreciated.)

1

There are 1 best solutions below

2
Léo SALVADOR On

I have already used this example for one of my projects, with a very similar need: https://stackoverflow.com/a/7784397/17534581

I would advise you to start from this, it is very complete.

UPDATE

Here are the main steps:

Create a custom class that overrides ItemsControl:

public class WizardProgressBar : ItemsControl
{
    #region Dependency Properties

    public static DependencyProperty ProgressProperty =
        DependencyProperty.Register("Progress",
                                    typeof(int),
                                    typeof(WizardProgressBar),
                                    new FrameworkPropertyMetadata(0, null, CoerceProgress));

    private static object CoerceProgress(DependencyObject target, object value)
    {
        WizardProgressBar wizardProgressBar = (WizardProgressBar)target;
        int progress = (int)value;
        if (progress < 0)
        {
            progress = 0;
        }
        else if (progress > 100)
        {
            progress = 100;
        }
        return progress;
    }

    #endregion // Dependency Properties

    static WizardProgressBar()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(WizardProgressBar), new FrameworkPropertyMetadata(typeof(WizardProgressBar)));
    }

    public WizardProgressBar()
    {
    }

    #region Properties

    public int Progress
    {
        get { return (int)base.GetValue(ProgressProperty); }
        set { base.SetValue(ProgressProperty, value); }
    }

    #endregion // Properties
}

Add the following ResourceDictionnary:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WizardProgressBarLibrary"
    xmlns:converters="clr-namespace:WizardProgressBarLibrary.Converters">

    <converters:IsLastItemConverter x:Key="IsLastItemConverter"/>
    <converters:IsProgressedConverter x:Key="IsProgressedConverter"/>

    <LinearGradientBrush x:Key="wizardBarBrush" StartPoint="0.5,0.0" EndPoint="0.5,1.0">
        <GradientStop Color="#FFE4E4E4" Offset="0.25"/>
        <GradientStop Color="#FFededed" Offset="0.50"/>
        <GradientStop Color="#FFFCFCFC" Offset="0.75"/>
    </LinearGradientBrush>
    
    <Style TargetType="{x:Type local:WizardProgressBar}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:WizardProgressBar}">
                    <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="Auto"/>
                            </Grid.RowDefinitions>
                            <ItemsControl ItemsSource="{TemplateBinding ItemsSource}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <UniformGrid Rows="1">
                                            <UniformGrid.Effect>
                                                <DropShadowEffect Color="Black"
                                                                  BlurRadius="3"
                                                                  Opacity="0.6"
                                                                  ShadowDepth="0"/>
                                            </UniformGrid.Effect>
                                        </UniformGrid>
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate>
                                        <DataTemplate.Resources>
                                            <Style TargetType="Path" x:Key="outerPath">
                                                <Setter Property="Data" Value="M0.0,0.0 L0.0,0.33 L1.0,0.33 L1.0,0.66 L0.0,0.66 L0.0,1.0"/>
                                                <Setter Property="StrokeThickness" Value="0"/>
                                                <Setter Property="Height" Value="21"/>
                                                <Setter Property="Stretch" Value="Fill"/>
                                                <Setter Property="Fill" Value="{StaticResource wizardBarBrush}"/>
                                                <Setter Property="StrokeEndLineCap" Value="Square"/>
                                                <Setter Property="StrokeStartLineCap" Value="Square"/>
                                                <Setter Property="Stroke" Value="Transparent"/>
                                            </Style>
                                            <Style TargetType="Path" x:Key="innerPath" BasedOn="{StaticResource outerPath}">
                                                <Setter Property="Data" Value="M0.0,0.0 L0.0,0.45 L1.0,0.45 L1.0,0.55 L0.0,0.55 L0.0,1.0"/>
                                                <Setter Property="Fill" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:WizardProgressBar}},
                                                                                        Path=Foreground}"/>
                                            </Style>
                                        </DataTemplate.Resources>
                                        <Grid SnapsToDevicePixels="True">
                                            <Grid.RowDefinitions>
                                                <RowDefinition Height="Auto"/>
                                                <RowDefinition Height="Auto"/>
                                            </Grid.RowDefinitions>
                                            <Grid >
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition/>
                                                    <ColumnDefinition/>
                                                </Grid.ColumnDefinitions>
                                                <Path Name="leftPath"
                                                      Style="{StaticResource outerPath}"/>
                                                <Path Grid.Column="1"
                                                      Name="rightPath"
                                                      Style="{StaticResource outerPath}"/>
                                                <Ellipse Grid.ColumnSpan="2"
                                                         HorizontalAlignment="Center"
                                                         Stroke="Transparent"
                                                         Height="20"
                                                         Width="20"
                                                         Fill="{StaticResource wizardBarBrush}"/>
                                                <Ellipse Grid.ColumnSpan="2"
                                                         HorizontalAlignment="Center"
                                                         Stroke="Transparent"
                                                         Height="14"
                                                         Width="14"
                                                         Fill="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:WizardProgressBar}},
                                                    Path=Foreground}">
                                                    <Ellipse.Visibility>
                                                        <MultiBinding Converter="{StaticResource IsProgressedConverter}"
                                                                      ConverterParameter="False">
                                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"/>
                                                            <Binding RelativeSource="{RelativeSource AncestorType={x:Type local:WizardProgressBar}}"
                                                                     Path="Progress"/>
                                                        </MultiBinding>
                                                    </Ellipse.Visibility>
                                                </Ellipse>
                                                <Path Name="leftFillPath"
                                                      Grid.Column="0"
                                                      Style="{StaticResource innerPath}">
                                                    <Path.Visibility>
                                                        <MultiBinding Converter="{StaticResource IsProgressedConverter}"
                                                                      ConverterParameter="False">
                                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"/>
                                                            <Binding RelativeSource="{RelativeSource AncestorType={x:Type local:WizardProgressBar}}"
                                                                     Path="Progress"/>
                                                        </MultiBinding>
                                                    </Path.Visibility>
                                                </Path>
                                                <Path Name="rightFillPath" Grid.Column="1"
                                                      Style="{StaticResource innerPath}">
                                                    <Path.Visibility>
                                                        <MultiBinding Converter="{StaticResource IsProgressedConverter}"
                                                                      ConverterParameter="True">
                                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"/>
                                                            <Binding RelativeSource="{RelativeSource AncestorType={x:Type local:WizardProgressBar}}"
                                                                     Path="Progress"/>
                                                        </MultiBinding>
                                                    </Path.Visibility>
                                                </Path>
                                            </Grid>
                                        </Grid>
                                        <DataTemplate.Triggers>
                                            <DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}"
                                                         Value="{x:Null}">
                                                <Setter TargetName="leftPath" Property="Visibility" Value="Collapsed"/>
                                                <Setter TargetName="leftFillPath" Property="Visibility" Value="Collapsed"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource IsLastItemConverter}}"
                                                         Value="True">
                                                <Setter TargetName="rightPath" Property="Visibility" Value="Collapsed"/>
                                                <Setter TargetName="rightFillPath" Property="Visibility" Value="Collapsed"/>
                                            </DataTrigger>
                                        </DataTemplate.Triggers>
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>
                            <ItemsControl Grid.Row="1" ItemsSource="{TemplateBinding ItemsSource}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <UniformGrid Rows="1"/>
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate>
                                        <TextBlock Grid.Row="1" Grid.ColumnSpan="2" Text="{Binding}" HorizontalAlignment="Center" Margin="0,5,0,0"/>
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

And then, the two following Converters:

public class IsLastItemConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        ContentPresenter contentPresenter = value as ContentPresenter;
        ItemsControl itemsControl = ItemsControl.ItemsControlFromItemContainer(contentPresenter);
        int index = itemsControl.ItemContainerGenerator.IndexFromContainer(contentPresenter);
        return (index == (itemsControl.Items.Count - 1));
    }
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

public class IsProgressedConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if ((values[0] is ContentPresenter &&
            values[1] is int) == false)
        {
            return Visibility.Collapsed;
        }
        bool checkNextItem = System.Convert.ToBoolean(parameter.ToString());
        ContentPresenter contentPresenter = values[0] as ContentPresenter;
        int progress = (int)values[1];
        ItemsControl itemsControl = ItemsControl.ItemsControlFromItemContainer(contentPresenter);
        int index = itemsControl.ItemContainerGenerator.IndexFromContainer(contentPresenter);
        if (checkNextItem == true)
        {
            index++;
        }
        WizardProgressBar wizardProgressBar = itemsControl.TemplatedParent as WizardProgressBar;
        int percent = (int)(((double)index / wizardProgressBar.Items.Count) * 100);
        if (percent < progress)
        {
            return Visibility.Visible;
        }
        return Visibility.Collapsed;
    }
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

You would be able to use the ProgressBar as follow:

        <controls:WizardProgressBar ItemsSource="{Binding Steps}"
                                Foreground="#FF086398"
                                Progress="{Binding Progress}"
                                SnapsToDevicePixels="True"
                                Margin="40"/>

Where Steps is an ObservableCollection and Progress an integer.

You can also add a void to set Progress from the name of the Step as string:

    public void SetProgressFromValue(string stepValue)
    {
        int foundStepIndice = Steps.ToList().FindIndex(x => x.Equals(stepValue));
        Progress = Convert.ToInt32(((double)(foundStepIndice + 1) / (double)Steps.Count()) * 100);
    }

Result:

enter image description here

UPDATE 2: Makes the ProgressBar interactive

If you want to make the ProgressBar interactive, you just have to add a Command triggered when a step is clicked. For example by grouping the two Ellipses in a grid and adding a MouseBinding on this Grid:

<Grid Grid.ColumnSpan="2" Cursor="Hand">
    <Ellipse
        HorizontalAlignment="Center"
        Stroke="Transparent"
        Height="20"
        Width="20"
        Fill="{StaticResource wizardBarBrush}">
    </Ellipse>
    <Ellipse 
        HorizontalAlignment="Center"
        Stroke="Transparent"
        Height="14"
        Width="14"
        Fill="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:WizardProgressBar}},
        Path=Foreground}">
        <Ellipse.Visibility>
            <MultiBinding Converter="{StaticResource IsProgressedConverter}"
                        ConverterParameter="False">
                <Binding RelativeSource="{RelativeSource TemplatedParent}"/>
                <Binding RelativeSource="{RelativeSource AncestorType={x:Type local:WizardProgressBar}}"
                        Path="Progress"/>
            </MultiBinding>
        </Ellipse.Visibility>
    </Ellipse>
    <Grid.InputBindings>
        <MouseBinding 
                Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:WizardProgressBar}}, Path=DataContext.StepClickedCommand}"
                CommandParameter="{Binding .}"
                Gesture="LeftClick"/>
    </Grid.InputBindings>
</Grid>

And add the Command in the ViewModel:

public ICommand StepClickedCommand
{
    get
    {
        return new RelayCommand<string>(SetProgressFromValue);
    }
}

Where SetProgressFromValue is the void I have quoted above.