Шаблон элемента управления ControlTemplate в WinRT

92

Вы уже видели, как задать шаблон DataTemplate свойству ContentTemplate класса, производного от ContentControl, и как использовать ItemTemplate для форматирования объектов данных. Вы также видели, как определить ItemsPanelTemplate для свойства ItemsPanel класса, производного от ItemsControl, чтобы предоставить панель для размещения потомков.

Третья разновидность шаблонов относится к типу ControlTemplate. Класс Control определяет свойство Template типа ControlTemplate, которое позволяет полностью переопределить визуальное оформление самого элемента управления - не содержимое, а ту часть, которая обычно называется «хромом» (chrome).

Существование свойства Template, пожалуй, является самым важным отличием между классами, производными от Control, и простыми классами, производными от FrameworkElement. Элементы управления содержат «хром», оформление которого находится под вашим полным контролем.

Каждый раз, когда вы думаете о создании собственного элемента управления, спросите себя - действительно ли это новый элемент управления или просто существующий элемент с немного измененным оформлением? Если повезет, вы сможете адаптировать существующий элемент управления простым изменением Style. В других случаях придется использовать свойство ControlTemplate. Свойство ControlTemplate, как и Style, часто определяется в виде ресурса, чтобы его можно было использовать совместно. Как и Style, свойство ControlTemplate обладает свойством TargetType, определяющим тип элемента управления, для которого проектируется шаблон. Свойство Template, определяемое Control, поддерживается свойством зависимости; это означает, что свойство Template может задаваться в style. Это очень типичная ситуация; вот как это может выглядеть в секции Resources:

<Page.Resources>
        <Style x:Key="btnStyle" TargetType="Button">
            <Setter Property="Margin" Value="10" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        ...
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
</Page.Resources>

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

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

Возьмем стандартную кнопку Button в том виде, в каком она может включаться в визуальное дерево. У нее имеется содержимое, обработчик события и несколько часто используемых свойств:

<Button Content="Щелкни по мне!"
        Click="Button_Click"
        VerticalAlignment="Center"
        HorizontalAlignment="Center" />

Давайте определим для этой кнопки новый шаблон ControlTemplate, выделив свойство Template как элемент свойства:

<Button Content="Щелкни по мне!"
        Click="Button_Click"
        VerticalAlignment="Center"
        HorizontalAlignment="Center">
            <Button.Template>
                <ControlTemplate TargetType="Button">
                    
                </ControlTemplate>
            </Button.Template>
</Button>

Обратите внимание на свойство TargetType в теге ControlTemplate. Иногда это свойство можно опустить, а шаблон все равно будет работать (при условии, что он не ссылается на свойство, определенное целевым элементом управления, но не определенное в классе Control).

Экземпляр кнопки с пустым шаблоном ControlTemplate создается, но не имеет визуального представления, поэтому пользователь не видит его (не говоря уже о том, чтобы щелкнуть на нем). Просто чтобы убедиться, что мы ничего не испортили, поместим временный элемент TextBlock между тегами ControlTemplate:

<Button Content="Щелкни по мне!"
        Click="Button_Click"
        VerticalAlignment="Center"
        HorizontalAlignment="Center">
            <Button.Template>
                <ControlTemplate TargetType="Button">
                    <TextBlock Text="Тестируем шаблон" />
                </ControlTemplate>
            </Button.Template>
</Button>

Теперь кнопка снова получила визуальное представление, которое состоит исключительно из текста, и снова стала функциональной. При касании или щелчке на TextBlock срабатывает событие Click. Однако оформление кнопки стало статичным. Ее внешний вид перестал изменяться при наведении указателя мыши или в процессе щелчка. В шаблоне наряду со стандартным внешним видом элемента управления также определяется его внешний вид в этих особых ситуациях. Элемент TextBlock можно заключить в Border:

<Button Content="Щелкни по мне!"
        Click="Button_Click"
        VerticalAlignment="Center"
        HorizontalAlignment="Center"
        FontSize="32">
            <Button.Template>
                <ControlTemplate TargetType="Button">
                    <Border BorderThickness="5" BorderBrush="LimeGreen" Padding="10">
                        <TextBlock Text="Тестируем шаблон" />
                    </Border>
                </ControlTemplate>
            </Button.Template>
</Button>

Результат выглядит так:

Простой шаблон для кнопки

Но стоит ли жестко программировать зеленую кисть в шаблоне? Если вы определяете шаблон для одной кнопки, как в нашем примере, это нормально. Однако, в общем случае шаблоны определяются как ресурсы, рассчитанные на многократное использование; в одних случаях рамка должна быть зеленой, в других она может быть окрашена в другой цвет. Класс Control определяет свойства BorderBrush и BorderThickness, а класс Button их наследует, так что эти свойства разумнее определить прямо в Button:

<Button Content="Щелкни по мне!"
        Click="Button_Click"
        VerticalAlignment="Center"
        HorizontalAlignment="Center"
        FontSize="32"
        BorderThickness="5" BorderBrush="LimeGreen"
        Padding="10">
            <Button.Template>
                <ControlTemplate TargetType="Button">
                    <Border>
                        <TextBlock Text="Тестируем шаблон" />
                    </Border>
                </ControlTemplate>
            </Button.Template>
</Button>

Но теперь элемент Border полностью исчезает из оформления Button! Элемент Border в шаблоне не «подхватывает» свойства, заданные для Button. Для ссылки на свойства, определяемые для Button, в элемент Border из шаблона необходимо включить соответствующую привязку. Это особый вид привязок, называемый TemplateBinding, и у него имеется собственное расширение разметки:

<Button Content="Щелкни по мне!"
        Click="Button_Click"
        VerticalAlignment="Center"
        HorizontalAlignment="Center"
        FontSize="32"
        BorderThickness="5" BorderBrush="LimeGreen"
        Padding="10">
            <Button.Template>
                <ControlTemplate TargetType="Button">
                    <Border BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Padding="{TemplateBinding Padding}">
                        <TextBlock Text="Тестируем шаблон" />
                    </Border>
                </ControlTemplate>
            </Button.Template>
</Button>

Конструкция TemplateBinding привязывает свойства элемента в визуальном дереве ControlTemplate к свойствам элемента управления, к которому применяется ControlTemplate. Теперь оформление Button снова включает зеленую рамку, как и прежде.

Синтаксис TemplateBinding чрезвычайно прост: целевым всегда является свойство зависимости элемента визуального дерева ControlTemplate. В ссылке всегда указывается свойство элемента управления, к которому применяется шаблон. Ничего другого в разметке TemplateBinding быть не может. TemplateBinding встречается только в визуальных деревьях ControlTemplate. На самом деле TemplateBinding представляет собой сокращенную запись для привязки RelativeSource. Следующие привязки тоже работают, но с точки зрения синтаксиса такая запись явно получается более громоздкой:

BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent},
                      Path=BorderBrush}"

Используйте подробную форму записи, если вам потребуется создать двустороннюю привязку в ControlTemplate. Привязка TemplateBinding является односторонней и не позволяет задать значение Mode.

Допустим, вы хотите, чтобы зеленая рамка использовалась по умолчанию для новых кнопок, но отдельные кнопки могли переопределить это свойство по умолчанию. В таком случае можно переопределить ControlTemplate как часть стиля. Учтите, что обычно стиль определяется как ресурс и совместно используется многими кнопками, но я в этом упражнении связываю его непосредственно с кнопкой:

<Button Content="Щелкни по мне!"
        Click="Button_Click"
        VerticalAlignment="Center"
        HorizontalAlignment="Center"
        FontSize="32">
            <Button.Style>
                <Style TargetType="Button">
                    <Setter Property="BorderThickness" Value="5" />
                    <Setter Property="BorderBrush" Value="LimeGreen" />
                    <Setter Property="Padding" Value="10" />
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="Button">
                                <Border BorderBrush="{TemplateBinding BorderBrush}"
                                        BorderThickness="{TemplateBinding BorderThickness}"
                                        Padding="{TemplateBinding Padding}">
                                    <TextBlock Text="Тестируем шаблон" />
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </Button.Style>
</Button>

Теперь вы можете задать свойства BorderBrush и BorderThickness для самого объекта Button, и заданные значения заменят настройки из Style. Давайте включим в стиль значение по умолчанию для свойств Background и Foreground, а также немного увеличим шрифт при помощи свойства FontSize:

<Button Content="Щелкни по мне!"
        Click="Button_Click"
        VerticalAlignment="Center"
        HorizontalAlignment="Center">
            <Button.Style>
                <Style TargetType="Button">
                    <Setter Property="BorderThickness" Value="5" />
                    <Setter Property="BorderBrush" Value="LimeGreen" />
                    <Setter Property="Padding" Value="10" />
                    <Setter Property="FontSize" Value="40" />
                    <Setter Property="Foreground" Value="LightCoral" />
                    <Setter Property="Background" Value="White" />
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="Button">
                                <Border BorderBrush="{TemplateBinding BorderBrush}"
                                        BorderThickness="{TemplateBinding BorderThickness}"
                                        Padding="{TemplateBinding Padding}"
                                        Background="{TemplateBinding Background}">
                                    <TextBlock Text="Тестируем шаблон" />
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </Button.Style>
</Button>

Обратите внимание на использование TemplateBinding со свойством Background объекта Border. Однако элементу TextBlock не нужны привязки TemplateBinding для свойств Foreground и FontSize, потому что эти свойства наследуются по визуальному дереву. Теперь элемент TextBlock выводится светло-оранжевым шрифтом чуть большего размера, чем прежде.

Шаблон кнопки с фоном

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

Возможно, вы захотите, чтобы у новой кнопки Button рамка Bortder имела скругленные углы. У классов Control и Button нет соответствующего свойства, поэтому если мы не хотим определять класс, производный от Button и имеющий свойство CornerRadius, закругление можно жестко запрограммировать. Фрагмент разметки с тегом ControlTemplate выглядит так:

<ControlTemplate TargetType="Button">
    <Border BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}"
            Padding="{TemplateBinding Padding}"
            Background="{TemplateBinding Background}"
            CornerRadius="10">
        <TextBlock Text="Тестируем шаблон" />
    </Border>
</ControlTemplate>

Вот что получается:

Шаблон кнопки со скругленными углами

Теперь решим одну проблему TextBlock, связанную с выводом временного текста. После того, что вы видели ранее, возникает мысль заменить временный текст привязкой TemplateBinding к свойству Content объекта Button:

<TextBlock Text="{TemplateBinding Content}" />

В данном примере такое решение сработает, но его идея глубоко ошибочна. Ранее я говорил, что свойство Content класса, производного от ContentControl - такого, как Button, - относится к типу object, а элемент TextBlock подходит только для текста. Решение не будет работать, даже если в качестве содержимого задано растровое изображение. К счастью, существует специальный класс для отображения содержимого в классах, производных от ContentControl. Этот класс называется ContentPresenter; как и ContentControl, он содержит свойство Content типа object:

<ControlTemplate TargetType="Button">
    <Border BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}"
            Padding="{TemplateBinding Padding}"
            Background="{TemplateBinding Background}"
            CornerRadius="10">
        <ContentPresenter Content="{TemplateBinding Content}" />
    </Border>
</ControlTemplate>

Класс ContentPresenter используется почти в каждом шаблоне для классов, производных от ContentControl. Он является производным от FrameworkElement, но также генерирует собственное визуальное дерево для отображения содержимого. В этом конкретном примере ContentPresenter создает TextBlock для отображения свойства Content.

Классу ContentPresenter также доверяется построение визуального дерева для отображения любого содержимого, основанного на свойстве ContentTemplate элемента управления. ContentPresenter имеет собственное свойство ContentTemplate, которое может быть привязано к свойству ContentTemplate элемента управления:

<ControlTemplate TargetType="Button">
    <Border BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}"
            Padding="{TemplateBinding Padding}"
            Background="{TemplateBinding Background}"
            CornerRadius="10">
        <ContentPresenter Content="{TemplateBinding Content}"
                          ContentTemplate="{TemplateBinding ContentTemplate}"/>
    </Border>
</ControlTemplate>

Эти две привязки в ContentPresenter настолько стандартны и важны, что определять их не обязательно! Класс ContentPresenter автоматически получает значения этих свойств из элемента управления, в котором он используется. Если вы предпочитаете опустить их - так и поступите. Лично мне как-то спокойнее видеть их на своем месте. Вспомните, что класс Control определяет свойство Padding, которое резервирует небольшое пространство между «хромом» элемента управления и его содержимым. Попробуйте задать свойство Padding в теге Button - ничего не происходит. Чтобы между Border и ContentPresenter оставалось свободное пространство, необходимо что-то добавить в ControlTemplate. Это может быть привязка TemplateBinding для свойства Padding объекта Border, но чаще TemplateBinding связывается со свойством Margin объекта ContentPresenter:

<Button Content="Щелкни по мне!"
        Click="Button_Click"
        VerticalAlignment="Center"
        HorizontalAlignment="Center"
        Padding="24">
     <Button.Style>
          <Style TargetType="Button">
            <Setter Property="BorderThickness" Value="5" />
            <Setter Property="BorderBrush" Value="LimeGreen" />
            <Setter Property="FontSize" Value="40" />
            <Setter Property="Foreground" Value="LightCoral" />
            <Setter Property="Background" Value="White" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                 Background="{TemplateBinding Background}"
                                 CornerRadius="10">
                            <ContentPresenter Content="{TemplateBinding Content}"
                                              ContentTemplate="{TemplateBinding ContentTemplate}"
                                              Margin="{TemplateBinding Padding}"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
          </Style>
    </Button.Style>
</Button>

Теперь попробуйте задать свойствам HorizontalAlignment и VerticalAlignment объекта Button значение Stretch. Объект Button, как и предполагалось, расширяется для заполнения страницы. Это означает, что такие свойства обрабатываются автоматически, и это хорошо. Однако содержимое выводится в левом верхнем углу кнопки. Control определяет два свойства с именами HorizontalContentAlignment и VerticalContentAlignment, управляющими позиционированием содержимого в кнопке, но попытавшись задать эти свойства, вы увидите, что они не работают.

Чтобы они заработали, в шаблон необходимо кое-что добавить. Как правило, эти свойства привязываются к свойствам HorizontalAlignment и VerticalAlignment объекта ContentPresenter:

<ControlTemplate TargetType="Button">
    <Border BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}"
            Background="{TemplateBinding Background}"
            CornerRadius="10">
        <ContentPresenter Content="{TemplateBinding Content}"
                          ContentTemplate="{TemplateBinding ContentTemplate}"
                          Margin="{TemplateBinding Padding}"
                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
    </Border>
</ControlTemplate>

Эти свойства позиционируют ContentPresenter в родителе, которым в данном случае является Border. Я добавлю в тег ContentPresenter еще один элемент TemplateBinding чтобы подготовить его к следующему шагу:

<Button Content="Щелкни по мне!"
        Click="Button_Click"
        VerticalAlignment="Stretch"
        HorizontalAlignment="Stretch"
        Padding="24">
    <Button.ContentTransitions>
        <TransitionCollection>
            <EntranceThemeTransition />
        </TransitionCollection>
    </Button.ContentTransitions>
    <Button.Style>
        <Style TargetType="Button">
            <Setter Property="BorderThickness" Value="5" />
            <Setter Property="BorderBrush" Value="LimeGreen" />
            <Setter Property="Padding" Value="10" />
            <Setter Property="FontSize" Value="40" />
            <Setter Property="Foreground" Value="LightCoral" />
            <Setter Property="Background" Value="White" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Background="{TemplateBinding Background}"
                                CornerRadius="10">
                            <ContentPresenter Content="{TemplateBinding Content}"
                                      ContentTemplate="{TemplateBinding ContentTemplate}"
                                      Margin="{TemplateBinding Padding}"
                                      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                      ContentTransitions="{TemplateBinding ContentTransitions}"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
         </Style>
    </Button.Style>
</Button>

Свойство ContentTransitions объекта ContentPresenter теперь привязано к свойств ContentTransitions объекта Button; для тестирования привязки я включил в Button переход EntranceThemeTransition. Теперь при загрузке Button текст входит на экран от правого края.

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