[WPF] 使用三种方式实现弧形/圆形进度条

1. 需求#

前天看到有人问弧形进度条怎么做,我模仿了一下,成果如下图所示:

当时我第一反应是可以用 Microsoft.Toolkit.Uwp.UI.Controls 里的 RadialGauge 实现,虽然这是个 UWP 的控件,不过代码没有很复杂,应该很轻松就能移植到 WPF:

但仔细想想,我实现过很多次圆形的进度条,这种弧形的进度条则没碰过。原型进度条基本只需要用 Ellipse 就能实现,而且只需要 Progress 一个参数,而弧形进度条则还需要 StartAngle 和 EndAngle 两个属性,而且计算复杂许多。于是兴致来了试试用不同的方式实现弧形进度条。

这篇文章只介绍了怎么显示弧形及怎么显示进度,只有原理,没有具体实现一个弧形进度条控件。

2. 使用 Path 及 ArcSegment#

Path 用于绘制曲线和复杂形状,而且 ArcSegment 用于描述 Path 中两点之间的一条椭圆弧。通常使用以下几个属性控制 ArcSegment:

属性描述
Point 终点(起始点在 Path 或前一个 Segment 中描述)。
Size X 轴和 Y 轴的半径。
IsLargeArc 圆弧是整个圆形中大的那部分,还是小的那部分。
SweepDirection 弧线绘制的方向。

具体说明可以看 这个文档

用 Path 和 ArcSegment 可以很好地实现弧形的进度条,它的 XAML 如下:

Copy
<Path Stroke="SlateBlue"
      StrokeThickness="4">
    <Path.Data>
        <PathGeometry>
            <PathFigure IsClosed="False" StartPoint="30,170">
                <ArcSegment IsLargeArc="True"
                            Point="170,170"
                            Size="96,96"
                            SweepDirection="Clockwise" />
            </PathFigure>
        </PathGeometry>
    </Path.Data>
</Path>

叠加两个不同颜色的 Path,就可以实现这种效果:

Path 和 ArcSegment 是一个很正统的方案,前面提到的 RadialGauge 就用了这个方案。不过它的计算很麻烦,三角函数我已经忘光了。

另外,请注意弧线两端都是平平的直角,这和需求不符,所以需要设置 StrokeStartLineCapStrokeEndLineCap 这两个属性的值为 Round

Copy
StrokeStartLineCap="Round" StrokeEndLineCap="Round"

它们控制线条两端边缘的轮廓,Round 表示一个直径等于线条粗细的半圆形。这样才能实现需求中的圆角:

顺便一提,这两个属性的类型是 PenLineCap 枚举,这个枚举的四个值分别代表以下几种形状:

3. 使用 Arc#

第二个方案是使用 Microsoft.Expression.Drawing 中的 Arc 形状直接画出一个弧形。如果安装了旧版的 Blend(好像 2017 或以前的都可以),可以在 资产->形状 里找到这个形状(我装的是英文版所以没有中文截图):

或者在 Nuget 上搜索 Microsoft.Expression.Drawing 找到一个符合自己项目的版本。

Arc 的用法很简单,只需要执行 StartAngleEndAngle 即可输出一个弧形:

Copy
<ed:Arc ArcThickness="12"
        ArcThicknessUnit="Pixel"
        EndAngle="150"
        Fill="#101a26"
        StartAngle="-150"
        Stretch="None"
        StrokeEndLineCap="Round"
        StrokeStartLineCap="Round" />

叠加两个不同颜色的 Arc,可以实现这种效果:

可是仔细看,就算用了 StrokeStartLineCapStrokeEndLineCap 两个属性,Arc 的两端任然是直角,这不符合需求,所以这个方案简单但不完美,我还要尝试下一个方案。

4. 使用 Ellipse#

这个方案还算有趣,Ellipse 明明是圆形,却能用来画弧形。为了用 Ellipse 显示进度,我们会用 StrokeDashArray 控制它的边框长度。StrokeDashArray 用于将边框变成虚线,它的值是一个 double 类型的有序集合,集合中的值指虚线中每一段的长度,长度单位是边框值的宽度。例如以下圆形:

Copy
<Ellipse StrokeDashArray="1,2,3"
         Stroke="#FFFF0EC4"
         StrokeThickness="10"
         Height="200"
         Width="200" />

边框宽度为 10,虚线的第一段是长度为 10 的实线,第二段为长度为 20 的空白,第三段为长度为 30 的实线,然后如此循环直到结束。

用 StrokeDashArray 做进度提示的基本做法就是将进度(Progress)通过 Converter 转换为分成两段的 StrokeDashArray,第一段为实线,表示当前进度,第二段为空白。假设一个 Shape 的边长是 100,当前进度为 50,则将 StrokeDashArray 设置成 {50,double.MaxValue} 两段。

为了实现弧形进度条,我们还需要控制 Ellipse 旋转的角度。具体来说我实现了一个 EllipseProgressBehavior,里面有 Progress、StartAngle 和 EndAngle 三个属性,具体代码在 这里。用这个 Behavior 控制 Ellipse 的边框长度和旋转角度,使用方式如下:

Copy
<Ellipse Margin="4"
         Stroke="#7bcdd9"
         StrokeThickness="4">
    <interactivity:Interaction.Behaviors>
        <local1:EllipseProgressBehavior EndAngle="150"
                                        Progress="50"
                                        StartAngle="-150" />
    </interactivity:Interaction.Behaviors>
</Ellipse>

叠加两个 Ellipse,即可实现需求中的弧形进度条。可是这时候弧形的两端都是直角,即使设置了 StrokeStartLineCapStrokeEndLineCap 两个属性都不起作用。对于用 StrokeDashArray 显示的边框,不能使用 StrokeStartLineCapStrokeEndLineCap 去控制它的两端的轮廓,而应该使用 StrokeDashCap:

Copy
StrokeDashCap="Round"

最终通过叠加两个 Ellipse 实现了户型进度条的需求:

5. 最后#

童话和寓言都喜欢把相似的内容说上三次,例如三只小猪,三顾茅庐,弗利萨的三段变身。所以不是我在研究回字有多少种写法,我只是遵循古法想把一种技术讲透而已。

6. 参考#

ArcSegment 类

几何图形

WPF 中的形状和基本图形概述

实用的Shape指南

7. 源码#

 

2025-05-30 11:26:31【出处】:https://www.cnblogs.com/dino623/p/arc_progress_bar.html

=======================================================================================

wpf 如何写一个圆形的进度条

先看一下效果吧

调用代码如下

<local:CycleProgressBar Width="100" Height="100" Background="#FFF68986" Foreground="#FFFA1F09"
                        Maximum="100" Minimum="0" Value="20" IsIndeterminate="False"/>

然后下面就来实现一下这个效果

第一步:先创建一个空的wpf项目

第二步:添加一个自定义控件,取名为CycleProgressBar

添加完以后,vs会自动生成一个类和一个Themes文件夹,下面有一个名为Generic的资源文件

Generic里面就是这个自定义控件的默认样式,里面只有一个border,我们就是通过改造这个默认的样式来实现圆形的进度条

到目前位置,都是vs自动生成的代码,不需要我们做任何操作

 

第三步:将父类设置成RangeBase,因为原生的progressbar就是继承的这个类,所以我们也继承这个类

 

第四步:添加依赖属性IsIndeterminate,这个属性用来控制进度条是不是一直转圈圈

复制代码
        public bool IsIndeterminate
        {
            get { return (bool)GetValue(IsIndeterminateProperty); }
            set { SetValue(IsIndeterminateProperty, value); }
        }

        // Using a DependencyProperty as the backing store for IsIndeterminate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsIndeterminateProperty =
            DependencyProperty.Register("IsIndeterminate", typeof(bool), typeof(CycleProgressBar), new PropertyMetadata(false));
复制代码

 

第五步:绘制控件的模板样式

在绘制之前,先添加一个nuget上面的引用,搜索expression.drawing,然后添加下面的引用

再在generic文件里面引入命名空间

xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing"

再写样式之前的准备工作就准备完了,后面就是开始写模板样式了,下面是样式的代码和注释

复制代码
 <local:ProgressBarValueToPercentage x:Key="ProgressBarValueToPercentage"/>

 <Style TargetType="{x:Type local:CycleProgressBar}">
     <Setter Property="Template">
         <Setter.Value>
             <ControlTemplate TargetType="{x:Type local:CycleProgressBar}">
                 <Grid>
                     <!--这个椭圆就是进度条的背景部分,把高度和宽度都绑定到宽度上,当然绑定到高度上也一样,主要是为了保证高度和宽度一直,这样子才会显示成圆形
                     StrokeThickness根据自己的实际情况设置,这个表示背景部分的宽度-->
                     <Ellipse Stroke="{TemplateBinding Background}" StrokeThickness="10"
                          Height="{TemplateBinding Width}" Width="{TemplateBinding Width}"/>
                     
                     <!--这个圆弧的作用就是显示进度条的进度-->
                     <ed:Arc x:Name="PART_Track" Width="{TemplateBinding Width}" Height="{TemplateBinding Width}"
                                     StartAngle="0" EndAngle="0" Fill="{TemplateBinding Foreground}" Panel.ZIndex="1"
                                     ArcThickness="10"
                                     Stretch="None" StrokeEndLineCap="Round" StrokeStartLineCap="Round" ArcThicknessUnit="Pixel" RenderTransformOrigin="0.5,0.5">
                         <ed:Arc.RenderTransform>
                             <TransformGroup>
                                 <RotateTransform/>
                             </TransformGroup>
                         </ed:Arc.RenderTransform>
                     </ed:Arc>

                     <!--这个进度条的作用就是当我们设置IsIndeterminate为true的时候,让这个进度条一直在那里转圈圈-->
                     <ed:Arc x:Name="PART_Track_Repeat" Width="{TemplateBinding Width}" Height="{TemplateBinding Width}"
                                     StartAngle="0" EndAngle="90" Fill="{TemplateBinding Foreground}" Panel.ZIndex="1"
                                     ArcThickness="10" Visibility="Collapsed"
                                     Stretch="None" StrokeEndLineCap="Round" StrokeStartLineCap="Round" 
                                     ArcThicknessUnit="Pixel" RenderTransformOrigin="0.5,0.5">
                         <ed:Arc.RenderTransform>
                             <TransformGroup>
                                 <RotateTransform/>
                             </TransformGroup>
                         </ed:Arc.RenderTransform>
                     </ed:Arc>
                     
                     <!--这个textblock的作用就是显示进度条的百分比-->
                     <TextBlock x:Name="tbPercentage" VerticalAlignment="Center" HorizontalAlignment="Center">
                         <TextBlock.Text>
                             <MultiBinding Converter="{StaticResource ResourceKey=ProgressBarValueToPercentage}">
                                 <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:CycleProgressBar}" Path="Maximum"/>
                                 <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:CycleProgressBar}" Path="Value"/>
                                 <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:CycleProgressBar}" Path="Minimum"/>
                             </MultiBinding>
                         </TextBlock.Text>
                     </TextBlock>
                 </Grid>
                 <ControlTemplate.Triggers>
                     <!--设置IsIndeterminate为true的时候,就把百分比和进度条的进度的圆弧隐藏,只保留一直转圈圈的那个圆弧-->
                     <!--然后就是一个动画,让圆弧一直转圈圈-->
                     <Trigger Property="IsIndeterminate" Value="True">
                         <Setter Property="Visibility" TargetName="PART_Track" Value="Hidden"/>
                         <Setter Property="Visibility" TargetName="PART_Track_Repeat" Value="Visible"/>
                         <Setter Property="Visibility" TargetName="tbPercentage" Value="Hidden"/>
                         <Trigger.EnterActions>
                             <BeginStoryboard>
                                 <Storyboard RepeatBehavior="Forever">
                                     <DoubleAnimationUsingKeyFrames Storyboard.TargetName="PART_Track_Repeat" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(RotateTransform.Angle)">
                                         <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
                                         <EasingDoubleKeyFrame KeyTime="00:00:01" Value="360"/>
                                     </DoubleAnimationUsingKeyFrames>
                                 </Storyboard>
                             </BeginStoryboard>
                         </Trigger.EnterActions>
                     </Trigger>
                     <Trigger Property="IsIndeterminate" Value="False">
                         <Setter TargetName="tbPercentage" Property="Visibility" Value="Visible"/>
                     </Trigger>
                     <MultiTrigger>
                         <MultiTrigger.Conditions>
                             <Condition Property="IsIndeterminate" Value="false"/>
                             <Condition Property="Value" Value="0"/>
                         </MultiTrigger.Conditions>
                         <Setter Property="Visibility" Value="Hidden" TargetName="PART_Track"/>
                     </MultiTrigger>
                 </ControlTemplate.Triggers>
             </ControlTemplate>
         </Setter.Value>
     </Setter>
 </Style>
复制代码

里面有一个名为ProgressBarValueToPercentage的转换,直接添加一个类,然后代码就在下面

复制代码
public class ProgressBarValueToPercentage : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var maximum = System.Convert.ToDouble(values[0]);
        var value = System.Convert.ToDouble(values[1]);
        var minimum = System.Convert.ToDouble(values[2]);

        if (maximum == 0)
        {
            return "0" + "%";
        }

        double progressValue = (value - minimum) / (maximum - minimum) * 100;
        return (Math.Round(progressValue)).ToString() + "%";
    }

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

到现在,样式部分就写完了,然后还要去后台代码里面实现具体的功能

第六步:实现后台代码功能

复制代码
public class CycleProgressBar : RangeBase
{
    public bool IsIndeterminate
    {
        get { return (bool)GetValue(IsIndeterminateProperty); }
        set { SetValue(IsIndeterminateProperty, value); }
    }

    // Using a DependencyProperty as the backing store for IsIndeterminate.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsIndeterminateProperty =
        DependencyProperty.Register("IsIndeterminate", typeof(bool), typeof(CycleProgressBar), new PropertyMetadata(false));

    private FrameworkElement _track;

    static CycleProgressBar()
    {
        //这段代码是创建控件的时候自带的,不用管,代码的意思就是去找generic里面名为CycleProgressBar的样式,
        //如果把这段代码删了,或者generic没有CycleProgressBar的样式,程序就会报错
        DefaultStyleKeyProperty.OverrideMetadata(typeof(CycleProgressBar), new FrameworkPropertyMetadata(typeof(CycleProgressBar)));
    }

    /// <summary>
    /// 计算进度条的值
    /// </summary>
    private void SetPartTrackValue()
    {
        double minimum = this.Minimum;
        double maximum = this.Maximum;
        double value = this.Value;
        double num = (maximum <= minimum) ? 1.0 : ((value - minimum) / (maximum - minimum));

        var EndAngle = num * 360;

        if (_track != null)
        {
            var arc = _track as Arc;
            arc.EndAngle = EndAngle;
        }
    }

    /// <summary>
    /// 应用控件模板的时候调用的方法
    /// </summary>
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        this._track = GetTemplateChild("PART_Track") as FrameworkElement;
        SetPartTrackValue();
    }

    /// <summary>
    /// 进度条的进度变化时触发
    /// </summary>
    /// <param name="oldValue"></param>
    /// <param name="newValue"></param>
    protected override void OnValueChanged(double oldValue, double newValue)
    {
        base.OnValueChanged(oldValue, newValue);
        SetPartTrackValue();
    }

    /// <summary>
    /// 最大值变化时触发
    /// </summary>
    /// <param name="oldMaximum"></param>
    /// <param name="newMaximum"></param>
    protected override void OnMaximumChanged(double oldMaximum, double newMaximum)
    {
        base.OnMaximumChanged(oldMaximum, newMaximum);
        SetPartTrackValue();
    }

    /// <summary>
    /// 最小值变化时触发
    /// </summary>
    /// <param name="oldMinimum"></param>
    /// <param name="newMinimum"></param>
    protected override void OnMinimumChanged(double oldMinimum, double newMinimum)
    {
        base.OnMinimumChanged(oldMinimum, newMinimum);
        SetPartTrackValue();
    }

    /// <summary>
    /// 控件大小变化时触发
    /// </summary>
    /// <param name="sizeInfo"></param>
    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
    {
        base.OnRenderSizeChanged(sizeInfo);
        SetPartTrackValue();
    }
}
复制代码

好了,控件的后台代码也写完了,就可以直接运行了

设置IsIndeterminate="True"以后,黄色部分就会一直转圈圈啦(脑补一下吧,没有gif,囧)

 

项目github地址:bearhanQ/WPFFramework: Share some experience (github.com)

QQ技术交流群:332035933;

 

 

2025-05-30 11:38:14【出处】:https://www.cnblogs.com/lvpp13/p/18356513

=======================================================================================

WPF-自定义进度条(条形、环形)

目录

1、条形进度条制作

1、环形进度条制作 


 

1、条形进度条制作

 GeometryCombineMode="Intersect":取交集

            DoubleAnimation mydbAnimation = new DoubleAnimation();
            mydbAnimation.From = -50;
            mydbAnimation.To = 400;
            mydbAnimation.Duration = new Duration(TimeSpan.FromSeconds(6));
            tranForm.BeginAnimation(TranslateTransform.XProperty, mydbAnimation);
        <Rectangle Width="400"
                   Height="20"
                   Margin="190,349,0,0" HorizontalAlignment="Center" VerticalAlignment="Center"
                   Fill="LightGray" />
        <Path Name="path"
              Width="400"
              Height="20" VerticalAlignment="Center" HorizontalAlignment="Center"
              Margin="190,349,0,0"
              Fill="DeepPink">
            <Path.Data>
                <CombinedGeometry GeometryCombineMode="Intersect">
                    <CombinedGeometry.Geometry1>
                        <RectangleGeometry Rect="0,0,400,20" />
                    </CombinedGeometry.Geometry1>
                    <CombinedGeometry.Geometry2>
                        <RectangleGeometry Rect="0,0,50,20">
                            <RectangleGeometry.Transform>
                                <TranslateTransform x:Name="tranForm" X="-6" />
                            </RectangleGeometry.Transform>
                        </RectangleGeometry>
                    </CombinedGeometry.Geometry2>
                </CombinedGeometry>
            </Path.Data>
        </Path>

 

 


2、环形进度条制作 

重写ProgressBar模板;

MyConvert:继承自IValueConverter接口的转化器;

hc:Arc:使用HandyControl框架的弧形控件;

ArcThicknessUnitPixel空心 、Percent实心;

Stretch="None":关闭伸展

    <Grid>
        <ProgressBar x:Name="probar"
                     Width="120"
                     Height="120"
                     Background="Blue"
                     FontSize="30"
                     FontWeight="Bold"
                     Foreground="Blue"
                     Maximum="1"
                     Minimum="0"
                     SmallChange="0.01"
                     Value="{Binding ElementName=sld, Path=Value}">
            <ProgressBar.Template>
                <ControlTemplate TargetType="ProgressBar">
                    <ControlTemplate.Resources>
                        <local:MyConvert x:Key="tod" />
                    </ControlTemplate.Resources>
                    <Grid>
                        <TextBlock HorizontalAlignment="Center"
                                   VerticalAlignment="Center"
                                   Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Value, StringFormat=p0}" />
                        <hc:Arc ArcThickness="15"
                                ArcThicknessUnit="Pixel"
                                EndAngle="360"
                                Fill="LightGray"
                                StartAngle="0"
                                Stretch="None" />
                        <hc:Arc ArcThickness="15"
                                EndAngle="{TemplateBinding Value,Converter={StaticResource tod}}"
                                Fill="{TemplateBinding Background}"
                                StartAngle="0"
                                Stretch="None" />
                    </Grid>
                </ControlTemplate>
            </ProgressBar.Template>
        </ProgressBar>
        <Slider x:Name="sld"
                Width="400"
                Background="Green"
                Maximum="1"
                Minimum="0" />
    </Grid>

 

 

 

2025-05-30 11:39:01【出处】:https://blog.csdn.net/rwo_bear/article/details/132478297

=======================================================================================

posted on 2025-05-30 11:27  jack_Meng  阅读(785)  评论(0)    收藏  举报

导航