akuruhinode's blog

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

WPF シンプルなLoadingアニメーションを作る

はじめに

ここではWPFのアニメーション機能を利用して、シンプルなLoadingアニメーションを作ってみようと思います。

具体的には、以下のようなアニメーションを作成します。

WPF Loading アニメーション1

ソースコード

早速ですが、ソースコードは以下の通りです。

<Window
    x:Class="LoadingAnimation1.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:LoadingAnimation1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Window.Resources>
        <!--  アニメーションサイズ  -->
        <sys:Double x:Key="LoadEllipseMinSize">15.0</sys:Double>
        <sys:Double x:Key="LoadEllipseMaxSize">45.0</sys:Double>
        <!--  アニメーション時間  -->
        <KeyTime x:Key="LoadKeyTime1">00:00:00</KeyTime>
        <KeyTime x:Key="LoadKeyTime2">00:00:0.20</KeyTime>
        <KeyTime x:Key="LoadKeyTime3">00:00:0.40</KeyTime>
        <KeyTime x:Key="LoadKeyTime4">00:00:0.60</KeyTime>
        <KeyTime x:Key="LoadKeyTime5">00:00:0.80</KeyTime>
        <KeyTime x:Key="LoadKeyTimeEnd">00:00:02</KeyTime>
        <!--  アニメーション作成  -->
        <Storyboard x:Key="LoadingAnimation">
            <!--  左から1番目のEllipseアニメーション  -->
            <DoubleAnimationUsingKeyFrames
                RepeatBehavior="Forever"
                Storyboard.TargetName="LoadEllipse1"
                Storyboard.TargetProperty="Width">
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime1}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime2}" Value="{StaticResource LoadEllipseMaxSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime3}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTimeEnd}" Value="{StaticResource LoadEllipseMinSize}" />
            </DoubleAnimationUsingKeyFrames>
            <!--  左から2番目のEllipseアニメーション  -->
            <DoubleAnimationUsingKeyFrames
                RepeatBehavior="Forever"
                Storyboard.TargetName="LoadEllipse2"
                Storyboard.TargetProperty="Width">
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime2}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime3}" Value="{StaticResource LoadEllipseMaxSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime4}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTimeEnd}" Value="{StaticResource LoadEllipseMinSize}" />
            </DoubleAnimationUsingKeyFrames>
            <!--  左から3番目のEllipseアニメーション  -->
            <DoubleAnimationUsingKeyFrames
                RepeatBehavior="Forever"
                Storyboard.TargetName="LoadEllipse3"
                Storyboard.TargetProperty="Width">
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime3}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime4}" Value="{StaticResource LoadEllipseMaxSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime5}" Value="{StaticResource LoadEllipseMinSize}" />
                <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTimeEnd}" Value="{StaticResource LoadEllipseMinSize}" />
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
    </Window.Resources>

    <Canvas>
        <!--  アニメーション対象  -->
        <Grid
            Canvas.Left="50"
            Canvas.Top="30"
            Height="50">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50" />
                <ColumnDefinition Width="50" />
                <ColumnDefinition Width="50" />
            </Grid.ColumnDefinitions>
            <Ellipse
                x:Name="LoadEllipse1"
                Grid.Column="0"
                Width="{StaticResource LoadEllipseMinSize}"
                Height="{Binding ElementName=LoadEllipse1, Path=Width}"
                Fill="Gray" />
            <Ellipse
                x:Name="LoadEllipse2"
                Grid.Column="1"
                Width="{StaticResource LoadEllipseMinSize}"
                Height="{Binding ElementName=LoadEllipse2, Path=Width}"
                Fill="Gray" />
            <Ellipse
                x:Name="LoadEllipse3"
                Grid.Column="2"
                Width="{StaticResource LoadEllipseMinSize}"
                Height="{Binding ElementName=LoadEllipse3, Path=Width}"
                Fill="Gray" />
        </Grid>

        <!--  アニメーションの有効、無効の切り替え  -->
        <Button
            Canvas.Left="50"
            Canvas.Top="120"
            Width="50"
            Click="Start_Click"
            Content="Start" />
        <Button
            Canvas.Left="150"
            Canvas.Top="120"
            Width="50"
            Click="Stop_Click"
            Content="Stop" />
    </Canvas>
using System.Windows;
using System.Windows.Media.Animation;

namespace LoadingAnimation1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Start_Click(object sender, RoutedEventArgs e)
        {
            var sb = this.Resources["LoadingAnimation"] as Storyboard;
            sb.Begin();
        }

        private void Stop_Click(object sender, RoutedEventArgs e)
        {
            var sb = this.Resources["LoadingAnimation"] as Storyboard;
            sb?.Stop();
        }
    }
}

このソースコードの実行結果は以下の通りです。
WPF Loading アニメーション2

解説

それぞれの処理内容について解説します。

サイズや実行時間の定義

ここではまずアニメーションで変化させるプロパティの値を定義しています。
必須ではありませんが、このように定義しておくと後から変更が楽になります。LoadEllipseMinSize、LoadEllipseMaxSizeを変更することで円を任意のサイズに変更できます。
なお、StringやDoubleなどのシステムの値をリソース値として定義する場合は、xmlns:sys="clr-namespace:System;assembly=mscorlib"を追加する必要があります。

<Window
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
・・・省略・・・

    <Window.Resources>
        <!--  アニメーションサイズ  -->
        <sys:Double x:Key="LoadEllipseMinSize">15.0</sys:Double>
        <sys:Double x:Key="LoadEllipseMaxSize">45.0</sys:Double>
        <!--  アニメーション時間  -->
        <KeyTime x:Key="LoadKeyTime1">00:00:00</KeyTime>
        <KeyTime x:Key="LoadKeyTime2">00:00:0.20</KeyTime>
        <KeyTime x:Key="LoadKeyTime3">00:00:0.40</KeyTime>
        <KeyTime x:Key="LoadKeyTime4">00:00:0.60</KeyTime>
        <KeyTime x:Key="LoadKeyTime5">00:00:0.80</KeyTime>
        <KeyTime x:Key="LoadKeyTimeEnd">00:00:02</KeyTime>

アニメーションの作成

以下の部分で実際のアニメーションの動作を作成しています。WPFでアニメーションを作成する場合は、Storyboardを利用します。そしてStoryboard内に具体的な動作を定義します。
アニメーションの作成方法はいくつかありますが、今回のように値を柔軟に遷移させたい場合はKeyFrameを使うのが良いです。

KeyFrameにもいくつかの種類が存在します。それらの違いは、変更するプロパティの型の違いです。ここではEllipseのWightとHeightを変更したいのでDoubleAnimationUsingKeyFramesを利用します。

他のKeyFrameやアニメーションの実現方法を知りたい場合は、以下に詳しい説明が記載されています。
アニメーションの概要 - WPF .NET Framework | Microsoft Docs

    <!--  アニメーション作成  -->
    <Storyboard x:Key="LoadingAnimation">
        <!--  左から1番目のEllipseアニメーション  -->
        <DoubleAnimationUsingKeyFrames
            RepeatBehavior="Forever"
            Storyboard.TargetName="LoadEllipse1"
            Storyboard.TargetProperty="Width">
            <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime1}" Value="{StaticResource LoadEllipseMinSize}" />
            <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime2}" Value="{StaticResource LoadEllipseMaxSize}" />
            <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime3}" Value="{StaticResource LoadEllipseMinSize}" />
            <SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTimeEnd}" Value="{StaticResource LoadEllipseMinSize}" />
        </DoubleAnimationUsingKeyFrames>
        <!--  左から2番目のEllipseアニメーション  -->
・・・省略・・・
        <!--  左から3番目のEllipseアニメーション  -->
・・・省略・・・
    </Storyboard>
</Window.Resources>



DoubleAnimationUsingKeyFramesで設定しているパラメータの説明は以下の通りです。(これ以外にもパラメータは存在します)

パラメータ 説明
RepeatBehavior アニメーションを繰り返す回数。Foreverにすると永遠に繰り返す。
Storyboard.TargetName 対象の名称。x:Nameで定義しておく。
Storyboard.TargetProperty 対象のプロパティ。プロパティの階層が深い場合は、正しい階層で記載する必要がある。



DoubleAnimationUsingKeyFrames内に定義するKeyFrameについても、いくつか種類が存在します。ここでは、値を滑らかに変更したいので、SplineDoubleKeyFrameを利用しています。
それぞれで設定している値は、上で定義したリソース値です。

<SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime1}" Value="{StaticResource LoadEllipseMinSize}" />
<SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime2}" Value="{StaticResource LoadEllipseMaxSize}" />
<SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTime3}" Value="{StaticResource LoadEllipseMinSize}" />
<SplineDoubleKeyFrame KeyTime="{StaticResource LoadKeyTimeEnd}" Value="{StaticResource LoadEllipseMinSize}" />


これに実際の値に当てはめてみると以下のようになります。

<SplineDoubleKeyFrame KeyTime="00:00:00" Value="15.0" />
<SplineDoubleKeyFrame KeyTime="00:00:0.20" Value="45.0" />
<SplineDoubleKeyFrame KeyTime="00:00:0.40" Value="15.0" />
<SplineDoubleKeyFrame KeyTime="00:00:02" Value="15.0" />

これは、以下のような動作になります。
1. 0秒の時点でのWightを15.0とする。
2. 0秒から0.20秒にかけて、Wightを15.0から45.0に変更する。
3. 0.20秒から0.40秒にかけて、Wightを45.0から15.0に変更する。
4. 0.40秒から2秒にかけて、Wightは15.0から変更しない。

一見、3か4が必要ないように思えるかもしれません。しかし3がないと、15.0から45.0への変化が0.20秒間隔で行われるのに対し、45.0から15.0への変化が0.20秒から2秒の1.80秒間隔で行われてしまうため、拡大と縮小の間隔が異なってしまいます。
また、4は3つのEllipseの終了時間を合わせるために利用しています。

<SplineDoubleKeyFrame KeyTime="00:00:00" Value="15.0" />
<SplineDoubleKeyFrame KeyTime="00:00:0.20" Value="45.0" />
<SplineDoubleKeyFrame KeyTime="00:00:0.40" Value="15.0" />
<SplineDoubleKeyFrame KeyTime="00:00:02" Value="15.0" />

ここでは、左から1番目のEllipseについてのみ説明していますが、他についても、実行時間をずらしているだけで、処理の流れは同じです。

アニメーション対象の作成

アニメーション対象の作成方法は、特に特別な処理は必要ありません。以下のCanvasや、Gridについても、アニメーションの動作確認のために定義したまでで、深い理由があるわけではありません。
ただし今回のようなコントロールのサイズを変更する場合は、以下のGridのように固定サイズのエリアを作成してからアニメーション対象を定義しないと、アニメーションによるサイズ変更に伴いコントロールの位置まで変化してしまう可能性があります。

<Canvas>
        <!--  アニメーション対象  -->
        <Grid
            Canvas.Left="50"
            Canvas.Top="30"
            Height="50">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50" />
                <ColumnDefinition Width="50" />
                <ColumnDefinition Width="50" />
            </Grid.ColumnDefinitions>
            <Ellipse
                x:Name="LoadEllipse1"
                Grid.Column="0"
                Width="{StaticResource LoadEllipseMinSize}"
                Height="{Binding ElementName=LoadEllipse1, Path=Width}"
                Fill="Gray" />
・・・省略・・・
        </Grid>

縦横比の固定

今回のアニメーションではEllipseのサイズを変更していますが、縦横比を固定しつつサイズを変更するにはWight、Heightの両方についてStoryboardを作成する必要があります。しかしまったく同じDoubleAnimationUsingKeyFramesをWight、Heightの2つ分作成するのは面倒なので、以下のようにHeightに自分のWightをBindingすることで、縦横比を固定させています。

<Ellipse
    x:Name="LoadEllipse1"
    Grid.Column="0"
    Width="{StaticResource LoadEllipseMinSize}"
    Height="{Binding ElementName=LoadEllipse1, Path=Width}"
    Fill="Gray" />

ストーリーボードの実行、停止

ここでは、C#上から、ボタンクリックイベントに同期してストーリーボードの有効、無効を切り替えています。以下のようにリソースからストーリーボードのオブジェクトを取得した後に、storyboard.Begin();することで開始、storyboard.Stop();することで停止します。

private void Start_Click(object sender, RoutedEventArgs e)
{
    var storyboard = this.Resources["LoadingAnimation"] as Storyboard;
    storyboard.Begin();
}

private void Stop_Click(object sender, RoutedEventArgs e)
{
    var storyboard = this.Resources["LoadingAnimation"] as Storyboard;
    storyboard.Stop();
}