Wpf 실시간 테마 구현 방법에 대해

안녕하세요. :smile:

WPF에서 리소스를 활용한 테마 변경 방법에 대한 질문입니다.

제가 사용한 방법은 동일한 x:Key를 갖는 2개 이상의 리소스 테마를 구현하고 이 리소스 테마를 Runtime 상에서 바꾸는 방식인데요.

제가 궁금한 점은 MergedDictionaries 컬렉션에서 추가하고 제거하는 것이 올바른 방법인지, 그리고 다른 방법에 대해서도 알고싶습니다.


아래는 제가 구현한 방식의 일부분입니다.

각각의 테마 파일에 같은 x:Key를 가진 리소스를 정의했습니다.

BrushResource.Dark.xaml

<SolidColorBrush Color="#FFEB33" x:Key="BRUSH.POINT"/>
<SolidColorBrush Color="#222222" x:Key="BRUSH.BG.01"/>
...

BrushResource.White.xaml

<SolidColorBrush Color="#FFEB33" x:Key="BRUSH.POINT"/>
<SolidColorBrush Color="#FFFFFF" x:Key="BRUSH.BG.01"/>
...

그리고 리소스를 DynamicResource 형식으로 모든 화면에서 사용하도록 하고 있습니다.

<Border Background="{DynamicResource BRUSH.POINT}"/>

마지막으로 실시간 테마 변경을 위한 Add/Remove 기능까지 구현했습니다.

private ResourceDictionary Dark;
private ResourceDictionary White;


private void StartApp()
{
        Dark = new ResourceDictionary { Source = new Uri("...Dark.xaml"); };
        White = new ResourceDictionary { Source = new Uri("...White.xaml"); };

        App.Current.Resources.MergedDictionaries.Add(Dark);        
}

private void TemeChange()
{
    if (App.Current.Resources.MergedDictionaries.Contains(Dark))
    {
        App.Current.Resources.MergedDictionaries.Remove(Dark);
        App.Current.Resources.MergedDictionaries.Add(White);
    }
    else if (App.Current.Resources.MergedDictionaries.Contains(White))
    {
        App.Current.Resources.MergedDictionaries.Remove(White);
        App.Current.Resources.MergedDictionaries.Add(Dark);
    }
}

보시는 바와 같이 직접 MergedDictionaries 컬렉션에서 직접 교체하는 방식으로 구현되어있습니다.

기능상으로는 문제가 없어보이지만 이 방법이 DynamicResource를 활용하기 위한 올바른 방법인지 잘 모르겠습니다.

읽어주셔서 감사합니다.

좋아요 3

런타임중에 테마를 바꿔야한다면 이 방법도 현명하다고 생각합니다. :blush:

좋아요 4

이 방법 말고 다른 효율적인 방법이 있나요? 저도 유사하게 썼던 기억입니다.

좋아요 3

테마를 지원하는 ModernWpf의 경우에도 유사하게 코딩되었음을 알 수 있습니다.

GitHub - Kinnara/ModernWpf: Modern styles and controls for your WPF applications


        internal void ApplyApplicationTheme(ApplicationTheme theme)
        {
            int targetIndex = DesignMode.DesignModeEnabled ? 1 : 0;

            if (SystemParameters.HighContrast)
            {
                EnsureHighContrastResources();

                if (IsMerged(_highContrastResources))
                {
                    if (CanBeAccessedAcrossThreads)
                    {
                        RefreshHighContrastResources();
                    }
                }
                else
                {
                    MergedDictionaries.InsertOrReplace(targetIndex, _highContrastResources);
                    MergedDictionaries.RemoveIfNotNull(_lightResources);
                    MergedDictionaries.RemoveIfNotNull(_darkResources);
                }
            }
            else
            {
                if (theme == ApplicationTheme.Light)
                {
                    EnsureLightResources();
                    MergedDictionaries.InsertOrReplace(targetIndex, _lightResources);
                    MergedDictionaries.RemoveIfNotNull(_darkResources);
                }
                else if (theme == ApplicationTheme.Dark)
                {
                    EnsureDarkResources();
                    MergedDictionaries.InsertOrReplace(targetIndex, _darkResources);
                    MergedDictionaries.RemoveIfNotNull(_lightResources);
                }
                else
                {
                    throw new ArgumentOutOfRangeException(nameof(theme));
                }

                MergedDictionaries.RemoveIfNotNull(_highContrastResources);
            }

            Debug.Assert(MergedThemeDictionaryCount == 1);
        }
...

...
좋아요 1

@dimohy @SangHyeon.Kim

이 앱도 다음과 같이 테마를 사용하네요!

public void Execute(ThemeType newTheme, bool followSystemTheme = false)
{
	ForkSettings.Default.Theme = newTheme;
	ForkSettings.Default.FollowSystemTheme = followSystemTheme;
	App.RefreshWindowBorderBrush();
	ResourceDictionary resourceDictionary = Application.Current.Resources.MergedDictionaries.Where((ResourceDictionary rd) => rd.Source != null).FirstOrDefault((ResourceDictionary rd) => Regex.Match(rd.Source.OriginalString, "(\\/Fork;component\\/Theme\\/Generic\\.)((Light)|(Dark))").Success);
	ResourceDictionary item = new ResourceDictionary
	{
		Source = newTheme.ResourceUri()
	};
	Application.Current.Resources.MergedDictionaries.Add(item);
	if (resourceDictionary != null)
	{
		Application.Current.Resources.MergedDictionaries.Remove(resourceDictionary);
	}
	NotificationCenter.Current.RaiseApplicationThemeChanged(this, newTheme);
}