Мне нужно нарисовать вот такой вот элемент в 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, потому что в этом случае пропорционально отскалируется и ширина границы. Скалировать нужно только геометрию:
Теперь скалирование можно задать через 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 class TypedValueExtension
Теперь можно написать так:
Получаем следующую картинку:
Разбиение на UserControl'ы сделаете сами, хорошо?
Комментариев нет:
Отправить комментарий