Элемент Grid в WinRT

107

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

Элемент Grid внешне напоминает таблицу HTML, но между ними существуют важные различия. У Grid нет средств определения границ или полей отдельных ячеек; элемент предназначен исключительно для построения макета. Все «украшения», относящиеся к представлению информации, должны применяться в родительских элементах или потомках: Grid можно поместить в Border, а элементы Border могут использоваться для оформления содержимого отдельных ячеек Grid.

Количество строк и ячеек Grid всегда задается явно; Grid не может определить информацию по количеству потомков. Потомки Grid обычно находятся в конкретной ячейке, то есть на пересечении строки и столбца, но они также могут занимать несколько строк и столбцов.

Хотя количество строк и столбцов может изменяться в программном коде время выполнения, делается это нечасто. Гораздо чаще количество строк и столбцов фиксируется в файле XAML. Для этого объекты типов RowDefinition и ColumnDefinition добавляются в коллекции RowDefinitions и ColumnDefinitions, определяемые классом Grid.

Размер каждой строки и столбца может определяться одним из трех способов:

В XAML для заполнения коллекций RowDefinitions и ColumnDefinitions используется синтаксис элементов свойств, поэтому типичное определение Grid выглядит так:

<Grid>

    <Grid.RowDefinitions>

        <RowDefinition Height="120" />

        <RowDefinition Height="Auto" />

        <RowDefinition Height="*" />

    </Grid.RowDefinitions>

    

    <Grid.ColumnDefinitions>

        <ColumnDefinition Width="80" />

        <ColumnDefinition Width="10*" />

        <ColumnDefinition Width="20*" />

        <ColumnDefinition Width="Auto" />

    </Grid.ColumnDefinitions>

    

    <!-- Потомки -->

</Grid>

Обратите внимание: свойства коллекций Grid называются RowDefinitions и ColumnDefinitions (в множественном числе), но содержат они объекты RowDefinition и ColumnDefinition (единственное число). Для элементов Grid, содержащих только одну строку или один столбец, свойства RowDefinitions и ColumnDefinitions задавать необязательно.

Элемент Grid из нашего примера состоит из трех строк и четырех столбцов. Он демонстрирует различные способы определения размеров строк и столбцов. Числа обозначают ширину (или высоту) в пикселах. Явное задание высоты строк и ширины столбцов используется реже двух других вариантов.

Слово Auto означает, что вычисленная высота строки (или ширина столбца) определяется максимальной высотой (или шириной) потомков этой строки (или столбца).

Как и в HTML, звездочка приказывает Grid выделить все доступное пространство. В нашем примере высота третьей строки вычисляется вычитанием высоты первой и второй строки из общей высоты Grid. Для вычисления ширин второго и третьего столбцов, ширины первого и четвертого столбцов вычитаются из общей ширины Grid. Числа перед звездочками задают пропорции; в данном случае ширина третьего столбца должна быть вдвое больше ширины второго столбца.

Звездочки применимы только в том случае, если размер Grid определяется родителем! Например, допустим, Grid является потомком StackPanel с вертикальной ориентацией. Элемент StackPanel предоставляет Grid неограниченную бесконечную высоту. Как Grid выделит эту бесконечную высоту своей средней строке? Никак. Спецификация со звездочкой вырождается в Auto.

Аналогичным образом, если Grid является потомком Canvas, a Grid не имеет явно заданных значений Height и Width, все спецификации со звездочкой вырождаются в Auto. То же самое происходит и с элементом Grid, у которого свойства HorizontalAlignment и VerticalAlignment отличны от значения по умолчанию Stretch. В приведенном ранее примере с Grid второй столбец может оказаться шире третьего, если этого потребуют размеры потомков в этих столбцах.

С другой стороны, при отсутствии объектов RowDefinition со звездочкой высота Grid определяется потомками. Элемент Grid может быть помещен в вертикальный элемент StackPanel или Canvas; ему можно присвоить значение VerticalAlignment отличное от значения по умолчанию - ничего страшного не произойдет.

Свойство Height объекта RowDefinition и свойство Width объекта ColumnDefinition относятся к типу GridLength - структуры, определенной в Windows.UI.Xaml и позволяющей задавать размеры Auto или размеры со звездочкой из кода. RowDefinition также определяет свойства MinHeight и MaxHeight, a ColumnDefinition определяет MinWidth и MaxWidth. Все эти свойства относятся к типу double и определяют минимальный и максимальный размеры в пикселах. Фактические размеры можно получить при помощи свойства ActualHeight класса RowDefinition и свойства ActualWidth класса ColumnDefinition.

Класс Grid также определяет четыре вложенных свойства, которые задаются для потомков Grid: Grid.Row и Grid.Column имеют значение по умолчанию 0, a Grid.RowSpan и Grid.ColumnSpan имеют значение по умолчанию 1. Так задается ячейка, в которой находится конкретный потомок и количество занимаемых им строк и столбцов. Ячейка может содержать более одного элемента.

В ячейки Grid можно вкладывать элементы Grid или другие панели, но вложение может замедлить формирование макета; будьте внимательны, если размер элемента с большим уровнем вложенности изменяется в ходе анимации или если с коллекциями Children часто выполняются операции добавления и удаления. Конечно, макет страницы не должен пересчитываться с частотой обновления экрана!

Ранее я представил версию WHATSIZE (первой программы, приведенной в журнальной статье по Windows-программированию) для Windows 8. Третья статья по Windows-программированию была опубликована в выпуске Microsoft Systems Journal за май 1987 года; в ней была представлена программа COLORSCR («Color Scroll»). Пользователь перемещает ползунки, задавая интенсивность красной, зеленой и синей составляющей, а справа отображается результат. (В те дни графические дисплеи обычно не обеспечивали вывода полного цветового диапазона, и для приближенного воспроизведения недостающих цветов использовалось смешение (dithering).) Под каждой полосой прокрутки также отображалось текущее значение. Программа поддерживала довольно примитивный метод динамического формирования макета, изменяя ширину полос прокрутки при измерении размеров окна.

Похоже, эта программа подходит для демонстрации простого использования Grid. Так как для нее потребуются шесть экземпляров TextBlock и три экземпляра Slider, файл XAML начинается с определения двух неявных стилей:

<Page.Resources>

    <Style TargetType="TextBlock">

        <Setter Property="Text" Value="00" />

        <Setter Property="FontSize" Value="24" />

        <Setter Property="HorizontalAlignment" Value="Center" />

        <Setter Property="Margin" Value="0 12" />

    </Style>



    <Style TargetType="Slider">

        <Setter Property="Maximum" Value="255" />

        <Setter Property="Orientation" Value="Vertical" />

        <Setter Property="IsDirectionReversed" Value="True" />

        <Setter Property="HorizontalAlignment" Value="Center" />

    </Style>

</Page.Resources>

Я решил выводить текущее значение каждого ползунка в шестнадцатеричном виде, поэтому определение стиля для TextBlock инициализирует свойство Text значением «00» - шестнадцатеричным значением, соответствующем минимальному положению Slider. Определение Grid начинается с определения трех строк (для каждого элемента управления Slider и для двух надписей TextBlock) и четырех столбцов. Обратите внимание: первые три столбца имеют одинаковую ширину, а четвертый столбец в три раза шире:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">

    <Grid.ColumnDefinitions>

        <ColumnDefinition Width="*" />

        <ColumnDefinition Width="*" />

        <ColumnDefinition Width="*" />

        <ColumnDefinition Width="3*" />

    </Grid.ColumnDefinitions>



    <Grid.RowDefinitions>

        <RowDefinition Height="Auto" />

        <RowDefinition Height="*" />

        <RowDefinition Height="Auto" />

    </Grid.RowDefinitions>



    ...

</Grid>

В оставшейся разметке XAML создаются экземпляры 10 потомков Grid. У каждого потомка задаются значения вложенных свойств Grid.Row и Grid.Column. Задавая атрибуты потомков Grid, я обычно размещаю вложенные свойства в начале, но после хотя бы одного атрибута, обеспечивающего быструю визуальную идентификацию элемента (такого, как Name или Text):

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">

    ...



    <!-- Красный ползунок -->

    <TextBlock Text="R"

               Grid.Column="0"

               Grid.Row="0"

               Foreground="Red" />



    <Slider Name="redSlider"

            Grid.Column="0"

            Grid.Row="1"

            Foreground="Red"

            ValueChanged="Slider_ValueChanged" />



    <TextBlock Name="redValue"

               Grid.Column="0"

               Grid.Row="2"

               Foreground="Red" />



    <!-- Зеленый ползунок -->

    <TextBlock Text="G"

               Grid.Column="1"

               Grid.Row="0"

               Foreground="Lime" />



    <Slider Name="greenSlider"

            Grid.Column="1"

            Grid.Row="1"

            Foreground="Lime"

            ValueChanged="Slider_ValueChanged" />



    <TextBlock Name="greenValue"

               Grid.Column="1"

               Grid.Row="2"

               Foreground="Lime" />



    <!-- Синий ползунок -->

    <TextBlock Text="B"

               Grid.Column="2"

               Grid.Row="0"

               Foreground="LightBlue" />



    <Slider Name="blueSlider"

            Grid.Column="2"

            Grid.Row="1"

            Foreground="LightBlue"

            ValueChanged="Slider_ValueChanged" />



    <TextBlock Name="blueValue"

               Grid.Column="2"

               Grid.Row="2"

               Foreground="LightBlue" />



    <!-- Результат -->

    <Rectangle Grid.Column="3"

               Grid.Row="0"

               Grid.RowSpan="3">

        <Rectangle.Fill>

            <SolidColorBrush x:Name="brushResult"

                             Color="Black" />

        </Rectangle.Fill>

    </Rectangle>

</Grid>

Обратите внимание: у всех элементов TextBlock и Slider свойству Foreground задаются значения, основанные на представляемом ими цвете.

Вложенное свойств Grid.RowSpan элемента Rectangle задается равным 3; это означает, что оно распространяется на все три строки. Свойству SolidColorBrush задается значение Black, соответствующее трем начальным значениям Slider. Если вам не удается правильно инициализировать все данные в файле XAML, обычно это следует делать в конструкторе из файла отделенного кода (или в обработчике события Loaded).

Все три элемента управления Slider используют общий обработчик события ValueChanged из файла отделенного кода:

public sealed partial class MainPage : Page

{

    public MainPage()

    {

        this.InitializeComponent();

    }



    private void Slider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)

    {

        byte r = (byte)redSlider.Value;

        byte g = (byte)greenSlider.Value;

        byte b = (byte)blueSlider.Value;



        redValue.Text = r.ToString("X2");

        greenValue.Text = g.ToString("X2");

        blueValue.Text = b.ToString("X2");



        brushResult.Color = Color.FromArgb(255, r, g, b);

    }

}

Обработчик события мог бы получить элемент управления Slider, инициировавший событие, преобразуя аргумент sender и получая новое значение из объекта RangeBaseValueChangedEventArgs. Но независимо от того, у какого элемента управления Slider изменилось текущее значение, обработчик события должен создать новое значение Color, а для этого нужны все три значения. Часть кода, в которой задаются все три значения Text (хотя изменяется только одно), выглядит неэффективно, но иначе нам пришлось бы обращаться к элементу TextBlock, связанному с конкретным элементом управления Slider, инициировавшим событие. Один из 16 777 216 возможных результатов выглядит так:

Использование Grid для сложной компоновки приложения

Ориентация и пропорции

Если запустить созданную программу на планшете, а потом развернуть его вертикально, макет начинает выглядеть немного странно. Более того, даже в альбомной ориентации режим Snap View может привести к перекрытию текстовых надписей. Если вы разрабатываете профессиональное приложение то должны обеспечить гибкую компоновку для разных ориентаций экрана и разрешений, чтобы пользователи оставили положительные отзывы о вашем программном продукте. В файл отделенного кода стоит включить логику изменения макета в зависимости от ориентации или пропорций экрана.

В этой конкретной программе задача значительно упростится, если разбить один элемент Grid на два, один из которых вложен в другой. Внутренний элемент Grid содержит три строки и три столбца для элементов TextBlock и элементов управления Slider. У внешнего элемента Grid всего два потомка: внутренний элемент Grid и Rectangle. В альбомном режиме внешний элемент Grid состоит из двух столбцов, а в книжном - из двух строк.

Внешний элемент Grid выглядит так:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"

      SizeChanged="Grid_SizeChanged">

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="*" />

            <ColumnDefinition x:Name="secondColDef" Width="*" />

        </Grid.ColumnDefinitions>



        <Grid.RowDefinitions>

            <RowDefinition Height="*" />

            <RowDefinition x:Name="secondRowDef" Height="0" />

        </Grid.RowDefinitions>



        <Grid Grid.Row="0"

              Grid.Column="0">



            <Grid.ColumnDefinitions>

                <ColumnDefinition Width="*" />

                <ColumnDefinition Width="*" />

                <ColumnDefinition Width="*" />

            </Grid.ColumnDefinitions>



            <Grid.RowDefinitions>

                <RowDefinition Height="Auto" />

                <RowDefinition Height="*" />

                <RowDefinition Height="Auto" />

            </Grid.RowDefinitions>



            <!-- Красный ползунок -->

            ...



        </Grid>



        <!-- Результат -->

        <Rectangle Name="rectangleResult"

                   Grid.Column="1"

                   Grid.Row="0">

            <Rectangle.Fill>

                <SolidColorBrush x:Name="brushResult"

                                 Color="Black" />

            </Rectangle.Fill>

        </Rectangle>

</Grid>

Коллекции RowDefinitions и ColumnDefinitions внешнего элемента Grid инициализированы для обоих вариантов ориентации: два столбца или две строки. В каждой коллекции второму элементу присвоено имя, чтобы к нему можно было обращаться из программного кода. Высота второй строки равна нулю, то есть исходная конфигурация предполагает альбомный режим.

Внутренний элемент Grid (содержащий элементы TextBlock и элементы управления Slider) всегда находится в первом столбце или первой строке:

<Grid Grid.Row="0"

      Grid.Column="0">

    ...

</Grid>

Задание атрибутов Grid.Row и Grid.Column в теге Grid выглядит немного странно, но они относятся к строкам и столбцам не текущего, а родительского элемента Grid. Значения по умолчанию этих вложенных свойств равны нулю, поэтому эти конкретные значения атрибутов не нужны.

Элемент Rectangle находится во втором столбце и первой строке:

<Rectangle Name="rectangleResult"

   Grid.Column="1"

   Grid.Row="0">

    ...

</Rectangle>

В этой версии программы Rectangle присвоено имя, поэтому вложенные свойства могут быть изменены из файла отделенного кода. Это происходит в обработчике события SizeChanged, назначенном внешнему элементу Grid:

private void Grid_SizeChanged(object sender, SizeChangedEventArgs e)

{

    // Альбомный режим

    if (e.NewSize.Width > e.NewSize.Height)

    {

        secondColDef.Width = new GridLength(1, GridUnitType.Star);

        secondRowDef.Height = new GridLength(0);



        Grid.SetColumn(rectangleResult, 1);

        Grid.SetRow(rectangleResult, 0);

    }

    // Книжный режим

    else

    {

        secondColDef.Width = new GridLength(0);

        secondRowDef.Height = new GridLength(1, GridUnitType.Star);



        Grid.SetColumn(rectangleResult, 0);

        Grid.SetRow(rectangleResult, 1);

    }

}

Этот код изменяет вторые определения RowDefinition и ColumnDefinition во внешнем элементе Grid. Оба определения относятся к элементу Rectangle, вложенные свойства которого изменяются таким образом, что он оказывается во втором столбце (для книжного режима) или второй строке (для альбомного режима).

Вот как выглядит программа в режиме Snap View:

Запуск программы в режиме Snap View

Мы еще вернемся к теме настройки окна программы для режима Snap View позже.

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