Пользовательские контейнеры

58

Технология Silverlight предоставляет мощную коллекцию контейнеров, однако многих все же не хватает. Разработчики Silverlight не добавили многие контейнеры, потому что стремились минимизировать размеры надстройки Silverlight и уменьшить время загрузки.

Разработчики приложений Silverlight могут сами создавать специальные контейнеры, аналогичные применяемым в WPF. Для этого нужно создать пользовательский класс, производный от Panel, и добавить в него соответствующий код размещений элементов. Далее рассматривается, как создать пользовательский контейнер, переносящий элементы на следующую строку.

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

В следующих разделах вы узнаете, как работает процедура размещения, и на этой основе научитесь создавать собственные контейнеры. Будет рассмотрен пользовательский контейнер UniformGrid — упрощенная версия элемента Grid, размещающая элементы в таблице с одинаковыми ячейками.

Двухэтапный процесс размещения

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

Добавить код, управляющий этими двумя этапами, можно, переопределив методы MeasureOverride() и ArrangeOverride(), определенные в классе FrameworkElement библиотеки Silverlight. Указанные методы замещают логику методов MeasureCore() и ArrangeCore(), определенных в классе UIElement. Последние два метода для кода приложения недоступны, и переопределить их невозможно.

Метод MeasureOverride()

Первый этап — вычисление пространства, необходимого каждому дочернему элементу, с помощью метода MeasureOverride(). Однако даже в методе MeasureOverride() дочерним элементам не предоставлено безграничное пространство. Как минимум, они ограничены пространством, доступным для панели. По необходимости их можно ограничить еще более жестко. Например, панель Grid с двумя пропорциональными строками предоставляет дочерним элементам половину доступной высоты. Панель StackPanel предоставляет первому элементу все доступное пространство, второму — все, что осталось после размещения первого, третьему — все, что осталось после размещения первых двух, и т.д.

Каждая реализация метода MeasureOverride() проходит в цикле по коллекции дочерних элементов и вызывает для каждого из них метод Measure(). Ему передается объект Size, определяющий максимально доступное пространство для дочернего элемента управления. В конце метода MeasureOverride() панель возвращает пространство, необходимое для отображения всех дочерних элементов, и желательные размеры элементов.

Ниже приведена базовая структура метода MeasureOverride() без специфических операторов установки размеров:

protected override Size MeasureOverride(Size availableSize)
{            
            // Проверка всех дочерних элементов
            foreach (UIElement child in this.Children) 
            {
                // Вычисление пространства, необходимого текущему
                // дочернему элементу с учетом ограничений availableSize
                Size childSize = new Size { ... };
                element.Measure(childSize);
                // Теперь можно извлечь запрошенный размер из element.DesiredSize
            }
            
            // Получение максимальных запрашиваемых размеров 
            // и вычисление максимальных размеров панели
            return new Size{ ... }
}

Метод Measure() не возвращает значения. После вызова метода Measure() через дочерний элемент запрошенный размер предоставляется в свойстве DesiredSize дочернего элемента. Эту информацию можно использовать для размещения следующих дочерних элементов и вычисления общего пространства, необходимого панели.

Вызов метода Measure() обязателен для каждого дочернего элемента, даже если вы не планируете ограничивать его размеры или применять его свойство DesiredSize. Многие элементы не отображаются на экране, пока через них не будет вызван метод Measure(). Если нужно предоставить дочернему элементу возможность занять все свободное пространство, которое он пожелает, передайте в качестве параметра объект Size со значениями Double.PositiveInfinity для обеих координат (обычно такой способ используется для элемента прокрутки ScrollViewer, поскольку он должен охватывать все содержимое). При значении PositiveInfinity дочерний элемент возвращает пространство, необходимое для его содержимого. В противном случае он возвращает одно из двух значений (меньшее из двух): либо пространство, необходимое для содержимого, либо доступное пространство.

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

Внимательные читатели уже, наверное, заметили сходство между методами Measure(), вызываемыми через каждый дочерний элемент, и методами MeasureOverride(), определяющими первый этап обработки панели. Фактически метод Measure() запускает метод MeasureOverride(). Следовательно, если контейнеры вложены друг в друга, то при вызове Measure() вы получите общий размер, необходимый для внешнего контейнера и всех его дочерних элементов.

Одна из причин того, что вычисление пространства должно выполняться в два этапа, состоит в необходимости учитывать внешние поля. При вызове метода Measure() он получает общее доступное пространство, а при вызове MeasureOverride() доступное пространство автоматически уменьшается для учета внешних полей (естественно, если не передано бесконечное значение размера).

Метод ArrangeOverride()

После получения размеров каждого элемента их можно разместить в доступном пространстве. Для этого надстройка Silverlight вызывает метод ArrangeOverride() текущей панели, а панель вызывает метод Arrange() каждого дочернего элемента, чтобы сообщить, сколько пространства ему выделено. Метод Arrange() запускает метод ArrangeOverride(), как и в предыдущем случае, когда Measure() запускает MeasureOverride().

При вычислении размеров элемента с помощью метода Measure() ему передается объект Size, определяющий границы доступного пространства. При размещении элемента с помощью метода Arrange() ему передается объект System.Windows.Rect, определяющий его размеры и позицию. В этот момент каждый элемент размещается как бы в стиле Canvas на основе координат X и Y, определяющих расстояние между левым верхним углом контейнера и элементом.

Ниже приведена базовая структура метода ArrangeOverride() без детализации кода, определяющего фактические размеры:

protected override Size ArrangeOverride(Size arrangeSize)
{
   // Проход no всем дочерним элементам 
   foreach (UIElement element in this.Children)
   {
      // Присвоение границ дочернему элементу
      Rect bounds = new Rect(...);
      element.Arrange(bounds);
      // Теперь можно извлечь фактические размеры из 
      // свойств element.ActualHeight и element.ActualWidth
   }
   
   // Вычисление пространства, занимаемого панелью;
   // оно будет использоваться для установки
   // свойств ActualHeight и ActualWidth панели
   return arrangeSize;
}

При размещении элементов передавать бесконечные размеры нельзя. Однако можно присваивать элементу желаемые размеры, передавая ему значение свойства DesiredSize. Кроме того, элементу можно предоставить больше пространства, чем он просит. Фактически это происходит довольно часто. Например, панель StackPanei по вертикали предоставляет дочернему элементу столько пространства, сколько он запросит, однако по горизонтали размер элемента ограничен шириной панели.

Аналогично этому на панели Grid фиксированные или пропорциональные строки могут быть крупнее, чем желаемые размеры элемента. И даже если элемент размещен в контейнере, размеры которого подгоняются под содержимое, элемент можно увеличить при условии явного задания размеров с помощью свойств Height и Width.

Когда размер элемента больше желаемого, вступают в игру свойства VerticalAlignment и HorizontalAlignment. С их помощью для содержимого элемента выбирается место в заданных границах.

Метод ArrangeOverride() всегда получает определенные размеры (т.е. не бесконечные), поэтому можно возвратить объект Size, который передается для установки окончательных размеров панели. Опасности занять пространство, необходимое для другого элемента, нет, потому что на первом этапе размещения гарантируется, что элементу не будет предоставлено больше пространства, чем ему нужно (естественно, если пространство ограничено).

Пользовательский контейнер UniformGrid

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

Панель UniformGrid полезна как простая альтернатива решетке Grid. Она не требует явного определения строк и столбцов и размещения вручную каждого дочернего элемента в правильной ячейке. Поэтому панель UniformGrid имеет смысл использовать для вывода набора изображений. Платформа WPF содержит немного более сложную версию данного элемента управления.

Как и в случае любой пользовательской панелью, создание панели UniformGrid начинается с объявления простого класса, наследующего базовый элемент управления Panel:

public class UniformGrid : Panel
{ ... }

Определение UniformGrid можно разместить непосредственно в приложении Silverlight. Однако если вы хотите повторно использовать эту панель во многих приложениях, лучше разместить ее в отдельной библиотеке классов. Тогда для ее применения нужно будет всего лишь добавить ссылку на скомпилированную библиотеку классов.

Концептуально панель UniformGrid довольно простая: она проверяет размеры доступного пространства, вычисляет количество и размеры необходимых ячеек и размещает дочерние элементы один за другим. Поведение панели UniformGrid настраивается с помощью двух свойств, Rows и Columns, которые можно устанавливать независимо друг от друга или совместно:

public int Columns { get; set; }
public int Rows { get; set; }

Ниже описано, как свойства Rows и Columns влияют на алгоритм размещения дочерних элементов:

Для реализации описанного выше поведения панель UniformGrid отслеживает реальное количество столбцов и строк. Оно равно свойствам Columns и Rows, если они установлены. В противном случае для подсчета дочерних элементов и определения размеров панели применяется пользовательский метод CalculateColumns(). Он может быть вызван на первом этапе размещения:

private int realColumns;
private int realRows;

private void CalculateColumns()
{
            // Подсчет элементов; если элементов нет, 
            // метод завершается
            double elementCount = this.Children.Count; 
            if (elementCount == 0) return;

            realRows = Rows; 
            realColumns = Columns;

            // Использование свойств, если они установлены 
            if ((realRows != 0) && (realColumns != 0)) return;

            // Если ни одно свойство не установлено,
            // вычисляется количество столбцов 
            if ((realColumns == 0) && realRows == 0) 
                realColumns = (int)Math.Ceiling(Math.Sqrt(elementCount));

            // Если установлено только свойство Rows
            // вычисляется количество столбцов 
            if (realColumns == 0) 
                realColumns = (int)Math.Ceiling(elementCount/realRows);

            // Если установлено только свойство Columns
            // вычисляется количество строк 
            if (realRows == 0) realRows =
                (int)Math.Ceiling(elementCount / realColumns);
}

Silverlight начинает процесс размещения, вызвав метод MeasureOverride() панели UniformGrid. Этот метод должен в свою очередь вызвать метод, вычисляющий количество столбцов, и поделить доступное пространство поровну между одинаковыми ячейками:

protected override Size MeasureOverride(Size availableSize)
{
            CalculateColumns();

            // Предоставление пространства ячейкам
            Size childConstraint = new Size(
                availableSize.Width / realColumns,
                availableSize.Height / realRows);
            ...

Теперь нужно измерить дочерние элементы. Необходимо учитывать, что метод Measure() может вернуть большее значение, если минимальный размер больше, чем помещается в доступном пространстве. Панель UniformGrid отслеживает наибольшие значения запрашиваемых ширины и высоты. И наконец, по завершении процесса измерения панель вычисляет размеры пространства, необходимого для размещения элемента с максимальными шириной и высотой. Метод MeasureOverride() возвращает запрашиваемые размеры:

...
            // Отслеживание максимальных запрашиваемых размеров 
            Size largestCell = new Size();
            
            // Проверка всех дочерних элементов
            foreach (UIElement child in this.Children) 
            {
                // Получение желаемого размера для элемента 
                child.Measure(childConstraint);
                
                // Запись максимального запрашиваемого размера 
                largestCell.Height = Math.Max(largestCell.Height,
                    child.DesiredSize.Height); 
                largestCell.Width = Math.Max(largestCell.Width, 
                    child.DesiredSize.Width);
            }
            
            // Получение максимальных запрашиваемых размеров 
            // и вычисление максимальных размеров панели 
            return new Size(largestCell.Width * realColumns, largestCell.Height * realRows);
}

Код метода ArrangeOverride() решает аналогичную задачу. Однако он не измеряет дочерние элементы. Вместо этого он получает окончательную разметку пространства, вычисляет размеры ячеек и позиционирует каждый дочерний элемент в соответствующих границах. Если достигнут конец панели, но еще остались дочерние элементы (это происходит, только если контейнер элемента управления установил лимитирующие значения Rows и Columns), они передаются контейнеру размером 0x0, который скрывает их:

protected override Size ArrangeOverride(Size finalSize)
{
            // Вычисление размеров каждой ячейки
            double cellWidth = finalSize.Width /realColumns;
            double cellHeight = finalSize.Height / realRows;
            
            // Определение позиции каждой ячейки 
            Rect childBounds = new Rect(0, 0, cellWidth, cellHeight);
            
            // Проверка всех элементов
            foreach (UIElement child in this.Children)
            {
                // Позиционирование дочерних элементов 
                child.Arrange(childBounds);

                // Перемещение границ в следующую позицию 
                childBounds.X += cellWidth;
                if (childBounds.X >= cellWidth * realColumns)
                {
                    // Переход к следующей строке
                    childBounds.Y += cellHeight;
                    childBounds.X = 0;
                }

                // Если элементов больше, чем ячеек,
                // дополнительные элементы скрываются
                if (childBounds.Y >= cellHeight * realRows)
                    childBounds = new Rect(0, 0, 0, 0);
            }

            return finalSize;
}

Применить панель UniformGrid несложно. Нужно лишь отобразить пространство имен в разметку XAML и определить UniformGrid таким же образом, как и любой другой контейнер. Ниже приведен пример размещения панели UniformGrid на панели StackPanei с дополнительным текстовым содержимым, позволяющим проверить, правильно ли вычислены размеры панели, и убедиться в том, что последующее содержимое не наслаивается на панель:

<UserControl x:Class="SilverlightTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SilverlightTest">

    <StackPanel Background="White">
        <TextBlock Margin="5" Text="Содержимое над UniformGrid" />
        <local:UniformGrid Margin="5" Background="LimeGreen">
            <Button Height="20" Content="Короткая кнопка" />
            <Button Width="150" Content="Длинная кнопка" />
            <Button Width="80" Height="40" Content="Кнопка" />
            <TextBlock Margin="5" Text="Здесь размещен текст ячейки" TextWrapping="Wrap" Width="80" />
            <Button Width="120" Height="30" Content="Короткая кнопка" />
            <TextBlock Margin="5" Text="Дополнительный текст" VerticalAlignment="Center"/>
            <Button Content="Безразмерная кнопка" />
        </local:UniformGrid>
        <TextBlock Margin="5" Text="Содержимое под UniformGrid" />
    </StackPanel>
</UserControl>

На рисунке ниже показан вывод приведенной выше разметки. Задавая разные размеры дочерних элементов UniformGrid, можно проверить, как работает панель. Например, в первой кнопке (Короткая кнопка) жестко закодировано свойство Height. В результате этого ее высота ограничена, а ширина автоматически становится равной ширине ячейки. Во второй кнопке (Длинная кнопка) жестко закодировано свойство Width. Это наиболее широкий элемент в UniformGrid, поэтому ее ширина определяет ширину ячеек всей таблицы. В результате ширина и высота ячейки точно совпадают с размерами кнопки Безразмерная кнопка — обе заполняют все доступное пространство ячейки. Больше всего пространства по вертикали занимают текстовые строки элемента TextBlock, поэтому они определяют высоту всех ячеек панели:

Панель UniformGrid
Пройди тесты
Лучший чат для C# программистов