【Material Design Themes】ComboBoxでPrimaryColorを切り替える

# はじめに
Material Design ThemesのPrimaryColorをコンボボックスで切り替えるためのユーザーコントロールを作成したので備忘録として公開します。この手順で同様にSecondaryColorを切り替えるためのユーザーコントロールも作成できます。
MVVMパターンで記述するため、CommunityToolkit.Mvvmも合わせて使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562156/7099f8e3-ca97-c206-02f8-6c2e66ab64fd.png)

前回、トグルボタンでダークモードに切り替えるためのユーザーコントロールを作成したので、説明が重複しますがご了承ください。前回の続きからの方は、プロジェクトフォルダに`Converters`フォルダを追加して、`PrimarySelectorViewModel.cs`の項目まで飛ばしてください。

https://qiita.com/ReCraft/items/a6b118b73e43859bffb3


# 情報
**アプリケーション**
- アプリケーション：WPFアプリケーション
- ターゲットフレームワーク：.Net 8.0

**ライブラリ**
- CommunityToolkit.Mvvm 8.2.2
- MaterialDesignThemes 5.0.0
- MaterialDesignColors 3.0.0

# プロジェクトの作成
プロジェクト名を`MaterialDesignApps`としてプロジェクトを作成しました。作成が完了したら、画像のようにフォルダを作成してください。前回の続きからの方は、`Converters`フォルダを追加してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562156/406a7c84-fdd7-bc40-7302-c781a190bb3c.png)


フォルダ作成ができたら`MainWindow`をViewsフォルダに移動してください。このとき、`MainWindow`の名前空間を`MaterialDesignApps.Views`に変更してください。また、`App.xaml`のStartupUriを`Views\MainWindow.xaml`に変更してください。

## ライブラリのインストール
NuGetパッケージの管理から`CommunityToolkit.Mvvm`と`MaterialDesignThemes`、`MaterialDesignColors`をインストールしてください。

# App.xamlの設定
```diff_xml:App.xaml
<Application
    x:Class="MaterialDesignApps.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MaterialDesignApps"
    xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
    StartupUri="Views\MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="Blue" SecondaryColor="Blue"/>
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>
```

# MainWindow.xamlの設定
次に、MainWindow.xamlに`Style="{StaticResource MaterialDesignWindow}"`を追加します。
```xml:Views\MainWindow.xaml
<Window ...
    Style="{StaticResource MaterialDesignWindow}">
    ...
</Window>
```

# 確認
先程の`App.xaml`の`BaseTheme`を`Dark`に変更して、アプリケーションを起動してください。
これまでの作業に問題がなければ、背景が黒色のアプリケーションが起動されます。
これで、WPFアプリケーションでMaterial Design Themesを使用する準備が整いました。

# ThemeHelper.cs
`Helpers`フォルダに`ThemeHelper.cs`を作成します。
```Csharp:Helpers\ThemeHelper.cs
using MaterialDesignColors;
using MaterialDesignThemes.Wpf;
using System.Windows;

namespace MaterialDesignApps.Helpers;

public class ThemeHelper
{
    /// <summary>
    /// BundledThemeの設定
    /// </summary>
    public static void SetBundledTheme(BundledTheme bundledTheme)
        => Application.Current.Resources.MergedDictionaries.Add(bundledTheme);

    /// <summary>
    /// BundledThemeの設定
    /// </summary>
    public static void SetBundledTheme(BaseTheme baseTheme, PrimaryColor primaryColor, SecondaryColor secondaryColor)
    {
        SetBundledTheme(new BundledTheme
        {
            BaseTheme = baseTheme,
            PrimaryColor = primaryColor,
            SecondaryColor = secondaryColor
        });
    }

    /// <summary>
    /// BaseThemeの取得
    /// </summary>
    public static BaseTheme GetBaseTheme() => GetBundledTheme()?.BaseTheme ?? BaseTheme.Light;

    /// <summary>
    /// PrimaryColorの取得
    /// </summary>
    public static PrimaryColor GetPrimaryColor() => GetBundledTheme()?.PrimaryColor ?? PrimaryColor.Blue;

    /// <summary>
    /// SecondaryColorの取得
    /// </summary>
    public static SecondaryColor GetSecondaryColor() => GetBundledTheme()?.SecondaryColor ?? SecondaryColor.Blue;

    /// <summary>
    /// BundledThemeの取得
    /// </summary>
    public static BundledTheme GetBundledTheme()
    {
        var bundledTheme = Application.Current.Resources.MergedDictionaries
                              .OfType<BundledTheme>()
                              .FirstOrDefault();
        return bundledTheme ?? new BundledTheme()
        {
            BaseTheme = BaseTheme.Light,
            PrimaryColor = PrimaryColor.Green,
            SecondaryColor = SecondaryColor.Green
        };
    }
}
```

# PrimarySelectorViewModel.cs
後に`PrimarySelector`というユーザーコントロールを作成するので、そのためのViewModelを作成します。
```Csharp:ViewModels\PrimarySelectorViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using MaterialDesignApps.Helpers;
using MaterialDesignColors;
using System.Collections.ObjectModel;

namespace MaterialDesignApps.ViewModels;

public partial class PrimarySelectorViewModel : ObservableObject
{
    public ObservableCollection<PrimaryColor> PrimaryColors { get; set; }

    [ObservableProperty]
    PrimaryColor selectedPrimary;

    partial void OnSelectedPrimaryChanged(PrimaryColor value)
    {
        var theme = ThemeHelper.GetBundledTheme();
        theme.PrimaryColor = value;
        ThemeHelper.SetBundledTheme(theme);
    }

    public PrimarySelectorViewModel()
    {
        PrimaryColors = new ObservableCollection<PrimaryColor>((PrimaryColor[])Enum.GetValues(typeof(PrimaryColor)));
        var currentPrimary = ThemeHelper.GetPrimaryColor();
        SelectedPrimary = currentPrimary;
    }
}
```

# PrimaryToBrush.cs
コンボボックスに色の玉を表示するため、PrimaryColorをSolidColorBrushに変換するためのコンバーターを作成します。
```Csharp:Converters\PrimaryToBrush.cs
using MaterialDesignColors;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;

namespace MaterialDesignApps.Converters;

public class PrimaryToBrush : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is PrimaryColor primaryColor)
        {
            var color = SwatchHelper.Lookup[(MaterialDesignColor)primaryColor];
            return new SolidColorBrush(color);
        }

        return Binding.DoNothing;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

```

# PrimarySelector.xaml
`Controls`フォルダにユーザーコントロールを作成します。作成した時点で一度ビルドを行います。
```Controls\PrimarySelector.xaml
<UserControl
    xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" 
    x:Class="MaterialDesignApps.Controls.PrimarySelector"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:local="clr-namespace:MaterialDesignApps.Controls"
    xmlns:vm="clr-namespace:MaterialDesignApps.ViewModels"
    xmlns:conv="clr-namespace:MaterialDesignApps.Converters"
    mc:Ignorable="d"
    d:DesignWidth="200">
    <UserControl.Resources>
        <ResourceDictionary>
            <conv:PrimaryToBrush x:Key="PrimaryToBrush"/>
        </ResourceDictionary>
    </UserControl.Resources>
    <UserControl.DataContext>
        <vm:PrimarySelectorViewModel/>
    </UserControl.DataContext>
    <Grid>
        <ComboBox
            Style="{StaticResource MaterialDesignOutlinedComboBox}"
            ItemsSource="{Binding PrimaryColors}"
            SelectedItem="{Binding SelectedPrimary}"
            materialDesign:HintAssist.Hint="Primary">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Ellipse
                            Width="16"
                            Height="16"
                            VerticalAlignment="Center"
                            Margin="0 0 10 0"
                            Fill="{Binding Converter={StaticResource PrimaryToBrush}}"/>
                        <TextBlock
                            Text="{Binding}"
                            VerticalAlignment="Center"/>
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
    </Grid>
</UserControl>
```

# MainWindow.xaml
MainWindowにコントロールを配置したら完了です。
```xml:Views\MainWindow.xaml
<Window
    xmlns:Controls="clr-namespace:MaterialDesignApps.Controls" 
    x:Class="MaterialDesignApps.Views.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:MaterialDesignApps.Views"
    Style="{StaticResource MaterialDesignWindow}"
    mc:Ignorable="d"
    Title="MainWindow" Height="200" Width="300">
    <Controls:PrimarySelector
        Width="180"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"/>
</Window>
```

# 起動
起動して、コンボボックスの切り替えて色が切り替われば完了です。

# 最後に
この記事は備忘録として作成しました。
同様の手順でSecondaryColorをコンボボックスで切り替えるためのユーザーコントロールを作成可能です。
コードの使用によって生じるいかなる損害も、作者は一切責任を負いません。