akuruhinode's blog

pythonやC#を中心に興味を持った内容について調べています。

WPF クリック時にエフェクトを表示するボタンを作成する

はじめに

UIデザインにおけるボタンは画面という平面上でどうやってクリック、タッチ感をユーザにフィードバックするのかを考える必要があります。

特にフラットデザインのような影を付けないシンプルなデザインの場合は、この操作感を出すのが難しいと考えています。

そこで、ここではクリックしたときに周囲にエフェクトを表示するボタンを作成してみます。

作成したボタン

今回は以下のようなボタンを作成してみました。


WPF スタイル エフェクトを表示するボタン1

特徴としては以下の通りです。
1. 基本的には影を付けず単色を使ったフラットなデザイン。
2. マウスホバー時に色を薄くすることで、クリックできそうな印象を与える。
3. クリック時にボタンの周りにエフェクトを表示することで、クリックしたことをフィードバックする。

ソースコード

ここではカスタムコントロールとして作成します。カスタムコントロールといっても特別なことはしていないので、単純にWindow.Resourcesにスタイル部分だけを定義することでも作成できます。

プロジェクトへの追加

プロジェクトの追加から「カスタムコントロール」を選択し、CustomButton1.csを追加します。

CustomButton1.cs

作成されたCustomButton1.csを開き、以下のコメントの通り継承元をButtonに変更します。

namespace WpfApp1
{
    public class CustomButton1 : Button // 継承元をControlからButtonに変更する
    {
        static CustomButton1()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomButton1), new FrameworkPropertyMetadata(typeof(CustomButton1)));
        }
    }


その後Generic.xamlを開き以下のように編集します。

Generic.xaml

<?xml version="1.0" encoding="Shift_JIS" ?>
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApp1">

    <Style TargetType="{x:Type local:CustomButton1}">
        <Setter Property="Cursor" Value="Hand" />
        <Setter Property="Background" Value="#00b894" />
        <Setter Property="Foreground" Value="#ffffff" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:CustomButton1}">
                    <Grid>
                        <!--  ボタンの外側  -->
                        <Border
                            x:Name="backborder"
                            Background="LightGray"
                            CornerRadius="4"
                            Opacity="0">
                            <!--  ぼかし効果を付ける  -->
                            <Border.Effect>
                                <BlurEffect Radius="2" />
                            </Border.Effect>
                        </Border>
                        <!--  ボタンのスタイル  -->
                        <Border
                            x:Name="buttonborder"
                            Margin="4"
                            Background="{TemplateBinding Background}"
                            CornerRadius="4">
                            <TextBlock
                                HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                FontWeight="Bold"
                                Foreground="{TemplateBinding Foreground}">
                                <ContentPresenter />
                            </TextBlock>
                        </Border>
                        <!--  ボタン内側  -->
                        <Border
                            x:Name="foreborder"
                            Margin="4"
                            Background="White"
                            CornerRadius="4"
                            Opacity="0" />
                    </Grid>
                    <ControlTemplate.Resources>               
                        <!--  クリック時の外側のアニメーション  -->
                        <Storyboard x:Key="BackColorAnimation">
                            <DoubleAnimation
                                BeginTime="0:0:0.05"
                                Storyboard.TargetName="backborder"
                                Storyboard.TargetProperty="Opacity"
                                From="0"
                                To="0.7"
                                Duration="0:0:0.15" />
                            <DoubleAnimation
                                BeginTime="0:0:0.15"
                                Storyboard.TargetName="backborder"
                                Storyboard.TargetProperty="Opacity"
                                From="0.7"
                                To="0"
                                Duration="0:0:0.55" />
                        </Storyboard>
                        <!--  クリック時の内側のアニメーション  -->
                        <Storyboard x:Key="ForeColorAnimation">
                            <DoubleAnimation
                                Storyboard.TargetName="foreborder"
                                Storyboard.TargetProperty="Opacity"
                                From="0"
                                To="0.4"
                                Duration="0:0:0.15" />
                            <DoubleAnimation
                                BeginTime="0:0:0.15"
                                Storyboard.TargetName="foreborder"
                                Storyboard.TargetProperty="Opacity"
                                From="0.4"
                                To="0"
                                Duration="0:0:0.5" />
                        </Storyboard>
                    </ControlTemplate.Resources>
                    <ControlTemplate.Triggers>
                        <!--  クリック時のスタイル設定  -->
                        <Trigger Property="IsPressed" Value="True">
                            <Trigger.EnterActions>
                                <BeginStoryboard Storyboard="{StaticResource BackColorAnimation}" />
                                <BeginStoryboard Storyboard="{StaticResource ForeColorAnimation}" />
                            </Trigger.EnterActions>
                        </Trigger>
                        <!--  マウスホバー時のスタイル設定  -->
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="buttonborder" Property="Opacity" Value="0.9" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

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"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfApp1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <local:CustomButton1
            Width="100"
            Height="40"
            Content="BUTTON" />
    </Grid>
</Window>

解説

Generic.xamlで作成したスタイルについて解説します。

色の定義

以下のようにBackground、Foregroundを定義しておき、それを実際のボタンとして表示するBroderのBackground、TextBlockのForegroundにTemplateBinding します。
そうすることで利用時にBackground、Foregroundを変更できるようにしておきます。

<Style TargetType="{x:Type local:CustomButton1}">
    <Setter Property="Background" Value="#0984e3" />
    <Setter Property="Foreground" Value="#ffffff" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:CustomButton1}">
            ・・・省略・・・
                    <!--  ボタンのスタイル  -->
                    <Border
                    ・・・省略・・・
                        Background="{TemplateBinding Background}"
                    ・・・省略・・・
                        <TextBlock
                    ・・・省略・・・
                            Foreground="{TemplateBinding Foreground}">

アニメーション用のBorderの作成

ここでは以下のように、Grid内に3つのBorderを重ねています。上から、ボタンの外側、ボタン本体、ボタンの内側になります。

ボタンの外側と内側のOpacityをアニメーションで変化させることでクリック時のアニメーションを実現します。外側と内側のBorderはアニメーション未実行時は表示しないので、Opacityを0にしています。

ボタン本体、ボタンの内側のMarginを指定することで外側のBorderが若干はみ出すようにしています。このはみ出している部分が外側のエフェクトになります。
外側のBorderにBlurEffectを設定することで、少しだけ外側のBorderをぼかしています。

<ControlTemplate TargetType="{x:Type local:CustomButton1}">
    <Grid>
    <!--  ボタンの外側  -->
        <Border
            x:Name="backborder"
            Background="LightGray"
            CornerRadius="4"
            Opacity="0">
            <!--  ぼかし効果を付ける  -->
            <Border.Effect>
                <BlurEffect Radius="2" />
            </Border.Effect>
        </Border>
        <!--  ボタンのスタイル  -->
        <Border
            x:Name="buttonborder"
            Margin="4"
            Background="{TemplateBinding Background}"
            CornerRadius="4">
            <TextBlock
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                FontWeight="Bold"
                Foreground="{TemplateBinding Foreground}">
                <ContentPresenter />
            </TextBlock>
        </Border>
        <!--  ボタン内側  -->
        <Border
            x:Name="foreborder"
            Margin="4"
            Background="White"
            CornerRadius="4"
            Opacity="0" />
    </Grid>

アニメーションの作成

ControlTemplate.Resourcesにアニメーションの動作を定義します。
今回は、ボタン外側、内側のOpacityを滑らかに変化させたいので、DoubleAnimation
を利用します。外側用、内側用の2つのStoryboardを定義していますが、それぞれ少しだけ時間をずらしています。具体的には、わずかに外側のStoryboardを遅延させています。
こうすることでStoryboardとしてはシンプルですがエフェクトが内側から外側へ広がる感じを表現してみました。

<ControlTemplate.Resources>               
    <!--  クリック時の外側のアニメーション  -->
    <Storyboard x:Key="BackColorAnimation">
        <DoubleAnimation
            BeginTime="0:0:0.05"
            Storyboard.TargetName="backborder"
            Storyboard.TargetProperty="Opacity"
            From="0"
            To="0.7"
            Duration="0:0:0.15" />
        <DoubleAnimation
            BeginTime="0:0:0.15"
            Storyboard.TargetName="backborder"
            Storyboard.TargetProperty="Opacity"
            From="0.7"
            To="0"
            Duration="0:0:0.55" />
    </Storyboard>
    <!--  クリック時の内側のアニメーション  -->
    <Storyboard x:Key="ForeColorAnimation">
        <DoubleAnimation
            Storyboard.TargetName="foreborder"
            Storyboard.TargetProperty="Opacity"
            From="0"
            To="0.4"
            Duration="0:0:0.15" />
        <DoubleAnimation
            BeginTime="0:0:0.15"
            Storyboard.TargetName="foreborder"
            Storyboard.TargetProperty="Opacity"
            From="0.4"
            To="0"
            Duration="0:0:0.5" />
    </Storyboard>
</ControlTemplate.Resources>

クリック時のアニメーション、マウスオーバー時のスタイルを設定

ControlTemplate.Triggersでトリガーを設定して、動作させるアニメーションのStoryboardを指定します。IsPressedをTriggerにEnterActionsを設定することで、ボタンクリック時にアニメーションが動作します。

アニメーションではないですが、IsMouseOverがTrue時にOpacity="0.9"とすることでマウスオーバー時にボタン色を少し薄くしています。

<ControlTemplate.Triggers>
    <!--  クリック時のスタイル設定  -->
    <Trigger Property="IsPressed" Value="True">
        <Trigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource BackColorAnimation}" />
            <BeginStoryboard Storyboard="{StaticResource ForeColorAnimation}" />
        </Trigger.EnterActions>
    </Trigger>
    <!--  マウスホバー時のスタイル設定  -->
    <Trigger Property="IsMouseOver" Value="True">
        <Setter TargetName="buttonborder" Property="Opacity" Value="0.9" />
    </Trigger>
</ControlTemplate.Triggers>

いろんな色で作ってみる

例えば、以下のようにすれば、カラフルなボタンを作成できます。設定している色は以下のサイトを参考にしています。
Flat UI Colors 2 - 14 Color Palettes, 280 colors 🎨

<Window
    x:Class="Anime_Button1.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:Anime_Button1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
            <local:CustomButton1
                Width="100"
                Height="40"
                Content="BUTTON" />
            <local:CustomButton1
                Width="100"
                Height="40"
                Background="#6c5ce7"
                Content="BUTTON" />
            <local:CustomButton1
                Width="100"
                Height="40"
                Background="#fd79a8"
                Content="BUTTON" />
            <local:CustomButton1
                Width="100"
                Height="40"
                Background="#fdcb6e"
                Content="BUTTON" />
        </StackPanel>
    </Grid>
</Window>


WPF スタイル エフェクトを表示するボタン2

アニメーション部分はすべて固定色として設定してOpacityを変更することで見た目上の色を変更していますので、このようにBackgroundだけを変更することで様々な色に対応できます。

ただしこれは、背景色が白の場合に限ります。背景に色がある場合、Opacityを変更すると、背景色の影響を受けてしまいます。
簡単な解決策としては、以下のように背景用のBorderを追加すれば対応可能です。

<Grid>
    <!--  ボタンの外側  -->
   ・・・省略・・・
    <!--  ボタン背景色が白以外の場合は、以下のようにOpacity="1"のBorderを追加すればよい  -->
    <Border
        Margin="4"
        Background="#ffffff"
        CornerRadius="4"
        Opacity="1" />
    <!--  ボタンのスタイル  -->
    ・・・省略・・・
    <!--  ボタン前側  -->
    ・・・省略・・・
</Grid>

様々なボタンクリックイベントの追加方法

クリック時にアニメーションを動作させたい場合このようにTriggerでIsPressedを指定するのが簡単です。IsPressedは汎用的に「クリック時の動作」として利用できるためです。例えば、以下のようなEventTriggerでMouseDownを指定する場合、ボタンのクリック動作としては反応しない右クリック動作でもStoryboardが動作してしまいます。

 <EventTrigger RoutedEvent="MouseDown">
    <BeginStoryboard>
        <Storyboard>
         ・・・省略・・・
        </Storyboard>
    </BeginStoryboard>
    <BeginStoryboard />
</EventTrigger>

左クリック動作だけに反応させたい場合は、MouseLeftButtonDownを利用する必要があります。
ただ、これはマウスの左クリックでしか利用できません。また、タッチ操作では、touchdownなど別のEventTriggerがあります。

このように、単純にクリック動作として利用したいだけでも、いろいろと考えないといけません。しかしButtonのIsPressedでは特に深く考えずに汎用的なボタンクリック動作として利用できます。


一方でIsPressedはSpaceキーでも反応しますので、キーボード入力に対応させたくない場合は注意が必要です。逆にキーボード入力に対応する場合、IsPressedではEnterキーでは反応しません。ボタンのOnClick()イベントはEnterキーでも反応しますので、Enterキーでもアニメーションを対応させたい場合はkeydownを利用するなどの対応が必要です。