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

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

Комментарии (1)


  1. CloudlyNosound
    04.09.2025 13:29

    Когда вижу аббревиатуру WPF всегда хочется пошутить: "ну про WordPress то мы слышали. а `f` что такое? ms forth?". ;)