Главная » WPF » Реализация нестандартного размещения

0

Чтобы   написать   собственный   менеджер   размещения,  обычно  создается класс, производный от Panel. Но прежде чем перейти к деталям, ненадолго отв# лечемся  и поговорим  о том, зачем вообще может понадобиться нестандартное размещение.

Наиболее  распространенных причины  две: «алгоритмическое размещение» (желание расположить элементы,  скажем, вдоль кривой) и производительность. Но ко второй причине следует относиться  с осторожностью и не забывать фунда# ментальное  правило: для оценки производительности необходимо тестирование. Часто  программист  думает, что приложение работает  слишком  медленно  (или быстро),  не протестировав его должным  образом. Именно  ради производитель# ности и был создан элемент  UniformGrid; для равномерного  распределения эле# ментов издержки  Grid ни к чему.

Алгоритмическое размещение  более интересно. Предположим, что ряд эле# ментов управления хочется расположить по окружности (рис. 4.31). Ясно, что ни один из готовых менеджеров  на это не рассчитан.

Памятуя о двухэтапной  модели размещения в WPF,  мы должны начать с вы# числения  предпочтительного размера контейнера.  В данном случае это сопряже# но с некоторыми трудностями, в основном, из#за нетривиальной математики. Мы немного упростим  алгоритм и воспользуемся моделью, показанной  на рис. 4.32.

Для  вычисления предпочтительного размера  проверим  размер  каждого  по# томка  и найдем  максимальную высоту  и ширину.  Очень  важно,  чтобы  метод Measure был вызван для каждого потомка9, иначе позабытые потомки не будут нарисованы:

protected override Size MeasureOverride(Size availableSize) {

double maxChildWidth = 0.0;

double maxChildHeight = 0.0;

Рис. 4.32. Модель для вычисления предпочтительного размера при размещении по окружности

Зная  максимальную ширину  и высоту,  можно  вычислить идеальный  размер панели,  найдя  радиус  окружности,  охватывающей все элементы.  Но  мы можем немного схитрить, так как распределяем элементы вдоль окружности равномерно:

protected override Size MeasureOverride(Size availableSize) {

double maxChildWidth = 0.0;

double maxChildHeight = 0.0;

// Измерить каждого потомка; в данном случае мы не налагаем

// на них ограничений.

//

foreach (UIElement child in InternalChildren) {

child.Measure(availableSize);

maxChildWidth = Math.Max(child.DesiredSize.Width, maxChildWidth);

maxChildHeight = Math.Max(child.DesiredSize.Height, maxChildHeight);

}

// Не идеальный алгоритм вычисления размера; мы вычисляем радиус

// окружности, которая вмещает все элементы по ширине, а затем

// вносим поправку на высоту.

double idealCircumference = maxChildWidth * InternalChildren.Count;

double idealRadius =

(idealCircumference / (Math.PI * 2) + maxChildHeight); Size ideal = new Size(idealRadius * 2, idealRadius * 2);

// и далее…

}

Последний шаг – попытаться «уложиться» в отведенную область. Поскольку мы го# товы смириться с перекрытием элементов, то нас устроит любой ее размер. Но в некото# рых случаях (например, если наш менеджер вложен в StackPanel), ширина или высота (или то и другое) может оказаться бесконечной. Важно не забывать про эту возможность, поскольку возвращать бесконечный предпочтительный размер запрещено:

protected override Size MeasureOverride(Size availableSize) {

double maxChildWidth = 0.0;

double maxChildHeight = 0.0;

// Измерить каждого потомка; в данном случае мы не налагаем

// на них ограничений.

//

foreach (UIElement child in InternalChildren) {

child.Measure(availableSize);

maxChildWidth = Math.Max(child.DesiredSize.Width, maxChildWidth);

maxChildHeight = Math.Max(child.DesiredSize.Height, maxChildHeight);

}

// Не идеальный алгоритм вычисления размера; мы вычисляем радиус

// окружности, которая вмещает все элементы по ширины, а затем

// вносим поправку на высоту.

double idealCircumference = maxChildWidth * InternalChildren.Count;

double idealRadius =

(idealCircumference / (Math.PI * 2) + maxChildHeight); Size ideal = new Size(idealRadius * 2, idealRadius * 2);

// Вычисляем собственный размер, помня о том, что можем получить

// бесконечное значение по любому направлению. В таком случае мы

// вернем значение с семантикой «вплоть до» нашего идеала, но

// согласимся на любые навязанные ограничения.

//

Size desired = ideal;

if (!double.IsInfinity(availableSize.Width)) {

if (availableSize.Width < desired.Width) {

desired.Width = availableSize.Width;

}

}

if (!double.IsInfinity(availableSize.Height)) {

if (availableSize.Height < desired.Height) {

desired.Height = availableSize.Height;

}

}

return desired;

}

Реализация этапа  установки  несколько  хитрее. Поскольку мы хотим  разме# щать потомков по окружности (а не по эллипсу), то необходимо вычислить мак# симальный  квадрат, который поместится  в отведенной области. Для выполнения самого преобразования поворота у нас есть метод RotateTransform, но центр по# ворота придется  найти самостоятельно. Все вместе приводит нас к модели, изоб# раженной  на рис. 4.33.

На первом шаге вычисляем местоположение и размеры прямоугольника, опи#

санного вокруг окружности размещения:

protected override Size ArrangeOverride(Size finalSize) {

// Вычисляем прямоугольник, описанный вокруг окружности, вдоль которой

// будут равномерно размещены потомки

//

Rect layoutRect;

if (finalSize.Width > finalSize.Height) {

layoutRect = new Rect(

(finalSize.Width   finalSize.Height) / 2

,0

,finalSize.Height

,finalSize.Height);

}

else {

layoutRect = new Rect(

0

,(finalSize.Height   finalSize.Width) / 2

,finalSize.Width

,finalSize.Width);

}

// и далее…

}

Угол поворота вычисляется тривиально, надо лишь разделить полный угол на число потомков:

protected override Size ArrangeOverride(Size finalSize) {

// Вычисляем прямоугольник, описанный вокруг окружности, вдоль которой

// будут равномерно размещены потомки

//

Rect layoutRect;

if (finalSize.Width > finalSize.Height) {

layoutRect = new Rect(

(finalSize.Width   finalSize.Height) / 2

,0

,finalSize.Height

,finalSize.Height);

}

else {

layoutRect = new Rect(

0

,(finalSize.Height   finalSize.Width) / 2

,finalSize.Width

,finalSize.Width);

}

double angleInc = 360.0 / InternalChildren.Count;

// и далее…

}

Что здесь происходит? Во#первых, для каждого потомка необходимо вызвать метод Arrange. Если этого не сделать, потомок не будет нарисован. Метод Arrange

принимает на входе прямоугольник, который описывает окончательный размер и положение потомка. В данном случае центр элемента совмещается с центром это# го прямоугольника:

protected override Size ArrangeOverride(Size finalSize) {

// Вычисляем прямоугольник, описанный вокруг окружности, вдоль которой

// будут равномерно размещены потомки

//

Rect layoutRect;

if (finalSize.Width > finalSize.Height) {

layoutRect = new Rect(

(finalSize.Width   finalSize.Height) / 2

,0

,finalSize.Height

,finalSize.Height);

}

else {

layoutRect = new Rect(

0

,(finalSize.Height   finalSize.Width) / 2

,finalSize.Width

,finalSize.Width);

}

double angleInc = 360.0 / InternalChildren.Count;

double angle = 0;

foreach (UIElement child in InternalChildren) { Point childLocation = new Point(

layoutRect.Left + ((layoutRect.Width  child.DesiredSize.Width) /

2)

,layoutRect.Top);

child.Arrange(new Rect(childLocation,child.DesiredSize));

}

// и далее…

}

После  того  как  положение   потомка  установлено,   можно  применить   метод

RotateTransform для поворота его вокруг центра прямоугольника:

protected override Size ArrangeOverride(Size finalSize) {

// Вычисляем прямоугольник, описанный вокруг окружности, вдоль которой

// будут равномерно размещены потомки

//

Rect layoutRect;

if (finalSize.Width > finalSize.Height) {

layoutRect = new Rect(

(finalSize.Width   finalSize.Height) / 2

,0

,finalSize.Height

,finalSize.Height);

}

else {

layoutRect = new Rect(

0

,(finalSize.Height   finalSize.Width) / 2

,finalSize.Width

,finalSize.Width);

}

double angleInc = 360.0 / InternalChildren.Count;

double angle = 0;

foreach (UIElement child in InternalChildren) { Point childLocation = new Point(

layoutRect.Left + ((layoutRect.Width  child.DesiredSize.Width) /

2)

,layoutRect.Top);

// Центр поворота находится в точке (0,0) относительно уже

// установленного положения потомка.

//

child.RenderTransform = new RotateTransform(

angle, child.DesiredSize.Width / 2, finalSize.Height / 2   layoutRect.Top); angle += angleInc;

child.Arrange(new Rect(childLocation,child.DesiredSize));

}

}

Хотя  реализовывать круговое  размещение  приходится  не так  уж  часто,  на этом примере мы продемонстрировали многие интересные  аспекты написания нестандартного менеджера размещения10.

Чего мы достигли

В этой  главе  мы  рассмотрели основополагающие принципы размещения и библиотеку  менеджеров размещения. Мы видели, что приложение собирается  из элементов  управления, а менеджеры  размещения играют важнейшую  роль  при определении местоположения и размеров этих элементов. Наличие развитой сис# темы размещения позволяет  строить  интерфейсы, способные  динамически ме# нять размеры в зависимости от действия пользователя, размера экрана или изме# няющегося содержимого.

10 Единственная достойная упоминания особенность нестандартных менеджеров размещения, ко) торая осталась не рассмотренной, ) это размещение, зависящее от некоторого свойства (или нес) кольких свойств), как, например, DockPanel. Определяя собственные свойства, мы можем указать, что они влияют на этап измерения, установки и рисования. Более подробно этот вопрос рассмат) ривается в приложении.

Источник: К. Андерсон  Основы  Windows Presentation Foundation. Пер. с англ. А. Слинкина — М.: ДМК Пресс, 2008 — 432 с.: ил.

По теме:

  • Комментарии