インフラジスティックス・ジャパン株式会社Blog

インフラジスティックス・ジャパン株式会社のチームメンバーが技術トレンド、製品Tips、サポート情報からライセンス、日々の業務から感じることなど、さまざまなトピックについてお伝えするBlogです。

【WPFのスタイリングを学ぶ】WPFでマテリアルデザインのボタンを作ろう。

f:id:furugen098:20190628181709p:plain

こんにちは。デベロッパーサポートの古堅(@furugen098)です。

WPFはXAMLにより柔軟なスタイル設定が可能ですが、はじめは理解するのが難しいですよね…。 この記事では、WPF標準コントロール "Button"に、マテリアルデザインを適応するための実装手順を記載しています。 記事をみながら一緒に実装したい方は"GitHub(勉強用、初期状態)"をダウンロードしてください。

GitHub(勉強用、初期状態)

GitHub - furugen/MaterialDesignButton at initial

GitHub(ボタンのスタイル適用完了版)

GitHub - furugen/MaterialDesignButton

この記事について

この記事の対象者

  • WPF初心者
  • WPFコントロールのスタイルの作成方法を学びたい。

学べること

  • オリジナルボタンの作り方
  • オリジナルボタンの配置方法
  • 外枠の付け方(楕円)
  • ラベルのフォント設定
  • 背景色の変更方法
  • 影の付け方
  • 簡単なアニメーションの実装

マテリアルデザインとは?

Googleが提唱しているデザインガイド。モダンで美しくユーザビリティが高いデザイン定義を提供しています。

マテリアルデザインのボタンのスタイル紹介

マテリアルデザインの「ボタン」のスタイルおよび動作については、下記ページの、"Interactive demo"にて動作確認ができます。

Buttons - Material Design

f:id:furugen098:20190628162048p:plain

スタイルの特徴

背景色は青紫、文字色は白

外枠は楕円

外周に影がついている。

マウスホバー時に背景色が変わり、影の深度が深くなる。

マウスクリックでリップルアニメーションを実行する。

(補足)リップルアニメーションとは?

波紋のように広がってくアニメーションです。実際の動作は、先ほど紹介したButtons - Material Designの"Interactive demo"のボタンをクリックすることで確認できます。

実装手順

まずは、カスタムコントロール "SampleButton" を作成しましょう。

プロジェクトを右クリック > 追加(D) > 新しい項目(W) を選択します。

f:id:furugen098:20190627131446p:plain

新しい項目の追加ウィンドウが表示されたら、 WPF > カスタムコントロールを選択。名前(下記例では、MaterialDesignButton.cs)を入力したら、追加(A)をクリックしましょう。

f:id:furugen098:20190627131947p:plain

"MaterialDesignButton.cs"という新規ファイルが作成され、また、Themesディレクトリの配下にGeneric.xamlファイルが作成されます。(Generic.xamlファイルは既に存在していれば上書き)

f:id:furugen098:20190627132458p:plain

解説

  • Generic.xaml

カスタムコントロールのスタイル(XAML)が記載してます。 アプリケーション全体で特定のコントロールに設定するスタイルは、このファイルに記載します。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SampleButton">

    <Style TargetType="{x:Type local:MaterialDesignButton}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MaterialDesignButton}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
  • MaterialDesignButton.cs

追加したカスタムコントロールクラスです。 以下はデフォルトで作成されるコード。

namespace SampleButton
{
    /// <summary>
    /// このカスタム コントロールを XAML ファイルで使用するには、手順 1a または 1b の後、手順 2 に従います。
    ///
    /// 手順 1a) 現在のプロジェクトに存在する XAML ファイルでこのカスタム コントロールを使用する場合
    /// この XmlNamespace 属性を使用場所であるマークアップ ファイルのルート要素に
    /// 追加します:
    ///
    ///     xmlns:MyNamespace="clr-namespace:SampleButton"
    ///
    ///
    /// 手順 1b) 異なるプロジェクトに存在する XAML ファイルでこのカスタム コントロールを使用する場合
    /// この XmlNamespace 属性を使用場所であるマークアップ ファイルのルート要素に
    /// 追加します:
    ///
    ///     xmlns:MyNamespace="clr-namespace:SampleButton;assembly=SampleButton"
    ///
    /// また、XAML ファイルのあるプロジェクトからこのプロジェクトへのプロジェクト参照を追加し、
    /// リビルドして、コンパイル エラーを防ぐ必要があります:
    ///
    ///     ソリューション エクスプローラーで対象のプロジェクトを右クリックし、
    ///     [参照の追加] の [プロジェクト] を選択してから、このプロジェクトを参照し、選択します。
    ///
    ///
    /// 手順 2)
    /// コントロールを XAML ファイルで使用します。
    ///
    ///     <MyNamespace:MaterialDesignButton/>
    ///
    /// </summary>
    public class MaterialDesignButton : Button
    {
        static MaterialDesignButton()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MaterialDesignButton), new FrameworkPropertyMetadata(typeof(MaterialDesignButton)));
        }
    }
}

MaterialDesignButton.cs の継承元を"Control"から"Button"に変更します。

f:id:furugen098:20190627143410p:plain

Generic.xaml の ControlTemplate以降のスタイルを書き換えます。

Generic.xaml

    <Style TargetType="{x:Type local:MaterialDesignButton}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MaterialDesignButton}">
                    <TextBlock>
                        <ContentPresenter></ContentPresenter>
                    </TextBlock>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
ContentPresenterとは?

コントロールは、何かしら表示する情報(コンテンツ)を持っています。 例えば、ボタンであれば、ボタンを表示する際の文字("登録ボタン"や、"更新ボタン"など)などがありますが、 ContentPresenterはそれらの要素を表示するためのコントロールとなります。 つまり、

<Button> ログイン </Button>

と記載した場合、"ログイン"がコンテンツ情報となります。これがContentPresenterで表現できます。 つまり、下記のようにContentPresenterが置き換わるイメージです。

    <Style TargetType="{x:Type local:MaterialDesignButton}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MaterialDesignButton}">
                    <TextBlock>
                        <!-- ContentPresenterが置き換わる。<Button> ログイン </Button> であれば、下記のようにログインという文字に置き換わる-->
                        ログイン
                    </TextBlock>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

MaterialDesignButton を画面に配置して実行確認してみましょう。

MainWindow.xaml

<Window x:Class="SampleButton.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:SampleButton"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="400">
    <Grid>
        <local:MaterialDesignButton Width="86" Height="30">BUTTON</local:MaterialDesignButton>
    </Grid>
</Window>

デバッグ実行で画面を起動すると、下記のような画面が表示されます。

f:id:furugen098:20190627162652p:plain

この段階では、"BUTTON"と表示されているテキストのようなものがあるだけですね。 ここからマテリアルデザインを実現するために装飾していきましょう!

背景色は青紫、文字色は白

Generic.xaml

    <Style TargetType="{x:Type local:MaterialDesignButton}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MaterialDesignButton}">
                    <Border Background="#6200ee">
                        <TextBlock Foreground="White">
                            <ContentPresenter></ContentPresenter>
                        </TextBlock>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

f:id:furugen098:20190627163725p:plain

Borderとは?

特定のコントロールを取り囲み、枠線や背景色のスタイルを設定することができるコントロールです。 今回は、TextBlockを囲み、背景色に青紫(6200ee)を設定しています。

TextBlockとは?

文字列を表示コントロールです。今回している文字色(Foreground)など、様々な装飾ができます。

外枠は楕円

Generic.xaml

    <Style TargetType="{x:Type local:MaterialDesignButton}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MaterialDesignButton}">
                    <Border Background="#6200ee" CornerRadius="4">
                        <TextBlock Foreground="White">
                            <ContentPresenter></ContentPresenter>
                        </TextBlock>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

f:id:furugen098:20190627164436p:plain

文字ラベルを縦、横の中央に配置する。文字を太字にする。

Generic.xaml

    <Style TargetType="{x:Type local:MaterialDesignButton}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MaterialDesignButton}">
                    <Border  Background="#6200ee" CornerRadius="4">
                        <TextBlock Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Bold" >
                            <ContentPresenter></ContentPresenter>
                        </TextBlock>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

f:id:furugen098:20190627164122p:plain

外周に影がついている。

    <Style TargetType="{x:Type local:MaterialDesignButton}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MaterialDesignButton}">
                    <Border  Background="#6200ee" CornerRadius="4">
                        <Border.Effect>
                            <DropShadowEffect BlurRadius="6" Direction="-90" RenderingBias="Quality" ShadowDepth="0"/>
                        </Border.Effect>
                        <TextBlock Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Bold" >
                            <ContentPresenter></ContentPresenter>
                        </TextBlock>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

f:id:furugen098:20190627164246p:plain

マウスホバー時に背景色が変わり、影の深度が深くなる。

   <Style TargetType="{x:Type local:MaterialDesignButton}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MaterialDesignButton}">
                    <Border  Background="#6200ee" CornerRadius="4" x:Name="btnBorder">
                        <Border.Effect>
                            <DropShadowEffect  BlurRadius="6" Direction="-90" RenderingBias="Quality" ShadowDepth="0" x:Name="shadowEffect" />
                        </Border.Effect>
                        <TextBlock Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Bold" >
                            <ContentPresenter></ContentPresenter>
                        </TextBlock>
                    </Border>
                    <ControlTemplate.Triggers>

                        <!-- マウスホバーが行われた際のスタイル設定 -->
                        <Trigger Property="IsMouseOver" Value="True">
                            <!-- 背景色の変更 -->
                            <Setter TargetName="btnBorder" Property="Background" Value="#8600ee"/>
                            <!-- 影の付け方の変更 -->
                            <Setter TargetName="btnBorder" Property="Effect">
                                <Setter.Value>
                                    <DropShadowEffect 
                                        BlurRadius="12" Direction="-90" RenderingBias="Quality" ShadowDepth="4" Opacity="0.8"
                                        ></DropShadowEffect>
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                        
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

f:id:furugen098:20190627170714g:plain

マウスクリックによりリップルアニメーションが行われる。

このステップは、少し複雑です。

下記はリップルアニメーションを実現するための手順になります。

  1. リップルアニメーションを表現するためのEffectを配置する。(通常時は見えないようにしておく)
  2. アニメーションを設定する。内容は、①で設定したEffectが波紋のように広がるようなアニメーションとする。
  3. ボタンがクリックされた際に、マウスの座標を取得/起点としたアニメーションを開始する。

それでは、1つ1つ対応していきましょう。

1. リップルアニメーションを表現するためのEffectを配置する。(通常時は見えないようにしておく)

Generic.xaml

   <Style TargetType="{x:Type local:MaterialDesignButton}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MaterialDesignButton}">
                    <Grid x:Name="PART_grid">
                        <Border  Background="#6200ee" CornerRadius="4" x:Name="PART_border">
                            <Border.Effect>
                                <DropShadowEffect  BlurRadius="6" Direction="-90" RenderingBias="Quality" ShadowDepth="0" x:Name="shadowEffect" />
                            </Border.Effect>
                            <TextBlock Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Bold" >
                            <ContentPresenter></ContentPresenter>
                            </TextBlock>
                        </Border>

                        <!-- リップルアニメーション用のエフェクトを配置-->
                        <Ellipse x:Name="PART_effectRipple" Fill="White" Width="0"
                                 Height="{Binding Path=Width, RelativeSource={RelativeSource Self}}" 
                                 HorizontalAlignment="Left" VerticalAlignment="Top"
                                 Opacity="0.5">
                        </Ellipse>

                    </Grid>

                    <ControlTemplate.Triggers>

                        <!-- マウスホバーが行われた際のスタイル設定 -->
                        <Trigger Property="IsMouseOver" Value="True">
                            <!-- 背景色の変更 -->
                            <Setter TargetName="PART_border" Property="Background" Value="#8600ee"/>
                            <!-- 影の付け方の変更 -->
                            <Setter TargetName="PART_border" Property="Effect">
                                <Setter.Value>
                                    <DropShadowEffect 
                                        BlurRadius="12" Direction="-90" RenderingBias="Quality" ShadowDepth="4" Opacity="0.8"
                                        ></DropShadowEffect>
                                </Setter.Value>
                            </Setter>
                        </Trigger>

                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

2. アニメーションを設定する。内容は、①で設定したEffectが波紋のように広がるようなアニメーションとする。

    <Style TargetType="{x:Type local:MaterialDesignButton}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MaterialDesignButton}">
                    <Grid x:Name="PART_grid" ClipToBounds="True">
                        <Border  Background="#6200ee" CornerRadius="4" x:Name="PART_border">
                            <Border.Effect>
                                <DropShadowEffect  BlurRadius="6" Direction="-90" RenderingBias="Quality" ShadowDepth="0" x:Name="shadowEffect" />
                            </Border.Effect>
                            <TextBlock Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Bold" >
                            <ContentPresenter></ContentPresenter>
                            </TextBlock>
                        </Border>

                        <!-- リップルアニメーション用のエフェクトを配置-->
                        <Ellipse x:Name="PART_effectRipple" Fill="White" Width="0"
                                 Height="{Binding Path=Width, RelativeSource={RelativeSource Self}}" 
                                 HorizontalAlignment="Left" VerticalAlignment="Top"
                                 Opacity="0.5">
                        </Ellipse>

                        <Grid.Resources>
                            <Storyboard x:Key="RippleAnimation" Storyboard.TargetName="PART_effectRipple">
                                <!-- 波形(リップル)のアニメーションを実行 -->
                                <DoubleAnimation Storyboard.TargetProperty="Width" From="0" Duration="0:0:0.5"/>
                                <ThicknessAnimation Storyboard.TargetProperty="Margin" Duration="0:0:0.5"/>

                                <!-- 元に戻すためのアニメーション-->
                                <DoubleAnimation Storyboard.TargetProperty="Width" To="0" BeginTime="0:0:0.5" Duration="0:0:0" />

                            </Storyboard>
                        </Grid.Resources>
                    </Grid>

                    <ControlTemplate.Triggers>

                        <!-- マウスホバーが行われた際のスタイル設定 -->
                        <Trigger Property="IsMouseOver" Value="True">
                            <!-- 背景色の変更 -->
                            <Setter TargetName="PART_border" Property="Background" Value="#8600ee"/>
                            <!-- 影の付け方の変更 -->
                            <Setter TargetName="PART_border" Property="Effect">
                                <Setter.Value>
                                    <DropShadowEffect 
                                        BlurRadius="12" Direction="-90" RenderingBias="Quality" ShadowDepth="4" Opacity="0.8"
                                        ></DropShadowEffect>
                                </Setter.Value>
                            </Setter>
                        </Trigger>

                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

3. ボタンがクリックされた際に、マウスの座標を取得/起点としたアニメーションを開始する。

MaterialDesignButton.cs

    public class MaterialDesignButton : Button
    {
        static MaterialDesignButton()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MaterialDesignButton), new FrameworkPropertyMetadata(typeof(MaterialDesignButton)));
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            Ellipse ellipse = GetTemplateChild("PART_effectRipple") as Ellipse;
            Grid grid = GetTemplateChild("PART_grid") as Grid;
            Storyboard animation = grid.FindResource("RippleAnimation") as Storyboard;

            this.AddHandler(MouseDownEvent, new RoutedEventHandler((sender, e) =>
            {
                // マウス座標を取得します。
                var mousePosition = (e as MouseButtonEventArgs).GetPosition(this);
                var startMargin = new Thickness(mousePosition.X, mousePosition.Y, 0, 0);
                // まずは、マウス座標をアニメーションの起点とします。
                ellipse.Margin = startMargin;

                // だんだんと、Effectコントロール大きくなるようなアニメーションを設定します。
                // 大きくなる最大幅はボタンの幅の2倍のサイズを定義しています。
                var maxWidth = Math.Max(ActualWidth, ActualHeight) * 2;
                DoubleAnimation sizeUpAnimation = animation.Children[0] as DoubleAnimation;
                sizeUpAnimation.To = maxWidth;

                // アニメーション開始と同時に、Effectコントロールの中心位置を移動します。
                // Effectのサイズが大きくなる(sizeUpAnimation)につれ、中心位置がズレていくためズレを修正するための中心位置の補正を行っています。
                ThicknessAnimation centerPositionMovingAnimation = animation.Children[1] as ThicknessAnimation;
                centerPositionMovingAnimation.From = startMargin;
                centerPositionMovingAnimation.To = new Thickness(mousePosition.X - maxWidth / 2, mousePosition.Y - maxWidth / 2, 0, 0);

                // アニメーションを開始します。
                ellipse.BeginStoryboard(animation);
            }), true);
        }

    }

起動して確認してみましょう! ボタンをクリックした時にリップルアニメーションが実行されます。 f:id:furugen098:20190628164310g:plain

(参考資料)WPFでリップルアニメーションだけを実装したい場合は、下記URLが参考になりますよ。

c# - Android's Ripple Effect in WPF - Stack Overflow

最後に

「スタイル要件は決まったけど、WPFコントロールのスタイルの実装方法が分からない…」「スタイル作ってみたけど構成に不安がある…」など。

弊社では、そんなお悩みを解決するための有償とレーニングを提供しています! 有償レーニングでは、弊社UIコントロールだけではなくWPF標準UIコントロールに関するスタイルのカスタマイズに関するご相談も承っております。

お気軽に弊社の お問い合わせ | 会社情報 | インフラジスティックス・ジャパン株式会社 より、ご相談くださいませ。