Создание собственного контейнера

0

Всем привет. Сегодня я опишу, как создать свой контейнер и покажу несколько примеров.

Понятно, что в большинстве случаев свой контейнер и не нужен. И тех, что предоставляет нам WPF вполне достаточно. Но, чтобы понять, как устроены некоторые вещи в WPF, порекомендовал бы всем реализовать, что то подобное.

Да и, наверное, каждый, кто только начинает изучать WPF, ознакомившись со стандартными контейнерами, загорается желанием сделать свой собственный, очень важный и нужный контейнер. По крайней мере, у меня такое желание появилось сразу же )). Но тогда я даже не знал что мой «уникальный» контейнер должен делать, не говоря уже о том, как его реализовать. 

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

Но потом у меня все же появился повод, что бы написать свой контейнер. Мне нужно было создать полноэкранную галерею в metro стиле. Такая себе имитация (по внешнему виду) Store приложения для Windows 7. Хотелось сделать все красиво и поэтому возможности стандартных контейнеров не подошли. Нужен был особый контейнер для плиточного расположения изображений. 

«Ну и повод. Зачем для этого  создавать свой контейнер, если можно просто правильно использовать WrapPanel?». - скорее всего подумаете вы. И будете правы. ) 

Первой причиной могу назвать все-таки свой давний интерес. А второй - выглядеть это будет не так как могло бы с помощью обычного WrapPanel. Увидите.

И все же WrapPanel почти то, что мне нужно, поэтому от него я и унаследуются. Назовем его, например, WrapAutoPanel:

public class WrapAutoPanel : WrapPanel {
    public static readonly DependencyProperty ItemsInLineProperty = DependencyProperty.Register(
            "ItemsInLine", 
            typeof(int),
            typeof(WrapAutoPanel),
            new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsMeasure),
            new ValidateValueCallback(WrapAutoPanel.IsItemsInLineValid));

    public int ItemsInLine {
        get {
            return (int)GetValue(ItemsInLineProperty);
        }
        set {
            SetValue(ItemsInLineProperty, value);
        }
    }

    private static bool IsItemsInLineValid(object value) {
        return ((int)value) > 0;
    }
}

Теперь, когда WrapAutoPanel обладает всеми возможностями WrapPanel, можно создавать свой функционал. Я добавлю свойство зависимостей ItemsInLine. Ему я буду задавать количество элементов в ряду, горизонтальном или вертикальном.

  • public static readonly DependencyProperty ItemsInLineProperty – собственно объявление свойства зависимости;
  • DependencyProperty.Register(...); - это метод, который регистрирует свойство зависимости. Он перегружен и не единственный метод для регистрации.

Думаю, что делает остальной код, не сложно будет догадаться, если просто посмотреть описание. Слава и хвала IntelliSense :-)

 

Теперь нужно переопределить два очень важных метода – MeasureOverride и ArrangeOverride:

protected virtual Size MeasureOverride(Size availableSize) - метод предназначенный для измерения размеров панели и дочерних элементов.

Все зависит от вашей собственной реализации, но думаю нужно обратить внимание на следующее:

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

Полный код моей реализации MeasureOverride:

protected override Size MeasureOverride(Size constraint) {
    int itemsInLine = this.ItemsInLine;
    bool horizontal = (this.Orientation == Orientation.Horizontal);
    double itemWidth = this.ItemWidth;
    double itemHeight = this.ItemHeight;
    double h = 0.0, w = 0.0;

    Size max = new Size(0.0, 0.0);
    Size availableSize = new Size();

    UIElementCollection childs = base.InternalChildren;
    UIElement elem = null;

    if (horizontal) {
        availableSize = new Size(
            !double.IsNaN(itemWidth) ? itemWidth : constraint.Width / itemsInLine, 
            !double.IsNaN(itemHeight) ? itemHeight : constraint.Height);
    }
    else {
        availableSize = new Size(
            !double.IsNaN(itemWidth) ? itemWidth : constraint.Width, 
            !double.IsNaN(itemHeight) ? itemHeight : constraint.Height / itemsInLine);
    }

    for (int i = 0, count = childs.Count; i < itemsInLine; i++) {
        for (int j = i; j < count; j += itemsInLine) {
            if((elem = childs[j]) != null) {
                elem.Measure(availableSize);
                if (horizontal) {
                    w += elem.DesiredSize.Width; 
                }
                else {
                    h += elem.DesiredSize.Height;
                }
            }
        }
        if (horizontal) {
            max.Width = Math.Max(max.Width, w);
            w = 0.0;
        }
        else {
            max.Height = Math.Max(max.Height, h);
            h = 0.0;
        }
    }
    return max;
}

В этом примере идет пробег по дочерним элементам, и выделяется им место на панели. Потом определяется максимальная ширина/высота панели в зависимости от свойства Orientation.

Основные моменты, которые думаю, стоит рассказать:

  • base.InternalChildren – возвращает коллекцию дочериных элементов. Хотя оно и protected, но также и internal, поэтому переопределить его не получится.
  • Метод Measure. Этот метод определяет размер по параметру, и записывает эго в DesiredSize элемента. Даже если availableSize буде точно не известный, Measure определит размер элемента.
    Например, так можно узнать полный размер элемента:
    UIElement elem = InternalChildren[0];
    elem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
    Теперь, в elem.DesiredSize указан полный размер.
  • Метод Measure обязателен. Он может, вызывается не один раз, например, что бы определить конечный размер элемента. Но если Measure ни разу не вызывался, то элемент не будет отрисован. Это можно проверить, посмотрев свойство элемента IsMeasureValid.
    Полноценное описание могло бы потянуть на целую статью, но если вкратце -  как-то так.
  • Свойства ItemWidth/ItemHeight могут быть и не заданы, а их значение по умолчанию double.NaN. Поэтому такие вещи лучше проверять.           

protected virtual Size ArrangeOverride(Size finalSize) – метод, который располагает дочерние элементы на панели.

Осталось его реализовать:

protected override Size ArrangeOverride(Size finalSize) {
    int itemsInLine = ItemsInLine;
    bool horizontal = (this.Orientation == Orientation.Horizontal);
    double x=0.0, y=0.0, w=0.0, h=0.0;

    UIElementCollection childs = base.InternalChildren;
    UIElement elem = null;
            
    for (int i = 0, count = childs.Count; i < itemsInLine; i++) {
        for (int j = i; j < count; j += itemsInLine) {
            if((elem = childs[j]) != null) {
                w = elem.DesiredSize.Width;
                h = elem.DesiredSize.Height;
                elem.Arrange(new Rect(x, y, w, h));
                if (horizontal) {
                    x += w;
                }
                else {
                    y += h;
                }
            }
        }
        if (horizontal) {
            x = 0.0;
            y += h;
        }
        else {
            y = 0.0;
            x += w;
        }
    }
    return finalSize;
}

Здесь тоже идет пробег по элементам, причем алгоритм по сути тот же. Единое, на что могу обратить ваше внимание метод Arrange. Как видите Arrange принимает Rect, и вы уже догадались зачем.


Ура! WrapAutoPanel готов. Можно испробовать его в действии. Осталось только написать разметку в xaml.Например, такую:

<Window x:Class="UserContainer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:y="clr-namespace:UserContainer"
        Title="MainWindow">
    <Grid>        
        <y:WrapAutoPanel ItemsInLine="2" Orientation="Horizontal" />
    </Grid>
</Window>

Я решил не писать здесь, реальный xaml пример, а показать вам нормальную демонстративную версию, того что получилось. А с ItemWidth/ItemHeight уже поэкспериментируете сами. 

Next