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

52

Клон Canvas

Самый быстрый способ понять работу двух методов, представленных в предыдущей статье — рассмотреть внутреннее устройство класса Canvas, который является простейшим контейнером компоновки. Чтобы создать собственную панель в стиле Canvas, нужно просто унаследовать класс от Panel и добавить методы MeasureOverride() и ArrangeOverride(), показанные ниже:

public class CanvasClone : System.Windows.Controls.Panel
{ ... }

Контейнер Canvas помещает дочерние элементы там, где они хотят разместиться, и выделяет им столько места, сколько им нужно. В результате нет необходимости вычислять, сколько доступного пространства следует выделить. Это делает метод MeasureOverride() чрезвычайно простым. Каждому дочернему элементу выделяется бесконечное пространство:

protected override Size MeasureOverride(Size availableSize)
{
      Size size = new Size(double.PositiveInfinity, double.PositiveInfinity);
      foreach (UIElement element in base.InternalChildren)
           element.Measure(size);
      return new Size();
}

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

Метод ArrangeOverride() не намного сложнее. Чтобы определить правильное местоположение каждого элемента. Canvas использует присоединенные свойства (Left, Right, Тор и Bottom). Как вы знаете (и увидите далее на примере WrapBreakPanel), присоединенные свойства реализованы двумя вспомогательными методами в определении класса: GetProperty() и SetProperty().

Рассматриваемый клон Canvas немного проще. Он имеет только два присоединенных свойства — Left и Тор (без излишних Right и Bottom). Ниже приведен код, используемый для размещения элементов:

protected override Size ArrangeOverride(Size finalSize)
        {
            foreach (UIElement element in base.InternalChildren)
            {
                double x = 0;
                double y = 0;
                double left = Canvas.GetLeft(element);
                if (!Double.IsNaN(left))
                    x = left;
                double top = Canvas.GetTop(element);
                if (!Double.IsNaN(top))
                    y = top;
                element.Arrange(new Rect(new Point(x, y), element.DesiredSize));
            }
            return finalSize;
        }

Улучшенная панель WrapPanel

Теперь, когда достаточно подробно изучена система панелей, стоит попробовать создать собственный контейнер компоновки, который добавит кое-что, отсутствующее в базовых панелях WPF. В этом разделе будет представлен пример, расширяющий возможности WrapPanel.

Панель WrapPanel выполняет простую функцию, которая оказывается весьма полезной. Она раскладывает свои дочерние элементы один за другим, переходя на следующую строку после заполнения текущей. В Windows Forms имеется подобный инструмент компоновки, называемый FlowLayoutPanel. В отличие от WrapPanel, панель FlowLayoutPanel обладает одной дополнительной возможностью — присоединенным свойством, которое могут использовать дочерние элементы для принудительного перевода строки. (Формально это не было присоединенным свойством, а свойством, добавленным поставщиком расширений, но эти две концепции являются аналогами.)

Хотя WrapPanel не обеспечивает такой возможности, добавить ее нетрудно. Все, что для этого понадобится — это пользовательская панель с добавленным необходимым присоединенным свойством. Ниже показан код класса WrapBreakPanel с добавленным присоединенным свойством LineBreakBeforeProperty. Установленное в true, это свойство вставляет немедленный перенос строки перед элементом:

public class WrapBreakPanel : Panel
    {
        public static DependencyProperty LineBreakBeforeProperty;

        static WrapBreakPanel()
        {
            FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();
            metadata.AffectsArrange = true;
            metadata.AffectsMeasure = true;
            LineBreakBeforeProperty = DependencyProperty.RegisterAttached("LineBreakBefore", typeof(bool), typeof(WrapBreakPanel), metadata);

        }
        ...
     }

Подобно любому свойству зависимости, LineBreakBefore определяется как статическое поле и регистрируется в статическом конструкторе класса. Единственное отличие в том, что вместо Register() используется метод RegisterAttached().

Объект FrameworkPropertyMetadata для свойства LineBreakBefore специально указывает на то, что оно затрагивает процесс компоновки. В результате при каждой установке этого свойства будет инициирован новый проход компоновки. Присоединенные свойства не помещаются в нормальные оболочки свойств, поскольку они не устанавливаются в классе, определяющем их. Вместо этого потребуется предоставить два статических метода, которые смогут использовать метод DependencyObject.SetValue() для установки этого свойства в любой произвольный элемент. Код, необходимый для свойства LineBreakBefore, выглядит так:

public static void SetLineBreakBefore(UIElement element, Boolean value)
{
    element.SetValue(LineBreakBeforeProperty, value);
}
public static Boolean GetLineBreakBefore(UIElement element)
{
    return (bool)element.GetValue(LineBreakBeforeProperty);
}

Единственное, что остается — принять во внимание это свойство при выполнении логики компоновки. Логика компоновки WrapBreakPanel основана на WrapPanel. Во время шага измерения элементы располагаются по строкам, так что при необходимости панель может вычислить размер общего пространства. Каждый элемент добавляется в текущую строку, если только он не слишком велик, чтобы уместиться ней, и не установлено свойство LineBreakBefore. Ниже приведен полный код метода MeasureOverride():

protected override Size MeasureOverride(Size constraint)
        {
            Size currentLineSize = new Size();
            Size panelSize = new Size();

            foreach (UIElement element in base.InternalChildren)
            {
                element.Measure(constraint);
                Size desiredSize = element.DesiredSize;

                if (GetLineBreakBefore(element) ||
                    currentLineSize.Width + desiredSize.Width > constraint.Width)
                {
                    // Перейти на новую строку
                    panelSize.Width = Math.Max(currentLineSize.Width, panelSize.Width);
                    panelSize.Height += currentLineSize.Height;
                    currentLineSize = desiredSize;

                    // Если элемент слишком широк, чтобы поместиться в строку, выделить для него новую строку
                    if (desiredSize.Width > constraint.Width)
                    {
                        panelSize.Width = Math.Max(desiredSize.Width, panelSize.Width);
                        panelSize.Height += desiredSize.Height;
                        currentLineSize = new Size();
                    }
                }
                else
                {
                    currentLineSize.Width += desiredSize.Width;

                    currentLineSize.Height = Math.Max(desiredSize.Height, currentLineSize.Height);
                }
            }
            
            // Вернуть размер, необходимый для размещения всех элементов
            panelSize.Width = Math.Max(currentLineSize.Width, panelSize.Width);
            panelSize.Height += currentLineSize.Height;
            return panelSize;
 }

Ключевая деталь приведенного кода — проверка свойства LineBreakBefore. Это реализует дополнительную логику, которая не обеспечивается обычной панелью WrapPanel.

Код ArrangeOverride() почти такой же, но несколько более утомительный. Отличие в том, что панель должна определить максимальную высоту строки (которая определяется по самому высокому элементу), прежде чем начать компоновку строки. Таким образом, каждый элемент получает полный объем доступного пространства, принимая во внимание полную высоту строки. Это тот же процесс, который применяется в компоновке обычной WrapPanel.

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

<lib:WrapBreakPanel>
                <Button Padding="10">Кнопка без переноса</Button>
                <Button Padding="10">Кнопка без переноса</Button>
                <Button Padding="10">Кнопка без переноса</Button>
                <Button Padding="10">Кнопка без переноса</Button>
                <Button Padding="10" lib:WrapBreakPanel.LineBreakBefore="True">Используем специальный перенос</Button>
                <Button Padding="10">Кнопка без переноса</Button>
                <Button Padding="10">Кнопка без переноса</Button>
            </lib:WrapBreakPanel>
Нестандартная панель WrapPanel
Пройди тесты
Лучший чат для C# программистов