Создание шаблонов

59

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

Как уже было показано, базовый элемент управления Button использует класс ButtonChrome для рисования своего характерного фона и рамки. Одна из причин, по которой Button применяет ButtonChrome вместо рисования примитивов WPF, состоит в том, что стандартный вид кнопки зависит от нескольких очевидных характеристик (отключена, имеет фокус, находится во время совершения щелчка) и других тонких факторов (текущая тема Windows). Реализация подобного рода логики только с помощью триггеров была бы затруднительной.

Однако при построении собственных специальных элементов управления, возможно, стандартизация и интеграция с темой не так важны. (На самом деле в WPF не столь строго подчеркивается необходимость стандартизации интерфейса, как в прежних технологиях построения пользовательских интерфейсов). Вместо этого больше внимания уделяется созданию привлекательных и оригинальных элементов управления, которые сочетаются с остальным пользовательским интерфейсом.

По этой причине создавать классы, подобные ButtonChrome, может и не понадобится. Взамен с помощью уже знакомых элементов (в том числе элементов для рисования, а также анимации) будут проектироваться самодостаточные шаблоны элементов управления без написания кода.

Простая кнопка

Чтобы применить специальный шаблон элемента управления, необходимо просто установить свойство Template элемента. Хотя можно определить встроенный шаблон (поместив дескриптор шаблона элемента внутрь дескриптора самого элемента управления), этот подход редко бывает оправдан. Дело в том, что шаблон почти всегда нужно использовать многократно, создавая обложки для нескольких экземпляров одного и того же элемента управления. Шаблон элемента управления должен быть определен как ресурс, на который можно будет ссылаться с помощью StaticResource.

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

В данном примере ресурс ButtonTemlate помещается в коллекцию Resources включающего окна. Однако в реальном приложении, скорее всего, будут использоваться ресурсы приложения.. Базовый эскиз шаблона элемента управления выглядит следующим образом:

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="Button">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    TextBlock.Foreground="White">
                <Border.Background>
                    <LinearGradientBrush>
                        <GradientStopCollection>
                            <GradientStop Offset="0" Color="LimeGreen"></GradientStop>
                            <GradientStop Offset="1" Color="LightBlue"></GradientStop>
                        </GradientStopCollection>
                    </LinearGradientBrush>
                </Border.Background>
                <ContentPresenter RecognizesAccessKey="True"></ContentPresenter>
            </Border>
        </ControlTemplate>
</Window.Resources>
...
<Button Margin="10" Width="110" Padding="5" Height="30" Template="{StaticResource ButtonTemplate}">It's TemplateButton</Button>

Этот шаблон элемента управления устанавливает свойство TargetType, чтобы явно указать, что он предназначен для кнопок. С точки зрения стиля этому соглашению всегда стоит следовать. Для элементов с содержимым, таким как кнопка, это также обязательно, иначе ContentPresenter не будет работать.

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

Приведенный выше элемент ContentPresenter устанавливает свойство RecognizesAccessKey в true. Хотя это не обязательно, но кнопка будет поддерживать клавиши доступа — подчеркнутые буквы, клавиши которых можно нажимать для быстрого выбора кнопки. В рассматриваемом случае, если кнопка содержит текст вроде "Click _Me", то пользователь может выбрать ее, нажав <Alt+M>. (Согласно стандартным настройкам Windows, подчеркивание скрывается, а ключ доступа — в данном случае М — отображается подчеркнутым, как только нажата клавиша <Alt>.) Если не установить RecognizesAccessKey в True, эта деталь игнорируется, и все знаки подчеркивания трактуются как обычные подчеркивания, отображаясь как часть содержимого кнопки.

Если элемент управления унаследован от ContentControl, его шаблон будет включать ContentPresenter, который указывает местоположение содержимого. Если элемент управления унаследован от ItemsControl, его шаблон будет включать ItemsPresenter, который указывает местоположение панели, содержащей список элементов. В редких случаях элемент управления может использовать унаследованную версию одного из этих классов; например, шаблон элемента управления ScrollViewer использует ScrollContentPresenter, унаследованный от ContentPresenter.

Привязки шаблона

В этом примере присутствует одна небольшая проблема. Прямо сейчас добавленный дескриптор для кнопки указывает значение Margin, равное 10. и значение Padding, равное 5. Контейнер StackPanel обращает внимание на свойство кнопки Margin, но игнорирует Padding, оставляя содержимое кнопки вплотную прижатым к границам. Проблема в том, что свойство Padding не оказывает никакого эффекта, если только явно не используется в шаблоне. Другими словами, извлечь значение Padding и применить его для добавления некоторого пространства вокруг содержимого является обязанностью шаблона.

К счастью, в WPF имеется инструмент, предназначенный специально для этой цели: привязки шаблона (template bindings). Используя привязку шаблона, шаблон может извлечь значение из элемента управления, к которому применен шаблон. В данном примере привязкой шаблона можно воспользоваться для извлечения значения свойства Padding и его использования для установки полей вокруг ContentPresenter:

<ContentPresenter RecognizesAccessKey="True" Margin="{TemplateBinding Padding}"></ContentPresenter>

Это обеспечивает достижение требуемого эффекта — добавления некоторого пространства между контуром и содержимым. На рисунке показана новая кнопка:

Кнопка снастроенным шаблоном управления

Привязки шаблонов подобны обычным привязкам данных, но являются облегченными, поскольку специально спроектированы для использования в шаблоне элемента управления. Они поддерживают только однонаправленную привязку данных (другими словами, передачу информации от элемента управления шаблону, но не наоборот), и не могут применяться для визуализации информации из свойства класса, унаследованного от Freezable. Оказавшись в ситуации, когда привязка шаблона не работает, можно воспользоваться вместо нее полноценной привязкой данных.

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

Единственный способ проверить, нужна ли привязка шаблона, состоит в том, чтобы просмотреть шаблон элемента управления, установленный по умолчанию. Заглянув в шаблон для класса Button, вы обнаружите, что он использует привязку шаблонов точно таким же образом, как этот специальный шаблон — берет свойство Padding, определенное в кнопке, и преобразует его в поле вокруг ContentPresenter.

Кроме того, выясняется, что стандартный шаблон кнопки включает и другие привязки шаблона, которые не используются в простом настроенном шаблоне, такие как HorizontalAlignment, VerticalAlignment и Background. Это значит, что если установить эти свойства в кнопке, они не окажут никакого эффекта на простой специальный шаблон.

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