Страницы

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

воскресенье, 15 декабря 2019 г.

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

#c_sharp #wpf #xaml


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

    


Ответы

Ответ 1



Ну что же, чистого решения на 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'ы сделаете сами, хорошо?

Ответ 2



Не знаю, решил ли автор свою проблему, предложу вариант по которому пошёл бы я: Запускаем Blend for Visual Studio (Если нет бленды-процесс создания необходимого контрола несколько усложнится). В качестве проекта выбираем "WpfControlLibrary". Студия создаёт проект и открывает нам пустую форму пользовательского контрола. Сейчас по умолчанию корневой элемент на нашем контроле-грид. Упаковываем его в Viewbox (он нужен для пропорционального масштабирования элемента ) и задаём нашему контролу фиксированный размер. Размечаем в гриде область где должны находится визуальные части нашего контрола и кидаем на эти области Image'ы и в качестве изображений ставим им нужные нам на данных местах эскизы элементов контрола (Данную процедуру можно пропустить-нужно исключительно для удобства отрисовки ) Выбираем нужные нам примитивы (Path, Rectangle, Ellipce и т.п) и обрисовываем ими наши эскизы. Далее можно сгруппировать несколько элементов в один (если например они логически являются одним элементом который должен реагировать на некоторые события (клик мыши например)) Добавляем на форму прочие элементы управления: TextBlock'и для отображения информации (цифры, подписи ) и т.п. На этом процесс создания внешнего вида элемента управления фактически завершён. Чтобы он был интерактивным и настраиваемым (цвет отдельных элементов, события клика мыши, отображение каких-либо данных и т.п.) создать нашему контролу соответствующие свойства, методы и события и сделать привязку к ним у интерактивных элементов нашего контрола и т.д. и т.п. (в общем тут чистой воды создание пользовательского контрола. Мануалов по данному вопросу много, расписывать не буду ) В рабочем проекте добавляем наш контрол на панель элементов и кидаем его на форму и пользуемся =) Если чего-то не хватает - возвращаемся к процессу редактирования свойств и поведения нашего контрола Если Blend'а нет-создаём шаблон пользовательского элемента в студии. Операции тут идентичны описаным выше, за исключением того, что обрисовывать Path'aми эскизы придётся указывая координаты в поле "Data" элемента Path. Это весьма трудоёмкий процесс...

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

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