
Градиентная кнопка в WPF: свой шаблон, свечение текста и тень
WPF позволяет оформить кнопку как на макете дизайнера: градиент, скругление, мягкая тень и светящийся текст. Всё это описывается стилем один раз и применяется ко всем кнопкам окна или приложения.
Стили WPF: оформление в одном месте
Разработчик, пришедший из Windows Forms, обычно настраивает внешний вид каждой кнопки в дизайнере: цвет фона, шрифт, рамку. Через полгода в проекте сотня кнопок, и чтобы поменять оттенок, приходится обходить все формы. WPF предлагает подход, знакомый веб-разработчикам по CSS: оформление выносится в стиль, а элементы только ссылаются на него.
Стиль в WPF это набор пар «свойство и значение», заданных через элементы Setter. Почти любое свойство, которое можно указать атрибутом в XAML, можно задать и сеттером. Если у стиля указан только TargetType без ключа x:Key, он становится неявным и автоматически применяется ко всем элементам этого типа внутри ресурсов, где объявлен.
Мы соберём неявный стиль для Button, который полностью заменяет шаблон кнопки, добавляет градиентный фон и эффекты, а затем оживим его триггерами на наведение и нажатие.
Градиентная заливка через LinearGradientBrush
Фон кнопки задаётся кистью. Сплошной цвет это SolidColorBrush, а для перехода цветов используется LinearGradientBrush. Направление градиента определяют точки StartPoint и EndPoint в относительных координатах от 0 до 1: значение 0,0 соответствует левому верхнему углу, 0,1 левому нижнему. Для классической «глянцевой» кнопки достаточно вертикального градиента с тремя-четырьмя опорными точками GradientStop.
Кисть удобно объявить отдельным ресурсом с ключом. Тогда её можно переиспользовать в нескольких стилях и поменять палитру в одном месте.
<Window.Resources>
<!-- Вертикальный «глянцевый» градиент -->
<LinearGradientBrush x:Key="ButtonFill" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#FF5A9BD5" Offset="0" />
<GradientStop Color="#FF2E75B6" Offset="0.5" />
<GradientStop Color="#FF1F4E79" Offset="0.51" />
<GradientStop Color="#FF2E75B6" Offset="1" />
</LinearGradientBrush>
<!-- Более светлый вариант для наведения -->
<LinearGradientBrush x:Key="ButtonFillHover" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#FF7DB3E3" Offset="0" />
<GradientStop Color="#FF2E75B6" Offset="1" />
</LinearGradientBrush>
</Window.Resources>Свой шаблон кнопки: ControlTemplate и TemplateBinding
Стандартная кнопка рисуется системной темой, и свойство Background влияет на неё лишь частично: в Windows 7 и новее при наведении тема всё равно подставляет свой голубой фон. Чтобы полностью управлять видом, нужно заменить шаблон через сеттер свойства Template.
Внутри шаблона мы рисуем Border со скруглёнными углами и кладём в него ContentPresenter. Это заполнитель, на место которого WPF выводит содержимое кнопки: текст, картинку или целую панель. Связка TemplateBinding Background передаёт фон, заданный у самой кнопки, внутрь шаблона, поэтому отдельные кнопки по-прежнему можно перекрасить атрибутом.
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="{StaticResource ButtonFill}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="14,5" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="border" CornerRadius="6" BorderThickness="1"
BorderBrush="#FF1F4E79" Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}">
<!-- Сюда WPF подставит содержимое кнопки -->
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True" />
</Border>
<!-- Триггеры шаблона см. ниже -->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>Свечение текста и тень: BitmapEffect против Effect
В первых версиях WPF эффекты задавались свойством BitmapEffect: DropShadowBitmapEffect для тени, OuterGlowBitmapEffect для свечения, а BitmapEffectGroup позволял объединить несколько эффектов. Это работало, но рендерилось программно и заметно нагружало процессор, особенно в списках с десятками элементов.
Начиная с .NET 3.5 SP1 появилось свойство Effect с аппаратно ускоренными DropShadowEffect и BlurEffect, а в .NET 4 классы BitmapEffect объявлены устаревшими и больше ничего не рисуют. Если вы сопровождаете старый проект и после перехода на новый фреймворк тени пропали, причина именно в этом.
Готового эффекта свечения в новом API нет, но его легко имитировать: DropShadowEffect с нулевой глубиной ShadowDepth и заметным BlurRadius рисует ореол равномерно со всех сторон. Применяем его к ContentPresenter, и светиться будет только текст. Тень всей кнопке задаём вторым эффектом на Border.
<Border x:Name="border" CornerRadius="6" BorderThickness="1"
BorderBrush="#FF1F4E79" Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}">
<!-- Мягкая тень под кнопкой -->
<Border.Effect>
<DropShadowEffect ShadowDepth="2" BlurRadius="6"
Opacity="0.4" Color="Black" />
</Border.Effect>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- Свечение текста: тень без смещения -->
<ContentPresenter.Effect>
<DropShadowEffect ShadowDepth="0" BlurRadius="8"
Color="PaleGreen" Opacity="0.9" />
</ContentPresenter.Effect>
</ContentPresenter>
</Border>Триггеры: реакция на наведение и нажатие
Раз мы заменили шаблон, кнопка потеряла визуальную обратную связь: она больше не подсвечивается при наведении и не «вдавливается» при клике. Это возвращается триггерами. Trigger следит за значением свойства, например IsMouseOver или IsPressed, и пока условие истинно, применяет свои сеттеры. Когда условие перестаёт выполняться, значения возвращаются сами, писать обратный код не нужно.
Через TargetName триггер может менять свойства именованных элементов шаблона. Порядок важен: если истинны несколько триггеров, побеждает последний, поэтому IsPressed ставим после IsMouseOver.
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background"
Value="{StaticResource ButtonFillHover}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<!-- Сдвиг на пиксель вниз имитирует нажатие -->
<Setter TargetName="border" Property="RenderTransform">
<Setter.Value>
<TranslateTransform Y="1" />
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="Orange" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Opacity" Value="0.5" />
</Trigger>
</ControlTemplate.Triggers>Что ещё полезно знать новичку в WPF
Несколько моментов, на которых часто спотыкаются при переходе с Windows Forms и ASP.NET:
- Grid в WPF это контейнер разметки по строкам и столбцам, а не таблица данных. Для табличного вывода есть DataGrid и ListView с режимом GridView, о них отдельные статьи раздела;
- свойство, заданное атрибутом прямо у элемента, имеет приоритет над сеттером стиля. Если стиль «не работает», проверьте, не перебит ли он локальным значением;
- неявный стиль в ресурсах окна действует только на это окно. Чтобы оформить всё приложение, перенесите его в App.xaml или в отдельный ResourceDictionary;
- привязка данных по умолчанию синхронная, а атрибут IsAsync=True у Binding позволяет получать медленные значения в фоне, не блокируя интерфейс;
- элементы управления WPF похожи на свои аналоги из Windows Forms по названию, но привязка данных и модель событий у них заметно отличаются.
Как это делают сегодня
Вопросы и ответы
Почему после перехода на .NET 4 пропали тени и свечение?
Классы BitmapEffect в .NET 4 устарели и больше не рисуются. Замените DropShadowBitmapEffect на DropShadowEffect, а свечение имитируйте тем же эффектом с ShadowDepth=0.
Как применить стиль не ко всем кнопкам, а только к некоторым?
Дайте стилю ключ x:Key и укажите его у нужных кнопок атрибутом Style со ссылкой StaticResource. Неявным стиль становится только без ключа.
Почему при наведении кнопка всё равно голубая?
Этот фон рисует системный шаблон кнопки, и свойство Background его не перекрывает. Помогает только замена шаблона через сеттер Template, как в примере.