Search code examples
c#wpfxamlprogress-barc#-6.0

Rectangular button with a progressbar along the contour in C# WPF


I need to create a rectangular button with rounded corners in C# 6.0 WPF. This button should have a progress bar instead of a frame that fills clockwise (starting from the middle of the top border).

I tried many ways to do this and even made a slightly workable version using Path, but without the rounded corners, which I need.

Please tell me how this can be done using xaml markup and how to manage progress.

Here is a regular button with rounded corners:

<UserControl x:Class="Example.ProgressButton"
             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:Example"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="button" Width="150" Height="60" Content="Click me" Background="LightGray" BorderBrush="Transparent">
            <Button.Style>
                <Style TargetType="{x:Type Button}">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type Button}">
                                <Border CornerRadius="10" Background="{TemplateBinding Background}" BorderBrush="Green" BorderThickness="2">
                                    <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </Button.Style>
        </Button>
    </Grid>
</UserControl>

Initially, the frame should not be visible (the size of the button should not change with or without the frame); when clicked, the progress bar begins to fill starting from the middle of the upper border clockwise. After filling 100%, the button should look like in the picture: button

In addition, the filling of the progress bar should be smooth even on rounded corners.

I have no idea how to do this at all. Help me please.

Update: I probably didn't explain my problem well, I'll try to clarify. I need to make the border itself as a progress bar (as an element or animation, it doesn’t matter), it should fill for the specified time approximately as in the picture: load_button


Solution

  • You may create a derived Border control that draws a stroked Geometry on top of its border. The length of the stroke can be controlled by the dash array of a Pen that is used to draw the border Geometry.

    The control declares two additional properties, ProgressBrush and ProgressValue, a double value in the range 0 to 1.

    The example below uses only the Left component of the BorderThickness and the TopLeft component of the CornerRadius, so it won't support irregular border thicknesses or corner radii.

    public class ProgressBorder : Border
    {
        public static readonly DependencyProperty ProgressBrushProperty = DependencyProperty.Register(
            nameof(ProgressBrush), typeof(Brush), typeof(ProgressBorder),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
    
        public static readonly DependencyProperty ProgressValueProperty = DependencyProperty.Register(
            nameof(ProgressValue), typeof(double), typeof(ProgressBorder),
            new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender));
    
        public Brush ProgressBrush
        {
            get => (Brush)GetValue(ProgressBrushProperty);
            set => SetValue(ProgressBrushProperty, value);
        }
    
        public double ProgressValue
        {
            get => (double)GetValue(ProgressValueProperty);
            set => SetValue(ProgressValueProperty, value);
        }
    
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);
    
            var w = RenderSize.Width;
            var h = RenderSize.Height;
            var t = BorderThickness.Left;
            var d = t / 2;
            var r = Math.Max(0, Math.Min(CornerRadius.TopLeft, Math.Min(w / 2 - t, h / 2 - t)));
            var geometry = new StreamGeometry();
    
            using (var dc = geometry.Open())
            {
                dc.BeginFigure(new Point(w / 2, d), true, true);
    
                dc.LineTo(new Point(w - d - r, d), true, true);
                dc.ArcTo(new Point(w - d, d + r), new Size(r, r), 0, false, SweepDirection.Clockwise, true, true);
    
                dc.LineTo(new Point(w - d, h - d - r), true, true);
                dc.ArcTo(new Point(w - d - r, h - d), new Size(r, r), 0, false, SweepDirection.Clockwise, true, true);
    
                dc.LineTo(new Point(d + r, h - d), true, true);
                dc.ArcTo(new Point(d, h - d - r), new Size(r, r), 0, false, SweepDirection.Clockwise, true, true);
    
                dc.LineTo(new Point(d, d + r), true, true);
                dc.ArcTo(new Point(d + r, d), new Size(r, r), 0, false, SweepDirection.Clockwise, true, true);
    
                dc.LineTo(new Point(w / 2, d), true, true);
            }
    
            var length = (2 * w + 2 * h + 4 * ((0.5 * Math.PI - 2) * r - t)) / t;
            var dashes = new double[] { ProgressValue * length, (1 - ProgressValue) * length };
            var pen = new Pen
            {
                Brush = ProgressBrush,
                Thickness = t,
                StartLineCap = PenLineCap.Flat,
                EndLineCap = PenLineCap.Flat,
                DashCap = PenLineCap.Flat,
                DashStyle = new DashStyle(dashes, 0),
                LineJoin = PenLineJoin.Round
            };
    
            drawingContext.DrawGeometry(null, pen, geometry);
        }
    }
    

    An example usage in XAML:

    <local:ProgressBorder
        Background="AliceBlue"
        BorderBrush="LightGray"
        ProgressBrush="Red"
        BorderThickness="10"
        CornerRadius="10,10,10,10">
        <TextBlock Text="Hello" Margin="10"/>
        <local:ProgressBorder.Triggers>
            <EventTrigger RoutedEvent="Loaded">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation
                           Storyboard.TargetProperty="ProgressValue"
                           To="1.0" Duration="0:0:3"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </local:ProgressBorder.Triggers>
    </local:ProgressBorder>