Пользовательские панели
89WPF --- Шаблоны и пользовательские элементы управления WPF --- Создание панелей компоновки
Итак, ранее была продемонстрирована разработка с нуля двух пользовательских элементов управления — CololPicker и FlipPanel. В следующих разделах вы ознакомитесь с двумя более специализированными вариантами: наследованием пользовательской панели и построением элемента управления, рисующего себя специальным образом.
Создание специальной пользовательской панели — специфическая, но довольно распространенная часть разработки пользовательских элементов управления. Как известно, панели размещают в себе один или более дочерних элементов и реализуют специфическую логику компоновки для соответствующего их расположения. Пользовательские панели — важный ингредиент, который необходим, когда строится собственная система "отрывных" панелей инструментов и стыкуемых окон. Пользовательские панели часто удобны для создания составных элементов управления, которым нужна специфическая нестандартная компоновка — вроде забавных стыкуемых панелей инструментов.
Вы уже знакомы с базовыми типами панелей, которые WPF предлагает для организации содержимого (StackPanel, DockPanel, WrapPanel, Canvas и Grid). Также вы видели, что некоторые элементы WPF используют собственные специализированные панели (вроде TabPanel, ToolBarOverflowPanel и VirtualizingPanel). В Интернете можно найти намного больше примеров пользовательских панелей. Ниже перечислены некоторые их них, достойные внимания:
Специальный контейнер Canvas, позволяющий перетаскивать свои дочерние элементы без дополнительного кода обработки событий.
Две панели, реализующие забавные эффекты в списке элементов (http://www.codeproject.com/WPF/Panels.asp).
Панель, использующая анимацию на основе кадров для трансформации одной компоновки в другую (http://j832.com/BagOTricks).
Двухшаговый процесс компоновки
Каждая панель использует один и тот же прием: двухшаговый процесс, отвечающий за изменение размеров и упорядочивание дочерних элементов. Первая стадия — измерение, когда панель определяет, насколько большими хотят быть ее дочерние компоненты. Вторая стадия — компоновка, когда каждый элемент получает свои границы. Необходимы два шага, поскольку панель должна учесть "пожелания" всех своих членов перед тем, как решить, как следует распорядиться доступным пространством.
Логика этих двух шагов добавляется за счет переопределения методов со странными именами MeasureOverride() и ArrangeOverride(), которые определены в классе FrameworkElement как часть системы компоновки WPF. Их странные имена говорят о том, что методы MeasureOverride() и ArrangeOverride() заменяют логику, содержащуюся в методах MeasureCore() и ArrangeCore(), определенных в классе UIElement. Последние — не переопределяемы.
Метод MeasureOverride()
Первый шаг состоит в определении с помощью метода MeasureOverride() того, сколько пространства желает занять каждый дочерний элемент. Однако даже в методе MeasureOverride() дочерние элементы не получают неограниченного пространства. В качестве абсолютного минимума дочерние элементы ограничены пространством, доступным в панели. Дополнительно их можно ограничить более строго. Например, Grid с двумя пропорционально размещенными строками предоставит каждому из дочерних элементов половину доступной высоты. StackPanel выделит все доступное пространство первому элементу, затем предоставит то, что осталось, второму, и т.д.
Каждая реализация MeasureOverride() отвечает за проход в цикле по коллекции дочерних элементов и вызов метода Measure() для каждого из них. При вызове методу Measure() передается ограничивающий прямоугольник — объект Size, определяющий максимально доступное пространство для дочернего элемента управления. К концу метода MeasureOverride() панель возвращает пространство, необходимое для отображения всех своих дочерних элементов и их желательные размеры.
Ниже приведена базовая структура метода MeasureOverride(), без специфических деталей, связанных с размерами:
protected override Size MeasureOverride(Size constraint)
{
// Проверить все дочерние элементы.
foreach (UIElement element in base.InternalChildren)
{
// Запросить у каждого дочернего элемента желательное для него
// пространство, применяя ограничение availableSize
Size availableSize = new Size (...);
element.Measure(availableSize);
// (Здесь можно прочитать element.DesiredSize, чтобы получить запрошенный размер.)
}
// Показать, сколько места требует данная панель.
// Будет использовано для установки свойства DesiredSize панели
return new Size (...);
}
Метод Measure() не возвращает значения. После вызова Measure() свойство DesiredSize данного элемента содержит запрошенный размер. Эту информацию можно использовать в своих вычислениях для будущих дочерних элементов (и определения общего размера, необходимого панели).
Вы должны вызвать Measure() для каждого дочернего элемента, даже если не хотите ограничивать размер этого элемента либо использовать его свойство DesiredSize. Многие элементы не отображают себя до тех пор, пока не будет вызван их метод Measure(). Чтобы предоставить дочернему элементу все пространство, которое он пожелает, передайте объект Size со значением Double.PositiveInfinity по обоим измерениям. (Такую стратегию использует ScrollViewer, поскольку он может обработать содержимое любого размера.) Дочерний элемент затем вернет размер пространства, необходимого его содержимому, или доступное пространство — в зависимости от того, что меньше.
В конце процесса измерения контейнер компоновки должен вернуть желаемый размер. Желаемый размер простой панели можно вычислить, комбинируя желаемые размеры каждого дочернего элемента.
В качестве желаемого размера панели можно просто вернуть ограничение, переданное методу MeasureOverride(). Хотя кажется разумным взять весь доступный размер, это приводит к проблемам, если контейнер принимает объект Size со значением Double.PositiveInfinity хотя бы по одному из двух измерений (что означает "возьми столько места, сколько хочешь"). Хотя бесконечный размер допустим в качестве ограничения, он не допустим как результирующее значение, поскольку WPF не сможет определить, насколько большим должен быть элемент. Более того, не следует запрашивать больше пространства, чем нужно на самом деле. В противном случае это приведет к появлению лишнего пустого пространства, и элементы, добавленные позже, после панели компоновки, будут скапливаться внизу окна.
Можно отметить, что существует близкое сходство между методом Measure(), вызываемым с каждым элементом, и методом MeasureOverride(), определяющим первый шаг логики компоновки панели. В действительности Measure() запускает метод MeasureOverride(). То есть, если поместить один контейнер компоновки внутрь другого, то при вызове Measure() получится общий размер, необходимый контейнеру компоновки и всем его дочерним элементам.
Одной из причин того, что процесс измерения проходит в два шага (метод Measure(), запускающий метод MeasureOverride()), является необходимость иметь дело с полями. При вызове методу Measure() передается все доступное пространство. Когда WPF вызывает MeasureOverride(), он автоматически сокращает доступное пространство, чтобы принять во внимание размер полей (если только не был передан бесконечный размер).
Метод ArrangeOverride()
После того как все элементы измерены, наступает время разместить их в пределах доступного пространства. Система компоновки вызывает метод ArrangeOverride() панели, а панель вызывает метод Arrange() для каждого дочернего элемента, чтобы сообщить ему, сколько пространства ему выделено. (Как и можно было предположить, Arrange() запускает метод ArrangeOverride(), подобно тому, как Measure() инициирует вызов метода MeasureOverride().)
При измерении элементов с помощью метода Measure() передается объект Size, задающий границы доступного пространства. При размещении элемента методом Arrange() передается объект System.Windows.Rect, определяющий размер и положение элемента. В данный момент это похоже на то, как элемент располагается по координатам X и Y стиля Canvas, которые определяют расстояние между верхним левым углом контейнера компоновки и элементом.
Элементы (и панели компоновки) вольны нарушать правила и пытаться рисовать за пределами выделенных им границ. Например, Line может перекрывать соседние элементы. Однако обычные элементы должны соблюдать выделенные им границы. Вдобавок большинство контейнеров будут отсекать те дочерние элементы, которые выходят за их границы.
Ниже приведена базовая структура метода ArrangeOverride() без специфических деталей, связанных с вычислением размеров:
protected override Size ArrangeOverride(Size arrangeSize)
{
// Перебрать все дочерние элементы.
foreach (UIElement element in base.InternalChildren)
{
// Назначить дочернему элементу его границы.
Rect bounds = new Rect(...);
element.Arrange(bounds);
// (Теперь вы можете прочитать element.ActualHeight и
// element.ActualWidth, чтобы определить его размеры.)
}
// Определить, сколько места займет эта панель.
// Эта информация будет использована для установки
// свойств ActualHeight и ActualWidth панели,
return arrangeSize;
}
При упорядочивании элементов нельзя передавать бесконечные размеры. Однако можно предоставить элементу его желаемый размер, передав значение свойства DesiredSize. Можно также дать элементу больше пространства, чем он требует. Фактически такое случается довольно часто. Например, вертикальная панель StackPanel предоставляет своим дочерним элементам столько высоты, сколько они требуют, но при этом выделяют им всю ширину самой панели. Аналогично Grid может использовать фиксированный или пропорциональный размер строк, который больше желаемого размера находящегося внутри элемента. И даже если расположить элемент в контейнере "размер по содержимому" (size-to-content), этот элемент может быть увеличен, если ему будет явно установлен размер через свойства Height и Width.
Когда элемент делается больше, чем его желаемый размер, вступают в действие свойства HorizontalAlignment и VerticalAlignment. Содержимое элемента помещается где-то внутри отведенного ему пространства и его надо как-то выравнивать. Поскольку метод ArrangeOverride() всегда принимает определенный размер (не бесконечный), можно вернуть переданный объект Size, чтобы установить финальный размер панели.
Фактически многие контейнеры компоновки предпринимают этот шаг, чтобы занять все выделенное им пространство. (Здесь отсутствует опасность захватить слишком много места, которое может понадобиться другому элементу, поскольку шаг измерения системы компоновки гарантирует, что не будет выделено больше места, чем необходимо, если его не хватает.)