Элемент StackPanel в WinRT

91

Класс Panel и классы, производные от него, образуют ядро системы формирования макета Windows Runtime. Panel определяет лишь несколько собственных свойств, но одно из них - Children - играет исключительно важную роль. Из всех элементов только классы, производные от Panel, поддерживают множественных потомков. Ниже показана часть иерархии классов с Panel и некоторыми производными классами:

Object
    DependencyObject
        UIElement
            FrameworkElement
                Panel
                    Canvas
                    Grid
                    StackPanel
                    VariableSizedWrapGrid

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

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

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
     <StackPanel>
            <TextBlock Text="Выравненный справа текст"
                       FontSize="48"
                       HorizontalAlignment="Right" />

            <Image Source="http://professorweb.ru/my/windows8/rt/level1/files/win8logo.png"
                   Stretch="None" />

            <TextBlock Text="Рисунок 1. Логотип операционной системы Windows 8"
                       FontSize="24"
                       HorizontalAlignment="Center" />

            <Ellipse Stroke="LimeGreen"
                     StrokeThickness="12" 
                     Fill="LightBlue" />

            <TextBlock Text="Выравненный слева текст"
                       FontSize="36"
                       HorizontalAlignment="Left" />
     </StackPanel>
</Grid>

В разметке XAML потомки StackPanel просто перечисляются в том порядке, в котором они должны выводиться на экране:

Вывод нескольких элементов в панели StackPanel

Обратите внимание: я назначил StackPanel потомком Grid. Панели могут быть вложенными - более того, они очень часто бывают вложенными. В этом конкретном случае я мог бы заменить Grid элементом StackPanel и задать для него то же свойство Background. Каждому элементу, находящемуся в StackPanel, выделяется столько высоты, сколько ему необходимо, но зато он может растягиваться по полной ширине панели (как первый и последний элементы TextBlock, выровненные по левому и правому краю). В StackPanel с вертикальным размещением элементов настройки VerticalAlignment потомков роли не играют и потому игнорируются.

Свойству Stretch элемента Image задано значение None, чтобы изображение выводилось со своими размерами. Если оставить значение по умолчанию Uniform, изображение будет растянуто на всю ширину StackPanel (которая совпадает с шириной Page), с пропорциональным увеличением вертикального размера. Это может привести к тому, что все элементы, расположенные ниже Image, будут вытеснены с экрана.

В разметке XAML также присутствует элемент Ellipse. Что с ним произошло? Как и всем остальным потомкам StackPanel, элементу Ellipse выделяется столько вертикального пространства, сколько реально необходимо. А так как реальной необходимости нет, эллипс уменьшается до полного исчезновения. Если вы хотите, чтобы эллипс был видим, по крайней мере присвойте ему ненулевое значение Height - например, 72.

Отображение фигуры в панели StackPanel

Если также задать свойству Stretch элемента Ellipse значение Uniform, вместо очень широкого эллипса вы получите окружность.

Элемент StackPanel занимает целую страницу. Почему я в этом уверен? Экспериментируя с панелями, очень полезно присваивать каждому элементу уникальный фон (Background), чтобы было видно, какую часть экрана он занимает. Например:

<StackPanel Background="DarkOrange">

Как и другие классы, производные от FrameworkElement, StackPanel также поддерживает свойства HorizontalAlignment и VerticalAlignment. Если этим свойствам задаются значения, отличные от значений по умолчанию, StackPanel более тесно прилегает к своим потомкам; изменения могут оказаться весьма заметными. Вот как выглядит то же приложение, если задать свойству Background элемента StackPanel значение DarkOrange, а свойствам HorizontalAlignment и VerticalAlignment - значение Center:

Подсветка фона у панели StackPanel

Ширина StackPanel теперь определяется максимальной шириной потомка.

Горизонтальное размещение

Также элемент StackPanel можно использовать для горизонтального размещения элементов; для этого его свойству Orientation задается значение Horizontal. Следующая программа демонстрирует пример такого рода:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel Orientation="Horizontal"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center">

            <TextBlock Text="Прямоугольник: "
                       VerticalAlignment="Center" />

            <Rectangle Fill="Red"
                       Width="72"
                       Height="72"
                       Margin="20 0"
                       VerticalAlignment="Center" />

            <TextBlock Text="Эллипс: "
                       VerticalAlignment="Center" />

            <Ellipse Stroke="LimeGreen"
                     StrokeThickness="3"
                     Fill="DarkOrange"
                     Width="72"
                     Height="72"
                     Margin="20 0"
                     VerticalAlignment="Center" />

            <TextBlock Text="Картинка: "
                       VerticalAlignment="Center" />

            <Image Source="http://professorweb.ru/my/windows8/rt/level1/files/win8logo.png" 
                   Stretch="Uniform"
                   Width="96"
                   Margin="20 0"
                   VerticalAlignment="Center" />

    </StackPanel>
</Grid>

Результат:

Использование панели StackPanel с горизонтальной ориентацией

На первый взгляд количество настроек выравнивания кажется излишним. Но если вы попытаетесь удалить настройки VerticalAlignment и HorizontalAlignment, результат будет выглядеть так:

Нарушении компоновки в StackPanel при изменении выравнивания панели

StackPanel сейчас занимает всю страницу, и каждый отдельный элемент занимает полную высоту StackPanel. Элемент TextBlock выравнивается по верхнему краю, а остальные элементы выравниваются по центру. Задание свойствам HorizontalAlignment и VerticalAlignment элемента Panel значения Center сужает пространство, занимаемое панелью, и перемещает ее в центр.

Нарушении компоновки в StackPanel при изменении выравнивания элементов внутри панели

Высота StackPanel сейчас определяется максимальной высотой потомка, но все элементы растягиваются до этой высоты. Чтобы выровнять все элементы по общей центральной оси, проще всего присвоить их свойствам VerticalAlignment значение Center.

Программа WhatSize с привязками и конвертером значений

В статье "События изменения размера и ориентации" мы рассмотрели создание программы WhatSize и я упоминал о том, что программа не может использовать привязки данных, потому что свойство Text элемента Run не является свойством зависимости. Только свойства зависимости могут быть приемниками в привязках данных.

К счастью, для отдельных строк текста объекты Run можно имитировать при помощи нескольких элементов TextBlock, размещенных по горизонтали на панели StackPanel. Давайте доработаем эту программу:

<Page ...
    FontSize="32" 
    Foreground="LimeGreen"
    x:Name="myPage">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Top">
            <TextBlock Text="&#x21A4; " />
            <TextBlock Text="{Binding ElementName=myPage, Path=ActualWidth}" />
            <TextBlock Text=" пикселей &#x21A6;" />
        </StackPanel>

        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center">
            <TextBlock Text="&#x21A5;" TextAlignment="Center" />

            <StackPanel Orientation="Horizontal"
                        HorizontalAlignment="Center">
                <TextBlock Text="{Binding ElementName=myPage, Path=ActualHeight}" />
                <TextBlock Text=" пикселей" />
            </StackPanel>

            <TextBlock Text="&#x21A7;" TextAlignment="Center" />
        </StackPanel>
    </Grid>
</Page>

Обратите внимание: корневому элементу теперь присваивается имя myPage, используемое в двух привязках данных для получения свойств ActualWidth и ActualHeight. Большое преимущество этой версии перед предыдущей заключается в том, что в ней уже не нужен обработчик события в файле программной логики. А вот как выглядит результат:

Получение размеров окна только с помощью разметки

Эти привязки данных автоматически преобразуют значения double в объекты string. Но что, если вы захотите, чтобы преобразование выполнялось немного иначе? Например, если результат должен содержать определенное количество разрядов в дробной части? Или, что более актуально для нашего примера, группы разрядов должны разделяться запятыми (1,366)?

Преобразование данных в ходе привязки можно настроить, передав объекту Binding небольшой фрагмент программного кода. У класса Binding имеется свойство Converter типа IValueConverter - интерфейса, содержащего два метода: Convert (для преобразования от источника привязки к приемнику) и ConvertBack (для преобразования от приемника обратно к источнику в двусторонних привязках).

Чтобы создать нестандартный преобразователь, следует создать класс, реализующий IValueConverter, и заполнить код двух методов. В следующем примере эти методы не делают ничего:

public class EmptyConverter : IValueConverter
{
    public object Convert(object value, Type targetType, 
        object parameter, string language)
    {
        return value;
    }

    public object ConvertBack(object value, Type targetType, 
        object parameter, string language)
    {
        return value;
    }
}

Если привязка будет использоваться только в одностороннем режиме, метод ConvertBack можно не реализовывать. В методе Convert аргумент value содержит значение, поступившее от источника привязки. В примере это значение имеет тип double. TargetType определяет тип приемника (string в примере WhatSize).

Если вы пишете преобразователь привязки специально для примера WhatSize (преобразование вещественных значений в строки с разделителями разрядов и без дробной части), метод Convert может быть совсем простым:

public object Convert(object value, Type targetType, 
    object parameter, string language)
{
    return ((double)value).ToString();
}

Но чаще преобразователи привязок пишутся в более общем виде. Например, преобразователь может обрабатывать аргументы value любого типа, реализующего интерфейс IFormattable (сюда относятся как double, так и другие числовые типы и DateTime). Интерфейс IFormattable определяет метод ToString() с двумя аргументами: форматной строкой и объектом, реализующим IFormatProvider (обычно объект CultureInfo).

Кроме value и targetType, метод Convert также получает аргументы parameter и language. Они берутся из двух свойств класса Binding, которые называются ConverterParameter и ConverterLanguage, и обычно задаются прямо в файле XAML. Это означает, что спецификация форматирования для ToString() может передаваться в аргументе parameter метода Convert, а объект CultureInfo может быть создан на основе аргумента language. Одна из возможных реализаций может выглядеть так:

using System;
using System.Globalization;
using Windows.UI.Xaml.Data;

namespace WinRTTestApp
{
    public class FormattedStringConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
            object parameter, string language)
        {
            if (value is IFormattable && parameter is string &&
                !String.IsNullOrEmpty(parameter as string) &&
                targetType == typeof(string))
            {
                if (String.IsNullOrEmpty(language))
                    return (value as IFormattable).ToString((parameter as string), null) + " пикселей";

                return (value as IFormattable).ToString((parameter as string),
                                                        new CultureInfo(language)) + " пикселей";
            }

            return value;
        }

        public object ConvertBack(object value, Type targetType, 
            object parameter, string language)
        {
            return value;
        }
    }
}

Метод Convert использует ToString только при выполнении нескольких условий. Если условия не выполняются, то метод просто возвращает значение аргумента value.

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

<Page ...
    FontSize="36"
    Foreground="LimeGreen"
    x:Name="myPage">
    
    <Page.Resources>
        <local:FormattedStringConverter x:Key="stringConverter" />
    </Page.Resources>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Top">
            <TextBlock Text="&#x21A4; " />
            <TextBlock Text="{Binding ElementName=myPage, Path=ActualWidth,
                       Converter={StaticResource stringConverter},
                       ConverterParameter=N0}" />
            <TextBlock Text=" &#x21A6;" />
        </StackPanel>

        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center">
            <TextBlock Text="&#x21A5;" TextAlignment="Center" />

            <StackPanel Orientation="Horizontal"
                        HorizontalAlignment="Center">
                <TextBlock Text="{Binding ElementName=myPage, Path=ActualHeight,
                       Converter={StaticResource stringConverter},
                       ConverterParameter=N0}" />
            </StackPanel>

            <TextBlock Text="&#x21A7;" TextAlignment="Center" />
        </StackPanel>
    </Grid>
</Page>

Обратите внимание на то, что расширение разметки Binding содержит встроенное расширение разметки StaticResource для ссылки на ресурс преобразователя. Ни в одном из расширений разметки кавычки не используются. После форматирования ширина выглядит чуть более эффектно:

Форматирование привязанных данных с помощью конвертера
Пройди тесты
Лучший чат для C# программистов