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


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

WPF を利用する上で、有益な資料を八巻さんがアップデートして公開してくれました。「Windowsフォームに対するWPFの真の優位性とは」という記事から、ダウンロードできますので、参考にしてください。私が、「XAML とは何か」で記述したディスプレイ ドライバー のことなども踏まえている資料になります。

第10章 座標変換

本章では、アニメーションなどで使用する座標変換を説明しています。WPF XAML も WinRT XAML と同じように座標変換を利用可能になっています。座標変換は、Windows Forms などでは高度なグラフィックスを描画する場合などに必要とされる技術でしたから、Direct X などのグラフィックス技術を扱わない技術者にとっても馴染みがないものになることでしょう。

10.1(P417) 概要

本節では、座標変換が座標 (x,y) を新しい座標 (x',y') に変換するものであり、WinRT XAML では RenderTransform、RenderTransformOrigin、Projection という 3つのプロパティでサポートされていることを説明しています。WPF XAML に置き換えると、Projection プロパティのみが適用できないものになります。なぜなら、WPF XAML が 3D オブジェクトをサポートしているためです。Projection は、Silverlight で導入されて、WinRT XAML でも採用された疑似的な 3D を扱う座標変換になります。最初に、RenderTransform の派生クラスである RotateTransform を使用する SimpleRotate プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Grid>
        <Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg"
           Stretch="None"
           HorizontalAlignment="Right"
           VerticalAlignment="Bottom">
            <Image.RenderTransform>
                <RotateTransform Angle="135" />
            </Image.RenderTransform>
        </Image>
    </Grid>
</Window>

この XAML は、組み込みスタイルを除けば WinRT XAML と同じになります。Angle 属性で指定されているように、135度に回転させることになります。
SimpleRotate

書籍に記載されていますが、RotateTransform は 左上角を基準に回転させる座標変換を行います。書籍では、Transform 派生クラスを簡単に説明してから、Projection を使った 3D エフェクトの説明で SimpleProjection プロジェクトを使った説明になります。すでに説明したように、WPF XAML は Projection をサポートしていまん。サポートしていませんから、Projection と同じような効果を得ようとすれば、3D オブジェクトを使用することになります。たとえば、Projection と同じような効果を得る記事として「Enter The Planerator - Dead-simple 3D in WPF, with a stupid name」があり、この記事に添付されている PlaneratorSolution を使って Visual Studio 2013 の WPF プロジェクトで実行した結果を示します。このプロジェクトは、サンプルに含めていませんので、試したいかたは自分で作成してみてください。
PlaneratorSolution

また、WPF で 3D オブジェクトの扱い方を学習される場合は、「WPF 3Dプログラミング―誰でも簡単に3Dゲームやツールが作れる最新技術! 」や「3D Programming for Windows」なども参考になることでしょう。「3D Programming for Windows」は、プログラミング Windows の著者であるペゾルドの著作になります。

逆説的に説明すれば、WinRT XAML では 3D グラフィックスをサポートしていないので、DirectX を使用しないと 3D グラフィックスを描画できないと言えます(この目的では、Visual C++/CX を使用するか、SharpDx を使用することもできます)。

10.2(P420) 手動回転と自動回転

本節では、TranslateTransform を使って何ができるかを説明しています。その 1つとして、RotateTransform の Angle プロパティにバインディングする RotateTheText プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Grid>
        <Border BorderBrush="Black"
                BorderThickness="1"
                HorizontalAlignment="Center"
                VerticalAlignment="Center">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Slider Name="slider"
                        Grid.Row="0"
                        Minimum="0"
                        Maximum="360" />
                <TextBlock Name="txtblk"
                           Text="Rotate Text with Slider"
                           Grid.Row="1"
                           FontSize="48">
                    <TextBlock.RenderTransform>
                        <RotateTransform Angle="{Binding ElementName=slider, Path=Value}" />
                    </TextBlock.RenderTransform>
                </TextBlock>
            </Grid>
        </Border>
    </Grid>
</Window>

この XAML も、組み込みのスタイルを除けば WinRT XAML と同じになります。Angle プロパティへ、スライダーをバインディングしており、角度をスライダーで指定するので値を 0度から 360度に指定しています。それでは、実行結果を示します。
RotateTheText

書籍では、回転が左上角を中心点に行われることから、これを画面の中央にする方法を説明しています。そして、中心座標を周りを回転するアニメーションにした RotateAroundCenter プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Grid>
        <TextBlock x:Name="txtblk"
                   Text="Rotated Text"
                   FontSize="48"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Top">
            <TextBlock.RenderTransform>
                <RotateTransform x:Name="rotate" />
            </TextBlock.RenderTransform>
        </TextBlock>
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard RepeatBehavior="Forever">
                    <DoubleAnimation Storyboard.TargetName="rotate"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:2" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

この XAML は、組み込みスタイルを除けば、WinRT XAML と同じになります。そして、中心座標を指定するために RotateTransform の CenterX と CenterY プロパティをコードで設定していますので、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        Loaded += (sender, args) =>
        {
            rotate.CenterX = txtblk.ActualWidth / 2;
        };

        SizeChanged += (sender, args) =>
        {
            rotate.CenterY = args.NewSize.Height / 2;
        };
    }
}

コードは、WinRT XAML と同じで、Loaded イベントで CenterX を設定し、SizeChanged イベントで CenterY を設定しています。もちろん、実行結果も同じになります。
RotateAroundCenter

つまり、TextBlock は Window の中心座標を中心として回転するようになっています。

10.3(P426) ビジュアル フィードバック

本節では、ユーザーが操作した結果に対する反応を返すアニメーションを説明しています。Windows ストア アプリでは、ユーザーの操作に対してフィードバックを返すことが推奨され、ビジュアル フィードバックのガイドラインにまとめられています。フィードバックを返す例として、JiggleButtonDemo プロジェクトのカスタム コントロールである JiggleButton.xaml を示します。

<Button x:Class="JiggleButtonDemo.JiggleButton"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
        mc:Ignorable="d" 
        d:DesignHeight="300" d:DesignWidth="300"
        RenderTransformOrigin="0.5 0.5"
        Click="OnJiggleButtonClick">
    <Button.Resources>
        <Storyboard x:Key="jiggleAnimation">
            <DoubleAnimation Storyboard.TargetName="rotate"
                             Storyboard.TargetProperty="Angle"
                             From="0" To="10" Duration="0:0:0.33"
                             AutoReverse="True">
                <DoubleAnimation.EasingFunction>
                    <ElasticEase EasingMode="EaseIn" />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
        </Storyboard>
    </Button.Resources>
    <Button.RenderTransform>
        <RotateTransform x:Name="rotate" />
    </Button.RenderTransform>
</Button>

この XAML は、名前空間とデザイン用の属性(d: プレフィックス) という違いがありますが、その他のコードは WinRT XAML と同じになっています。そして、Click イベントによって RotateTransform を使ったアニメーションを開始しますので、JiggleButton.xaml.cs を示します。

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

namespace JiggleButtonDemo
{
    public partial class JiggleButton : Button
    {
        public JiggleButton()
        {
            InitializeComponent();
        }

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

名前空間を除けば、WinRT XAML と同じになります。このコードで、ボタンをクリックするとボタンが 10度の回転を行うことが理解できます。それでは、このカスタム コントロールを使用する MainWindow.xaml の抜粋を示します。

<Window ...
        xmlns:local="clr-namespace:JiggleButtonDemo"
        ... >
    <Grid>
        <local:JiggleButton Content="JiggleButton Demo"
                            FontSize="24"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center" />
    </Grid>
</Window>

もちろん、これまでに説明してきたように組み込みスタイルを除けば WinRT XAML と同じになりますから、実行結果も同じになります。
JiggleButtonDemo

書籍では、Button 派生クラスに対する注意事項を説明していますので、書籍も熟読してください。

10.4(P428) 平行移動

本節では、平行移動を行う TranslateTransform の説明をしています。最初にテキストに対してエンボス加工により「浮き出した状態」に加工し、エングレープ加工にっより「彫り込まれた状態」にする TextEffexts プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontFamily" Value="Times New Roman" />
            <Setter Property="FontSize" Value="192" />
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
    </Window.Resources>
    <Grid Background="Gray">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBlock Text="EMBOSS"
                   Foreground="White"
                   Grid.Row="0" />
        <TextBlock Text="EMBOSS"
                   Grid.Row="0"
                   Foreground="Gray" >
            <TextBlock.RenderTransform>
                <TranslateTransform X="-2" Y="-2" />
            </TextBlock.RenderTransform>
        </TextBlock>
        <TextBlock Text="ENGRAVE"
                   Foreground="White"
                   Grid.Row="1" />
        <TextBlock Text="ENGRAVE"
                   Grid.Row="1"
                   Foreground="Gray" >
            <TextBlock.RenderTransform>
                <TranslateTransform X="2" Y="2" />
            </TextBlock.RenderTransform>
        </TextBlock>
        <TextBlock Text="Drop Shadow"
                   Grid.Row="2"
                   Foreground="Black">
            <TextBlock.RenderTransform>
                <TranslateTransform X="6" Y="6" />
            </TextBlock.RenderTransform>
        </TextBlock>
        <TextBlock Text="Drop Shadow"
                   Foreground="White"
                   Grid.Row="2" />
    </Grid>
</Window>

このXAMLは、組み込みのスタイルを除けば同じになります。それでは、実行結果を示します。
TextEffects

今度は、1 ピクセルずつずらして視覚的な奥行きを表現する DepthText プロジェクトの MainWindow.xaml の抜粋を示します(MainWindow.xaml は Grid を定義しているだけになります)。

public partial class MainWindow : Window
{
    const int COUNT = 48;   // ~1/2 inch

    public MainWindow()
    {
        InitializeComponent();

        Grid grid = this.Content as Grid;
        Brush foreground = new SolidColorBrush(Colors.Black);
        Brush grayBrush = new SolidColorBrush(Colors.Gray);

        for (int i = 0; i < COUNT; i++)
        {
            bool firstOrLast = i == 0 || i == COUNT - 1;
            TextBlock txtblk = new TextBlock
            {
                Text = "DEPTH",
                FontSize = 192,
                FontWeight = FontWeights.Bold,
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center,
                RenderTransform = new TranslateTransform
                {
                    X = COUNT - i - 1,
                    Y = i - COUNT + 1,
                },
                Foreground = firstOrLast ? foreground : grayBrush
            };
            grid.Children.Add(txtblk);
        }
    }
}

このコードは、Forground 変数に設定する Brush を変更しただけで、WinRT XAML と同じになります。それでは、実行結果を示します。
DepthText

TranslateTransform の説明は書籍に記述されていますので、書籍の熟読をお願いします。

10.5(P432) TransformGroup

本節では、複数の座標変換を扱う TransformGroup の説明をしています。その説明として、RotateTransform の中心座標を指定する手法の 1つとして ImageRotate プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Grid>
        <Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg"
               Stretch="None"
               HorizontalAlignment="Center"
               VerticalAlignment="Center">
            <Image.RenderTransform>
                <TransformGroup>
                    <TranslateTransform X="-160" Y="-200" />
                    <RotateTransform x:Name="rotate" />
                    <TranslateTransform X="160" Y="200" />
                </TransformGroup>
            </Image.RenderTransform>
        </Image>
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard RepeatBehavior="Forever">
                    <DoubleAnimation Storyboard.TargetName="rotate"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:3">
                        <DoubleAnimation.EasingFunction>
                            <ElasticEase EasingMode="EaseInOut" />
                        </DoubleAnimation.EasingFunction>
                    </DoubleAnimation>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

この XAML は、組み込みのスタイルと EventTrigger を除けば WinRT XAML と同じになります。それでは、実行結果を示します。
ImageRotate

書籍では、このプロセスを分解して説明するために RorationCenterDemo プロジェクトを使用しています。それでは、RotationCenterDemo プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="Text" Value="Rotate around Center" />
            <Setter Property="FontSize" Value="48" />
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
    </Window.Resources>
    <Grid>
        <TextBlock Name="txtblk"
                   Foreground="#D0D0D0" />
        <TextBlock Foreground="#A0A0A0">
            <TextBlock.RenderTransform>
                <TranslateTransform x:Name="translateBack1" />
            </TextBlock.RenderTransform>
        </TextBlock>
        <TextBlock Foreground="#707070">
            <TextBlock.RenderTransform>
                <TransformGroup>
                    <TranslateTransform x:Name="translateBack2" />
                    <RotateTransform Angle="45" />
                </TransformGroup>
            </TextBlock.RenderTransform>
        </TextBlock>
        <TextBlock Foreground="Black">
            <TextBlock.RenderTransform>
                <TransformGroup>
                    <TranslateTransform x:Name="translateBack3" />
                    <RotateTransform Angle="45" />
                    <TranslateTransform x:Name="translate" />
                </TransformGroup>
            </TextBlock.RenderTransform>
        </TextBlock>
    </Grid>
</Window>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。今度は、TranslateTransform の X と Y プロパティを設定している、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        Loaded += (sender, args) =>
        {
            translateBack1.X =
            translateBack2.X =
            translateBack3.X = -(translate.X = txtblk.ActualWidth / 2);
            translateBack1.Y =
            translateBack2.Y =
            translateBack3.Y = -(translate.Y = txtblk.ActualHeight / 2);
        };
    }
}

もちろん、コードは WinRT XAML と同じになります。それでは、実行結果を示します。
RotationCenterDemo

この座標変換を理解できれば、ImageRotate プロジェクトの座標変換も理解できることでしょう。

今度は、座標変換とアニメーションを組み合わせてプロペラを動かす Propeller プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Grid>
        <Polygon Points="40   0,  60  0, 53 47, 
                        100  40, 100 60, 53 53, 
                         60 100, 40 100, 47 53, 
                          0  60,  0  40, 47 47"
                 Stroke="Black"
                 Fill="SteelBlue"
                 HorizontalAlignment="Center"
                 VerticalAlignment="Center"
                 RenderTransformOrigin="0.5 0.5">
            <Polygon.RenderTransform>
                <TransformGroup>
                    <RotateTransform x:Name="rotate1" />
                    <TranslateTransform X="300" />
                    <RotateTransform x:Name="rotate2" />
                </TransformGroup>
            </Polygon.RenderTransform>
        </Polygon>
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="rotate1"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:0.5"
                                     RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="rotate2"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:6"
                                     RepeatBehavior="Forever" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

この XAML も、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
Propeller

このアニメーションは、最初の DoubleAnimation によってプロペラを回転させて、次の DoubleAnimation によって半径300論理ピクセルの円を回転させるものになっています。なぜ、このようになるのかについては、書籍を熟読してください。

10.6(P438) 拡大縮小

本節では、拡大と縮小を行う ScaleTransform を説明しています。最初の例題として、第2章 の「2.8 ViewBox による拡大と縮小」と同じことを行う、OppositelyScaledText プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Grid>
        <TextBlock Text="Scaled Text"
                   FontSize="144"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   RenderTransformOrigin="0.5 0.5">
            <TextBlock.RenderTransform>
                <ScaleTransform x:Name="scale" />
            </TextBlock.RenderTransform>
        </TextBlock>
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="scale"
                                     Storyboard.TargetProperty="ScaleX"
                                     BeginTime="0:0:2"
                                     From="1" To="0.01" Duration="0:0:2"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="scale"
                                     Storyboard.TargetProperty="ScaleY"
                                     From="10" To="0.1" Duration="0:0:2"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
OppositelyScaledText

ScaleTransform の動作については、書籍に説明がありますので熟読をお願いします。今度は反射効果を実現するために ReflectedFadeOutImage プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg"
               HorizontalAlignment="Center" />
        <Grid RenderTransformOrigin="0 1"
              HorizontalAlignment="Center">
            <Grid.RenderTransform>
                <ScaleTransform ScaleY="-1" />
            </Grid.RenderTransform>
            <Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg" />
            <Rectangle>
                <Rectangle.Fill>
                    <LinearGradientBrush StartPoint="0 0" EndPoint="0 1" >
                        <GradientStop Offset="0" 
                            Color="Black" />
                        <GradientStop Offset="1" Color="Transparent" />
                    </LinearGradientBrush>
                </Rectangle.Fill>
            </Rectangle>
        </Grid>
    </Grid>
</Window>

このコードは、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
ReflectedFadeOutImage

反射の Image 要素の Source 属性には、同じ値を指定していますが、望ましいのはバインディング記述を使うことでしょう。書籍でも少し話題として記述されていますが、WPF XAML であれば VisualBrush を使うことで同じ効果を簡単に得ることができます。

<Image Source="http://www.charlespetzold.com/pw6/PetzoldJersey.jpg"
       x:Name="ReflectedVisual" HorizontalAlignment="Center" />
<Rectangle Height="{Binding ElementName=ReflectedVisual, Path=ActualHeight}"
           Width="{Binding ElementName=ReflectedVisual, Path=ActualWidth}"
           Grid.Row="1"
           HorizontalAlignment="Center">
    <Rectangle.Fill>
        <VisualBrush Opacity="0.75" Stretch="None"
                     Visual="{Binding ElementName=ReflectedVisual}">
            <VisualBrush.RelativeTransform>
                <TransformGroup>
                    <ScaleTransform ScaleX="1" ScaleY="-1" />
                    <TranslateTransform  Y="1" />
                </TransformGroup>
            </VisualBrush.RelativeTransform>
        </VisualBrush>
    </Rectangle.Fill>
    <Rectangle.OpacityMask>
        <LinearGradientBrush StartPoint="0 0" EndPoint="0 1" >
            <GradientStop Offset="0" 
                    Color="Black" />
            <GradientStop Offset="1" Color="Transparent" />
        </LinearGradientBrush>
    </Rectangle.OpacityMask>
</Rectangle>

このコードは、Rectangleの塗りつぶしに VisualBrush を使い、RelativeTransform に ScaleTransform と TranslateTransform を用いることで実現しています。WinRT XAML では、RelativeTransform には 1 つの Transform オブジェクトしか指定できないので、書籍で説明した手法となっています。この手法が書籍と異なるのは、反射した画像の下側の色が白色に薄くなることです。これは、書籍が背景がダークのテーマを利用しているためになります。今回の WPF XAML のように背景色が白であれば、Color に 「Black」ではなく「White」を指定するのが正常な考え方になります。

10.7(P441) アナログ時計の作成

本節では、ここまでに説明してきた座標変換とアニメーションを使ってアナログ時計を作成する手法を説明しています。どのような過程を経て作成されたかについては、書籍を熟読してください。それでは、AnalogClock プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Window.Resources>
        <Style TargetType="Path">
            <Setter Property="Stroke" Value="Black" />
            <Setter Property="StrokeThickness" Value="2" />
            <Setter Property="StrokeStartLineCap" Value="Round" />
            <Setter Property="StrokeEndLineCap" Value="Round" />
            <Setter Property="StrokeLineJoin" Value="Round" />
            <Setter Property="StrokeDashCap" Value="Round" />
            <Setter Property="Fill" Value="Blue" />
        </Style>
    </Window.Resources>
    <Grid>
        <Viewbox>
            <!-- Grid containing all graphics based on (0, 0) origin, 100-pixel radius -->
            <Grid Width="200" Height="200">
                <!-- Transform for entire clock -->
                <Grid.RenderTransform>
                    <TranslateTransform X="100" Y="100" />
                </Grid.RenderTransform>
                <!-- Small tick marks -->
                <Path Fill="{x:Null}"
                      StrokeThickness="3"
                      StrokeDashArray="0 3.14159">
                    <Path.Data>
                        <EllipseGeometry RadiusX="90" RadiusY="90" />
                    </Path.Data>
                </Path>
                <!-- Large tick marks -->
                <Path Fill="{x:Null}"
                      StrokeThickness="6"
                      StrokeDashArray="0 7.854">
                    <Path.Data>
                        <EllipseGeometry RadiusX="90" RadiusY="90" />
                    </Path.Data>
                </Path>
                <!-- Hour hand pointing straight up -->
                <Path Data="M 0 -60 C 0 -30, 20 -30, 5 -20 L 5 0
                                    C 5 7.5, -5 7.5, -5 0 L -5 -20
                                    C -20 -30, 0 -30, 0 -60">
                    <Path.RenderTransform>
                        <RotateTransform x:Name="rotateHour" />
                    </Path.RenderTransform>
                </Path>
                <!-- Minute hand pointing straight up -->
                <Path Data="M 0 -80 C 0 -75, 0 -70, 2.5 -60 L 2.5 0
                                    C 2.5 5, -2.5 5, -2.5 0 L -2.55 -60
                                    C 0 -70, 0 -75, 0 -80">
                    <Path.RenderTransform>
                        <RotateTransform x:Name="rotateMinute" />
                    </Path.RenderTransform>
                </Path>
                <!-- Second hand pointing straight up -->
                <Path Data="M 0 10 L 0 -80">
                    <Path.RenderTransform>
                        <RotateTransform x:Name="rotateSecond" />
                    </Path.RenderTransform>
                </Path>
            </Grid>
        </Viewbox>
    </Grid>
</Window>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。そして、分離コードで時計回りの角度を計算していますので、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        CompositionTarget.Rendering += OnCompositionTargetRendering;
    }

    void OnCompositionTargetRendering(object sender, object args)
    {
        DateTime dt = DateTime.Now;
        rotateSecond.Angle = 6 * (dt.Second + dt.Millisecond / 1000.0);
        rotateMinute.Angle = 6 * dt.Minute + rotateSecond.Angle / 60;
        rotateHour.Angle = 30 * (dt.Hour % 12) + rotateMinute.Angle / 12;
    }
}

コードは、WinRT XAML と同じになります。それでは、実行結果を示します。
AnalogClock

書籍では、秒針の動きをどのようにするかということについての説明がありますので、興味があれば熟読をお願いします。

10.8(P447) 傾斜

本節では、傾斜(Skew)と呼ばれる座標変換を説明しています。この具体例として、SkewTransform の AngleX と AngleY プロパティを変化させる SlewPlusSkew プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Grid>
        <TextBlock Text="SKEW"
                   FontSize="288"
                   FontWeight="Bold"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   RenderTransformOrigin="0.5 0.5">
            <TextBlock.RenderTransform>
                <SkewTransform x:Name="skew" />
            </TextBlock.RenderTransform>
        </TextBlock>
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard SpeedRatio="0.5" RepeatBehavior="Forever">
                    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="skew"
                                                   Storyboard.TargetProperty="AngleX">
                        <!-- Back and forth for 4 seconds -->
                        <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" />
                        <LinearDoubleKeyFrame KeyTime="0:0:1" Value="90" />
                        <LinearDoubleKeyFrame KeyTime="0:0:2" Value="0" />
                        <LinearDoubleKeyFrame KeyTime="0:0:3" Value="-90" />
                        <LinearDoubleKeyFrame KeyTime="0:0:4" Value="0" />
                        <!-- Do nothing for 4 seconds -->
                        <DiscreteDoubleKeyFrame KeyTime="0:0:8" Value="0" />
                        <!-- Back and forth for 4 seconds -->
                        <LinearDoubleKeyFrame KeyTime="0:0:9" Value="90" />
                        <LinearDoubleKeyFrame KeyTime="0:0:10" Value="0" />
                        <LinearDoubleKeyFrame KeyTime="0:0:11" Value="-90" />
                        <LinearDoubleKeyFrame KeyTime="0:0:12" Value="0" />
                    </DoubleAnimationUsingKeyFrames>
                    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="skew"
                                                   Storyboard.TargetProperty="AngleY">
                        <!-- Do nothing for 4 seconds -->
                        <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" />
                        <DiscreteDoubleKeyFrame KeyTime="0:0:4" Value="0" />
                        <!-- Back and forth for 4 seconds -->
                        <LinearDoubleKeyFrame KeyTime="0:0:5" Value="-90" />
                        <LinearDoubleKeyFrame KeyTime="0:0:6" Value="0" />
                        <LinearDoubleKeyFrame KeyTime="0:0:7" Value="90" />
                        <LinearDoubleKeyFrame KeyTime="0:0:8" Value="0" />
                        <!-- Back and forth for 4 seconds -->
                        <LinearDoubleKeyFrame KeyTime="0:0:9" Value="-90" />
                        <LinearDoubleKeyFrame KeyTime="0:0:10" Value="0" />
                        <LinearDoubleKeyFrame KeyTime="0:0:11" Value="90" />
                        <LinearDoubleKeyFrame KeyTime="0:0:12" Value="0" />
                    </DoubleAnimationUsingKeyFrames>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

この XAML は、これまでに説明してきたように組み込みのスタイルと EventTrigger を除けば WinRT XAML と同じになります。それでは、実行結果を示します。
SkewPlusSkew

10.9(P450) 派手な登場

本節では、ページが表示される時に要素を登場させるアニメーションの実現方法を説明しています。この目的のために書籍では概要を説明して、SkewSlideInText プロジェクト を使用します。それでは、MainWindow.xaml の抜粋を示します。

<Window ... >
    <Grid>
        <TextBlock Text="Hello!"
                   FontSize="192"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   RenderTransformOrigin="0.5 1">
            <TextBlock.RenderTransform>
                <TransformGroup>
                    <SkewTransform x:Name="skew" />
                    <TranslateTransform x:Name="translate" />
                </TransformGroup>
            </TextBlock.RenderTransform>
        </TextBlock>
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="translate"
                                     Storyboard.TargetProperty="X"
                                     From="-1000" Duration="0:0:1" />
                    <DoubleAnimationUsingKeyFrames
                                     Storyboard.TargetName="skew"
                                     Storyboard.TargetProperty="AngleX">
                        <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="15" />
                        <LinearDoubleKeyFrame KeyTime="0:0:1" Value="30" />
                        <EasingDoubleKeyFrame KeyTime="0:0:1.5" Value="0">
                            <EasingDoubleKeyFrame.EasingFunction>
                                <ElasticEase />
                            </EasingDoubleKeyFrame.EasingFunction>
                        </EasingDoubleKeyFrame>
                    </DoubleAnimationUsingKeyFrames>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

この XAML は、組み込みのスタイルと EventTrigger を除けば WinRT XAML と同じになります。ストリーボードを見れば理解できますが、左側(-1000)から右側へ平行移動した後に SkewTransform とイージング関数によって跳ね返るような効果を与えています。それでは、実行結果を示します。
SkewSlideInText

書籍では、この平行移動の詳細な説明がありますので熟読をお願いします。

10.10(P451) 座標変換と数学

本節では、座標変換を行う Transform オブジェクトと数学の関係を説明しています。その中で、ScaleTransform、TranslateTransform、RotateTransform がどのような式で表現できるかと Matrix 構造体を解説しています。そして、何種類かの回転を行う RenderTransform を説明するために CommonMatrixTransforms プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="24" />
            <Setter Property="RenderTransformOrigin" Value="0 0.5" />
        </Style>
    </Window.Resources>
    <Grid>
        <!-- Move origin to center -->
        <Canvas HorizontalAlignment="Center"
                VerticalAlignment="Center">
            <TextBlock Text="  RenderTransform='1 0 0 1 0 0'" 
                       RenderTransform="1 0 0 1 0 0" />
            <TextBlock Text="  RenderTransform='.7 .7 -.7 .7 0 0'"
                       RenderTransform=".7 .7 -.7 .7 0 0" />
            <TextBlock Text="  RenderTransform='0 1 -1 0 0 0'"
                       RenderTransform="0 1 -1 0 0 0" />
            <TextBlock Text="  RenderTransform='-.7 .7 -.7 -.7 0 0"
                       RenderTransform="-.7 .7 -.7 -.7 0 0" />
            <TextBlock Text="  RenderTransform='-1 0 0 -1 0 0'" 
                       RenderTransform="-1 0 0 -1 0 0" />
            <TextBlock Text="  RenderTransform='-.7 -.7 .7 -.7 0 0'"
                       RenderTransform="-.7 -.7 .7 -.7 0 0" />
            <TextBlock Text="  RenderTransform='0 -1 1 0 0 0'"
                       RenderTransform="0 -1 1 0 0 0" />
            <TextBlock Text="  RenderTransform='.7 -.7 .7 .7 0 0"
                       RenderTransform=".7 -.7 .7 .7 0 0" />
        </Canvas>
    </Grid>
</Window>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
CommonMatrixTransforms

書籍では、コードで操作することに関して説明がありますので、興味があれば熟読をお願いします。

10.11(P460) 複合変換

本節では、座標変換を組み合わせる場合の注意事項などを説明しています。この説明として、CompositeTransform を利用しています。CompositeTransform は WinRT XAML 固有であり、WPF XAML では TransformGroup を用いることになります。それでは、TitledShadow プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="Text" Value="quirky" />
            <Setter Property="FontFamily" Value="Times New Roman" />
            <Setter Property="FontSize" Value="192" />
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
    </Window.Resources>
    <Grid>
        <!-- Shadow TextBlock -->
        <TextBlock Foreground="Gray"
                   RenderTransformOrigin="0 1">
            <TextBlock.RenderTransform>
                <TransformGroup>
                    <ScaleTransform ScaleY="1.5" />
                    <SkewTransform AngleX="-60" />
                </TransformGroup>
            </TextBlock.RenderTransform>
        </TextBlock>
        <!-- TextBlock with all styled properties -->
        <TextBlock />
    </Grid>
</Window>

この XAML は、組み込みのスタイルと説明した CompositeTransform を除けば WinRT XAML と同じになります。CompositeTransform を TransformGroup に置き換えていますから、ScaleTransform と SkewTransform を組み合わせています。この理由は、CompositeTransform で ScaleY と SkewX プロパティを組み合わせていたからです。それでは、実行結果を示します。
TiltedShadow

書籍では、この変換の効果をデセンダーのない状態にするにはどうしたら良いかを説明しています。興味があれば、熟読をお願いします。

10.12(P463) ジオメトリの変換

本節では、Geometry クラスに対して変換を適用することを説明しています。ここで説明している内容は、そのまま WPF XAML に適用できますので、書籍の熟読をお願いします。

10.13(P464) ブラシの変換

本節では、Brush クラスに対して変換を適用することを説明しています。この説明のために第3章の RaibowEight プロジェクトを用いて、RaibowEightTransform プロジェクトの Main Window.xaml の抜粋を示します。

<Window ... >
    <Grid>
        <Viewbox>
            <Path StrokeThickness="50"
                  Margin="0 25 0 0">
                <Path.Data>
                    <PathGeometry>
                        <PathFigure StartPoint="110 0">
                            <ArcSegment Size="90 90" Point="110 180" 
                                        SweepDirection="Clockwise" />
                            <ArcSegment Size="110 110" Point="110 400" 
                                        SweepDirection="Counterclockwise" />
                            <ArcSegment Size="110 110" Point="110 180" 
                                        SweepDirection="Counterclockwise" />
                            <ArcSegment Size="90 90" Point="110 0" 
                                        SweepDirection="Clockwise" />
                        </PathFigure>
                    </PathGeometry>
                </Path.Data>
                <Path.Stroke>
                    <LinearGradientBrush StartPoint="0 0" EndPoint="1 1"
                                         SpreadMethod="Repeat">
                        <LinearGradientBrush.RelativeTransform>
                            <TranslateTransform x:Name="translate" />
                        </LinearGradientBrush.RelativeTransform>

                        <GradientStop Offset="0.00" Color="Red" />
                        <GradientStop Offset="0.14" Color="Orange" />
                        <GradientStop Offset="0.28" Color="Yellow" />
                        <GradientStop Offset="0.43" Color="Green" />
                        <GradientStop Offset="0.57" Color="Blue" />
                        <GradientStop Offset="0.71" Color="Indigo" />
                        <GradientStop Offset="0.86" Color="Violet" />
                        <GradientStop Offset="1.00" Color="Red" />
                    </LinearGradientBrush>
                </Path.Stroke>
            </Path>
        </Viewbox>
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="translate"
                                     Storyboard.TargetProperty="Y"
                                     From="0" To="-1.36" Duration="0:0:10"
                                     RepeatBehavior="Forever" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

この XAML は、組み込みのスタイルと EventTrigger、EnableDependentAnimation を除けば WinRT XAML と同じになります。それでは、実行結果を示します。
RainbowEightTransform

書籍では、Path 要素やグラデーションなどのトリックとも言える方法を解説しています。実際に動かしてみると虹色がアニメーションで動いているのが分かりますから、なぜ?という疑問を持たれたなら、書籍を熟読してください。

10.14(P469) 要素の配置先の取得

本節では、前節の RainbowEightTransform プロジェクトの説明に関係して Matrix の計算結果を取得することを説明しています。この説明を使って、CompoiteTransform のプロパティをアニメーション化するサンプルとして、WheresMyElement プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ... >
    <Grid x:Name="contentGrid">
        <TextBlock x:Name="txtblk"
                   Text="Tap to Find"
                   FontSize="96"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   RenderTransformOrigin="0.5 0.5">
            <TextBlock.RenderTransform>
                <TransformGroup>
                    <TranslateTransform x:Name="translateTransform" />
                    <RotateTransform x:Name="rotateTransform" />
                    <ScaleTransform x:Name="sclaeTransform" />
                    <SkewTransform x:Name="skewTransform" />
                </TransformGroup>
                <!--<CompositeTransform x:Name="transform" />-->
            </TextBlock.RenderTransform>
        </TextBlock>
        <Polygon x:Name="polygon" Stroke="Blue" />
        <Path x:Name="path" Stroke="Red" />
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard x:Name="storyboard">
                    <DoubleAnimation Storyboard.TargetName="translateTransform"
                                     Storyboard.TargetProperty="X"
                                     From="-300" To="300" Duration="0:0:2.11"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="translateTransform"
                                     Storyboard.TargetProperty="Y"
                                     From="-300" To="300" Duration="0:0:2.23"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="rotateTransform"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:2.51"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="sclaeTransform"
                                     Storyboard.TargetProperty="ScaleX"
                                     From="1" To="2" Duration="0:0:2.77"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="sclaeTransform"
                                     Storyboard.TargetProperty="ScaleY"
                                     From="1" To="2" Duration="0:0:3.07"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="skewTransform"
                                     Storyboard.TargetProperty="AngleX"
                                     From="-30" To="30" Duration="0:0:3.31"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="skewTransform"
                                     Storyboard.TargetProperty="AngleY"
                                     From="-30" To="30" Duration="0:0:3.53"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

この XAML は、組み込みのスタイルと EventTrigger、CompositeTransform、CompositeTransform に伴う DoubleAnimation の変更を除けば WinRT XAML と同じになります。すでに説明したように CompositeTransformは、TransformGroup へ置き換えて、TranslateTransform、RotateTransform、ScaleTransform、SkewTransform を組み合わせています。この変更に伴って、DoubleAnimation の TagetName 添付プロパティを変更しています。今度は、MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    bool storyboardPaused = false;

    public MainWindow()
    {
        InitializeComponent();
    }

    protected override void OnTouchDown(TouchEventArgs e)
    {
        if (storyboardPaused)
        {
            storyboard.Resume(this);
            storyboardPaused = false;
            return;
        }
        GeneralTransform xform = txtblk.TransformToVisual(contentGrid);
        // Draw blue polygon around element
        polygon.Points.Clear();
        polygon.Points.Add(xform.Transform(new Point(0, 0)));
        polygon.Points.Add(xform.Transform(new Point(txtblk.ActualWidth, 0)));
        polygon.Points.Add(xform.Transform(new Point(txtblk.ActualWidth, txtblk.ActualHeight)));
        polygon.Points.Add(xform.Transform(new Point(0, txtblk.ActualHeight)));
        // Draw red bounding box
        path.Data = new RectangleGeometry
        {
            Rect = xform.TransformBounds(new Rect(new Point(0, 0), txtblk.DesiredSize))
        };
        storyboard.Pause(this);
        storyboardPaused = true;
        base.OnTouchEnter(e);
    }
}

このコードは、OnTapped イベントを書き換えただけになります。また、GeneralTransform の TransformPoint メソッドを Transform メソッドに変更しています。これは、TransformPoint メソッドが WinRT 固有だからです。これ以外は、WinRT XAML と同じになります。記述しているコードを読めば理解できると思いますが、タッチした時に TextBlock の周囲を Polygon で囲むことです。それでは、実行結果を示します。
WheresMyElement

書籍には、TransformVisual メソッドなどの説明がありますので熟読をお願いします。

10.15(P472) 投影変換

本節では、3 次元グラフィックス変換をサポートするための Matrix3D などを説明しています。WPF XAML は 3D グラフィックスをサポートしていますから、Matrix3D クラスを System.Windows.Media.Media3D 名前空間で定義しています。そして、説明として使用しているのが、ThreeDeepSpinningText プロジェクトと TapToFlip プロジェクト がありますが、両方とも Projection を使用しています。10.1 で説明したように、WPF XAML は Projection という疑似 3D 効果をサポートしていませんので、この節のサンプルは興味があれば 3D グラフィックスを使って、自分で移植を試みてください。そして、できれば結果を公開してください。

10.16(P480) Matirx3D の拡張

本節では、Matrix3D 構造体を使って様々な座標変換を説明しています。この説明のために、NonAffineStretch プロジェクトを Projection と Matrix3DProjection を使って説明しています。WPF XAML は 3D オブジェクトをサポートしますから、TranslateTransform3D などの様々な変換も提供しています。その代わり、Projection のような疑似 3D 効果をサポートしていません。つまり、本節も前節と同じで、興味があれば自分で移植を試みてください。

座標変換に関しては、細かな違いを除けば 3D オブジェクトに対するサポートが WinRT XAML と WPF XAML では異なります。大事なことは、3D オブジェトを含めてユーザー エクスぺリエンスを高めるために統一的に扱えるようになっていることです。WPF XAML で 3D オブジェクトをバリバリに使って下さいと言うつもりはありません。むしろ、3D オブジェクトをバリバリに使いたいのであれば、WPF よりも Direct3D を使った方が良いでしょう。WinRT XAML は、Silverlight の経験を生かして WPF XAML の 3D オブジェクト サポートではなく疑似 3D 効果のサポートだけにしているのも、ユーザー エクスぺリエンスを高めるために効果的だと判断したからでしょう。つまり、アニメーションや座標変換もユーザー エクスぺリエンスを高めるために使用するのが大切なことなのです。
ここまで説明してきた違いを意識しながら( 3D 効果を除く)、第10章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。

ch10.zip

Comments (0)

Skip to main content