Словари ресурсов WPF. Переключаем тему приложения на лету

от автора

Для кого эта статья:

Эта статья будет полезна разработчикам, которые только начинают писать 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, придётся либо прописать наследование, либо также обновлять ресурсы конкретных окон.


ссылка на оригинал статьи https://habr.com/ru/articles/943908/