.NET Core 3.1 GUIテーマ

# ■はじめに

`.NET Core 3.1`と`Visual Studio`でコントロールの見栄えを変えたり標準以外のコントロールを使ってみます。
![000-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/2373e8a9-f955-d604-62fa-847b35344db2.png)


キーワード：ライト/ダークテーマ, NumericUpDown, プログレスリング, ウォーターマーク, トグルスイッチ, InputBox

# ■環境

* Windows 10
* Visual Studio 2019 Version 16.7.4
* .NET Core 3.1
* MahApps.Metro 2.2.0

# ■ベース画面作成

[こちら](https://qiita.com/Kosen-amai/items/a8df0f4acc877a249297 ".NET Core 3.1とVisual Studioでデスクトップアプリ") を参考に「準備」と「プロジェクトの作成」をしてください。
プロジェクト名は`WpfAppThemeSample`にしました。

そして画面を以下のように作成します。

```Xml:MainWindow.xaml
<Window
    x:Class="WpfAppThemeSample.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:local="clr-namespace:WpfAppThemeSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">

    <Window.Resources>
        <!--  タブの既定スタイル  -->
        <Style TargetType="TabItem">
            <Setter Property="Width" Value="120" />
        </Style>
        <!--  ボタンの既定スタイル  -->
        <Style TargetType="Button">
            <Setter Property="Margin" Value="8,8,8,0" />
        </Style>
        <!--  画面下部のボタンスタイル  -->
        <Style x:Key="FooterButton" TargetType="Button">
            <Setter Property="Width" Value="100" />
            <Setter Property="Margin" Value="8,0,0,0" />
        </Style>
        <!--  テキストボックスの既定スタイル  -->
        <Style TargetType="TextBox">
            <Setter Property="Margin" Value="8,8,8,0" />
        </Style>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <!--  テーマ切り替え  -->
        <GroupBox Margin="8" Header="テーマ">
            <StackPanel Orientation="Horizontal">
                <RadioButton
                    Margin="8"
                    Content="ライト"
                    IsChecked="True" />
                <RadioButton Margin="8" Content="ダーク" />
            </StackPanel>
        </GroupBox>

        <TabControl Grid.Row="1" Margin="8">
            <!--#region ■タブ1-->
            <TabItem Header="タブ1" IsSelected="True">
                <ScrollViewer>
                    <StackPanel>
                        <ListBox
                            Width="200"
                            Height="80"
                            Margin="8"
                            HorizontalAlignment="Left"
                            SelectedIndex="2">
                            <ListBoxItem Content="あいうえお" />
                            <ListBoxItem Content="かきくけこ" />
                            <ListBoxItem Content="さしすせそ" />
                            <ListBoxItem Content="たちつてと" />
                            <ListBoxItem Content="なにぬねの" />
                            <ListBoxItem Content="はひふへほ" />
                        </ListBox>
                        <CheckBox
                           Margin="8,8,8,0"
                           Content="チェックボックス"
                           IsChecked="True" />
                        <CheckBox Margin="8,8,8,0" Content="チェックボックス" />
                        <TextBox Text="" />
                        <TextBox Text="" />
                        <Button Content="ボタン" />
                        <Button Content="ボタン" />
                        <Button Content="ボタン" />
                        <Button Content="ボタン" />
                        <Button Content="ボタン" />
                        <Button Content="ボタン" />
                    </StackPanel>

                </ScrollViewer>
            </TabItem>
            <!--#endregion-->

            <!--#region ■タブ2-->
            <TabItem Header="タブ2">
                <StackPanel>
                    <TextBox />
                </StackPanel>
            </TabItem>
            <!--#endregion-->
        </TabControl>

        <StackPanel
            Grid.Row="2"
            Margin="8"
            HorizontalAlignment="Right"
            Orientation="Horizontal">
            <Button Content="InputBox" Style="{StaticResource FooterButton}" />
            <Button Content="ボタン2" Style="{StaticResource FooterButton}" />
            <Button Content="ボタン3" Style="{StaticResource FooterButton}" />
        </StackPanel>
    </Grid>
</Window>
```

実行してみます。
![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/22c41f88-0b0e-20d7-34f6-881317023e98.png)

# ■デザインの変更

## ◇ライブラリのインストール

ソリューションエクスプローラーでプロジェクトを右クリック、メニューから`NuGetパッケージの管理`を選択します。
![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/980c75d6-2300-99e1-7685-725c57cf0730.png)

`参照`タブの検索ボックスに`metro`と入力し、`MahApps.Metro`をインストールします。
![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/f95224e7-8ea3-ed64-28af-8dc7e4acb742.png)

## ◇App.xamlの修正

ResourceDictionaryを追加します。

```Xml:App.xaml（修正後）
<Application
    x:Class="WpfAppThemeSample.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfAppThemeSample"
    StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>
```

## ◇MainWindow.xamlの修正

`xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"`を追加し、
`Window`を`mah:MetroWindow`に変更します。

```Xml:MainWindow.xaml（修正前）
<Window
    x:Class="WpfAppThemeSample.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:local="clr-namespace:WpfAppThemeSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
：
</Window>
```

```Xml:MainWindow.xaml（修正後）
<mah:MetroWindow
    x:Class="WpfAppThemeSample.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:local="clr-namespace:WpfAppThemeSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
：
</mah:MetroWindow>
```

スタイル定義に`BasedOn`を追加します。

```Xml:MainWindow.xaml（修正後）
        <!--  タブの既定スタイル  -->
        <Style BasedOn="{StaticResource MahApps.Styles.TabItem}" TargetType="TabItem">
            <Setter Property="Width" Value="120" />
        </Style>
        <!--  ボタンの既定スタイル  -->
        <Style BasedOn="{StaticResource MahApps.Styles.Button}" TargetType="Button">
            <Setter Property="Margin" Value="8,8,8,0" />
        </Style>
        <!--  画面下部のボタンスタイル  -->
        <Style
            x:Key="FooterButton"
            BasedOn="{StaticResource MahApps.Styles.Button}"
            TargetType="Button">
            <Setter Property="Width" Value="100" />
            <Setter Property="Margin" Value="8,0,0,0" />
        </Style>
        <!--  テキストボックスの既定スタイル  -->
        <Style BasedOn="{StaticResource MahApps.Styles.TextBox}" TargetType="TextBox">
            <Setter Property="Margin" Value="8,8,8,0" />
        </Style>
```

## ◇MainWindow.xaml.csの修正

`MainWindow.xaml.cs`ファイルを開き、継承元の記述を消します。
![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/5fdd94b8-08c0-f368-15a6-137252f9db42.png)

```CSharp:MainWindow.xaml.cs（修正前）
public partial class MainWindow : Window
```

```CSharp:MainWindow.xaml.cs（修正後）
public partial class MainWindow
```

実行してみます。
![005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/166e0090-a658-e828-65ad-d8394d001e84.png)
デザインが変わりました。

# ■大文字小文字

タイトルバーの`MainWindow`と下部ボタンの`InputBox`が勝手に大文字に変えられています。
これを無効にして元々設定されたテキストの通りに表示するようにします。

`mah:MetroWindow`に`TitleCharacterCasing="Normal"`を追加します。
`FooterButton`のスタイル定義に`<Setter Property="mah:ControlsHelper.ContentCharacterCasing" Value="Normal" />`を追加します。

# ■ウィンドウスタイル設定

## ◇最大化ボタン、最小化ボタン

ウィンドウのサイズ変更は有効にしたまま、最大化ボタンと最小化ボタンを非表示にします。

`mah:MetroWindow`に
`ResizeMode="CanResizeWithGrip"`（サイズ変更グリップ表示）、
`ShowMaxRestoreButton="False"`（最大化ボタン非表示）、
`ShowMinButton="False"`（最小化ボタン非表示）を追加します。

ここまででXamlは以下のようになっています。

```Xml
<mah:MetroWindow
    x:Class="WpfAppThemeSample.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:local="clr-namespace:WpfAppThemeSample"
    xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    ResizeMode="CanResizeWithGrip"
    ShowMaxRestoreButton="False"
    ShowMinButton="False"
    TitleCharacterCasing="Normal"
    mc:Ignorable="d">
```

```Xml
<!--  画面下部のボタンスタイル  -->
<Style
    x:Key="FooterButton"
    BasedOn="{StaticResource MahApps.Styles.Button}"
    TargetType="Button">
    <Setter Property="Width" Value="100" />
    <Setter Property="Margin" Value="8,0,0,0" />
    <Setter Property="mah:ControlsHelper.ContentCharacterCasing" Value="Normal" />
</Style>
```

実行してみます。
タイトルバーとボタンの英字がちゃんと指定された通り、大文字小文字が区別されて表示されました。
最小化ボタンと最大化ボタンも消えました。
ウィンドウの右下にサイズ変更がしやすいようにグリップが表示されました。
![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/90694e94-9fcc-b889-a3c8-fa4f060671cf.png)

## ◇ウィンドウ縁を光らす

`mah:MetroWindow`に`GlowBrush="{DynamicResource MahApps.Brushes.Accent}"`を追加します。
ウィンドウの縁がうっすらと光ります。

![021.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/80481c71-1e9a-a110-f9d2-2545efa09437.png)

拡大
![022.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/7fab7a07-c5ae-b864-ce34-8a8ae375c0a9.png)


```Xml:MainWindow.xaml
<mah:MetroWindow
    x:Class="WpfAppThemeSample.MainWindow"
    ：
    GlowBrush="{DynamicResource MahApps.Brushes.Accent}"
    ：
    mc:Ignorable="d">
```

# ■ダークテーマ

現在の白基調の配色と、黒基調の配色を切り替えられるようにします。
2つのラジオボタンに`Checked`イベントの処理を設定します。

```Xml:MainWindow.xaml
<!--  テーマ切り替え  -->
<GroupBox Margin="8" Header="テーマ">
    <StackPanel Orientation="Horizontal">
        <RadioButton
            Margin="8"
            Checked="ThemeRadioButton_Checked"
            Content="ライト"
            IsChecked="True" />
        <RadioButton
            Margin="8"
            Checked="ThemeRadioButton_Checked"
            Content="ダーク" />
    </StackPanel>
</GroupBox>
```

```CSharp:MainWindow.xaml.cs
using ControlzEx.Theming;
using System.Windows;
using System.Windows.Controls;

namespace WpfAppThemeSample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ThemeRadioButton_Checked(object sender, RoutedEventArgs e)
        {
            if (((RadioButton)sender).Content.ToString() == "ライト")
            {
                // ライトテーマを設定
                ThemeManager.Current.ChangeTheme(this, "Light.Blue");
            }
            else
            {
                // ダークテーマを設定
                ThemeManager.Current.ChangeTheme(this, "Dark.Blue");
            }
        }

    }
}
```

実行してみます。
`ダーク`のラジオボタンを選択すると黒ベースの配色になりました。
![007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/aa3535a6-f4b0-5114-186f-004f734ce1fa.png)

# ■テキストボックス

タブ2に配置していたテキストボックスを修正します。

## ◇ウォーターマーク

プレースホルダとも言います。
テキストボックス未入力時に薄く説明文を表示します。
![008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/cb563025-ab2a-a2eb-b841-3fbdb7cd036c.png)

`mah:TextBoxHelper.Watermark`に設定した文字列が、テキストボックス空欄時に薄字で表示されます。

## ◇テキストクリアボタン

`mah:TextBoxHelper.ClearTextButton="True"`でテキスト内容をクリアする`×`ボタンが表示されます。

```Xml:MainWindow.xaml
<TabItem Header="タブ2">
    <StackPanel>
        <TextBox
            mah:TextBoxHelper.ClearTextButton="True"
            mah:TextBoxHelper.Watermark="何か入力してください。"
            Text="" />
    </StackPanel>
</TabItem>
```

## ◇フォーカス時全選択

テキストボックスにフォーカスが来たときに全選択するようにします。
スタイル定義に`TextBoxHelper.SelectAllOnFocus`を追加します。

```Xml:MainWindow.xaml（修正後）
<Window.Resources>
    ：
    <!--  テキストボックスの既定スタイル  -->
    <Style BasedOn="{StaticResource MahApps.Styles.TextBox}" TargetType="TextBox">
        <Setter Property="Margin" Value="8,8,8,0" />
        <Setter Property="mah:TextBoxHelper.SelectAllOnFocus" Value="True" />
    </Style>
</Window.Resources>
```

# ■NumericUpDown

数値入力に適したテキストボックスです。
直接入力することも、`+`, `-`ボタンで数値を増減することもできます。
![009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/2f9f1779-5091-dcd6-727b-15b3fbed7b0c.png)

タブ2の中のStackPanelに以下を追加します。

```Xml:MainWindow.xaml
<mah:NumericUpDown
    Width="100"
    Margin="8"
    Maximum="999"
    Minimum="1"
    Value="10" />
```

# ■トグルスイッチ

On/Offするスイッチです。
（たまに●を一生懸命ドラッグしている人がいますが、クリックで切り替わります）
![010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/5aec6ea4-2b74-f7df-5a37-b579fd8f357f.png)

タブ2のStackPanelに追加します。

```Xml:MainWindow.xaml
<mah:ToggleSwitch
    Margin="8"
    Header="何か処理"
    IsOn="False"
    OffContent="停止"
    OnContent="処理中"
    Toggled="ToggleSwitch_Toggled" />
```

```CSharp:MainWindow.xaml.cs
private void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{

}
```

[参考]ヘッダーもOn/Offのテキスト切り替えも不要な場合は`Content`にテキストを設定します。
![023.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/33d7dd1f-a8a2-4dd7-555c-5b488c37c0c9.png)

```Xml
<mah:ToggleSwitch Content="電源" IsOn="True"/>
```

# ■プログレスリング

処理中表示に使う、●がぐるぐる回るやつです。
![011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/aeb25e05-f055-5976-e955-0d79092ea367.png)

タブ2のStackPanelに追加します。

```Xml:MainWindow.xaml
<mah:ProgressRing x:Name="ProgressR" IsActive="False" />
```

トグルスイッチ切り替え時の処理を記述します。
トグルスイッチがOnになったら処理中表示にするようにします。

```CSharp:MainWindow.xaml.cs
private void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
    ProgressR.IsActive = ((MahApps.Metro.Controls.ToggleSwitch)sender).IsOn;
}
```

![012.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/07eca3ac-faeb-2c83-ed51-46d0a935f7bb.png)

# ■InputBox

文字入力するためのポップアップ画面です。
![013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/1c74d25b-1612-f597-22f7-5af823a0e3c1.png)

InputBoxボタンに`Click`イベントの処理を追加します。

```Xml:MainWindow.xaml
<Button
    Click="InputBoxButton_Click"
    Content="InputBox"
    Style="{StaticResource FooterButton}" />
<Button Content="ボタン2" Style="{StaticResource FooterButton}" />
<Button Content="ボタン3" Style="{StaticResource FooterButton}" />
```

`MahApps.Metro.Controls.Dialogs`をusingしておきます。
`InputBoxButton_Click`定義に`async`を付け足してください。
`ShowInputAsync`でInputBoxを表示します。

```CSharp:MainWindow.xaml.cs
using MahApps.Metro.Controls.Dialogs;
：

private async void InputBoxButton_Click(object sender, RoutedEventArgs e)
{
    var settings = new MetroDialogSettings() { DefaultText = "最初に表示しておく文字列" };
    string result = await this.ShowInputAsync(
        "タイトル", "何か入力してください。", settings);
    if (result == null)
    {
        // キャンセル
        return;
    }
    else
    {
        await this.ShowMessageAsync("タイトル", $"「{result}」が入力されました。");
    }
}
```

# ■タブ

## ◇タブ切り替え時にアニメーション

`TabControl`を`mah:MetroAnimatedTabControl`に変更するとタブ切り替え時に中身が少しスライドして表示されるようになります。

```Xml:MainWindow.xaml（修正前）
<TabControl Grid.Row="1" Margin="8">
```

```Xml:MainWindow.xaml（修正後）
<mah:MetroAnimatedTabControl Grid.Row="1" Margin="8">
```

## ◇アクティブタブに下線

アクティブなタブに下線を表示します。

![015.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/de4bc924-2584-0b17-6fae-071782f330ae.png)

タブコントロールに`TabControlHelper.Underline`を設定します。

```Xml:MainWindow.xaml（修正後）
<mah:MetroAnimatedTabControl
    Grid.Row="1"
    Margin="8"
    mah:TabControlHelper.Underlined="SelectedTabItem">
```