Градиентная кнопка в WPF: свой шаблон, свечение текста и тень

РазделWPFУровеньСредний Чтение8 минОпубликовано ТегиWPFXAMLСтили

WPF позволяет оформить кнопку как на макете дизайнера: градиент, скругление, мягкая тень и светящийся текст. Всё это описывается стилем один раз и применяется ко всем кнопкам окна или приложения.

Главное. Внешний вид кнопки в WPF полностью определяется стилем и шаблоном: градиент задаётся кистью, свечение и тень свойством Effect, а обратная связь триггерами. Устаревший BitmapEffect в новых проектах использовать не стоит.

Стили 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 (кисти)XML
<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>
Две точки с близкими Offset (0.5 и 0.51) дают резкую границу посередине кнопки. Это простой способ получить эффект блика без картинок.

Свой шаблон кнопки: ControlTemplate и TemplateBinding

Стандартная кнопка рисуется системной темой, и свойство Background влияет на неё лишь частично: в Windows 7 и новее при наведении тема всё равно подставляет свой голубой фон. Чтобы полностью управлять видом, нужно заменить шаблон через сеттер свойства Template.

Внутри шаблона мы рисуем Border со скруглёнными углами и кладём в него ContentPresenter. Это заполнитель, на место которого WPF выводит содержимое кнопки: текст, картинку или целую панель. Связка TemplateBinding Background передаёт фон, заданный у самой кнопки, внутрь шаблона, поэтому отдельные кнопки по-прежнему можно перекрасить атрибутом.

Стиль кнопкиXML
<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>
RecognizesAccessKey=True сохраняет горячие клавиши: подпись «_Сохранить» по-прежнему будет нажиматься через Alt+С.

Свечение текста и тень: BitmapEffect против Effect

В первых версиях WPF эффекты задавались свойством BitmapEffect: DropShadowBitmapEffect для тени, OuterGlowBitmapEffect для свечения, а BitmapEffectGroup позволял объединить несколько эффектов. Это работало, но рендерилось программно и заметно нагружало процессор, особенно в списках с десятками элементов.

Начиная с .NET 3.5 SP1 появилось свойство Effect с аппаратно ускоренными DropShadowEffect и BlurEffect, а в .NET 4 классы BitmapEffect объявлены устаревшими и больше ничего не рисуют. Если вы сопровождаете старый проект и после перехода на новый фреймворк тени пропали, причина именно в этом.

Готового эффекта свечения в новом API нет, но его легко имитировать: DropShadowEffect с нулевой глубиной ShadowDepth и заметным BlurRadius рисует ореол равномерно со всех сторон. Применяем его к ContentPresenter, и светиться будет только текст. Тень всей кнопке задаём вторым эффектом на Border.

Эффекты внутри шаблонаXML
<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.TriggersXML
<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>
Не забывайте про фокус клавиатуры и состояние IsEnabled. Без них кнопка выглядит красиво, но пользователь, работающий с клавиатурой, не видит, где он находится.

Что ещё полезно знать новичку в WPF

Несколько моментов, на которых часто спотыкаются при переходе с Windows Forms и ASP.NET:

  • Grid в WPF это контейнер разметки по строкам и столбцам, а не таблица данных. Для табличного вывода есть DataGrid и ListView с режимом GridView, о них отдельные статьи раздела;
  • свойство, заданное атрибутом прямо у элемента, имеет приоритет над сеттером стиля. Если стиль «не работает», проверьте, не перебит ли он локальным значением;
  • неявный стиль в ресурсах окна действует только на это окно. Чтобы оформить всё приложение, перенесите его в App.xaml или в отдельный ResourceDictionary;
  • привязка данных по умолчанию синхронная, а атрибут IsAsync=True у Binding позволяет получать медленные значения в фоне, не блокируя интерфейс;
  • элементы управления WPF похожи на свои аналоги из Windows Forms по названию, но привязка данных и модель событий у них заметно отличаются.

Как это делают сегодня

WPF живёт и на .NET 8 и 9, а в .NET 9 появилась встроенная тема Fluent, включаемая через ThemeMode и дающая современные кнопки без собственного шаблона. Для оформления в духе Windows 11 подходят также WPF UI и ModernWpf. Если нужен кроссплатформенный интерфейс, похожий XAML со стилями и шаблонами есть в Avalonia, а для новых приложений только под Windows Microsoft рекомендует WinUI 3.

Вопросы и ответы

Почему после перехода на .NET 4 пропали тени и свечение?

Классы BitmapEffect в .NET 4 устарели и больше не рисуются. Замените DropShadowBitmapEffect на DropShadowEffect, а свечение имитируйте тем же эффектом с ShadowDepth=0.

Как применить стиль не ко всем кнопкам, а только к некоторым?

Дайте стилю ключ x:Key и укажите его у нужных кнопок атрибутом Style со ссылкой StaticResource. Неявным стиль становится только без ключа.

Почему при наведении кнопка всё равно голубая?

Этот фон рисует системный шаблон кнопки, и свойство Background его не перекрывает. Помогает только замена шаблона через сеттер Template, как в примере.

Читайте также