Для кого эта статья:
Эта статья будет полезна разработчикам, которые только начинают писать WPF. Здесь будет рассмотрена механика динамических ресурсов - опытные WPF-разработчики вряд ли найдут что-то полезное для себя.
В современном мире отсутствие возможности выбора темы в приложении считается моветоном. Пользователи любят выбирать удобную для себя цветовую схему, особенно при работе по ночам. В WPF такое поведение не организовано “из коробки”, поэтому мы создаём свою реализацию: задаём ресурсы (цвета и стили), даём пользователю переключать их на лету. О реализации этого механизма мы и поговорим в этой статье.
Для создания такой возможности воспользуемся словарями ресурсов (ResourceDictionary):
ResourceDictionary – репозиторий, внутри которого мы можем определять ресурсы: цвета, стили и т.д. Основной плюс: ключи (x:Key) служат именами, что при условии использования DynamicResource позволит нам менять их во время работы с приложением Создадим папку Themes, а в ней два таких словаря: Light.xaml и Dark.xaml. И зададим в них параметры цветов.
Themes/Light.xaml:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Color x:Key="WindowBackgroundColor">#FFFFFFFF</Color>
<SolidColorBrush x:Key="WindowBackgroundBrush" Color="{StaticResource WindowBackgroundColor}"/>
<SolidColorBrush x:Key="ButtonBackgroundBrush" Color="LightGray"/>
<SolidColorBrush x:Key="WindowForegroundBrush" Color="#000000"/>
</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="WindowBackgroundColor">#FF2D2D30</Color>
<SolidColorBrush x:Key="WindowBackgroundBrush" Color="{StaticResource WindowBackgroundColor}"/>
<SolidColorBrush x:Key="ButtonBackgroundBrush" Color="Gray"/>
<SolidColorBrush x:Key="WindowForegroundBrush" Color="#FFFFFF"/>
</ResourceDictionary>
Важно, что в обоих словарях используются одинаковые ключи (WindowBackgroundBrush, WindowForegroundBrush и т.д.). Благодаря этому переключение словаря автоматически изменит все привязанные к этим ключам элементы.
В App.xaml подключим одну из тем по умолчанию. Если мы этого не сделаем, приложение при запуске будет использовать стандартные цвета, так как не будет знать про наши «стили»:
App.xaml:
<Application x:Class="WpfApp1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes/Light.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Теперь в разметке окна вместо стандартных цветов мы используем наши ключи:
MainWindow.xaml:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF Theme Demo" Height="200" Width="400">
<Grid Background="{DynamicResource WindowBackgroundBrush}">
<TextBlock Text="Пример текста" Foreground="{DynamicResource WindowForegroundBrush}"
FontSize="16" Margin="10"/>
<Button Background="{DynamicResource ButtonBackgroundBrush}" HorizontalAlignment="Left" Content="Light" Width="80" Height="30" Margin="80,0,0,0"
Click="Light_Click" />
<Button Background="{DynamicResource ButtonBackgroundBrush}" HorizontalAlignment="Left" Content="Dark" Width="80" Height="30" Margin="160,0,0,0"
Click="Dark_Click" />
</Grid>
</Window>
Чтобы менять тему (например, по нажатию кнопки или через меню), нам нужно удалить уже использующийся словарь и «подгрузить» нужный. Для этого действия создадим метод ApplyTheme:
MainWindow.xaml.cs:
void ApplyTheme(string themePath)
{
// Загружаем словарь ресурсов из файла
var themeDict = new ResourceDictionary { Source = new Uri(themePath, UriKind.Relative) };
// Очищаем текущие словари и добавляем новый
Application.Current.Resources.MergedDictionaries.Clear();
Application.Current.Resources.MergedDictionaries.Add(themeDict);
}
Важно удалять старый словарь (Clear()), иначе может дублироваться ресурс с тем же ключом.
В функциях кнопок вызываем наш метод ApplyTheme, на вход которому передаём путь до нужного нам в контексте кнопки файла ресурсного словаря:
MainWindow.xaml.cs:
private void Light_Click(object sender, RoutedEventArgs e)
{
ApplyTheme("Themes/Light.xaml");
}
private void Dark_Click(object sender, RoutedEventArgs e)
{
ApplyTheme("Themes/Dark.xaml");
}
По итогу MainWindow.xaml.cs выглядит так
using System;
using System.Windows;
namespace WpfApp1
{
public partial class MainWindow : Window
{
void ApplyTheme(string themePath)
{
// Загружаем словарь ресурсов из файла
var themeDict = new ResourceDictionary { Source = new Uri(themePath, UriKind.Relative) };
// Очищаем текущие словари и добавляем новый
Application.Current.Resources.MergedDictionaries.Clear();
Application.Current.Resources.MergedDictionaries.Add(themeDict);
}
public MainWindow()
{
InitializeComponent();
}
private void Light_Click(object sender, RoutedEventArgs e)
{
ApplyTheme("Themes/Light.xaml");
}
private void Dark_Click(object sender, RoutedEventArgs e)
{
ApplyTheme("Themes/Dark.xaml");
}
}
}
Результат работы программы
Запускаем наш проект в Visual Studio:

Получаем следующее:
Вид приложения при запуске (используется белая тема, указанная нами в App.xaml):

Нажали на кнопку «Dark»:

Нажали кнопку «Light»:

Важные моменты
DynamicResource vs StaticResource. Как уже сказано, все ресурсы (кисти, цвета, стили), которые меняются при смене темы, должны использовать DynamicResource. StaticResource выгоден с точки зрения производительности, но он не «откликается» на изменения ресурсов во время работы приложения.
Порядок словарей. Если вы подключаете несколько словарей через MergedDictionaries, учитывайте, что в случае конфликтующих ключей будет использовано значение из последнего словаря в списке.
Несколько окон. Если в приложении несколько открытых окон, изменение Application.Current.Resources затронет их все. Но если у окна есть собственная коллекция Resources, придётся либо прописать наследование, либо также обновлять ресурсы конкретных окон.
CloudlyNosound
Когда вижу аббревиатуру WPF всегда хочется пошутить: "ну про WordPress то мы слышали. а `
f
` что такое? ms forth?". ;)