WPF 甜甜圈进度条

dbo*_*eam 5 c# wpf

我正在尝试将《WPF 4 Unleashed》一书中找到的饼图 ProgressBar 调整为看起来像甜甜圈。我觉得我已经成功了一半,但我不知道如何解决最后一个问题。

这是一张图片,说明了我想要的以及我已经实现的目标:

在此输入图像描述

  1. 这就是我想要的样子。
  2. 使用下面的代码就是这样的。
  3. 我在 stackoverflow 的另一个问题中发现了一个建议,即在路径上使用剪切并将笔画厚度加倍。正如您所看到的,路径现在已正确定位,但任何低于 50% 的进度都不会正确绘制,如您所见。

所以我的问题是,如何将其修复为我想要的样子?

以下是我正在使用的相关xaml:

<ControlTemplate x:Key="DonutProgressBar" TargetType="{x:Type ProgressBar}">
    <ControlTemplate.Resources>
        <conv:ValueMinMaxToIsLargeArcConverter x:Key="ValueMinMaxToIsLargeArcConverter" />
        <conv:ValueMinMaxToPointConverter x:Key="ValueMinMaxToPointConverter" />
    </ControlTemplate.Resources>
    <Grid>
        <Viewbox>
            <Grid Width="20" Height="20">
                <Ellipse x:Name="Background"
                         Stroke="{TemplateBinding BorderBrush}"
                         StrokeThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness.Top}"
                         Width="20"
                         Height="20"
                         Fill="{TemplateBinding Background}" />
                <Path x:Name="Donut" 
                      Stroke="{TemplateBinding Foreground}"
                      StrokeThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness.Top}">
                    <Path.Data>
                        <PathGeometry>
                            <PathGeometry.Figures>
                                <PathFigure StartPoint="10,0">
                                    <ArcSegment Size="10,10" SweepDirection="Clockwise">
                                        <ArcSegment.Point>
                                            <MultiBinding Converter="{StaticResource ValueMinMaxToPointConverter}">
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Value" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Minimum" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Maximum" />
                                            </MultiBinding>
                                        </ArcSegment.Point>
                                        <ArcSegment.IsLargeArc>
                                            <MultiBinding Converter="{StaticResource ValueMinMaxToIsLargeArcConverter}">
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Value" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Minimum" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Maximum" />
                                            </MultiBinding>
                                        </ArcSegment.IsLargeArc>
                                    </ArcSegment>
                                </PathFigure>
                            </PathGeometry.Figures>
                        </PathGeometry>
                    </Path.Data>
                </Path>
            </Grid>
        </Viewbox>
    </Grid>
</ControlTemplate>

...
<ProgressBar Width="70" Height="70" Value="40" Template="{StaticResource DonutProgressBar}" Background="{x:Null}" BorderBrush="#1F000000"  BorderThickness="6,6,1,1" />
Run Code Online (Sandbox Code Playgroud)

...以及转换器:

public class ValueMinMaxToPointConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        double value = (double)values[0];
        double minimum = (double)values[1];
        double maximum = (double)values[2];

        double current = (value / (maximum - minimum)) * 360;

        if (current == 360)
            current = 359.999;

        current = current - 90;

        current = current * (Math.PI / 180.0);

        double x = 10 + 10 * Math.Cos(current);
        double y = 10 + 10 * Math.Sin(current);

        return new Point(x, y);
    }

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

public class ValueMinMaxToIsLargeArcConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        double value = (double)values[0];
        double minimum = (double)values[1];
        double maximum = (double)values[2];

        return ((value * 2) >= (maximum - minimum));
    }

    public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}
Run Code Online (Sandbox Code Playgroud)

Pet*_*iho 1

你的代码非常接近。问题不在于剪辑。您只是没有考虑到,当描边路径时,描边是在路径的中心绘制的。这意味着从几何角度来看,笔划本身必须位于您想要绘制的位置的中间。

在您的特定实现中,这意味着您需要在三个不同的位置考虑笔画粗细:

  1. 圆弧的起点。起点需要垂直偏移以考虑笔画厚度。
  2. 圆弧的大小。需要减小圆弧的大小,以便路径保持在较大圆的笔划的中心。
  3. 圆弧的终点。与起点一样,这需要调整,但在这种情况下,需要调整的是计算中圆弧的半径。

例如,您可以添加几个转换器:

class ThicknessToStartPointConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (!(value is double))
        {
            return Binding.DoNothing;
        }

        // Need to start the arc in the middle of the intended stroke
        return new Point(10, ((double)value) / 2);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

class ThicknessToSizeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (!(value is double))
        {
            return Binding.DoNothing;
        }

        double widthHeight = 10 - ((double)value) / 2;

        return new Size(widthHeight, widthHeight);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
Run Code Online (Sandbox Code Playgroud)

然后更新您的 XAML,如下所示:

<PathFigure StartPoint="{Binding StrokeThickness, ElementName=Donut, Converter={StaticResource thicknessToStartPointConverter}}">
  <ArcSegment Size="{Binding StrokeThickness, ElementName=Donut, Converter={StaticResource thicknessToSizeConverter}}" SweepDirection="Clockwise">
    <ArcSegment.Point>
      <MultiBinding Converter="{StaticResource ValueMinMaxToPointConverter}">
        <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Value" />
        <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Minimum" />
        <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Maximum" />
        <Binding Path="StrokeThickness" ElementName="Donut"/>
      </MultiBinding>
    </ArcSegment.Point>
    <ArcSegment.IsLargeArc>
      <MultiBinding Converter="{StaticResource ValueMinMaxToIsLargeArcConverter}">
        <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Value" />
        <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Minimum" />
        <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Maximum" />
      </MultiBinding>
    </ArcSegment.IsLargeArc>
  </ArcSegment>
</PathFigure>
Run Code Online (Sandbox Code Playgroud)

当然,还有转换器所需的资源:

<l:ThicknessToStartPointConverter x:Key="thicknessToStartPointConverter"/>
<l:ThicknessToSizeConverter x:Key="thicknessToSizeConverter"/>
Run Code Online (Sandbox Code Playgroud)

然后你就会得到你想要的。

可能有一种方法可以将背景Ellipse元素和Path元素组合在一起,这样就可以Path在没有上述内容的情况下绘制 ,即使用硬编码大小 10,然后Grid同等地调整两个子元素的大小,使它们正确对齐。但我没有看到任何明显的解决方案,也不想花时间去弄清楚。以上应该可以很好地满足您的目的。:)