WPF Donut ProgressBar

时间:2016-04-20 18:27:38

标签: c# wpf

我正在努力调整WPF 4 Unleashed一书中的馅饼ProgressBar看起来像甜甜圈。我觉得我已经走了一半,但我不知道如何解决最后一个问题。

这是一张图片,展示了我想要的以及我设法实现的目标:

enter image description here

  1. 这就是我想要的样子。
  2. 使用下面的代码就是这样。
  3. 我在stackoverflow的另一个问题中找到了一个建议,就是在路径上使用剪辑并使笔划粗细加倍。正如您所看到的那样,路径现在正确定位,但正如您所见,未正确绘制任何低于50%的进度。
  4. 所以我的问题是,如何修复它看起来像我想要的?

    以下是我正在使用的相关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" />
    

    ......和转换器:

    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();
        }
    }
    

3 个答案:

答案 0 :(得分:2)

这里真正的问题是WPF缺少Arc控件。而不是试图挑战现有的框架以满足您的要求,为什么不自己添加呢?有许多WPF Arc实现在网络上浮动,它们看起来非常相似,只要确保选择一个在角度DP更改时更新视觉效果的实现。这应该符合您的目的:

public class Arc : Shape
{
    public double StartAngle
    {
        get { return (double)GetValue(StartAngleProperty); }
        set { SetValue(StartAngleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for StartAngle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty StartAngleProperty =
        DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc), new PropertyMetadata(0.0, AnglesChanged));

    public double EndAngle
    {
        get { return (double)GetValue(EndAngleProperty); }
        set { SetValue(EndAngleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for EndAngle.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty EndAngleProperty =
        DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc), new PropertyMetadata(0.0, AnglesChanged));


    protected override Geometry DefiningGeometry
    {
        get
        {
            return GetArcGeometry();
        }
    }

    private static void AnglesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var arc = d as Arc;
        if (arc != null)
            arc.InvalidateVisual();
    }

    private Geometry GetArcGeometry()
    {
        Point startPoint = PointAtAngle(Math.Min(StartAngle, EndAngle));
        Point endPoint = PointAtAngle(Math.Max(StartAngle, EndAngle));
        Size arcSize = new Size(Math.Max(0, (RenderSize.Width - StrokeThickness) / 2),
        Math.Max(0, (RenderSize.Height - StrokeThickness) / 2));
        bool isLargeArc = Math.Abs(EndAngle - StartAngle) > 180;
        StreamGeometry geom = new StreamGeometry();
        using (StreamGeometryContext context = geom.Open())
        {
            context.BeginFigure(startPoint, false, false);
            context.ArcTo(endPoint, arcSize, 0, isLargeArc,
            SweepDirection.Counterclockwise, true, false);
        }
        geom.Transform = new TranslateTransform(StrokeThickness / 2, StrokeThickness / 2);
        return geom;
    }

    private Point PointAtAngle(double angle)
    {
        double radAngle = angle * (Math.PI / 180);
        double xRadius = (RenderSize.Width - StrokeThickness) / 2;
        double yRadius = (RenderSize.Height - StrokeThickness) / 2;
        double x = xRadius + xRadius * Math.Cos(radAngle);
        double y = yRadius - yRadius * Math.Sin(radAngle);
        return new Point(x, y);
    }
}

为了维护一个干净的架构,我更喜欢将自定义形状放在一个单独的类库中,并引用PresentationFramework,这样做也允许你通过在{{{{{{{ 3}}:

[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "YourNamespace")]

你现在有了一个可重复使用的弧形,你可以像椭圆一样使用它,所以用这样的东西替换你的整个路径XAML:

<Arc
    Stroke="{TemplateBinding Foreground}"
    StrokeThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness.Top}"
    StartAngle="90" EndAngle="-45" />

结果:

this page

显然我在这里对开始和结束角度进行硬编码,但根据你已经完成的工作,我确信你可以毫无困难地编写一个简单的多转换器来计算从值/分钟开始的角度/最大

答案 1 :(得分:1)

您的代码非常接近。问题不在于裁剪。您只是没有考虑到在描边路径时,笔划以路径为中心绘制。这意味着几何上,笔划本身必须位于您想要绘制的 middle 中。

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

  1. 弧的起点。起点需要垂直偏移以考虑行程厚度。
  2. 弧的大小。需要减小弧的大小,以便路径保持在较大圆的行程中心。
  3. 弧的终点。与起点一样,需要对其进行调整,但在这种情况下,计算中需要调整的是弧的半径。
  4. 例如,您可以添加几个转换器:

    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();
        }
    }
    

    然后将您的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>
    

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

    <l:ThicknessToStartPointConverter x:Key="thicknessToStartPointConverter"/>
    <l:ThicknessToSizeConverter x:Key="thicknessToSizeConverter"/>
    

    然后你会得到你想要的东西。

    可能有一种方法可以将背景Ellipse元素和Path元素组合在一起,这样就可以在没有上述元素的情况下绘制Path,即硬编码大小为10,然后让Grid平均调整两个子元素的大小,使它们正确排列。但我没有看到任何明显的解决方案,并且不想花时间去弄明白。以上应该适合您的目的。 :)

答案 2 :(得分:1)

我需要在WPF中重新创建GitHub Pull Request状态圈。

GitHub Pull Request Status

我采用了另一种方法,并留下了一些示例代码here。这个想法是创建可以计算颜色并应用蒙版以获得形状的代码。

我从代码开始,该代码可以根据所需的圆弧半径和完成百分比创建多边形。

public static IEnumerable<Point> GeneratePoints(double size, float percentage)
{
    if (percentage < 0 || percentage > 1)
    {
        throw new ArgumentException();
    }

    var halfSize = size / 2;
    var origin = new Point(halfSize, halfSize);
    var topMiddle = new Point(halfSize, 0);
    var topRight = new Point(size, 0);
    var bottomRight = new Point(size, size);
    var bottomLeft = new Point(0, size);
    var topLeft = new Point(0, 0);

    if (percentage == 1)
    {
        return new[] { topLeft, topRight, bottomRight, bottomLeft };
    }

    var degrees = percentage * 360;
    var adjustedDegrees = (degrees + 90) % 360;

    if (adjustedDegrees >= 90 && adjustedDegrees < 135)
    {
        var angleDegrees = adjustedDegrees - 90;
        var angleRadians = ToRadians(angleDegrees);
        var tan = Math.Tan(angleRadians);
        var oppositeEdge = tan * halfSize;
        return new[] { origin, topMiddle, new Point(halfSize + oppositeEdge, 0) };
    }

    if (adjustedDegrees >= 135 && adjustedDegrees < 180)
    {
        var angleDegrees = adjustedDegrees - 135;
        var angleRadians = ToRadians(angleDegrees);
        var tan = Math.Tan(angleRadians);
        var oppositeEdge = tan * halfSize;
        return new[] { origin, topMiddle, topRight, new Point(size, oppositeEdge) };
    }

    if (adjustedDegrees >= 180 && adjustedDegrees < 225)
    {
        var angleDegrees = adjustedDegrees - 180;
        var angleRadians = ToRadians(angleDegrees);
        var tan = Math.Tan(angleRadians);
        var oppositeEdge = tan * halfSize;
        return new[] { origin, topMiddle, topRight, new Point(size, halfSize + oppositeEdge) };
    }

    if (adjustedDegrees >= 225 && adjustedDegrees < 270)
    {
        var angleDegrees = adjustedDegrees - 225;
        var angleRadians = ToRadians(angleDegrees);
        var tan = Math.Tan(angleRadians);
        var oppositeEdge = tan * halfSize;
        return new[] { origin, topMiddle, topRight, bottomRight, new Point(size - oppositeEdge, size) };
    }

    if (adjustedDegrees >= 270 && adjustedDegrees < 315)
    {
        var angleDegrees = adjustedDegrees - 270;
        var angleRadians = ToRadians(angleDegrees);
        var tan = Math.Tan(angleRadians);
        var oppositeEdge = tan * halfSize;
        return new[] { origin, topMiddle, topRight, bottomRight, new Point(halfSize - oppositeEdge, size) };
    }

    if (adjustedDegrees >= 315 && adjustedDegrees < 360)
    {
        var angleDegrees = adjustedDegrees - 315;
        var angleRadians = ToRadians(angleDegrees);
        var tan = Math.Tan(angleRadians);
        var oppositeEdge = tan * halfSize;
        return new[] { origin, topMiddle, topRight, bottomRight, bottomLeft, new Point(0, size - oppositeEdge) };
    }

    if (adjustedDegrees >= 0 && adjustedDegrees < 45)
    {
        var angleDegrees = adjustedDegrees;
        var angleRadians = ToRadians(angleDegrees);
        var tan = Math.Tan(angleRadians);
        var oppositeEdge = tan * halfSize;
        return new[] { origin, topMiddle, topRight, bottomRight, bottomLeft, new Point(0, halfSize - oppositeEdge) };
    }

    if (adjustedDegrees >= 45 && adjustedDegrees < 90)
    {
        var angleDegrees = adjustedDegrees - 45;
        var angleRadians = ToRadians(angleDegrees);
        var tan = Math.Tan(angleRadians);
        var oppositeEdge = tan * halfSize;
        return new[] { origin, topMiddle, topRight, bottomRight, bottomLeft, topLeft, new Point(oppositeEdge, 0) };
    }

    return new Point[0];
}

public static double ToRadians(float val)
{
    return (Math.PI / 180) * val;
}

该代码允许我创建以下内容。

enter image description here

将其与适当的形状一起剪切:

<Polygon.Clip>
    <CombinedGeometry GeometryCombineMode="Exclude">
        <CombinedGeometry.Geometry1>
            <EllipseGeometry Center="125 125" RadiusX="125" RadiusY="125" />
        </CombinedGeometry.Geometry1>
        <CombinedGeometry.Geometry2>
            <EllipseGeometry Center="125 125" RadiusX="100" RadiusY="100" />
        </CombinedGeometry.Geometry2>
    </CombinedGeometry>
</Polygon.Clip>

enter image description here

通过添加一些百分比并覆盖多边形,我可以实现这一目标。

enter image description here