2016-04-20 2 views
5

Я пытаюсь адаптировать пирог ProgressBar, найденный в книге WPF 4 Unleashed, чтобы выглядеть пончиком. Я чувствую, что я на полпути, но я не знаю, как решить последнюю проблему.WPF Donut ProgressBar

Вот картина, иллюстрирующая то, что я хочу и что мне удалось достичь:

enter image description here

  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" /> 

... и преобразователи:

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

ответ

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

А затем обновить код 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 равномерно изменяют оба дочерних элемента, заставляя их правильно выстраиваться в линию. Но я не видел никаких очевидных решений в этом направлении и не хотел тратить время на это. Вышеизложенное должно отлично работать для ваших целей. :)

+0

Спасибо, мне удалось заставить его работать с вашими предложениями и пересчитанным радиусом. – dbostream

0

Настоящая проблема заключается в отсутствии элемента управления Arc из WPF. Вместо того, чтобы пытаться обуздать существующую структуру в соответствии с вашими требованиями, почему бы просто не добавить ее самостоятельно? Существует множество реализаций 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, делая таким образом также позволяет удалить пространство имен пути размещения следующая строка в вашем проекте, как описано на this page:

[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" /> 

Результат:

enter image description here

Очевидно, я жесткого кодирования начальные и конечные углы здесь, но на основе того, что вы уже сделали, я уверен, что вы не будете иметь никаких проблем, пишущие простой мультиконвертер для расчета углов со значениями/мин/макс.

+0

Спасибо за ваше решение, сейчас я буду использовать предложение Питера, но я подумаю о том, чтобы изменить его, когда я начинаю работать над своим настоящим проектом. – dbostream

Смежные вопросы