DynamicResource, StaticResource, Runtime Theme Switching, ResourceDictionary, MergedDictionaries, Application.Current.Resources, SystemColors, SystemFonts, MaterialDesignInXamlToolkit, MahApps.Metro, HandyControl.У WPF ресурси можна отримувати двома способами: {StaticResource} та {DynamicResource}. Ця різниця є фундаментальною для побудови систем тематизації, і її важливо зрозуміти перш ніж переходити до практики.
{StaticResource} вирішується один раз: при завантаженні XAML-файлу. Це означає, що якщо ресурс змінився після завантаження — елемент цього не дізнається. StaticResource трохи швидший, оскільки не відстежує зміни. Але для тематизації — абсолютно непридатний.
{DynamicResource} відстежує ресурс під час виконання: якщо ресурс у словнику замінено — всі елементи, що посилаються на нього через DynamicResource, автоматично оновляться. Саме це є механізмом, що дозволяє перемикати теми без перезавантаження вікна.
Проілюструємо різницю:
<!-- Light тема у ресурсах: -->
<SolidColorBrush x:Key="AppBackground" Color="#F9FAFB"/>
<!-- Кнопка з StaticResource --- -->
<!-- При зміні AppBackground після завантаження — -->
<!-- Background кнопки НЕ зміниться -->
<Button Background="{StaticResource AppBackground}" Content="Не реагує на тему"/>
<!-- Кнопка з DynamicResource --- -->
<!-- При зміні AppBackground — Background оновиться -->
<Button Background="{DynamicResource AppBackground}" Content="Реагує на тему"/>
Правило для тематизованих проєктів: всі ресурси, що залежать від теми, мають використовувати DynamicResource. Ресурси, що не змінюються ніколи (розміри, фіксовані шрифти, константи) — можуть використовувати StaticResource.
Перш ніж реалізовувати перемикання тем, потрібно зрозуміти, як організувати ресурси у файловій структурі реального проєкту. Зберігати все в App.xaml — рішення, яке не масштабується. Вже при 50+ ресурсах це стає некерованим.
Рекомендована структура:
MyApp/
├── App.xaml ← реєстрація тем
├── Themes/
│ ├── Light.xaml ← кольори світлої теми
│ └── Dark.xaml ← кольори темної теми
├── Styles/
│ ├── Buttons.xaml ← стилі кнопок (DynamicResource на кольори)
│ ├── TextBoxes.xaml ← стилі полів вводу
│ ├── Typography.xaml ← стилі тексту
│ └── Cards.xaml ← стилі карток/панелей
└── Resources/
├── Icons.xaml ← іконки (Path Geometry)
└── Converters.xaml ← ресурси конвертерів
Принцип поділу: Themes/ містять лише кольори та кольорові ресурси (Brush, Color). Styles/ містять стилі контролів, що посилаються на теми через {DynamicResource}. Це забезпечує повне розділення: замінюємо тему — стилі автоматично беруть нові кольори.
<!-- Themes/Light.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Семантичні ключі — не "LightBlue", а "AppBackground" -->
<Color x:Key="AppBackgroundColor">#F9FAFB</Color>
<Color x:Key="AppSurfaceColor">#FFFFFF</Color>
<Color x:Key="AppBorderColor">#E5E7EB</Color>
<Color x:Key="AppTextPrimaryColor">#111827</Color>
<Color x:Key="AppTextSecondaryColor">#6B7280</Color>
<Color x:Key="AppAccentColor">#4F46E5</Color>
<Color x:Key="AppAccentHoverColor">#3730A3</Color>
<Color x:Key="AppSuccessColor">#10B981</Color>
<Color x:Key="AppDangerColor">#EF4444</Color>
<Color x:Key="AppWarningColor">#F59E0B</Color>
<!-- Brush-и на основі Color-ів -->
<SolidColorBrush x:Key="AppBackgroundBrush"
Color="{StaticResource AppBackgroundColor}"/>
<SolidColorBrush x:Key="AppSurfaceBrush"
Color="{StaticResource AppSurfaceColor}"/>
<SolidColorBrush x:Key="AppBorderBrush"
Color="{StaticResource AppBorderColor}"/>
<SolidColorBrush x:Key="AppTextPrimaryBrush"
Color="{StaticResource AppTextPrimaryColor}"/>
<SolidColorBrush x:Key="AppTextSecondaryBrush"
Color="{StaticResource AppTextSecondaryColor}"/>
<SolidColorBrush x:Key="AppAccentBrush"
Color="{StaticResource AppAccentColor}"/>
<SolidColorBrush x:Key="AppAccentHoverBrush"
Color="{StaticResource AppAccentHoverColor}"/>
<SolidColorBrush x:Key="AppSuccessBrush"
Color="{StaticResource AppSuccessColor}"/>
<SolidColorBrush x:Key="AppDangerBrush"
Color="{StaticResource AppDangerColor}"/>
</ResourceDictionary>
<!-- Themes/Dark.xaml — ідентичні ключі, інші значення -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Color x:Key="AppBackgroundColor">#0F172A</Color>
<Color x:Key="AppSurfaceColor">#1E293B</Color>
<Color x:Key="AppBorderColor">#334155</Color>
<Color x:Key="AppTextPrimaryColor">#F8FAFC</Color>
<Color x:Key="AppTextSecondaryColor">#94A3B8</Color>
<Color x:Key="AppAccentColor">#818CF8</Color>
<Color x:Key="AppAccentHoverColor">#A5B4FC</Color>
<Color x:Key="AppSuccessColor">#34D399</Color>
<Color x:Key="AppDangerColor">#F87171</Color>
<Color x:Key="AppWarningColor">#FBBF24</Color>
<SolidColorBrush x:Key="AppBackgroundBrush"
Color="{StaticResource AppBackgroundColor}"/>
<SolidColorBrush x:Key="AppSurfaceBrush"
Color="{StaticResource AppSurfaceColor}"/>
<SolidColorBrush x:Key="AppBorderBrush"
Color="{StaticResource AppBorderColor}"/>
<SolidColorBrush x:Key="AppTextPrimaryBrush"
Color="{StaticResource AppTextPrimaryColor}"/>
<SolidColorBrush x:Key="AppTextSecondaryBrush"
Color="{StaticResource AppTextSecondaryColor}"/>
<SolidColorBrush x:Key="AppAccentBrush"
Color="{StaticResource AppAccentColor}"/>
<SolidColorBrush x:Key="AppAccentHoverBrush"
Color="{StaticResource AppAccentHoverColor}"/>
<SolidColorBrush x:Key="AppSuccessBrush"
Color="{StaticResource AppSuccessColor}"/>
<SolidColorBrush x:Key="AppDangerBrush"
Color="{StaticResource AppDangerColor}"/>
</ResourceDictionary>
AppBackground, а не LightGray. Семантичне іменування описує призначення ресурсу, а не його колір. Так AppBackground у Light темі може бути #F9FAFB, а у Dark — #0F172A. Стилі залишаються незмінними. Якщо ж ключ — LightGrayBrush — це ім'я перестає мати сенс у Dark темі.У App.xaml реєструємо початковий набір словників через MergedDictionaries:
<!-- App.xaml -->
<Application x:Class="MyApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- 1. Спочатку тема (кольори) -->
<ResourceDictionary Source="Themes/Light.xaml"/>
<!-- 2. Потім стилі (посилаються на кольори через DynamicResource) -->
<ResourceDictionary Source="Styles/Buttons.xaml"/>
<ResourceDictionary Source="Styles/TextBoxes.xaml"/>
<ResourceDictionary Source="Styles/Typography.xaml"/>
<ResourceDictionary Source="Styles/Cards.xaml"/>
<!-- 3. Ресурси (конвертери, іконки) -->
<ResourceDictionary Source="Resources/Converters.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Порядок важливий: тема Light.xaml завантажується перед стилями. Якщо стиль у Buttons.xaml використовує {DynamicResource AppAccentBrush} — на момент завантаження стилю ресурс вже існує у словнику.
Тепер реалізуємо патерн ThemeService — сервіс, що відповідає за перемикання теми. Механізм: знаходимо у MergedDictionaries поточний словник теми, замінюємо його новим. Оскільки всі елементи використовують DynamicResource — вони автоматично відмалюються.
// Services/ThemeService.cs
public enum AppTheme { Light, Dark }
public static class ThemeService
{
private const string LightThemeSource = "Themes/Light.xaml";
private const string DarkThemeSource = "Themes/Dark.xaml";
public static AppTheme CurrentTheme { get; private set; } = AppTheme.Light;
public static void ApplyTheme(AppTheme theme)
{
var mergedDicts = Application.Current.Resources.MergedDictionaries;
// Знаходимо поточний словник теми (перший у MergedDictionaries)
var themeDict = mergedDicts.FirstOrDefault(d =>
d.Source?.OriginalString.Contains("Themes/") == true);
if (themeDict != null)
mergedDicts.Remove(themeDict);
// Визначаємо URI нової теми
var newThemeUri = theme == AppTheme.Dark
? new Uri(DarkThemeSource, UriKind.Relative)
: new Uri(LightThemeSource, UriKind.Relative);
// Завантажуємо та вставляємо новий словник
var newThemeDict = new ResourceDictionary { Source = newThemeUri };
mergedDicts.Insert(0, newThemeDict); // Вставляємо на початок
CurrentTheme = theme;
}
public static void ToggleTheme() =>
ApplyTheme(CurrentTheme == AppTheme.Light ? AppTheme.Dark : AppTheme.Light);
}
// ViewModel: кнопка перемикання теми
public class SettingsViewModel : ObservableObject
{
private bool _isDarkTheme = false;
public bool IsDarkTheme
{
get => _isDarkTheme;
set
{
SetProperty(ref _isDarkTheme, value);
ThemeService.ApplyTheme(value ? AppTheme.Dark : AppTheme.Light);
}
}
}
mergedDicts.Insert(0, newThemeDict) — обов'язково на позицію 0. Якщо вставити в кінець — словник теми буде після стилів, і стилі вже завантажені зі StaticResource. Тема обов'язково має бути першою у MergedDictionaries, до стилів. Для DynamicResource порядок важливий при ініціалізації.Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<StackPanel.Resources>
<!-- Симульована "тема" прямо в Resources для демонстрації -->
<SolidColorBrush x:Key="AppBackgroundBrush" Color="#F9FAFB"/>
<SolidColorBrush x:Key="AppSurfaceBrush" Color="White"/>
<SolidColorBrush x:Key="AppBorderBrush" Color="#E5E7EB"/>
<SolidColorBrush x:Key="AppTextPrimaryBrush" Color="#111827"/>
<SolidColorBrush x:Key="AppAccentBrush" Color="#4F46E5"/>
<SolidColorBrush x:Key="AppAccentHoverBrush" Color="#3730A3"/>
<!-- Стиль картки використовує DynamicResource -->
<Style x:Key="ThemeCard" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Padding" Value="20"/>
</Style>
<!-- Стиль кнопки через DynamicResource -->
<Style TargetType="Button">
<Setter Property="Background" Value="{DynamicResource AppAccentBrush}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="14,8"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource AppAccentHoverBrush}"/>
</Trigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<!-- Фон через DynamicResource -->
<Border Background="{DynamicResource AppBackgroundBrush}"
CornerRadius="16" Padding="16">
<StackPanel Spacing="12">
<!-- Картка 1 -->
<Border Style="{StaticResource ThemeCard}">
<StackPanel Spacing="6">
<TextBlock Text="Статистика місяця"
FontWeight="SemiBold"
Foreground="{DynamicResource AppTextPrimaryBrush}"
FontSize="14"/>
<TextBlock Text="Всі значення використовують DynamicResource"
Foreground="{DynamicResource AppTextSecondaryBrush}"
FontSize="12"/>
</StackPanel>
</Border>
<!-- Метрики -->
<UniformGrid Columns="3" Rows="1">
<Border Style="{StaticResource ThemeCard}" Margin="0,0,6,0">
<StackPanel>
<TextBlock Text="142" FontSize="24" FontWeight="Bold"
Foreground="{DynamicResource AppAccentBrush}"/>
<TextBlock Text="Задачі" FontSize="11"
Foreground="{DynamicResource AppTextSecondaryBrush}"/>
</StackPanel>
</Border>
<Border Style="{StaticResource ThemeCard}" Margin="3,0">
<StackPanel>
<TextBlock Text="89%" FontSize="24" FontWeight="Bold"
Foreground="#10B981"/>
<TextBlock Text="Виконано" FontSize="11"
Foreground="{DynamicResource AppTextSecondaryBrush}"/>
</StackPanel>
</Border>
<Border Style="{StaticResource ThemeCard}" Margin="6,0,0,0">
<StackPanel>
<TextBlock Text="15" FontSize="24" FontWeight="Bold"
Foreground="#F59E0B"/>
<TextBlock Text="В черзі" FontSize="11"
Foreground="{DynamicResource AppTextSecondaryBrush}"/>
</StackPanel>
</Border>
</UniformGrid>
<Button Content="Переключити тему"
Command="{Binding ToggleThemeCommand}"/>
</StackPanel>
</Border>
</StackPanel>
WPF надає доступ до системних кольорів і шрифтів через статичні класи SystemColors та SystemFonts. Ці ресурси автоматично оновлюються при зміні теми ОС (Класична, Aero, High Contrast).
<!-- SystemColors через StaticResource (один раз) -->
<Border Background="{x:Static SystemColors.WindowBrush}">
<TextBlock Text="Системний колір вікна"
Foreground="{x:Static SystemColors.WindowTextBrush}"/>
</Border>
<!-- SystemColors через DynamicResource (реагує на зміну теми ОС) -->
<Border Background="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}">
<TextBlock Text="Колір виділення ОС"
Foreground="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
</Border>
SystemColors ресурс | Значення |
|---|---|
SystemColors.WindowBrush | Фон вікна |
SystemColors.WindowTextBrush | Текст у вікні |
SystemColors.HighlightBrush | Фон виділення (selection) |
SystemColors.HighlightTextBrush | Текст при виділенні |
SystemColors.ControlBrush | Фон контролів |
SystemColors.GrayTextBrush | Приглушений текст |
SystemColors є кращим вибором, ніж кастомні ресурси. Вони автоматично дотримуються налаштувань High Contrast і адаптуються до теми ОС без жодного коду.Створення повноцінної системи стилів з нуля — величезна робота. Потрібно стилізувати десятки контролів (Button, TextBox, ComboBox, DataGrid, Slider, меню, скроллбари), врахувати всі стани (hover, pressed, disabled) та реалізувати Light/Dark теми.
У більшості професійних WPF-проєктів використовують готові UI-бібліотеки, що надають цілісну дизайн-систему. Зробимо короткий огляд трьох найпопулярніших.
Найпопулярніша бібліотека для WPF. Реалізує Google Material Design.
PackIcon), розширені контроли (ColorZone, DialogHost, Snackbar, DrawerHost).Install-Package MaterialDesignThemesApp.xaml додати словники Material Design (описано в документації бібліотеки).Одна з найстаріших і найстабільніших бібліотек.
MetroWindow — заміна стандартного Window з кастомним TitleBar), гнучка система колірних акцентів (Blue, Red, Green тощо), додаткові контроли (ToggleSwitch, NumericUpDown).Китайська бібліотека, яка стрімко набирає популярність завдяки величезній кількості кастомних контролів.
Carousel, Rate, Watermark, Timeline, StepBar, Divider).Ціль: Організувати кольори у словники та підключити їх в App.xaml.
Завдання:
Themes у проєкті.Light.xaml та Dark.xaml (тип ResourceDictionary).AppBackground, AppForeground, PrimaryColor з різними значеннями.App.xaml підключіть лише Light.xaml через MergedDictionaries.MainWindow.xaml встановіть Background="{DynamicResource AppBackground}".Перевірка: Запустіть додаток. У App.xaml змініть Source словника на Dark.xaml, перезапустіть. Вікно має змінити колір.
Ціль: Навчитись динамічно замінювати словники у Application.Current.Resources.
Завдання:
MainWindow кнопку "Перемкнути тему".ToggleTheme() (або прив'яжіть до команди у MVVM).new ResourceDictionary { Source = ... } для Dark, додайте в колекцію на індекс 0.App.xaml, що використовують ці динамічні ресурси.Перевірка: При натисканні на кнопку додаток має миттєво змінювати кольори без перезапуску. Жоден стиль не ламається.
Ціль: Встановити та налаштувати третєсторонню бібліотеку тем.
Завдання:
MaterialDesignThemes.<Application.Resources.MergedDictionaries>.Button, TextBox, CheckBox.Перевірка: Запустіть додаток. Всі стандартні контроли мають отримати Material-вигляд (flat кнопки з анімацією натискання (ripple), поля вводу з підкресленням) без жодного рядка коду або додаткових атрибутів (це сила implicit стилів).
Тематизація WPF-додатку базується на механізмі {DynamicResource}. Він дозволяє підписатись на оновлення ресурсу в runtime. На відміну від нього, {StaticResource} «фіксує» значення під час ініціалізації вікна (швидше, але не підходить для тем).
Організація ресурсів:
Themes/Light.xaml — лише кольори та пензлі (Brushes). Використовуємо семантичні ключі (AppSurfaceBrush, а не WhiteBrush).Styles/*.xaml — шаблони контролів, що беруть кольори через DynamicResource.Перемикання тем у коді зводиться до маніпуляцій з колекцією Application.Current.Resources.MergedDictionaries — старий словник видаляється, новий вставляється на нульову позицію. Всі DynamicResource посилання автоматично реалізують оновлення UI.
Для системного вигляду ОС (який реагує на налаштування Windows) використовуємо статичні класи SystemColors та SystemFonts.
Для професійних проєктів стилізація «з нуля» економічно невиправдана. Індустріальним стандартом є використання готових UI-бібліотек. MaterialDesignInXaml — найпопулярніша бібліотека (сучасний вигляди, анімації, сотні іконок). Альтернативи — MahApps.Metro (корпоративний Windows стиль) та HandyControl (сотні кастомних контролів).
Наступна тема — Адаптивний UI та Ресурси в Avalonia. Avalonia пропонує значно елегантніший спосіб тематизації. Замість ручної маніпуляції з ResourceDictionary, вона використовує механізм ThemeVariant (Light/Dark перемикання «з коробки») та нову систему ресурсів, що дозволяє перевизначати окремі кольорові токени на льоту. Ми розглянемо, як портувати набуті знання тематизації з WPF в сучасний Avalonia-підхід.
Pseudo-classes в Avalonia — замість WPF Triggers
Avalonia замінила WPF Triggers на CSS pseudo-classes. Вбудовані :pointerover, :focus, :pressed, :checked, кастомні PseudoClasses.Set(), data-driven стилізація через compiled bindings — повний посібник.
Avalonia Themes — Fluent Design та система тематизації
Вбудована система тем Avalonia — Fluent Theme, Simple Theme, ThemeVariant для Dark/Light режимів, runtime switching та порівняння з WPF