プログラミング Windows 第6版 第9章 WPF編

この記事では、「プログラミング Windows 第6版」を使って WPF XAML の学習を支援することを目的にしています。この目的から、書籍と併せて読まれることをお勧めします。

第9章 アニメーション

本章では、アニメーションを解説しています。ここまでにも、何回かアニメーションがサンプルコードにも含まれていました。WPF XAML と WinRT XAMLでは、アニメーションの基本的な考え方は同じになります。しかし、大きな違いもあります。それは、WPF XAML のアニメーションは型に厳しく(専用のタイムラインの使用が強制される)、WinRT XAML のアニメーションは型にたいして緩い(別の表現では柔軟とも言えます)という特徴があります。これは、Silverlight に代表される XAML 系の UI フレームワークに共通する特徴にもなっています。この理由は、フレームワークが提供するランタイムをコンパクトにまとめていることと関係があります。一方で、WPF XAML のようなフルスタックのフレームワークは、型に対する厳密性が強制されることになります。また、アニメーションは、XAML 系の UI 技術に共通するもので、Windows Forms には含まれない技術になります。このため、ユーザー エクスぺリエンス(UX) の観点からは、利用者であるユーザーが操作方法に気付くために使用したり、タッチ操作に対する反応として利用したりします。ユーザーの操作体験を当たり前のように感じさせるための手段として、アニメーションの利用を考える必要があると私は考えます。

9.1(P363) Windows.UI.Xaml.Media.Animation 名前空間

本節では、CompositionTarget.Rendering イベントを題材に WinRT に組み込まれているアニメーションの概要を説明しています。WPF XAML では、System.Windows.Media.Animation 名前空間で組み込みのアニメーションが定義されています。考え方は同じですが、WinRT XAML と WPF XAML で異なるのは、組み込みのアニメーション ライブラリが WPF XAML には用意されていない点になります。

9.2(P364) アニメーションの基礎

本節では、アニメーションの基礎を説明するために DoubleAnimation というタイムライン を用いて説明しています。最初に、SimpleAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Storyboard x:Key="storyboard">
            <DoubleAnimation Storyboard.TargetName="txtblk"
                             Storyboard.TargetProperty="FontSize"
                             From="1" To="144" Duration="0:0:3" />
        </Storyboard>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBlock x:Name="txtblk"
                   Text="Animated Text"
                   Grid.Row="0"
                   FontSize="48"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center" />
        <Button Content="Trigger!"
                Grid.Row="1"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Click="OnButtonClick" />
    </Grid>
</Window>

基本的なコードは、WinRT XAML と同じですが、違いは DoubleAnimation から EnbaleDependentAnimation プロパティを削除していることと組み込みスタイルを変更した点になります。このプロパティは、WinRT 固有のものであり、アニメーションを UI スレッドを使用するかどうかを指定するものになります。一方で、WPF XAML のアニメーションは UI スレッドで実行されるという特徴があります。それでは、このアニメーションを起動する MainWindow.xaml.cs を示します。

 using System.Windows;
using System.Windows.Media.Animation;

namespace SimpleAnimation
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void OnButtonClick(object sender, RoutedEventArgs e)
        {
            (this.Resources["storyboard"] as Storyboard).Begin();

        }
    }
}

リソースから DoubleAnimation を取得して、Begin メソッドでアニメーションを実行します。もちろん、コードは名前空間を除けば WinRT XAML と同じになります。
SimpleAnimation

DoubleAnimation クラスとは、オブジェクトの Double 型のプロパティを From から To に対して指定した時間(Duration) で変化させるためのタイムラインになります。 書籍では、AutoReverse、RepeatBehavior などの様々な設定によって、アニメーションがどのように変化するかを解説しています。今度は、SimpleAnimation プロジェクトと同等のアニメーションをコードで実現する SimpleAnimationCode プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Content" Value="Trigger!" />
            <Setter Property="FontSize" Value="48" />
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="Margin" Value="12" />
        </Style>
    </Window.Resources>
    <Grid>
        <Grid HorizontalAlignment="Center"
              VerticalAlignment="Center">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <Button Grid.Row="0" Grid.Column="0" Click="OnButtonClick" />
            <Button Grid.Row="0" Grid.Column="1" Click="OnButtonClick" />
            <Button Grid.Row="0" Grid.Column="2" Click="OnButtonClick" />
            <Button Grid.Row="1" Grid.Column="0" Click="OnButtonClick" />
            <Button Grid.Row="1" Grid.Column="1" Click="OnButtonClick" />
            <Button Grid.Row="1" Grid.Column="2" Click="OnButtonClick" />
            <Button Grid.Row="2" Grid.Column="0" Click="OnButtonClick" />
            <Button Grid.Row="2" Grid.Column="1" Click="OnButtonClick" />
            <Button Grid.Row="2" Grid.Column="2" Click="OnButtonClick" />
        </Grid>
    </Grid>
</Window>

コードは、組み込みスタイル以外は WinRT XAML と同じになります。アニメーションを実現する OnButtonClick イベント ハンドラーを MainWindow.xaml.cs より抜粋します。

 private void OnButtonClick(object sender, RoutedEventArgs e)
{
    DoubleAnimation anima = new DoubleAnimation
    {
        To = 96,
        Duration = new Duration(new TimeSpan(0, 0, 1)),
        AutoReverse = true,
        RepeatBehavior = new RepeatBehavior(3)
    };
    Storyboard.SetTarget(anima, sender as Button);
    Storyboard.SetTargetProperty(anima, new PropertyPath(Button.FontSizeProperty));

    Storyboard storyboard = new Storyboard();
    storyboard.Children.Add(anima);
    storyboard.Begin();

}

コードは、既に説明したように EnableDependentAnimation プロパティを削除しています。また、SetTargetProperty メソッドの第2引数の指定方法が、WinRT XAML が文字列なのに対して、WPF XAML では PropertyPath オブジェクトのインスタンスになる点も異なります。実行結果は、もちろん同じになります。
SimpleAnimationCode

もちろん、書籍にはコードの解説もありますので熟読をお願いします。

9.4(P375) その他の DoubleAnimation

本節では、前節までが FontSize プロパティをアニメーションしていたので、その他の Double  型 を持つプロパティを使ったアニメーションを説明しています。それでは、ElipseBlobAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Storyboard x:Key="storyboard"
                    RepeatBehavior="Forever"
                    AutoReverse="True">
            <DoubleAnimation Storyboard.TargetName="ellipse"
                             Storyboard.TargetProperty="Width"
                             From="100" To="600" Duration="0:0:1" />
            <DoubleAnimation Storyboard.TargetName="ellipse"
                             Storyboard.TargetProperty="Height"
                             From="600" To="100" Duration="0:0:1" />
        </Storyboard>
    </Window.Resources>
    <Grid>
        <Ellipse x:Name="ellipse">
            <Ellipse.Fill>
                <LinearGradientBrush>
                    <GradientStop Offset="0" Color="Pink" />
                    <GradientStop Offset="1" Color="LightBlue" />
                </LinearGradientBrush>
            </Ellipse.Fill>
        </Ellipse>
    </Grid>
</Window>

もちろん、EnableDependentAnimation プロパティを除けば WinRT XAML と同じになります。このアニメーションは、AutoReverse プロパティを指定しているので永遠に実行されるようになります。それでは、MainWindow.xaml.cs を示します。

 using System.Windows;
using System.Windows.Media.Animation;

namespace EllipseBlobAnimation
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += (sender, args) =>
            {
                (this.Resources["storyboard"] as Storyboard).Begin();
            };
        }
    }
}

実行結果も同じになります。
EllipseBlobAnimation

今度は、Shape オブジェクトの StrokeThickness プロパティをアニメーション化する AnimateStrokeThickness プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Storyboard x:Key="storyboard">
            <DoubleAnimation Storyboard.TargetName="ellipse"
                             Storyboard.TargetProperty="StrokeThickness"
                             From="1" To="100" Duration="0:0:4"
                             AutoReverse="True"
                             RepeatBehavior="Forever" />
        </Storyboard>
    </Window.Resources>

    <Grid>
        <Ellipse x:Name="ellipse"
                 Stroke="Red"
                 StrokeDashCap="Round"
                 StrokeDashArray="0 2" />
    </Grid>
</Window>

もちろん、EnableDependentAnimation プロパティ以外は同じになります。このアニメーションは、StrokeDashArray プロパティが設定されていることから点線のようなものになります。
AnimateStrokeThickness

今度は、StrokeDashOffset プロパティを使用する AnimateDashOffset プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Storyboard x:Key="storyboard">
            <DoubleAnimation Storyboard.TargetName="path"
                             Storyboard.TargetProperty="StrokeDashOffset"
                             From="0" To="1.5" Duration="0:0:1"
                             RepeatBehavior="Forever" />
        </Storyboard>
    </Window.Resources>
    <Grid>
        <Viewbox>
            <Path x:Name="path"
                  Margin="12"
                  Stroke="Black"
                  StrokeThickness="24"
                  StrokeDashArray="0 1.5"
                  StrokeDashCap="Round"
                  Data="M 100   0
                        C  45   0,   0  45, 0 100
                        S  45 200, 100 200
                        S 200 150, 250 100
                        S 345   0, 400   0
                        S 500  45, 500 100
                        S 455 200, 400 200
                        S 300 150, 250 100
                        S 155   0, 100   0" />
        </Viewbox>
    </Grid>
</Window>

このアニメーションも EnableDependentAnimation プロパティを除けば、同じになります。実行結果を示します。
AnimateDashOffset

書籍では、Path の定義などに関する説明がありますので、Path クラスを活用するのであれば熟読をお願いします。今度は、Opacity プロパティを使用する CheshireCat プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Storyboard x:Key="storyboard">
            <DoubleAnimation Storyboard.TargetName="image2"
                             Storyboard.TargetProperty="Opacity"
                             From="0" To="1" Duration="0:0:2"
                             AutoReverse="True"
                             RepeatBehavior="Forever" />
        </Storyboard>
    </Window.Resources>
    <Grid>
        <Viewbox>
            <Grid>
                <Image Source="Images/alice23a.gif"
                       Width="640" />

                <TextBlock FontFamily="Century Schoolbook"
                           FontSize="24"
                           Foreground="Black"
                           TextWrapping="Wrap"
                           TextAlignment="Justify"
                           Width="320"
                           Margin="0 0 24 60"
                           HorizontalAlignment="Right"
                           VerticalAlignment="Bottom">
                    &#x2003;&#x2003;“All right,” said the Cat; and this 
                    time it vanished quite slowly, beginning with the end 
                    of the tail, and ending with the grin, which
                    remained some time after the rest of it had gone.
                    <LineBreak />
                    <LineBreak />
                    &#x2003;&#x2003;“Well! I’ve often seen a cat without a 
                    grin,” thought Alice; “but a grin without a cat! It’s 
                    the most curious thing I ever saw in all my life!”
                </TextBlock>
                <Image x:Name="image2"
                       Source="Images/alice24a.gif"
                       Stretch="None"
                       VerticalAlignment="Top">
                    <Image.Clip>
                        <RectangleGeometry Rect="320 70 320 240" />
                    </Image.Clip>
                </Image>
            </Grid>
        </Viewbox>
    </Grid>
</Window>

このコードも EnableDpendentAnimation プロパティを除けば、同じになります。それでは、実行結果を示します。
CheshireCat

猫の画像の Opacity がアニメーションによって変化しますが、スクリーン ショットでは判別しにくいので、ご自分で実行してみてください。

9.5(P382) 添付プロパティのアニメーション

本節では、アニメーションを使ってオブジェクトを移動することを説明しています。このために、Canvas オブジェクトの添付プロパティを使用しています。それでは、AttachedPropertyAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Storyboard x:Key="storyboard">
            <DoubleAnimation Storyboard.TargetName="ellipse"
                             Storyboard.TargetProperty="(Canvas.Left)"
                             From="0" Duration="0:0:2.51"
                             AutoReverse="True"
                             RepeatBehavior="Forever" />
            <DoubleAnimation Storyboard.TargetName="ellipse"
                             Storyboard.TargetProperty="(Canvas.Top)"
                             From="0" Duration="0:0:1.01"
                             AutoReverse="True"
                             RepeatBehavior="Forever" />
        </Storyboard>
    </Window.Resources>

    <Grid>
        <Canvas SizeChanged="OnCanvasSizeChanged"
                Margin="0 0 48 48">
            <Ellipse x:Name="ellipse"
                     Width="48"
                     Height="48"
                     Fill="Red" />
        </Canvas>
    </Grid>
</Window>

コードは、組み込みスタイルを除けば同じになります。書籍にも説明がありますが、Storybord の TargetProperty に括弧付の特殊な構文を使用しています。今度は、MainWindow.xaml.cs を示します。

 using System;
using System.Windows;
using System.Windows.Media.Animation;

namespace AttachedPropertyAnimation
{
    public partial class MainWindow : Window
    {
        Storyboard storyboard;
        TimeSpan time;

        public MainWindow()
        {
            InitializeComponent();
            Loaded += (sender, args) =>
            {
                (this.Resources["storyboard"] as Storyboard).Begin();
            };
        }

        private void OnCanvasSizeChanged(object sender, SizeChangedEventArgs e)
        {
            bool isTime = false;
            Storyboard storyboard = this.Resources["storyboard"] as Storyboard;
            try
            {
                time = storyboard.GetCurrentTime();
                isTime = true;
            }
            catch { }

            storyboard.Stop();
            // Canvas.Left animation
            DoubleAnimation anima = storyboard.Children[0] as DoubleAnimation;
            anima.To = e.NewSize.Width;
            // Canvas.Top animation
            anima = storyboard.Children[1] as DoubleAnimation;
            anima.To = e.NewSize.Height;
            storyboard.Begin();
            if (isTime)
                storyboard.Seek(time);
        }
    }
}

コードでは、OnCanvasSizeChanged イベント ハンドラを大きく変更しています。変更した内容を次に示します。

  • isTime 変数を追加して、ストリーボードの経過時間を取得する。
  • アニメーションを停止(Stop)してから、To プロパティを設定する。
  • アニメーションを開始(Begin)してから、ストーリーボードの経過時間を進める(Seek)。

WinRT XAML では、ストーリーボードを停止することなく、To プロパティを変更していますが、WPF XAML は同じように実行中に To プロパティを変更することはできません。厳密には、変更してもアニメーションの実行に反映されないのです。このために、アニメーションを停止してから To プロパティを変更してから アニメーションを開始して、経過時間だけアニメーションを進めるという手法を取っています。
AttachedPropertyAnimation

スクリーン ショットではわかりませんが、赤い円がウィンドウに向かって動いて行き、跳ね返るように動いていきます。WinRT XAML では、実行中にウィンドウ サイズを変更することを前提にしていることから、To プロパティを実行中にアニメーションに反映させるという改良が WPF XAML の仕組みに対して行われていると理解すれば良いでしょう。一方で、WPF XAML では ウィンドウ サイズなどが変更された場合は、アニメーションの動きもプログラマが調整する必要があるということになります。これは、単純にアニメーションを開始するまえにアニメーションするプロパティの開始値(From) と 終了値(To) が決まっていないければならないからです。このサンプルで説明したように、WPF XAML では工夫さえすれば、動的にアニメーションで変化させる値を変更することも可能にする手段はあるということになります。無いのであれば、無いなりに工夫しましょうということです。

9.6(P385) イージング関数

本節では、前節までに説明した DubleAnimation タイムラインが直線的なアニメーションを実現するのに対して、曲線的に変化させるイージング関数を説明しています。そして、イージング関数を説明するために AnimationEaseGrapher プロジェクトを使用して、提供される様々なイージング関数を体験できるようにしています。それでは、AnimationEaseGrapher  プロジェクト の MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Storyboard x:Key="storyboard"
                    FillBehavior="Stop">
            <DoubleAnimation Storyboard.TargetName="redBall"
                             Storyboard.TargetProperty="(Canvas.Left)"
                             From="-6" To="994" Duration="0:0:3" />
            <DoubleAnimation x:Name="anima2"
                             Storyboard.TargetName="redBall"
                             Storyboard.TargetProperty="(Canvas.Top)"
                             From="-6" To="494" Duration="0:0:3" />
        </Storyboard>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <!-- Control panel -->
        <Grid Grid.Column="0"
              VerticalAlignment="Center">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <!-- Easing function (populated by code) -->
            <StackPanel x:Name="easingFunctionStackPanel"
                        Grid.Row="0"
                        Grid.RowSpan="3"
                        Grid.Column="0"
                        VerticalAlignment="Center">
                <RadioButton Content="None"
                             Margin="6"
                             Checked="OnEasingFunctionRadioButtonChecked" />
            </StackPanel>
            <!-- Easing mode -->
            <StackPanel x:Name="easingModeStackPanel"
                        Grid.Row="0"
                        Grid.Column="1"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center">
                <RadioButton Content="Ease In"
                             Margin="6"
                             Checked="OnEasingModeRadioButtonChecked">
                    <RadioButton.Tag>
                        <EasingMode>EaseIn</EasingMode>
                    </RadioButton.Tag>
                </RadioButton>
                <RadioButton Content="Ease Out"
                            Margin="6"
                            Checked="OnEasingModeRadioButtonChecked">
                    <RadioButton.Tag>
                        <EasingMode>EaseOut</EasingMode>
                    </RadioButton.Tag>
                </RadioButton>
                <RadioButton Content="Ease In/Out"
                             Margin="6"
                             Checked="OnEasingModeRadioButtonChecked">
                    <RadioButton.Tag>
                        <EasingMode>EaseInOut</EasingMode>
                    </RadioButton.Tag>
                </RadioButton>
            </StackPanel>
            <!-- Easing properties (populated by code) -->
            <StackPanel x:Name="propertiesStackPanel"
                        Grid.Row="1"
                        Grid.Column="1"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center" />
            <!-- Demo button -->
            <Button Grid.Row="2"
                    Grid.Column="1"
                    Content="Demo!"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Click="OnDemoButtonClick" />
        </Grid>

        <!-- Graph using arbitrary coordinates and scaled to window -->
        <Viewbox Grid.Column="1">
            <Grid Width="1000"
                  Height="500"
                  Margin="0 250 0 250">
                <!-- Rectangle outline -->
                <Polygon Points="0 0, 1000 0, 1000 500, 0 500"
                         Stroke="Black"
                         StrokeThickness="3" />
                <Canvas>
                    <!-- Linear transfer -->
                    <Polyline Points="0 0, 1000 500"
                              Stroke="Black"
                              StrokeThickness="1"
                              StrokeDashArray="3 3" />
                    <!-- Points set by code based on easing function -->
                    <Polyline x:Name="polyline"
                              Stroke="Blue"
                              StrokeThickness="3" />
                    <!-- Animated ball -->
                    <Ellipse x:Name="redBall"
                             Width="12"
                             Height="12"
                             Fill="Red" />
                </Canvas>
            </Grid>
        </Viewbox>
    </Grid>
</Window>

コード自体は、組み込みスタイルを除けば WinRT XAML と同じになります。分離コードで、イージング関数をリフレクションを使って RadioButton として追加をしていますので、MainWindow.xaml.cs の抜粋を示します。

 public partial class MainWindow : Window
{
    EasingFunctionBase easingFunction;

    public MainWindow()
    {
        InitializeComponent();

        Loaded += OnMainPageLoaded;
    }

    void OnMainPageLoaded(object sender, RoutedEventArgs args)
    {
        Type baseType = typeof(EasingFunctionBase);
        TypeInfo baseTypeInfo = baseType.GetTypeInfo();
        Assembly assembly = baseTypeInfo.Assembly;
        // Enumerate through all Windows Runtime types
        foreach (Type type in assembly.ExportedTypes)
        {
            TypeInfo typeInfo = type.GetTypeInfo();
            // Create RadioButton for each easing function
            if (typeInfo.IsPublic &&
                baseTypeInfo.IsAssignableFrom(typeInfo) &&
                type != baseType)
            {
                RadioButton radioButton = new RadioButton
                {
                    Content = type.Name,
                    Tag = type,
                    Margin = new Thickness(6),
                };
                radioButton.Checked += OnEasingFunctionRadioButtonChecked;
                easingFunctionStackPanel.Children.Add(radioButton);
            }
        }
        // Check the first RadioButton in the StackPanel (the one labeled "None")
        (easingFunctionStackPanel.Children[0] as RadioButton).IsChecked = true;
    }

    private void OnEasingFunctionRadioButtonChecked(object sender, RoutedEventArgs e)
    {
        RadioButton radioButton = sender as RadioButton;
        Type type = radioButton.Tag as Type;
        easingFunction = null;
        propertiesStackPanel.Children.Clear();
        // type is only null for "None" button
        if (type != null)
        {
            TypeInfo typeInfo = type.GetTypeInfo();
            // Find a parameterless constructor and instantiate the easing function
            foreach (ConstructorInfo constructorInfo in typeInfo.DeclaredConstructors)
            {
                if (constructorInfo.IsPublic && constructorInfo.GetParameters().Length == 0)
                {
                    easingFunction = constructorInfo.Invoke(null) as EasingFunctionBase;
                    break;
                }
            }
            // Enumerate the easing function properties
            foreach (PropertyInfo property in typeInfo.DeclaredProperties)
            {
                // We can only deal with properties of type int and double
                if (property.PropertyType != typeof(int) &&
                    property.PropertyType != typeof(double))
                {
                    continue;
                }
                // Create a TextBlock for the property name
                TextBlock txtblk = new TextBlock
                {
                    Text = property.Name + ":"
                };
                propertiesStackPanel.Children.Add(txtblk);
                // Create a Slider for the property value
                Slider slider = new Slider
                {
                    Width = 144,
                    Minimum = 0,
                    Maximum = 10,
                    Tag = property
                };
                if (property.PropertyType == typeof(int))
                {
                    
                    //slider.StepFrequency = 1;
                    slider.SmallChange = 1;
                    slider.Value = (int)property.GetValue(easingFunction);
                }
                else
                {
                    //slider.StepFrequency = 0.1;
                    slider.SmallChange = 0.1;
                    slider.Value = (double)property.GetValue(easingFunction);
                }
                // Define the Slider event handler right here
                slider.ValueChanged += (sliderSender, sliderArgs) =>
                {
                    Slider sliderChanging = sliderSender as Slider;
                    PropertyInfo propertyInfo = sliderChanging.Tag as PropertyInfo;
                    if (property.PropertyType == typeof(int))
                        property.SetValue(easingFunction, (int)sliderArgs.NewValue);
                    else
                        property.SetValue(easingFunction, (double)sliderArgs.NewValue);

                    DrawNewGraph();
                };
                propertiesStackPanel.Children.Add(slider);
            }
        }
        // Initialize EasingMode radio buttons
        foreach (UIElement child in easingModeStackPanel.Children)
        {
            RadioButton easingModeRadioButton = child as RadioButton;
            easingModeRadioButton.IsEnabled = easingFunction != null;
            easingModeRadioButton.IsChecked =
                easingFunction != null &&
                easingFunction.EasingMode == (EasingMode)easingModeRadioButton.Tag;
        }
        DrawNewGraph();
    }

    private void OnEasingModeRadioButtonChecked(object sender, RoutedEventArgs e)
    {
        RadioButton radioButton = sender as RadioButton;
        easingFunction.EasingMode = (EasingMode)radioButton.Tag;
        DrawNewGraph();
    }

    private void OnDemoButtonClick(object sender, RoutedEventArgs e)
    {
        // Set the selected easing function and start the animation
        Storyboard storyboard = this.Resources["storyboard"] as Storyboard;
        (storyboard.Children[1] as DoubleAnimation).EasingFunction = easingFunction;
        storyboard.Begin();
    }

    void DrawNewGraph()
    {
        polyline.Points.Clear();
        if (easingFunction == null)
        {
            polyline.Points.Add(new Point(0, 0));
            polyline.Points.Add(new Point(1000, 500));
            return;
        }
        for (decimal t = 0; t <= 1; t += 0.01m)
        {
            double x = (double)(1000 * t);
            double y = 500 * easingFunction.Ease((double)t);
            polyline.Points.Add(new Point(x, y));
        }
    }
}

コードで WinRT XAML と異なるのは、OnEasingFunctionRadioButtonChecked イベント ハンドラー内の Slider オブジェクトに対する StepFrequency プロパティを SmallChange プロパティに変更した点になります。それでは、実行結果を示します。
AnimationEaseGrapher

イージング関数とコードそのものの説明は書籍で行われていますので、書籍を熟読してください。今度は、円周上をぐるぐる回るオブジェクトを実現するために、CircleAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Storyboard x:Key="storyboard" SpeedRatio="3">
            <DoubleAnimation Storyboard.TargetName="ball"
                             Storyboard.TargetProperty="(Canvas.Left)"
                             From="-350" To="350" Duration="0:0:2"
                             AutoReverse="True"
                             RepeatBehavior="Forever">
                <DoubleAnimation.EasingFunction>
                    <SineEase EasingMode="EaseInOut" />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>

            <DoubleAnimation Storyboard.TargetName="ball"
                             Storyboard.TargetProperty="(Canvas.Top)"
                             BeginTime="0:0:1"
                             From="-350" To="350" Duration="0:0:2"
                             AutoReverse="True"
                             RepeatBehavior="Forever">
                <DoubleAnimation.EasingFunction>
                    <SineEase EasingMode="EaseInOut" />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
        </Storyboard>
    </Window.Resources>
    <Grid>
        <Canvas HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Margin="0 0 48 48">
            <Ellipse x:Name="ball"
                     Width="48"
                     Height="48"
                     Fill="Red" />
        </Canvas>
    </Grid>
</Window>

コードは、組み込みスタイルを除けば同じになります。実行結果を示します。
CircleAnimationpng

スクリーン ショットでは区別できませんが、赤い円が円周上をアニメーションで動きます。これもイージング関数によって実現していますから、説明は書籍を熟読してください。

9.7(P395) XAML アニメーション

本節では、前節までのアニメーションが Loaded イベント ハンドラーによって開始していたことに対して、自動的にアニメーションを開始させる仕組みである Triggers プロパティを説明しています。この動きを示すために ForeverColorAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Grid>
        <Grid.Background>
            <SolidColorBrush x:Name="gridBrush" />
        </Grid.Background>
        <TextBlock Text="Color Animation"
                   FontFamily="Times New Roman"
                   FontSize="96"
                   FontWeight="Bold"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center">
            <TextBlock.Foreground>
                <SolidColorBrush x:Name="txtblkBrush" />
            </TextBlock.Foreground>
        </TextBlock>
        
    </Grid>
    
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard RepeatBehavior="Forever"
                            AutoReverse="True">
                    <ColorAnimation Storyboard.TargetName="gridBrush"
                                    Storyboard.TargetProperty="Color"
                                    From="Black" To="White" Duration="0:0:2" />

                    <ColorAnimation Storyboard.TargetName="txtblkBrush"
                                    Storyboard.TargetProperty="Color"
                                    From="White" To="Black" Duration="0:0:2" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

このコードで違うところは、EnventTrigger 要素の RoutedEvent 属性の設定になります。WinRT XAMLでは、EventTrigger 要素は Loaded イベントにしか使用することができないので、記述自体を省略することが求められます。これに対して WPF XAML では、他のイベントでも使用できるようになっていることから、明示的に Loaded イベントを指定しています。次に、ColorAnimation タイムラインが記述されていることにも注意をしてください。Brush オブジェクトの Color プロパティをアニメーションする場合には、当たり前ですが型が Double ではないため DoubleAnimation タイムラインを使用できないことから、Color プロパティ専用のタイムラインである ColorAnimation を使用します。それでは、実行結果を示します。
ForeverColorAnimation

今度は図形を変化させる SquaringTheCircle プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Grid>
        <Canvas HorizontalAlignment="Center"
                VerticalAlignment="Center">
            <Path Fill="Gray"
                  Stroke="Black" 
                  StrokeThickness="3" >
                <Path.Data>
                    <PathGeometry>
                        <PathFigure x:Name="bezier1" IsClosed="True">
                            <BezierSegment x:Name="bezier2" />
                            <BezierSegment x:Name="bezier3" />
                            <BezierSegment x:Name="bezier4" />
                            <BezierSegment x:Name="bezier5" />
                        </PathFigure>
                    </PathGeometry>
                </Path.Data>

                <Path.Triggers>
                    <EventTrigger RoutedEvent="Loaded">
                        <BeginStoryboard>
                            <Storyboard RepeatBehavior="Forever">
                                <PointAnimation Storyboard.TargetName="bezier1"
                                                Storyboard.TargetProperty="StartPoint"
                                                From="0 200" To="0 250"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier2"
                                                Storyboard.TargetProperty="Point1"
                                                From="110 200" To="125 125"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier2"
                                                Storyboard.TargetProperty="Point2"
                                                From="200 110" To="125 125"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier2"
                                                Storyboard.TargetProperty="Point3"
                                                From="200 0" To="250 0"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier3"
                                                Storyboard.TargetProperty="Point1"
                                                From="200 -110" To="125 -125"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier3"
                                                Storyboard.TargetProperty="Point2"
                                                From="110 -200" To="125 -125"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier3"
                                                Storyboard.TargetProperty="Point3"
                                                From="0 -200" To="0 -250"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier4"
                                                Storyboard.TargetProperty="Point1"
                                                From="-110 -200" To="-125 -125"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier4"
                                                Storyboard.TargetProperty="Point2"
                                                From="-200 -110" To="-125 -125"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier4"
                                                Storyboard.TargetProperty="Point3"
                                                From="-200 0" To="-250 0"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier5"
                                                Storyboard.TargetProperty="Point1"
                                                From="-200 110" To="-125 125"
                                                AutoReverse="True" />
                                <PointAnimation Storyboard.TargetName="bezier5"
                                                Storyboard.TargetProperty="Point2"
                                                From="-110 200" To="-125 125"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier5"
                                                Storyboard.TargetProperty="Point3"
                                                From="0 200" To="0 250"
                                                AutoReverse="True" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Path.Triggers>
            </Path>
        </Canvas>
    </Grid>
</Window>

このコードが WinRT XAML と異なるのは、組み込みスタイル、Storyboard の EnableDependentAnimation プロパティ、EventTrigger の RoutedEvent プロパティになります。また、PointAnimation タイムラインが使用されていることも注意してください。PointAnimation は、Point プロパティ(System.Windows.Point) をアニメーションさせるための専用のタイムラインとなります。それでは、実行結果を示します。
SquaringTheCircle

9.8(P400) カスタムクラスのアニメーション

本節では、カスタム コントロールとして PieSlice を定義して扇形にアニメーションさせる方法を説明しています。それでは、AnimatedPieSlice プロジェクトの PieSlice.cs を示します。

 using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace AnimatedPieSlice
{
    // Path が seald クラスの為 に Shape に変更
    public class PieSlice : Shape
    {
        PathFigure pathFigure;
        LineSegment lineSegment;
        ArcSegment arcSegment;

        static PieSlice()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Path), new FrameworkPropertyMetadata(typeof(Path)));
            CenterProperty = DependencyProperty.Register("Center",
                typeof(Point), typeof(PieSlice),
                new PropertyMetadata(new Point(100, 100), OnPropertyChanged));

            RadiusProperty = DependencyProperty.Register("Radius",
                typeof(double), typeof(PieSlice),
                new PropertyMetadata(100.0, OnPropertyChanged));

            StartAngleProperty = DependencyProperty.Register("StartAngle",
                typeof(double), typeof(PieSlice),
                new PropertyMetadata(0.0, OnPropertyChanged));

            SweepAngleProperty = DependencyProperty.Register("SweepAngle",
                typeof(double), typeof(PieSlice),
                new PropertyMetadata(90.0, OnPropertyChanged));
            // DataProperty を追加
            DataProperty = DependencyProperty.Register("Data",
                typeof(Geometry), typeof(PieSlice),
                new PropertyMetadata(null));
        }

        public PieSlice()
        {
            pathFigure = new PathFigure { IsClosed = true };
            lineSegment = new LineSegment();
            arcSegment = new ArcSegment { SweepDirection = SweepDirection.Clockwise };
            pathFigure.Segments.Add(lineSegment);
            pathFigure.Segments.Add(arcSegment);

            PathGeometry pathGeometry = new PathGeometry();
            pathGeometry.Figures.Add(pathFigure);

            this.Data = pathGeometry;

            UpdateValues();
        }

        public static DependencyProperty CenterProperty { private set; get; }

        public static DependencyProperty RadiusProperty { private set; get; }

        public static DependencyProperty StartAngleProperty { private set; get; }

        public static DependencyProperty SweepAngleProperty { private set; get; }
        // DataProperty を追加
        public static DependencyProperty DataProperty { set; get; }

        public Point Center
        {
            set { SetValue(CenterProperty, value); }
            get { return (Point)GetValue(CenterProperty); }
        }

        public double Radius
        {
            set { SetValue(RadiusProperty, value); }
            get { return (double)GetValue(RadiusProperty); }
        }

        public double StartAngle
        {
            set { SetValue(StartAngleProperty, value); }
            get { return (double)GetValue(StartAngleProperty); }
        }

        public double SweepAngle
        {
            set { SetValue(SweepAngleProperty, value); }
            get { return (double)GetValue(SweepAngleProperty); }
        }

        static void OnPropertyChanged(DependencyObject obj,
                                      DependencyPropertyChangedEventArgs args)
        {
            (obj as PieSlice).UpdateValues();
        }

        void UpdateValues()
        {
            pathFigure.StartPoint = this.Center;

            double x = this.Center.X + this.Radius * Math.Sin(Math.PI * this.StartAngle / 180);
            double y = this.Center.Y - this.Radius * Math.Cos(Math.PI * this.StartAngle / 180);
            lineSegment.Point = new Point(x, y);

            x = this.Center.X + this.Radius * Math.Sin(Math.PI * (this.StartAngle +
                                                                  this.SweepAngle) / 180);

            y = this.Center.Y - this.Radius * Math.Cos(Math.PI * (this.StartAngle +
                                                                  this.SweepAngle) / 180);
            arcSegment.Point = new Point(x, y);
            arcSegment.IsLargeArc = this.SweepAngle >= 180;

            arcSegment.Size = new Size(this.Radius, this.Radius);
        }

        // Data プロパティと DefiningGeometry プロパティを追加
        public Geometry Data
        {
            get { return (Geometry)GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }
        protected override Geometry DefiningGeometry
        {
            get 
            {
                Geometry data = this.Data;
                if (data == null)
                {
                    data = Geometry.Empty;
                }
                return data;
            }
        }
    }
}

コードは、WinRT XAML と大きく異なっている箇所がありますので、次に示します。

  • 継承元のクラスを Path より Shape に変更。
    WinRT XAML の Path クラスは継承可能ですが、WPF XAML の Path クラスは継承できないためです。
    WPF XAML では、Shape クラスの派生型が継承できない(Seald)クラスになっています。
  • DefaultStyleKeyProperty.OverrideMetadata メソッドを追加し、Path クラスのスタイルを適用。
    作成したクラスを WinRT XAML と同じように Path を継承したスタイルに合わせるためです。
  • 依存関係プロパティとして Data プロパティを追加。
  • DefiningGeometry プロパティをオーバーライド

DefaultStyleKeyProperty、Data 依存関係プロパティ などは、WinRT XAML と同じように Path クラスを継承したのと同じようにするために追加しています(DefaultStyleKeyProperty は、追加しなくても同じ動作をすることを確認していますが、念のために追加しています)。この PieSlice カスタム コントロールを使用する MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Grid>
        <local:PieSlice x:Name="pieSlice"
                        Center="400 400"
                        Radius="200"
                        Stroke="Red"
                        StrokeThickness="3"
                        Fill="Yellow" />
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="pieSlice"
                                     Storyboard.TargetProperty="SweepAngle"
                                     From="1" To="359" Duration="0:0:3"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

XAML は、組み込みのスタイルと EventTrigger 要素の RoutedEvent 属性を除けば同じになります。もちろん、実行結果も同じになります。
AnimatedPieSlice

作成した PieSlice クラスに関する説明は、書籍を参照してください。

9.9(P404) キーフレーム アニメーション

本節では、複数のアニメーションをタイムラインによって組み合わせるキーフレーム アニメーションを説明しています。この目的で円を画面上で動かす SimpleKeyFrameAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Grid>
        <Path Fill="Blue">
            <Path.Data>
                <EllipseGeometry x:Name="ellipse"
                                 RadiusX="24"
                                 RadiusY="24" />
            </Path.Data>
        </Path>

    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <PointAnimationUsingKeyFrames Storyboard.TargetName="ellipse"
                                                  Storyboard.TargetProperty="Center"
                                                  RepeatBehavior="Forever">
                        <DiscretePointKeyFrame KeyTime="0:0:0" Value="100 100" />
                        <LinearPointKeyFrame KeyTime="0:0:2" Value="700 700" />
                        <LinearPointKeyFrame KeyTime="0:0:2.1" Value="700 100" />
                        <LinearPointKeyFrame KeyTime="0:0:4.1" Value="100 700" />
                        <LinearPointKeyFrame KeyTime="0:0:4.2" Value="100 100" />
                    </PointAnimationUsingKeyFrames>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

XAML自体は、組み込みスタイルと EventTrigger を除けば同じになります。このアニメーションのポイントは、PointAnimationUsingKeyFrames を使って キーフレームのコレクションに従ってアニメーションを行っているところにあります。キーフレームの説明は書籍にありますので、ここでは基本的な動きだけを次に示します。

  • 中心座標が (100,100) が開始位置となります。
  • 2 秒後に中心座標を (700,700) へ移動します。
    右側、ななめ下へ
  • 2.1 秒後に中心座標を (700,100) へ移動します。
    x座標は同じで、開始位置へ(つまり、上へ)
  • 4.1 秒後に中心座標を (100,700) へ移動します。
    x座標を開始位置にし、y座標を下へ(つまり、ななめ左下へ)
  • 4.2 秒後に中心座標を (100,100) へ移動します。
    開始位置へ(つまり、上へ)
  • Forever によって繰り返します。

念のため、実行結果を示します。
SimpleKeyFrameAnimation

もちろん、スクリーン ショットではアニメーションの動きを確認できませんので、自分で動かしてみてください。今度は、背景色をアニメーション化する RainbowAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Grid>
        <Grid.Background>
            <SolidColorBrush x:Name="brush" />
        </Grid.Background>
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard RepeatBehavior="Forever">
                    <ColorAnimationUsingKeyFrames Storyboard.TargetName="brush"
                                                  Storyboard.TargetProperty="Color">
                        <DiscreteColorKeyFrame KeyTime="0:0:0" Value="#FF0000" />
                        <LinearColorKeyFrame KeyTime="0:0:1" Value="#FFFF00" />
                        <LinearColorKeyFrame KeyTime="0:0:2" Value="#00FF00" />
                        <LinearColorKeyFrame KeyTime="0:0:3" Value="#00FFFF" />
                        <LinearColorKeyFrame KeyTime="0:0:4" Value="#0000FF" />
                        <LinearColorKeyFrame KeyTime="0:0:5" Value="#FF00FF" />
                        <LinearColorKeyFrame KeyTime="0:0:6" Value="#FF0000" />
                    </ColorAnimationUsingKeyFrames>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

このXAMLは、WinRT XAML と基本的に同じもの(EventTrigger 以外)になりますので、実行すれば Grid の背景色が7色に変化するようになります。
RainbowAnimation

今度は、Grid の背景色をグラデーションを使って回転させる GradientBrushPointAnimation プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Grid>
        <Grid.Background>
            <LinearGradientBrush x:Name="gradientBrush">
                <GradientStop Offset="0" Color="Red" />
                <GradientStop Offset="1" Color="Blue" />
            </LinearGradientBrush>
        </Grid.Background>
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard RepeatBehavior="Forever">
                    <PointAnimationUsingKeyFrames Storyboard.TargetName="gradientBrush"
                                                  Storyboard.TargetProperty="StartPoint">
                        <LinearPointKeyFrame KeyTime="0:0:0" Value="0 0" />
                        <LinearPointKeyFrame KeyTime="0:0:1" Value="1 0" />
                        <LinearPointKeyFrame KeyTime="0:0:2" Value="1 1" />
                        <LinearPointKeyFrame KeyTime="0:0:3" Value="0 1" />
                        <LinearPointKeyFrame KeyTime="0:0:4" Value="0 0" />
                    </PointAnimationUsingKeyFrames>

                    <PointAnimationUsingKeyFrames Storyboard.TargetName="gradientBrush"
                                                  Storyboard.TargetProperty="EndPoint">
                        <LinearPointKeyFrame KeyTime="0:0:0" Value="1 1" />
                        <LinearPointKeyFrame KeyTime="0:0:1" Value="0 1" />
                        <LinearPointKeyFrame KeyTime="0:0:2" Value="0 0" />
                        <LinearPointKeyFrame KeyTime="0:0:3" Value="1 0" />
                        <LinearPointKeyFrame KeyTime="0:0:4" Value="1 1" />
                    </PointAnimationUsingKeyFrames>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

この XAML も、基本的には WinRT XAML と同じもの(EventTrigger 以外)になります。キーフレーム アニメーションの定義を読めば、グラデーションの開始位置(StartPoint)と終了位置(EndPoint) を移動させることが理解できるでしょう。それでは、実行結果を示します。
GradientBrushPointAnimation

9.10(P408) オブジェクト アニメーション

本節では、WinRT XAML で良く使うと考えられるオブジェクト アニメーションを実現する ObjectKeyFrame の派生クラスの説明をしています。このキーフレームの使い方は、WPF XAML とはかなり異なっています。最初に FastNotFluid プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Grid Background="Gray">
        <Canvas SizeChanged="OnCanvasSizeChanged"
                Margin="0 0 96 96">
            <Ellipse x:Name="ellipse"
                     Width="96"
                     Height="96" Fill="Black" />
        </Canvas>
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard x:Name="animation">
                    <DoubleAnimation x:Name="horzAnima"
                                     Storyboard.TargetName="ellipse"
                                     Storyboard.TargetProperty="(Canvas.Left)"
                                     From="0" Duration="0:0:2.51"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever" />
                    <DoubleAnimation x:Name="vertAnima"
                                     Storyboard.TargetName="ellipse"
                                     Storyboard.TargetProperty="(Canvas.Top)"
                                     From="0" Duration="0:0:1.01"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever" />
                    <ObjectAnimationUsingKeyFrames
                                        Storyboard.TargetName="ellipse"
                                        Storyboard.TargetProperty="(UIElement.Visibility)"
                                        RepeatBehavior="Forever">
                        <DiscreteObjectKeyFrame KeyTime="0:0:0" Value="{x:Static Visibility.Visible}" />
                        <DiscreteObjectKeyFrame KeyTime="0:0:0.2" Value="{x:Static Visibility.Collapsed}" />
                        <DiscreteObjectKeyFrame KeyTime="0:0:0.25" Value="{x:Static Visibility.Visible}" />
                        <DiscreteObjectKeyFrame KeyTime="0:0:0.3" Value="{x:Static Visibility.Collapsed}" />
                        <DiscreteObjectKeyFrame KeyTime="0:0:0.45" Value="{x:Static Visibility.Visible}" />
                    </ObjectAnimationUsingKeyFrames>
                    <ColorAnimationUsingKeyFrames
                                        Storyboard.TargetName="ellipse"
                                        Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                                        RepeatBehavior="Forever">
                        <EasingColorKeyFrame KeyTime="0:0:0" 
                            Value="Black" />
                        <EasingColorKeyFrame KeyTime="0:0:0.2" 
                            Value="White" />
                        <EasingColorKeyFrame KeyTime="0:0:0.4" 
                            Value="Black" />
                        <EasingColorKeyFrame KeyTime="0:0:0.6" 
                            Value="Black" />
                    </ColorAnimationUsingKeyFrames>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

この XAML は、EventTrigger 要素の RoutedEvent 属性以外にも違いがありますから、キーフレーム アニメーションの説明を示します。

  • DoubleAnimation の horzAnima は、同じで Canvas.Left 添付プロパティを指定しています。
  • DoubleAnimation の vertAnima は、同じで Canvas.Top 添付プロパティを指定しています。
  • ObjectAnimationUsingKeyFrames は、TargetProperty 添付プロパティの指定方法が異なります。
    具体的には、「(UIElement.Visibility)」 を指定しています。一方で、WinRT XAML は文字列で Visibility を指定しています。
    WPF XAML では、継承したプロパティに関しては継承元のクラスを指定して記述しなければなりません(型に対する厳密性)。
  • ColorAnimationUsingKeyFrames は、ObjectAnimationUsingKeyFrames を置き換えています。
    また、TargetProperty 添付プロパティの指定方法が異なります。具体的には、「(Shape.Fill).(SolidColorBrush.Color)」を指定しています。
    ColorAnimationUsingKeyFrames に変更したことで、EasingColorKeyFrame に変更しています。

すでに説明していますが、WinRT XAML の ObjectAnimationUsingKeyFrame は様々な用途に使えるの便利なキーフレーム になります。一方で WPF XAML は、型に対する厳密性が要求されるようになっているためことから専用のキーフレームを指定する必要と TargetProperty 添付プロパティには指定するプロパティを実装する派生元のクラスを使った記述を行う必要があります。これが、Fill プロパティを指定する場合に行っている「(Shape.Fill).(SolidColorBrush.Color)」の意味になります。この構文は、Shape クラスで実装する Fill プロパティ(Brush 型)を SolidColorBrush 型の Color プロパティ を指定しています。今度は、アニメーションの上限値を設定している、MainWindow.xaml.cs の OnCanvasSizeChanged イベント ハンドラーを示します。

 private void OnCanvasSizeChanged(object sender, SizeChangedEventArgs e)
{
    bool isError = false;
    TimeSpan time = new TimeSpan();
    try
    {
        time = animation.GetCurrentTime();
        isError = false;
    }
    catch
    {
        isError = true;
    }
    animation.Stop();

    horzAnima.To = e.NewSize.Width;
    vertAnima.To = e.NewSize.Height;

    animation.Begin();
    if (!isError)
        animation.Seek(time);
}

WinRT XAML のコードとは、かなり異なります。すでに説明していますが、To プロパティを動的に変化させても動作中のアニメーションに反映しないためになります。それでは、実行結果を示します。
FastNotFluid

ここでは、WinRT XAML と WPF XAML のアニメーションの違いを簡単に説明します。最初に、良く使われる基本的なアニメーションを示します。

プロパティ型 From/To WPF WinRT
Double DoubleAnimation O O
Color ColorAnimation O O
Point PointAnimation O O
Rect RectAnimation O X
Size SizeAnimation O X
Int32 Int32Animation O X

今度は、キーフレーム アニメーションを抜粋して示します。

プロパティ型 From/To WPF WinRT
Object ObjectAnimationUsingKeyFrames O O
Color ColorAnimationUsingKeyFrames O O
Point PointAnimationUsingKeyFrames O O
Double DoubleAnimationUsingKeyFrames O O
Boolean BooleaAnimationUsingKeyFrames O X
String StringAnimationUsingKeyFrames O X

このように、WPF XAML では型に応じたアニメーションやキーフレームの型が豊富に用意されています。一方で、WinRT XAML は WPF XAML の一部分だけが提供されています(もちろん、WinRT XAML だけに含まれるアニメーションもあります)。このために、WinRT XAML では ObjectAnimationUsingKeyFrames が何度も活躍することになります。そして、TargetProperty 添付プロパティに対する記述方法は、WinRT XAML が簡略記法を許可しているだけになります。アニメーションさせるプロパティによっては、簡略記法が使えないので括弧を使った派生元の型(クラス)を指定してプロパティを指定したことが、私には何度もあります。つまり、WinRT XAML でも括弧を使用した派生元の型を指定する記述は有効なので、アニメーションで例外が発生するような場合は書き直してみてください(Blend for Visual Studio でアニメーションの設定を行うと、省略記法ではなく括弧を利用した派生元の型を指定する形式の XAML を生成してくれます)。アニメーションに指定するプロパティや、タイムラインとキーフレームにこのような違いがあるので、本記事の冒頭で WPF XAML は型を厳密に指定しなければならないと説明しました。一方で、Silverlight から始まる XAML 系の UI フレームワークの経験から良く使われるであろうアニメーションに的を絞って、コンパクトに仕上げたのが WinRT XAML などの UI フレームワークの特徴だと考えることができます。

9.11(P411) 定義済みのアニメーションと遷移

本節では、WinRT XAML で用意されている組み込みのアニメーション ライブラリを説明しています。WPF XAML では、ほんの一部(Popup など)を除いて、組み込みのアニメーション ライブラリは用意されていません。これは、WPF が登場した時代にタッチ操作などを含めてアニメーションの一般的な用途が決まっていなかったことが原因だと考えられます。Windows 8 のストア アプリの設計では、ページ遷移を含めて一般化したアニメーション ライブラリが定義されたので、WinRT XAML でも提供することで、全てのユーザーに統一化したアニメーションを提供できるようになりました。ここでは、一部のアニメーションを定義した precnfiguredAnimations プロジェクトの Mainwindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Margin" Value="0 6" />
        </Style>
        <Storyboard x:Key="fadeIn"
                    Storyboard.TargetName="button"
                    Storyboard.TargetProperty="Opacity">
            <DoubleAnimation From="0.0" To="1.0" Duration="0:0:4" />
        </Storyboard>
        <Storyboard x:Key="fadeOut"
                    Storyboard.TargetName="button"
                    Storyboard.TargetProperty="Opacity">
            <DoubleAnimation From="1.0" To="0.0" Duration="0:0:4" />
        </Storyboard>
        <Storyboard x:Key="popIn"
                    Storyboard.TargetName="button"
                    Storyboard.TargetProperty="Opacity">
            <DoubleAnimation From="0.0" To="1.0" Duration="0:0:0.5" />
        </Storyboard>
        <Storyboard x:Key="popOut"
                    Storyboard.TargetName="button"
                    Storyboard.TargetProperty="Opacity">
            <DoubleAnimation From="1.0" To="0.0" Duration="0" />
        </Storyboard>
    </Window.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <StackPanel x:Name="animationTriggersStackPanel"
                    Grid.Column="0"
                    VerticalAlignment="Center">
            <Button Content="Fade In"
                    Tag="fadeIn" 
                    Click="OnButtonClick" />
            <Button Content="Fade Out"
                    Tag="fadeOut" 
                    Click="OnButtonClick" />
            <Button Content="Pop In"
                    Tag="popIn" 
                    Click="OnButtonClick" />
            <Button Content="Pop Out"
                    Tag="popOut" 
                    Click="OnButtonClick" />

            <Button Content="Reposition"
                    Tag="reposition" 
                    Click="OnButtonClick" />
            <Button Content="Pointer Up"
                    Tag="pointerUp" 
                    Click="OnButtonClick" />
            <Button Content="Pointer Down"
                    Tag="pointerDown" 
                    Click="OnButtonClick" />
            <Button Content="Swipe Back"
                    Tag="swipeBack" 
                    Click="OnButtonClick" />
            <Button Content="Swipe Hint"
                    Tag="swipeHint" 
                    Click="OnButtonClick" />
            <Button Content="Drag Item"
                    Tag="dragItem" 
                    Click="OnButtonClick" />
            <Button Content="Drop Target Item"
                    Tag="dropTargetItem" 
                    Click="OnButtonClick" />
            <Button Content="Drag Over"
                    Tag="dragOver" 
                    Click="OnButtonClick" />
        </StackPanel>
        <!-- Animation target -->
        <Button x:Name="button"
                Grid.Column="1"
                Content="Big Button"
                FontSize="48"
                HorizontalAlignment="Center"
                VerticalAlignment="Center" />
    </Grid>
</Window>

XAML を読めば、違いは明らかなことでしょう。Fade Out から Pop Out のみのアニメーションを定義しています。定義した時間も、これが正しいというものではなく、そのようなアニメーションに感じるというものになります。つまり、WPF XAML では、全てのアニメーションを自分で定義しなければならないのです。WPF XAML で用意されている組み込みのアニメーションは、Popup クラスに用意された PopupAnimation プロパティに指定できる PopupAnimation 列挙などだけになります。WinRT XAML に用意されている組み込みアニメーションを確認する場合は、書籍を熟読するのとサンプルを自分で動かしてみてください。

ここまで説明してた違いを意識しながら、第9章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。

ch09.zip