0

I want to make a custom ProgressBar(with CornerRadius) control ,My thought is using 2 borders(1:border,2:indicator),So I can set the same CornerRadius,but something wrong with the clip. I did clipping the indicator,but it will overflow the border,here is my code

The xmal code:

<Border x:Name="PART_Border"
                            CornerRadius="{TemplateBinding CornerRadius}"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <Border.Clip>

                            <MultiBinding Converter="{convertors:BorderClipConverter}">
                                <Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}"/>
                                <Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}"/>
                                <Binding Path="CornerRadius" RelativeSource="{RelativeSource Self}"/>
                            </MultiBinding>
                        </Border.Clip>
                        <Border x:Name="PART_Indicator"
                                HorizontalAlignment="Left"
                                CornerRadius="{TemplateBinding CornerRadius}"
                                Background="{TemplateBinding Foreground}">
                        </Border>
                    </Border>

The cs Code:

using System;
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace AirControl
{
    public class AirProgressBar : ProgressBar
    {
        public static readonly DependencyProperty CornerRadiusProperty =
            DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(AirProgressBar),
                new PropertyMetadata(default(CornerRadius)));

        public new static readonly DependencyProperty IsIndeterminateProperty = DependencyProperty.Register(
            "IsIndeterminate", typeof(bool), typeof(AirProgressBar),
            new PropertyMetadata(default(bool), PropertyChangedCallback));

        public new static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
            "Value", typeof(double), typeof(AirProgressBar),
            new PropertyMetadata(default(double), ProgressValueChanged));

        private static void ProgressValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var airProgressBar = d as AirProgressBar;
        }

        private Border border;
        private Border indicator;

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

        public AirProgressBar()
        {
            SizeChanged += (sender, args) =>
            {
                DoAnimation();
                CalcWidth();
            };
        }

        public double Value
        {
            get => (double)GetValue(ValueProperty);
            set => SetValue(ValueProperty, value);
        }

        public new bool IsIndeterminate
        {
            get => (bool)GetValue(IsIndeterminateProperty);
            set => SetValue(IsIndeterminateProperty, value);
        }

        public CornerRadius CornerRadius
        {
            get => (CornerRadius)GetValue(CornerRadiusProperty);
            set => SetValue(CornerRadiusProperty, value);
        }

        private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var airProgressBar = d as AirProgressBar;
            airProgressBar?.DoAnimation();
            airProgressBar?.CalcWidth();
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            border = (GetTemplateChild("PART_Border") as Border)!;
            indicator = (GetTemplateChild("PART_Indicator") as Border)!;
        }

        private void CalcWidth()
        {
            if (IsIndeterminate)
            {
                return;
            }

            if (border is null)
            {
                return;
            }

            Value = Math.Max(0d, Value);
            var percentage = Value / 100;
            indicator.Width = ActualWidth * percentage;
        }

        private void DoAnimation()
        {
            if (IsIndeterminate is false)
            {
                return;
            }

            if (border is null)
            {
                return;
            }

            indicator.Width = ActualWidth / 4;
            Storyboard sb = new() { RepeatBehavior = RepeatBehavior.Forever };
            var thicknessAnimation = new ThicknessAnimation
            {
                Duration = new Duration(TimeSpan.FromMilliseconds(6000)),
                From = new Thickness(-indicator.Width, indicator.Margin.Top,
                    indicator.Margin.Right, indicator.Margin.Bottom),
                To = new Thickness(Width, indicator.Margin.Top,
                    -indicator.Width, indicator.Margin.Bottom)
            };
            Storyboard.SetTargetProperty(thicknessAnimation, new PropertyPath("Margin"));
            sb.Children.Add(thicknessAnimation);
            sb.Begin(indicator);

        }
    }
}

The converter Code:Click me

The problem:

image

nsj
  • 61
  • 6

2 Answers2

2

I fix it:

I found out that I should put 2 boders in a Grid,then set the Grid's Clip.The out border determine Progressbar's size, the inner border is the indicator.

here is the repo:

CS:

using System;
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace AirControl
{
    public class AirProgressBar : ProgressBar
    {
        public static readonly DependencyProperty CornerRadiusProperty =
            DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(AirProgressBar),
                new PropertyMetadata(default(CornerRadius)));

        public new static readonly DependencyProperty IsIndeterminateProperty = DependencyProperty.Register(
            "IsIndeterminate", typeof(bool), typeof(AirProgressBar),
            new PropertyMetadata(default(bool), PropertyChangedCallback));

        public new static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
            "Value", typeof(double), typeof(AirProgressBar),
            new PropertyMetadata(default(double), ProgressValueChanged));

        private static void ProgressValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var airProgressBar = d as AirProgressBar;
            airProgressBar.CalcWidth();
        }

        private Border border;
        private Border indicator;

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

        public AirProgressBar()
        {
            SizeChanged += (sender, args) =>
            {
                DoAnimation();
                CalcWidth();
            };
        }

        public double Value
        {
            get => (double)GetValue(ValueProperty);
            set => SetValue(ValueProperty, value);
        }

        public new bool IsIndeterminate
        {
            get => (bool)GetValue(IsIndeterminateProperty);
            set => SetValue(IsIndeterminateProperty, value);
        }

        public CornerRadius CornerRadius
        {
            get => (CornerRadius)GetValue(CornerRadiusProperty);
            set => SetValue(CornerRadiusProperty, value);
        }

        private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var airProgressBar = d as AirProgressBar;
            airProgressBar?.DoAnimation();
            airProgressBar?.CalcWidth();
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            border = (GetTemplateChild("PART_Border") as Border)!;
            indicator = (GetTemplateChild("PART_Indicator") as Border)!;
            if (border.CornerRadius is { } radius && radius.TopLeft >= 1)
            {
                indicator.CornerRadius = new CornerRadius(CornerRadius.TopLeft - BorderThickness.Left, CornerRadius.TopRight - BorderThickness.Top,
                    CornerRadius.BottomRight - BorderThickness.Right, CornerRadius.BottomLeft - BorderThickness.Bottom);
            }
        }

        private void CalcWidth()
        {
            if (IsIndeterminate)
            {
                return;
            }

            if (border is null)
            {
                return;
            }

            Value = Math.Max(0d, Value);
            var percentage = Value / 100;
            indicator.Width = border.ActualWidth * percentage;
        }

        private void DoAnimation()
        {
            if (IsIndeterminate is false)
            {
                return;
            }

            if (border is null)
            {
                return;
            }

            indicator.Width = ActualWidth / 4;
            Storyboard sb = new() { RepeatBehavior = RepeatBehavior.Forever };
            var thicknessAnimation = new ThicknessAnimation
            {
                Duration = new Duration(TimeSpan.FromMilliseconds(2000)),
                From = new Thickness(-indicator.Width, indicator.Margin.Top,
                    indicator.Margin.Right, indicator.Margin.Bottom),
                To = new Thickness(Width, indicator.Margin.Top,
                    -indicator.Width, indicator.Margin.Bottom)
            };
            Storyboard.SetTargetProperty(thicknessAnimation, new PropertyPath("Margin"));
            sb.Children.Add(thicknessAnimation);
            sb.Begin(indicator);
        }
    }
}

XAML:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:AirControl"
                    xmlns:convertors="clr-namespace:AirControl.Convertors">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Colors.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <Style TargetType="{x:Type local:AirProgressBar}">
        <Setter Property="Foreground" Value="{StaticResource MainBackground}" />
        <Setter Property="Background" Value="{StaticResource MainForeground}" />
        <Setter Property="BorderBrush" Value="{StaticResource DefaultBorderBrush}" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="CornerRadius" Value="4" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:AirProgressBar}">
                    <Grid x:Name="PART_Grid">
                        <Grid.Clip>
                            <MultiBinding Converter="{convertors:ClipConverter}">
                                <Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}" />
                                <Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}" />
                                <Binding Path="CornerRadius" ElementName="PART_Border" />
                                <Binding Path="BorderThickness" ElementName="PART_Border"/>
                            </MultiBinding>
                        </Grid.Clip>

                        <Border x:Name="PART_Border"
                            CornerRadius="{TemplateBinding CornerRadius}"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">

                        </Border>
                        <Border x:Name="PART_Indicator"
                                VerticalAlignment="Center"
                                HorizontalAlignment="Left"
                                Height="{TemplateBinding Height}"
                                Background="{TemplateBinding Foreground}" />
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="Orientation" Value="Vertical">
                            <Setter Property="LayoutTransform" TargetName="PART_Border">
                                <Setter.Value>
                                    <RotateTransform Angle="-90" />
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>  
nsj
  • 61
  • 6
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 04 '22 at 07:14
1

There seems to be mistakes caused by not taking into account the BorderThickness of outer Border properly.

  1. Regarding the left side of bar, the corner radius of inner Border should be smaller than that of outer Border. That said, the corner radius of inner Border seems unnecessary because inner Border is supposed to be clipped anyways.

  2. Regarding the right side of bar, you miscalculated the width of inner Border. You need to decrease it by the value of BorderThickness of outer Border.

Although I am not sure what is your intension behind, I would suggest the following modifications.

The converter originally created by Marat Khasanov can be modified to utilize BorderThickness as well so as to be used for inner Border.

public class BorderInnerClipConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values is { Length: 4 } &&
            values[0] is (double width and >= double.Epsilon) &&
            values[1] is (double height and >= double.Epsilon) &&
            values[2] is CornerRadius radius &&
            values[3] is Thickness thickness)
        {
            var rect = new Rect(0, 0, (width - thickness.Left - thickness.Right), (height - thickness.Top - thickness.Bottom));
            var radiusX = radius.TopLeft - thickness.Left;
            var radiusY = radius.TopLeft - thickness.Top;

            var clip = new RectangleGeometry(rect, radiusX, radiusY);
            clip.Freeze();

            return clip;
        }

        return DependencyProperty.UnsetValue;
    }

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

Then the ControlTemplate for AirProgressBar would be as follows.

<local:BorderInnerClipConverter x:Key="BorderInnerClipConverterKey"/>

<ControlTemplate x:Key="AirProgressBarTemplate" TargetType="{x:Type local:AirProgressBar}">
    <Border x:Name="PART_Border"
            CornerRadius="{TemplateBinding CornerRadius}"
            BorderThickness="{TemplateBinding BorderThickness}"
            BorderBrush="{TemplateBinding BorderBrush}"
            Background="{TemplateBinding Background}">
        <Border x:Name="PART_Indicator"
                HorizontalAlignment="Left"
                Background="{TemplateBinding Foreground}">
            <Border.Clip>
                <MultiBinding Converter="{StaticResource BorderInnerClipConverterKey}">
                    <Binding Path="ActualWidth" ElementName="PART_Border"/>
                    <Binding Path="ActualHeight" ElementName="PART_Border"/>
                    <Binding Path="CornerRadius" ElementName="PART_Border"/>
                    <Binding Path="BorderThickness" ElementName="PART_Border"/>
                </MultiBinding>
            </Border.Clip>
        </Border>
    </Border>
</ControlTemplate>

Finally, while there are several unclear parts in your csharp code, CalcWidth method would be as follows.

private void CalcWidth()
{
    if (IsIndeterminate)
    {
        return;
    }

    if (border is null)
    {
        return;
    }

    Value = Math.Max(0d, Value);
    var percentage = Value / 100;
    indicator.Width = (border.ActualWidth - border.BorderThickness.Left - border.BorderThickness.Right) * percentage;
}
emoacht
  • 2,764
  • 1
  • 13
  • 24
  • 1
    I tried this, but not suit me, I found a better solution: [ClippingBorder](https://github.com/SonyWWS/ATF/blob/8044bc01dbe0522cab65762960b6b58681c8dce0/Framework/Atf.Gui.Wpf/Controls/ClippingBorder.cs) it's not perfect but enough for me, if you have wondering about my code, here is my repo: [Air](https://github.com/nieshaoju/Air) – nsj Dec 25 '21 at 15:57