我正在努力调整WPF 4 Unleashed一书中的馅饼ProgressBar看起来像甜甜圈。我觉得我已经走了一半,但我不知道如何解决最后一个问题。
这是一张图片,展示了我想要的以及我设法实现的目标:
所以我的问题是,如何修复它看起来像我想要的?
以下是我正在使用的相关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();
}
}
答案 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" />
结果:
显然我在这里对开始和结束角度进行硬编码,但根据你已经完成的工作,我确信你可以毫无困难地编写一个简单的多转换器来计算从值/分钟开始的角度/最大
答案 1 :(得分:1)
您的代码非常接近。问题不在于裁剪。您只是没有考虑到在描边路径时,笔划以路径为中心绘制。这意味着几何上,笔划本身必须位于您想要绘制的 middle 中。
在您的特定实现中,这意味着您需要在三个不同的位置考虑笔划粗细:
例如,您可以添加几个转换器:
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状态圈。
我采用了另一种方法,并留下了一些示例代码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;
}
该代码允许我创建以下内容。
将其与适当的形状一起剪切:
<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>
通过添加一些百分比并覆盖多边形,我可以实现这一目标。