Страницы

Поиск по вопросам

среда, 24 октября 2018 г.

Создание круга, поделенного на сектора с сегментами

Мне нужно нарисовать вот такой вот элемент в WPF. Каждому сегменту я должен иметь возможность указать его уникальный цвет. Также требуется, чтобы этот элемент занимал все доступное ему место. В голову приходят варианты только с полным описанием всей логики отрисовки в Code-Behind или созданием собственной панели (но это еще сложнее выйдет). Может это как-нибудь можно в xaml описать?


Ответ

Ну что же, чистого решения на XAML'е у меня не вышло, кое-где пришлось применять тригонометрию. Но почти чистое решение есть.
Начнём с простого случая: размер квадратного поля известен заранее. Пускай он будет 200 × 200.
Для того, чтобы нарисовать кусочек интерфейса, нам нужно немного покопаться в PathGeometry. Для первого сегмента пишем нечто вот такое:

Я вычислял координаты как будто бы центр лежит в начале координат для того, чтобы остальные куски получались аналогично. Мне пришлось, понятно, сдвинуться в центр при помощи трансляции.

Добавим вторую точно такую же часть, но с другим углом поворота:

Результат обнадёживает:

Хорошо, теперь попробуем добавить текстовую метку в середину каждого куска. Для начала, мы не хотим заморачиваться с динамическим вычислением размера текста, а хотим центрировать текст относительно данной точки. Но если положить текст в контейнер нулевого размера, то он обрежется границами контейнера. Поэтому сделаем кастомный декоратор, который будет (1) не обрезать контент, (2) центрировать его внутри себя, и (3) обладать нулевым размером.
class CenteringNoClipper : Decorator { // не обрезаем контент своими краями protected override Geometry GetLayoutClip(Size layoutSlotSize) { return null; }
protected override Size MeasureOverride(Size constraint) { // даём контенту сколько угодно места Child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); // а сами занимаем ноль return new Size(); }
protected override Size ArrangeOverride(Size arrangeSize) { var childSize = Child.DesiredSize; // центрируем контент в том размере, какой он хочет Child.Arrange(new Rect(new Point(-childSize.Width/2, -childSize.Height/2), childSize)); // а сами занимаем ноль места return new Size(); } }
Имея такой декоратор, текстовую метку легко положить там, где нам хочется:

Получаем вот такой результат:

Что осталось сделать? Нужно отойти от заданной ширины в 200 пикселей, и разбросать код по UserControl'ам, чтобы избежать дублирования.
Для того, чтобы отойти от ширины, нужно воспользоваться скалированием. Хитрость состоит в том, что нельзя скалировать весь Path, потому что в этом случае пропорционально отскалируется и ширина границы. Скалировать нужно только геометрию:
M 0,-1 A 1,1 30 0 1 0.5,-0.86602 L 0.25,-0.43301 A 0.5,0.5 30 0 0 0,-0.5 z
Теперь скалирование можно задать через Binding
Следующая проблема — размер области. Нам нужен контрол, который сохраняет aspect ratio. Готового такого нет, но это будет полезная штука, так что засучим рукава и напишем его. (Поскольку контрол полезен сам по себе, я написал его более общим образом.)
public class AspectRatioDecorator : Decorator { #region dp double AspectRatio with validator ValidateAspectRatio public static readonly DependencyProperty AspectRatioProperty = DependencyProperty.Register( "AspectRatio", typeof(double), typeof(AspectRatioDecorator), new FrameworkPropertyMetadata( 1.0, FrameworkPropertyMetadataOptions.AffectsMeasure), ValidateAspectRatio);
public double AspectRatio { get { return (double)GetValue(AspectRatioProperty); } set { SetValue(AspectRatioProperty, value); } }
static bool ValidateAspectRatio(object value) { if (!(value is double)) return false;
var aspectRatio = (double)value; return aspectRatio > 0 && !double.IsInfinity(aspectRatio) && !double.IsNaN(aspectRatio); } #endregion
#region dp HorizontalAlignment HorizontalChildAlignment public HorizontalAlignment HorizontalChildAlignment { get { return (HorizontalAlignment)GetValue(HorizontalChildAlignmentProperty); } set { SetValue(HorizontalChildAlignmentProperty, value); } }
public static readonly DependencyProperty HorizontalChildAlignmentProperty = DependencyProperty.Register( "HorizontalChildAlignment", typeof(HorizontalAlignment), typeof(AspectRatioDecorator), new FrameworkPropertyMetadata( HorizontalAlignment.Center, FrameworkPropertyMetadataOptions.AffectsArrange), ValidateHorizontalChildAlignment);
static bool ValidateHorizontalChildAlignment(object value) { if (!(value is HorizontalAlignment)) return false;
var horizontalAlignment = (HorizontalAlignment)value; return horizontalAlignment != HorizontalAlignment.Stretch; } #endregion
#region dp VerticalAlignment VerticalChildAlignment public VerticalAlignment VerticalChildAlignment { get { return (VerticalAlignment)GetValue(VerticalChildAlignmentProperty); } set { SetValue(VerticalChildAlignmentProperty, value); } }
public static readonly DependencyProperty VerticalChildAlignmentProperty = DependencyProperty.Register( "VerticalChildAlignment", typeof(VerticalAlignment), typeof(AspectRatioDecorator), new FrameworkPropertyMetadata( VerticalAlignment.Top, FrameworkPropertyMetadataOptions.AffectsArrange), ValidateVerticalChildAlignment);
static bool ValidateVerticalChildAlignment(object value) { if (!(value is VerticalAlignment)) return false;
var verticalAlignment = (VerticalAlignment)value; return verticalAlignment != VerticalAlignment.Stretch; } #endregion
protected override Size MeasureOverride(Size constraint) { if (Child == null) // we have no child, so we need no space return new Size(0, 0);
constraint = SizeToRatio(constraint, false); Child.Measure(constraint);
if (double.IsInfinity(constraint.Width) || double.IsInfinity(constraint.Height)) return SizeToRatio(Child.DesiredSize, true);
return constraint; }
public Size SizeToRatio(Size size, bool expand) { double ratio = AspectRatio;
double height = size.Width / ratio; double width = size.Height * ratio;
if (expand) { width = Math.Max(width, size.Width); height = Math.Max(height, size.Height); } else { width = Math.Min(width, size.Width); height = Math.Min(height, size.Height); }
return new Size(width, height); }
protected override Size ArrangeOverride(Size arrangeSize) { if (Child == null) return arrangeSize;
var constrainedSize = arrangeSize;
var fwChild = Child as FrameworkElement; if (fwChild != null) { constrainedSize.Height = Math.Min(constrainedSize.Height, fwChild.MaxHeight); constrainedSize.Width = Math.Min(constrainedSize.Width, fwChild.MaxWidth); }
var newSize = SizeToRatio(constrainedSize, false);
double widthDelta = arrangeSize.Width - newSize.Width; double heightDelta = arrangeSize.Height - newSize.Height;
double top = 0; double left = 0;
if (!double.IsNaN(widthDelta) && !double.IsInfinity(widthDelta)) switch (HorizontalChildAlignment) { case HorizontalAlignment.Left: break; case HorizontalAlignment.Center: left = widthDelta / 2; break; case HorizontalAlignment.Right: left = widthDelta; break; };
if (!double.IsNaN(heightDelta) && !double.IsInfinity(heightDelta)) switch (VerticalChildAlignment) { case VerticalAlignment.Top: break; case VerticalAlignment.Center: top = heightDelta / 2; break; case VerticalAlignment.Bottom: top = heightDelta; break; };
var finalRect = new Rect(new Point(left, top), newSize); Child.Arrange(finalRect);
return arrangeSize; } }
Кроме того, чтобы забиндить размер без особенных трюков, нам нужен скалирующий IValueConverter
class ScalingConverter : IValueConverter { public object Convert( object value, Type targetType, object parameter, CultureInfo culture) { if (value == null || parameter == null) // поддержка редактора WPF return DependencyProperty.UnsetValue; double v = (double)value; double p = (double)parameter; return v * p; }
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture) { throw new NotSupportedException(); } }
Ну и для того, чтобы нормально передавать параметры в конвертер, сделаем типизирующий markup extension:
public class DoubleExtension : TypedValueExtension { public DoubleExtension(double value) : base(value) { } }
public class TypedValueExtension : MarkupExtension { public TypedValueExtension(T value) { Value = value; } public T Value { get; set; } public override object ProvideValue(IServiceProvider sp) { return Value; } }
Теперь можно написать так:
M 0,-1 A 1,1 30 0 1 0.5,-0.86602 L 0.25,-0.43301 A 0.5,0.5 30 0 0 0,-0.5 z M 0,-1 A 1,1 30 0 1 0.5,-0.86602 L 0.25,-0.43301 A 0.5,0.5 30 0 0 0,-0.5 z
Получаем следующую картинку:

Разбиение на UserControl'ы сделаете сами, хорошо?

Комментариев нет:

Отправить комментарий