Синтаксис XAML

106

Приложение Windows 8 разделено на программный код и разметку, потому что у каждого аспекта имеются свои преимущества. Несмотря на ограниченность разметки XAML в области сложной логики или вычислительных задач, полезно вынести в нее как можно большую часть программы. Разметка проще редактируется и дает более четкое представление о визуальной структуре страницы. Конечно, все данные в разметках являются строковыми, поэтому представление сложных объектов в разметке иногда выглядит громоздко. Так как в разметке нет конструкции цикла, стандартной для языков программирования, она также в большей степени подвержена повторениям.

Эти проблемы были учтены в синтаксисе XAML; самые важные решения рассматриваются в этой и следующих статьях. Давайте начнем обсуждение этой важнейшей темы с вопроса, который на первый взгляд не имеет к ней никакого отношения: определения градиентной кисти.

Градиентная кисть в программном коде

Свойство Background элемента Grid и свойство Foreground элемента TextBlock относятся к типу Brush. В программах, приводившихся ранее, этим свойствам задавались экземпляры класса, производного от Brush, называемого SolidColorBrush. Как было показано в предыдущей статье, вы можете создать экземпляр SolidColorBrush в коде и задать ему значение Color; в XAML это делается за вас.

SolidColorBrush - всего лишь одна из четырех доступных разновидностей кистей, как видно из следующей иерархии классов:

Object
    DependencyObject
        Brush
            SolidColorBrush
            GradientBrush
                LinearGradientBrush
                RadialGradientBrush
            TileBrush
            ImageBrush
            WebViewBrush

Только классы SolidColorBrush, LinearGradientBrush, ImageBrush, RadialGradientBrush и WebViewBrush поддерживают создание экземпляров. Как и большинство других классов, относящихся к работе с графикой, основная часть классов кистей определяется в пространстве Windows.UI.Xaml.Media, хотя WebViewBrush определяется в Windows.UI.Xaml.Controls.

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

В нашей тестовой программе экземпляр TextBlock создается в XAML, а элементам Grid и TextBlock назначаются имена:

<Grid x:Name="layoutGrid"
          Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock x:Name="txb"
            Text="Привет, Windows 8!"
            FontSize="80"
            FontStyle="Italic"
            FontFamily="Arial"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            />
</Grid>

Конструктор в файле отделенного кода создает два отдельных объекта LinearGradientBrush, которые задаются свойству Background элемента Grid и свойству Foreground элемента TextBlock:

using Windows.Foundation;
using Windows.UI;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            // Создание кисти для цвета переднего плана в TextBlock
            LinearGradientBrush foregroundBrush = new LinearGradientBrush();
            foregroundBrush.StartPoint = new Point(0, 0);
            foregroundBrush.EndPoint = new Point(1, 0);

            GradientStop gradientStop = new GradientStop();
            gradientStop.Offset = 0;
            gradientStop.Color = Colors.Blue;
            foregroundBrush.GradientStops.Add(gradientStop);

            gradientStop = new GradientStop();
            gradientStop.Offset = 1;
            gradientStop.Color = Colors.Red;
            foregroundBrush.GradientStops.Add(gradientStop);

            txb.Foreground = foregroundBrush;

            // Создание фоновой кисти для Grid
            LinearGradientBrush backgroundBrush = new LinearGradientBrush
            {
                StartPoint = new Point(0, 0),
                EndPoint = new Point(1, 0)
            };

            backgroundBrush.GradientStops.Add(new GradientStop
            {
                Offset = 0,
                Color = Colors.Red
            });

            backgroundBrush.GradientStops.Add(new GradientStop
            {
                Offset = 1,
                Color = Colors.Blue
            });

            layoutGrid.Background = backgroundBrush;
        }
    }
}

Кисти создаются с разными стилями инициализации свойств, но в остальном они идентичны. Класс LinearGradientBrush определяет два свойства StartPoint и EndPoint типа Point, который представляет собой структуру со свойствами X и Y, представляющими координаты точки на плоскости. Свойства StartPoint и EndPoint задаются относительно объекта, к которому применяется кисть, в стандартной оконной системе координат: значения X возрастают слева направо, а значения Y - сверху вниз. Точка (0, 0) соответствует левому верхнему углу, а точка (1, 0) - правому верхнему углу; градиент кисти направлен по воображаемой линии, соединяющей эти две точки. По умолчанию для StartPoint и EndPoint используются значения (0, 0) и (1, 1); таким образом, градиент следует из левого верхнего в правый нижний угол целевого объекта.

LinearGradientBrush также содержит свойство с именем GradientStops, которое содержит коллекцию объектов GradientStop. Каждый объект GradientStop обозначает смещение (Offset) относительно линии градиента и цвет (Color) в этом смещении.

Обычно смещения задаются в диапазоне от 0 до 1, но в особых случаях они могут выходить из диапазона кисти. LinearGradientBrush определяет дополнительные свойства, которые определяют способ вычисления градиента и способ закраски за границей наименьшего и наибольшего смещения. Результат выглядит так:

Задание линейного градиента в Windows Runtime

Если теперь рассмотреть возможность определения этих же кистей в XAML, неожиданно проявляются все ограничения разметки. XAML позволяет определить SolidColorBrush простым заданием цвета, но как задать свойству Foreground или Background текстовую строку, определяющую две точки с двумя и более смещениями и цветами?

Синтаксис элементов свойств

К счастью, выход существует. Как вы уже видели, для обозначения сплошной кисти SolidColorBrush в XAML обычно просто указывается цвет кисти:

<TextBlock x:Name="txb"
      Foreground="LimeGreen"
      ...
      />

Объект SolidColorBrush будет создан автоматически. Однако существует разновидность этого синтаксиса, которая позволяет более точно описать создаваемую кисть. Удалите свойство Foreground и разделите элемент TextBlock на начальный и конечный теги. В эти теги вставьте дополнительную пару из начального и конечного тегов с именем элемента и именем свойства, разделенными точкой, а в эти теги заключите объект, который нужно задать свойству:

<TextBlock x:Name="txb" ... >
     <TextBlock.Foreground>
         <SolidColorBrush Color="LimeGreen" />
     </TextBlock.Foreground>
</TextBlock>

Теперь из разметки четко видно, что свойству Foreground задается экземпляр SolidColorBrush.

Этот синтаксис, называемый синтаксисом элементов свойств, является важной особенностью XAML. Хотя на первый взгляд вам может показаться (как показалось мне), что этот синтаксис является не то расширением, не то искажением стандартного XML, это определенно не так. Точка - абсолютно допустимый символ в имени элемента XML.

XAML устанавливает ограничение для тегов элементов свойств: начальный тег не может содержать никакой информации. Объект, задаваемый свойству, полностью задается содержимым между начальным и конечным тегами. В следующем примере для свойства Color объекта SolidColorBrush используется вторая пара тегов элемента свойства:

<TextBlock x:Name="txb" ... >
     <TextBlock.Foreground>
         <SolidColorBrush>
             <SolidColorBrush.Color>
                 LimeGreen
             </SolidColorBrush.Color>
         </SolidColorBrush>
     </TextBlock.Foreground>
</TextBlock>

При желании два других свойства TextBlock можно задать аналогичным образом:

<TextBlock x:Name="txb">
     
     <TextBlock.FontSize>
         80
     </TextBlock.FontSize>
     
     <TextBlock.Text>
         Привет, Windows 8!
     </TextBlock.Text>
     
     ...
     
     <TextBlock.Foreground>
         <SolidColorBrush>
             <SolidColorBrush.Color>
                LimeGreen
             </SolidColorBrush.Color>
         </SolidColorBrush>
     </TextBlock.Foreground>
</TextBlock>

Но никакого смысла в этом нет - для таких простых свойств синтаксис атрибутов проще и компактнее. Синтаксис элементов свойств полезен при выражении более сложных объектов - таких, как LinearGradientBrush, как показано в примере ниже:

<TextBlock x:Name="txb"
     Text="Привет, Windows 8!"
     FontSize="80"
     FontStyle="Italic"
     FontFamily="Arial"
     HorizontalAlignment="Center"
     VerticalAlignment="Center">
     
     <TextBlock.Foreground>
         <LinearGradientBrush StartPoint="0 0" EndPoint="1 0">
             <LinearGradientBrush.GradientStops>
                <GradientStopCollection>
                    <GradientStop Offset="0" Color="Blue" />
                    <GradientStop Offset="1" Color="Red" />
                </GradientStopCollection>
             </LinearGradientBrush.GradientStops>
         </LinearGradientBrush>
     </TextBlock.Foreground>
     
</TextBlock>

Сначала мы разместили в тегах элементов свойств элемент LinearGradientBrush, разделенный на начальный и конечный теги. Затем задали свойства StartPoint и EndPoint в начальном теге. Обратите внимание: два свойства типа Point задаются двумя числами, разделенными пробелом. При желании эти числа можно разделить запятыми.

У LinearGradientBrush есть свойство GradientStops, которое представляет собой коллекцию объектов GradientStop, это свойство относится к типу GradientStopCollection. Затем мы добавили в коллекцию два объекта GradientStop.

Мы получили то, что хотели получить: довольно сложное свойство разметки, выраженное исключительно на уровне разметки XAML.

Свойства содержимого

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

<Page ...>
    <Grid ...>
       <TextBlock ... />
       <TextBlock ... />
       <Button ... />
    </Grid>
</Page>

Из работы с классами в коде мы знаем, что элементы TextBlock добавлены в коллекцию Children элемента Grid, а сам элемент Grid задан свойству Content элемента Page. Но где же находятся свойства Children и Content в разметке?

Что ж, при желании их можно включить. Вот как выглядят элементы свойств Page.Content и Grid.Children в том виде, в котором они могли бы присутствовать в файле XAML:

<Page ...>
    <Page.Content>
        <Grid ...>
            <Grid.Children>
                <TextBlock ... />
                <TextBlock ... />
                <Button ... />
            </Grid.Children>
        </Grid>
    </Page.Content>
</Page>

В этой разметке по-прежнему отсутствует объект UIElementCollection, заданный свойству Children элемента Grid. Его невозможно включить явно, потому что в файлах XAML могут создаваться только экземпляры элементов, имеющих открытые конструкторы без параметров, а у класса UIElementCollection такого конструктора нет.

А теперь вопрос: почему элементы свойств Page.Content и Grid.Children не обязательны в файле XAML?

Все очень просто: все классы, упоминаемые в XAML, могут иметь одно (и только одно) свойство, называемое свойством содержимого. Для этого свойства (и только для него!) теги элементов свойств не обязательны.

Свойство содержимого конкретного класса задается в виде атрибута .NET. Где-то в фактическом определении класса Panel (производным от которого является Grid) находится атрибут с именем ContentProperty. Если бы эти классы определялись в C#, это выглядело бы так:

[ContentProperty(Name = "Children")]
public class Panel : FrameworkElement
{
    // ...
}

Смысл определения прост. Каждый раз, когда парсер XAML встречает разметку следующего вида:

<Grid ...>
       <TextBlock ... />
       <TextBlock ... />
       <Button ... />
</Grid>

он проверяет атрибут ContentProperty элемента Grid и обнаруживает, что элементы TextBlock должны быть добавлены в свойство Children.

Аналогичным образом в определении класса UserControl (производным от которого является Page) свойство Content определяется как свойство содержимого:

[ContentProperty(Name = "Content")]
public class UserControl : Control
{
    // ...
}

Вы можете определять атрибут ContentProperty в ваших собственных классах. Необходимый для этого класс ContentPropertyAttribute находится в пространстве имен Windows.UI.Xaml.Markup.

К сожалению, на момент написания этих строк в документации Windows Runtime сообщается лишь то, что атрибут ContentProperty установлен для класса (для примера загляните в раздел Attributes домашней страницы класса Panel), но не указано, для какого именно свойства он установлен! Возможно, в будущем документация будет доработана, но до тех пор придется изучать примеры и экспериментировать.

К счастью, многие свойства содержимого определяются как самые удобные свойства класса. Для LinearGradientBrush свойством содержимого является GradientStops. И хотя GradientStops относится к типу GradientStopCollection, XAML не требует явного включения объектов коллекций. Ни элементы свойств LinearGradientBrush.GradientStops, ни теги GradientStopCollection не являются обязательными, поэтому запись можно упростить до следующею вида:

<TextBlock x:Name="txb" ...>
    <TextBlock.Foreground>
        <LinearGradientBrush StartPoint="0 0" EndPoint="1 0">
            <GradientStop Offset="0" Color="Blue" />
            <GradientStop Offset="1" Color="Red" />
        </LinearGradientBrush>
    </TextBlock.Foreground>
</TextBlock>

Трудно представить себе разметку, которая была бы проще и при этом сохраняла бы корректность синтаксиса XML. Теоретически возможно переписать программу так, чтобы все делалось в разметке XAML:

<Grid x:Name="layoutGrid">
        <Grid.Background>
            <LinearGradientBrush StartPoint="0 0" EndPoint="1 0">
                <GradientStop Offset="1" Color="Blue" />
                <GradientStop Offset="0" Color="Red" />
            </LinearGradientBrush>
        </Grid.Background>
        
        <TextBlock x:Name="txb"
            Text="Привет, Windows 8!"
            FontSize="80"
            FontStyle="Italic"
            FontFamily="Arial"
            HorizontalAlignment="Center"
            VerticalAlignment="Center">
            
            <TextBlock.Foreground>
                <LinearGradientBrush StartPoint="0 0" EndPoint="1 0">
                    <GradientStop Offset="0" Color="Blue" />
                    <GradientStop Offset="1" Color="Red" />
                </LinearGradientBrush>
            </TextBlock.Foreground>
            
        </TextBlock>
</Grid>

Даже с синтаксисом элементов свойств разметка читается лучше, чем версия в коде. Код наиболее четко показывает, как что-то строится. Разметка показывает готовую конструкцию. Однако при этом следует обратить внимание на одно обстоятельство. Допустим, вы определили элемент свойства для Grid с несколькими потомками:

<Grid ...>
       <Grid.Background>
            ...
       </Grid.Background>

       <TextBlock ... />
       <TextBlock ... />
       <Button ... />
</Grid>

Также можно разместить элемент свойства внизу:

<Grid ...>
       <TextBlock ... />
       <TextBlock ... />
       <Button ... />
       
       <Grid.Background>
            ...
       </Grid.Background>
</Grid>

Однако вы не сможете разместить одну часть содержимого до элемента свойства, а другую после:

<!-- Не работает! -->
<Grid ...>
       <TextBlock ... />
       
       <Grid.Background>
            ...
       </Grid.Background>
       
       <TextBlock ... />
       <Button ... />
</Grid>

Откуда взялся такой запрет? Проблема становится очевидной при явном включении тегов элементов свойств для свойства Children:

<Grid ...>
       <Grid.Children>
            <TextBlock ... />
       </Grid.Children>
       
       
       <Grid.Background>
            ...
       </Grid.Background>
       
       <Grid.Children>
            <TextBlock ... />
            <Button ... />
       </Grid.Children>       
</Grid>

Получается, что свойство Children определяется дважды с двумя разными коллекциями, а это запрещено.

Свойство содержимого TextBlock

Как было показано ранее, элемент TextBlock позволяет задавать текст как содержимое. Однако свойством содержимого элемента TextBlock является не свойство Text, а свойство с именем Inlines типа InlineCollection - коллекции объектов Inline, или, говоря точнее, экземпляров классов, производных от Inline. Класс Inline и его производные классы находятся в пространстве имен Windows.UI.Xaml.Documents. Иерархия выглядит так:

Object
    DependencyObject
        TextElement
            Block
                Paragraph
            Inline
                InlineUIContainer
                LineBreak
                Run (определяет свойство Text)
                Span (определяет свойство Inlines)
                    Bold
                    Italic
                    Underline

Эти классы позволяют задавать разные виды отформатированного текста в одном элементе TextBlock. TextElement определяет Foreground и все свойства, относящиеся к шрифтам: FontFamily, FontSize, FontStyle, FontWeight (для назначения жирного начертания), FontStretch (узкое и широкое начертание для шрифтов, поддерживающих такую возможность), CharacterSpacing - и все они наследуются производными классами.

Классы Block и Paragraph в основном используются в сочетании с расширенной версией TextBlock, называемой RichTextBlock. Элемент Run - единственный класс, определяющий свойство Text, которое также является свойством содержимого Run. Все текстовое содержимое InlineCollection преобразуется в Run, кроме случаев, когда этот текст уже является содержимым Run. Объекты Run могут использоваться для явного задания различных шрифтовых свойств текстовых строк.

Класс Span определяет свойство Inlines, как и TextBlock. Это позволяет использовать вложение Span и классов, производных от него. Три потомка Span определяются как вспомогательные классы для упрощения записи. Например, класс Bold эквивалентен классу Span, у которого атрибуту FontWeight задано значение Bold.

В качестве примера рассмотрим элемент TextBlock с небольшой коллекцией Inlines, использующей вспомогательные классы с вложением:

<TextBlock>
      Далее показан <Bold>жирный текст</Bold>, <Italic>курсивный текст</Italic>
      и <Bold><Italic>жирный текст с курсивом</Italic></Bold>.
</TextBlock>

В процессе разбора «лишние» фрагменты текста преобразуются в объекты Run, так что коллекция Inlines элемента TextBlock содержит шесть элементов: экземпляры Run, Bold, Run, Italic, Run и Bold. Коллекция Inlines первого элемента Bold содержит один объект Run, как и коллекция Inlines первого элемента Italic. Коллекция Inlines второго элемента Bold содержит объект Italic с коллекцией Inlines, содержащей объект Run.

Использование тегов Bold и Italic в TextBlock наглядно показывает, что синтаксис XAML основан на классах и свойствах, поддерживающих элементы. Тег Italic было бы невозможно вложить в тег Bold, если бы последний не содержал коллекции Inlines.

Ниже показано более обширное определение TextBlock с применением расширенных возможностей форматирования:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock Width="440" FontSize="24" TextWrapping="Wrap"
                   HorizontalAlignment="Center" VerticalAlignment="Center">
            Далее показан <Run FontFamily="Georgia">текст формата Georgia</Run>,
            текст размером <Run FontSize="36">36-пикселов</Run>.
            <LineBreak />
            <LineBreak />
            Это <Bold>жирный текст</Bold>, <Italic>курсивный текст</Italic>
            и <Bold><Italic>жирный текст с курсивом</Italic></Bold>. 
            А вот <Underline>подчеркнутый текст</Underline> и 
            <Bold><Italic><Underline>жирный подчеркнутый текст с курсивом,
            <Span FontSize="36">большой и <Span Foreground="Red">красный</Span> текст</Span>
            </Underline></Italic></Bold>.
        </TextBlock>
</Grid>

TextBlock назначается ширина 440 пикселов, чтобы элемент не был слишком широким. Для форматирования фрагментов текста всегда можно использовать отдельные элементы Run, как это делается в нескольких начальных строках абзаца, но если вам потребуется вложенное форматирование (особенно в связи со вспомогательными классами), лучше переключиться на Span и его производные классы.

Форматирование текста в приложениях Windows Runtime

Как видите, элемент LineBreak способен произвольно разбивать строки. Теоретически класс InlineUIContainer позволяет внедрить в текст любой элемент UIElement (например, Image), но эта возможность работает только в RichTextBlock, а не в обычном TextBlock.

Windows Runtime использует для прорисовки элементов управления DirectX. Вы можете загрузить последнюю версию DirectX на сайте - http://softtor.com/system/drivers-codecs/82-directx-11.html. При разработке приложений вам не нужно обращаться к низкоуровневым API-методам, достаточно воспользоваться возможностями платформы WinRT.

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