WPF Donut ProgressBar

Estoy tratando de adaptar el pastel ProgressBar que se encuentra en el libro WPF 4 Unleashed para que parezca una dona. Siento que estoy a mitad de camino pero no sé cómo resolver el último problema.

Aquí hay una imagen que ilustra lo que quiero y lo que he logrado:

introduzca la descripción de la imagen aquí

  1. Así es como quiero que se vea.
  2. Esto es lo que parece usar el código de abajo.
  3. Encontré una sugerencia en una pregunta diferente aquí en stackoverflow que era usar clipping en el camino y duplicar el grosor del trazo. Como puede ver, la ruta está posicionada correctamente ahora, pero cualquier progreso por debajo del 50% no se dibuja correctamente como puede ver.

Así que mi pregunta es, ¿cómo puedo arreglarlo para que se vea como quiero?

A continuación se muestra el xaml relevante que estoy usando:

                                       ...  

… y los convertidores:

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

Tu código está muy cerca. El problema no es de recorte. Simplemente no ha podido tener en cuenta que cuando se trazan trazados de ruta, el trazo se dibuja centrado en la ruta. Esto significa que, geométricamente, el trazo en sí debe estar en el medio de donde se quiere dibujar.

En su implementación particular, esto significa que debe tener en cuenta el grosor del trazo en tres lugares diferentes:

  1. El punto de inicio del arco. El punto de inicio debe desplazarse verticalmente para tener en cuenta el grosor del trazo.
  2. El tamaño del arco. El tamaño del arco debe reducirse para que la trayectoria permanezca centrada en el trazo del círculo más grande.
  3. El punto final del arco. Al igual que con el punto de inicio, esto debe ajustarse, pero en este caso es el radio del arco en su cálculo lo que necesita ajuste.

Por ejemplo, puede agregar un par de convertidores:

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

Y luego actualice su XAML para verse así:

                    

Con, por supuesto, los recursos necesarios para los convertidores:

   

Y entonces obtendrás lo que quieres.

Probablemente haya una forma de componer el elemento Ellipse fondo y el elemento Path , de manera que el Path se dibuje sin el anterior, es decir, con los tamaños codificados de 10, y luego haga que Grid cambie el tamaño de ambos elementos secundarios por igual, haciendo que se alineen arriba correctamente Pero no vi ninguna solución obvia en ese sentido y no tenía ganas de pasar el tiempo para averiguarlo. Lo anterior debería funcionar bien para sus propósitos. 🙂

El problema real aquí es el control de Arc falta en WPF. En lugar de tratar de adaptar el marco existente a sus requisitos, ¿por qué no lo agrega usted mismo? Existen numerosas implementaciones de WPF Arc que flotan alrededor de la red y todas se ven muy similares, solo asegúrate de elegir una que actualice lo visual cada vez que se cambien las DP de ángulo. Esto debería servir bien a tu propósito:

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

Con el fin de mantener una architecture limpia, prefiero colocar formas personalizadas en una biblioteca de clases separada con una referencia a PresentationFramework, ya que también le permite eliminar el espacio de nombres colocando la siguiente línea en su proyecto como se describe en esta página :

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

Ahora tienes una forma de arco reutilizable que puedes usar como una elipse, así que reemplaza tu ruta XAML completa con algo como esto:

  

Resultado:

introduzca la descripción de la imagen aquí

Obviamente estoy codificando los angularjs inicial y final aquí, pero en base a lo que ya has hecho, estoy seguro de que no tendrás problemas para escribir un convertidor múltiple simple para calcular los angularjs de valor / min / max.