目次

キーワード

概要

XAML とプログラムコード(WPF)」では、 x:Code タグかコードビハインド中にイベントハンドラを記述することで、 イベント処理を行っていました。 これとは別に、イベントトリガやストーリーボードという仕組みを使って、 (コードを含まない)XAML だけでもかなり多彩なイベント処理が可能です。

おさらい

本題のアニメーションの話に入る前に、 「スタイル」とか「メディア」辺りの話を復習。

Style

複数の要素に一律同じ見た目を適用したい場合、 スタイルというものを使います。

<WrapPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="200" Height="200" Background="#cccccc">

  <WrapPanel.Resources>
    <Style TargetType="{x:Type Rectangle}">
      <Setter Property="Width" Value="80"/>
      <Setter Property="Height" Value="80"/>
      <Setter Property="Margin" Value="10"/>
      <Setter Property="Fill" Value="#8080ff"/>
    </Style>
  </WrapPanel.Resources>

  <Rectangle />
  <Rectangle />
  <Rectangle />
  <Rectangle />

</WrapPanel>

Brush

Shapes なら Fill 属性、 Controls なら Background 属性で、 背景色を指定できるわけですが (参考: 「図形」、 「コントロール」)、 これらには Brush を指定します。

Brush には、 単色塗りつぶしの SolidColorBrush、 グラデーションをかける LinearGradientBrush, RadialGradientBrush などがあります。 その他にも、 背景に画像や図形などのパターンを表示する ImageBrush や DrawingBrush などもあります。

ここでは、主に SolidColorBrush, LinearGradientBrush, RadialGradientBrush を使って説明するので、 この3つに関して例を挙げておきます。

<WrapPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="200" Height="200" Background="#cccccc">

  <WrapPanel.Resources>
    <Style TargetType="{x:Type Rectangle}">
      <Setter Property="Width" Value="90"/>
      <Setter Property="Height" Value="90"/>
      <Setter Property="Margin" Value="5"/>
    </Style>
  </WrapPanel.Resources>

  <!-- 単色塗りつぶし -->
  <Rectangle Fill="MistyRose"/>

  <!-- ↑を Property Element Syntax で書いたもの -->
  <Rectangle>
    <Rectangle.Fill>
      <SolidColorBrush Color="MistyRose"/>
    </Rectangle.Fill>
  </Rectangle>

  <!-- 放射状グラデーション(白黒) -->
  <Rectangle>
    <Rectangle.Fill>
      <RadialGradientBrush>
        <GradientStop Color="#ffffff" Offset="0" />
        <GradientStop Color="#000000" Offset="1" />
      </RadialGradientBrush>
    </Rectangle.Fill>
  </Rectangle>

  <!-- 線形グラデーション(虹色) -->
  <Rectangle>
    <Rectangle.Fill>
      <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
        <GradientStop Color="#ff8080" Offset="0" />
        <GradientStop Color="#ffc080" Offset="0.125" />
        <GradientStop Color="#ffff80" Offset="0.25" />
        <GradientStop Color="#c0ff80" Offset="0.375" />
        <GradientStop Color="#80ff80" Offset="0.5" />
        <GradientStop Color="#80ffc0" Offset="0.625" />
        <GradientStop Color="#80ffff" Offset="0.75" />
        <GradientStop Color="#80c0ff" Offset="0.875" />
        <GradientStop Color="#8080ff" Offset="1" />
      </LinearGradientBrush>
    </Rectangle.Fill>
  </Rectangle>

</WrapPanel>

Transform

メディア」で説明したように、 WPF の GUI 要素は、 RenderTransform 属性によって、 拡大・回転などの変形を施すことができます。

x 軸, y 軸方向の拡大を表す ScaleTransform、 軸沿いに斜めに崩すような SkewTransform、 回転を表す RotateTransform、 平行移動を表す TranslateTransform などがあります。 また、MatrixTransform では、行列を使った線形変換もできます (回転・拡大などと行列の関係は、 「固有値」を参照)。

さらに、TransformGroup を使って複数の変形を一度にかけることもできます。

<WrapPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="200" Height="200" Background="#cccccc">

  <WrapPanel.Resources>
    <Style TargetType="{x:Type Rectangle}">
      <Setter Property="Width" Value="80"/>
      <Setter Property="Height" Value="80"/>
      <Setter Property="Margin" Value="10"/>
      <Setter Property="Fill" Value="#8080ff"/>
    </Style>
  </WrapPanel.Resources>

  <!-- 縦横拡大 -->
  <Rectangle>
    <Rectangle.RenderTransform>
      <ScaleTransform CenterX="50" CenterY="50" ScaleX="0.5" ScaleY="0.5"/>
    </Rectangle.RenderTransform>
  </Rectangle>

  <!-- 回転 -->
  <Rectangle>
    <Rectangle.RenderTransform>
      <RotateTransform CenterX="50" CenterY="50" Angle="10"/>
    </Rectangle.RenderTransform>
  </Rectangle>

  <!-- 拡大+傾斜+回転 -->
  <Rectangle>
    <Rectangle.RenderTransform>
      <TransformGroup>
        <ScaleTransform CenterX="0" CenterY="50" ScaleX="1.5" ScaleY="0.5"/>
        <SkewTransform CenterX="100" CenterY="100" AngleX="-20"/>
        <RotateTransform CenterX="50" CenterY="50" Angle="10"/>
      </TransformGroup>
    </Rectangle.RenderTransform>
  </Rectangle>

</WrapPanel>

アニメーション

WPF のアニメーションには、 大まかに分けて以下の三つのものがあります。

  • 「イベントトリガ + ストーリーボード」を XAML 中に書く

  • コードビハインド中で、BeginAnimation メソッドを呼び出す

  • CompositionTarget.Rendering イベントを使う

このうち、ここでは、 XAML だけで書くことのできるイベントリガ + ストーリーボードを中心に説明したいと思います。

XAML のアニメーションはいろいろ複雑ではあるんですが、 概ね、以下の3点を把握すれば大丈夫だと思います。

  1. イベントトリガ … 処理を始めるきっかけ。 「イベントが発生した」とか。

  2. トリガアクション … 処理の内容。 「アニメーションを開始・一時停止・再開する」など。

  3. ストーリーボード … アニメーションの具体的な中身。 コントロールや図形などの変化のさせ方の台本。

イベントトリガ

トリガというのは、 「プロパティの値が変わった瞬間」とか、 「イベントが発生した瞬間」とかの、 処理を始めるきっかけのことです。

WPF の FrameworkElement(コントロールも図形も、大半、この FrameworkElement のサブクラス)には、 Triggers という名前のプロパティがあります。 この Triggers に対して、 Trigger または EventTrigger を子要素として追加することで、 トリガの設定ができます。

「プロパティの値が変わった瞬間」に処理を行うのが Trigger なんですが、 こちらはいまいちできることが限られるので、ここでは説明を割愛。

「イベントが発生した瞬間」に処理を行うのが EventTrigger です。 例えば、「表示された瞬間からアニメーションを開始」(Loaded イベントをトリガにする)ということをしたければ、 以下のようにします。

<WrapPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="200" Height="200" Background="#cccccc">

  <WrapPanel.Resources>
    <Style TargetType="{x:Type Rectangle}">
      <Setter Property="Width" Value="50"/>
      <Setter Property="Height" Value="50"/>
      <Setter Property="Margin" Value="25"/>
      <Setter Property="Fill" Value="#8080ff"/>
    </Style>
  </WrapPanel.Resources>

  <Rectangle>
    <Rectangle.Triggers>
      <EventTrigger  RoutedEvent="Rectangle.Loaded">
        <BeginStoryboard>
          <Storyboard>
            <DoubleAnimation
              Storyboard.TargetProperty="Opacity"
              From="1" To="0.2"
              RepeatBehavior="Forever"
              AutoReverse="true"
              Duration="0:0:1"
              />
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Rectangle.Triggers>
  </Rectangle>

</WrapPanel>

EventTrigger の子要素 BeginStoryboard に関しては後ほど説明します。 (この例では、表示された瞬間からずっと、 四角形の透明度が薄くなったり濃くなったり点滅し続けます。)

ちなみに、Trigger, EventTrigger の他にも、 Binding で設定した値をトリガにする DataTrigger や、 複数の条件がそろったときに初めてトリガする MultiTrigger などもあります。

スタイル中のイベントトリガ

イベントトリガはスタイル中にも記述できます。

例えば、以下のようにすると、 全ての四角形が同じように点滅し始めます。

<WrapPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="200" Height="200" Background="#cccccc">

  <WrapPanel.Resources>
    <Style TargetType="{x:Type Rectangle}">
      <Setter Property="Width" Value="50"/>
      <Setter Property="Height" Value="50"/>
      <Setter Property="Margin" Value="25"/>
      <Setter Property="Fill" Value="#8080ff"/>

      <Style.Triggers>
        <EventTrigger  RoutedEvent="Rectangle.Loaded">
          <BeginStoryboard>
            <Storyboard>
              <DoubleAnimation
                Storyboard.TargetProperty="Opacity"
                From="1" To="0.2" Duration="0:0:1"
                RepeatBehavior="Forever" AutoReverse="true" />
            </Storyboard>
          </BeginStoryboard>
        </EventTrigger>
      </Style.Triggers>
    </Style>
  </WrapPanel.Resources>

  <Rectangle/>
  <Rectangle/>
  <Rectangle/>
  <Rectangle/>

</WrapPanel>

トリガアクション

「処理開始のきっかけ」である EventTrigger の中身には、 「処理の内容」である TriggerAction というものを指定します。

TriggerAction には、音声データの再生(SoundPlayerAction)などもありますが、 ここでは、 ストーリーボードがらみのものを中心に説明します。

詳しくは次節で説明しますが、 XAML では、ストーリーボードというものを使ってアニメーションを行います。 で、TriggerAction としては、 ストーリーボードの開始(BeginStoryBoard)、 停止(StopStoryBoard)、 一時停止(PauseStoryBoard)、 再開(ResumeStoryBoard)などがあります。

例えば、 以下のようにすると、 表示と同時に点滅を開始して、 マウスが上に乗った瞬間に点滅を一時停止、 マウスが離れた瞬間に点滅を再開できます。

<WrapPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="200" Height="200" Background="#cccccc">

  <WrapPanel.Resources>
    <Style TargetType="{x:Type Rectangle}">
      <Setter Property="Width" Value="50"/>
      <Setter Property="Height" Value="50"/>
      <Setter Property="Margin" Value="25"/>
      <Setter Property="Fill" Value="#8080ff"/>
    </Style>
  </WrapPanel.Resources>

  <Rectangle>
    <Rectangle.Triggers>

      <EventTrigger RoutedEvent="Rectangle.Loaded">
        <BeginStoryboard Name="BlinkBegin">
          <Storyboard>
            <DoubleAnimation
              Storyboard.TargetProperty="Opacity"
              From="1" To="0.2" Duration="0:0:1"
              RepeatBehavior="Forever" AutoReverse="true" />
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>

      <EventTrigger RoutedEvent="Mouse.MouseEnter">
        <PauseStoryboard BeginStoryboardName="BlinkBegin"/>
      </EventTrigger>

      <EventTrigger RoutedEvent="Mouse.MouseLeave">
        <ResumeStoryboard BeginStoryboardName="BlinkBegin"/>
      </EventTrigger>

    </Rectangle.Triggers>
  </Rectangle>

</WrapPanel>

ストーリーボード

さて、ようやくアニメーション本体であるストーリーボードの話になります。 ちなみに、ストーリーボード(story board)という単語は、 映画やアニメの画コンテ・絵コンテのことです。 要するに、いつ、何を動かすかとか、アニメーションの脚本を描く物。

XAML のストーリーボード(Storyboard)ですが、 DoubleAnimation や ColorAnimation という子要素を複数並べて、 いつ、何の値を、どう変化させるかを指定します。

具体的な説明のために、 先ほどまでにたびたび例示してきた「透明・不透明の点滅」のストーリーボードの部分を抜き出してきてみましょう。

<Storyboard>
  <DoubleAnimation
    Storyboard.TargetProperty="Opacity"
    From="1" To="0.2" Duration="0:0:1"
    RepeatBehavior="Forever" AutoReverse="true" />
</Storyboard>

DoubleAnimation というのは、名前どおり、 double 型の値を変化させるものです。 DoubleAnimation の他にも、「型名 + Animation」という名前のクラスがいくつかあって、 いずれもその型の値を変化させるためのものです。 (例えば、ColorAnimation、CharAnimation、PointAnimation などがあります。)

何の値を変えるかは Storyboard.TargetProperty 属性で指定します。 この例の場合、Rectangle の中にこのストーリーボードが書かれているので、 Rectangle の Opacity(透明度)プロパティの値が変化します。

値を何から何に変化させるかは、From, To 属性で指定します。 「どこからどこまで」ではなくて、 「変化量」を指定したい場合には By 属性を使います。

From から To の値まで、どのくらいの時間かけて変化させるかは Duration で指定します。 Duration の中身には、この例の場合「0:0:1」と書かれていますが、 これは「0時間0分1秒」という意味です。 要するに、「時:分:秒」という形式で指定します。

その他、この例では、To の値に達した後、逆に From の値に戻るのかどうかを表す AutoReverse="true" と、 その後さらに、永久ループするかどうかを表す RepeatBehavior="Forever" が指定されています。

また、この例の場合、 イベントがトリガされた瞬間からアニメーションを開始しているので省略されていますが、 開始時間を遅らせたい場合、 BeginTime 属性を指定します。

TargetName

上記の場合、 Rectangle 内でトリガしたイベント内で、 Rectangle のプロパティの値を変更していますが、 「ボタンを押したときに Rectangle の背景色を変える」というように、 トリガ主とアニメーションのターゲットを別にすることもできます。 これには、以下のように、Storyboard.TargetName 属性を使います。

<WrapPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="200" Height="200" Background="#cccccc">

  <WrapPanel.Resources>
    <Style TargetType="{x:Type Rectangle}">
      <Setter Property="Width" Value="50"/>
      <Setter Property="Height" Value="50"/>
      <Setter Property="Margin" Value="25"/>
      <Setter Property="Fill" Value="#8080ff"/>
    </Style>
  </WrapPanel.Resources>

  <Rectangle Name="rect"/>

  <Button Content="Click Me" Width="80" Height="30">
    <Button.Triggers>
      <EventTrigger RoutedEvent="Button.Click">
        <BeginStoryboard>
          <Storyboard>
            <ColorAnimation
              Storyboard.TargetName="rect"
              Storyboard.TargetProperty="Fill.Color"
              To="#ff8080" Duration="0:0:0"/>
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Button.Triggers>
  </Button>
</WrapPanel>

サンプル→

ButtonClick.xaml 。 4色版。

TergetProperty を階層的に指定

この例では、 ボタンクリック後の Rectangle の色を、 Storyboard.TargetProperty="Fill.Color" で指定しています。 これは、省略せずにきちんと書くなら、 Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" となります。 このように、TargetProperty には、階層的なプロパティの指定の仕方ができます。

ところで、 Shape クラスの Fill プロパティは、 実際には Brush クラス(「抽象クラス」)です。 Shape.Fill に 「Attribute Syntax」 で色を設定すると、 自動的に SolidColorBrush に変換されるので、 この例の場合はこれでうまくいきます。 対して、Fill に LinearGradientBrush などを設定しているとうまく動作しません。 (エラーになったりはしないけども、何も起きない。)

複数のアニメーションを設定

ストーリーボード内には、複数のアニメーションを同時に指定できます。

以下の例では、色の変化と回転を同時に行っています。 この例では、Rectangle の上にマウスを乗せると、 Rectangle の色が変わって回転し始めます(3秒間)。

<WrapPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="200" Height="200">
  
  <WrapPanel.Resources>
    <Style TargetType="{x:Type Rectangle}">
      <Setter Property="Width" Value="50"/>
      <Setter Property="Height" Value="50"/>
      <Setter Property="Margin" Value="25"/>
      <Setter Property="Fill" Value="#8080ff"/>
      <Setter Property="RenderTransform">
        <Setter.Value>
          <RotateTransform CenterX="25" CenterY="25" Angle="0"/>
        </Setter.Value>
      </Setter>
    </Style>
  </WrapPanel.Resources>

  <Rectangle>
    <Rectangle.Triggers>
      <EventTrigger RoutedEvent="Mouse.MouseEnter">
        <BeginStoryboard>
          <Storyboard>
            <ColorAnimation
              Storyboard.TargetProperty="Fill.Color"
              To="#ff8080" Duration="0:0:0"/>
            <DoubleAnimation
              Storyboard.TargetProperty="RenderTransform.Angle"
              To="0" Duration="0:0:0"/>
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
      <EventTrigger RoutedEvent="Mouse.MouseLeave">
        <BeginStoryboard>
          <Storyboard>
            <ColorAnimation
              Storyboard.TargetProperty="Fill.Color"
              To="#8080ff" Duration="0:0:3"/>
            <DoubleAnimation
              Storyboard.TargetProperty="RenderTransform.Angle"
              To="360" Duration="0:0:3"/>
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Rectangle.Triggers>
  </Rectangle>

</WrapPanel>

サンプル→

MouseEnter.xaml 。 Rectangle を4×4で並べたらちょっと面白かった。

TergetProperty で配列的にアクセス

RenderTransform 属性で、拡大・傾斜・回転などの変形をかけたい場合、 TransformGroup 内に ScaleTransform や RotateTransform などを複数並べることになります。

こういう場合、以下のように、配列的に [0] とか [1] とかを使って TergetProperty を設定することができます。

<WrapPanel
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="200" Height="200" Background="#cccccc">

  <WrapPanel.Resources>
    <Style TargetType="{x:Type Rectangle}">
      <Setter Property="Width" Value="50"/>
      <Setter Property="Height" Value="50"/>
      <Setter Property="Margin" Value="25"/>
      <Setter Property="Fill" Value="#8080ff"/>
      <Setter Property="RenderTransform">
        <Setter.Value>
          <TransformGroup>
            <RotateTransform CenterX="25" CenterY="25" Angle="0"/>
            <TranslateTransform X="0" Y="0"/>
          </TransformGroup>
        </Setter.Value>
      </Setter>
    </Style>
  </WrapPanel.Resources>

  <Rectangle>
    <Rectangle.Triggers>
      <EventTrigger RoutedEvent="Rectangle.Loaded">
        <BeginStoryboard Name="BlinkBegin">
          <Storyboard>
            <DoubleAnimation
              Storyboard.TargetProperty="RenderTransform.Children[0].Angle"
              From="0" To="360" Duration="0:0:3"
              RepeatBehavior="Forever"/>
            <DoubleAnimation
              Storyboard.TargetProperty="RenderTransform.Children[1].X"
              From="0" To="20" Duration="0:0:0.1212"
              RepeatBehavior="Forever" AutoReverse="true"/>
            <DoubleAnimation
              Storyboard.TargetProperty="RenderTransform.Children[1].Y"
              From="0" To="20" Duration="0:0:0.1413"
              RepeatBehavior="Forever" AutoReverse="true"/>
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Rectangle.Triggers>
  </Rectangle>

</WrapPanel>

サンプル→

Gradation.xaml 。 LinearGradientBrush の GradientStops なんかも配列的にアクセス。 グラデーションの色をアニメーションして、回転・拡大・平行移動を同時にかけたら、 なかなか気持ち悪いのができた。

その他のアニメーション方式

DoubleAnimation などを使うと、 From から To の値に線形に値が変化します。 これに対して、もう少し凝った値の変化のさせ方もできます。

例えば、 DoubleAnimationUsingKeyFrame を使えば、 「時刻 xx に値 XX に、 時刻 yy に値 YY に、・・・」 というように、「いつ何の値にするか」を複数並べてアニメーションを作ることができます。

また、 DoubleAnimationUsingPath を使えば、 パス(複数の点をベジエ補間やスプライン補間で滑らかにつないだもの)に沿って値を変化させることができます。

BeginAnimation

XAML だけでアニメーション設定を完結させるには、 これまでに説明したような、イベントトリガ→イベントアクション→ストーリーボードという手順を踏む必要がありますが、 コードビハインド中では、BeginAnimation メソッドを呼び出してアニメーションを開始させることもできます。

(BeginAnimation は Animatable クラスのメソッド。 Contorol や Shape などは Animatable のサブクラス。)

例えば、「イベントトリガ」節で例に挙げた、 四角形を点滅表示させるものを BeginAnimation を使って書き直すと以下のようになります (XAML + コードビハインドの C# ファイル)。

<Window x:Class="WPFApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:c="clr-namespace:WPFApplication1"
  Title="Window1" Height="200" Width="200"
  >

  <Rectangle Name="rect1" Width="50" Height="50" Fill="#8080ff"/>
</Window>
using System;
using System.Windows;
using System.Windows.Media.Animation;

namespace WPFApplication1
{
  public partial class Window1 : System.Windows.Window
  {
    public Window1()
    {
      InitializeComponent();

      DoubleAnimation ani = new DoubleAnimation(
        1, 0.2, new TimeSpan(0, 0, 1));
      ani.RepeatBehavior = RepeatBehavior.Forever;
      ani.AutoReverse = true;
      this.rect1.BeginAnimation(UIElement.OpacityProperty, ani);
    }
  }
}

CompositionTarget.Rendering

ストーリーボードや BeginAnimation によるアニメーションは、 「タイムラインベース」です。

コンピュータ上のアニメーションというのは、連続的に動いているように見えて、 実はパラパラ漫画のような離散的なものです。 人間の目をごまかせるくらい高速に絵を切り替えることで、 動いている用に見えています。

で、タイムラインベースのアニメーションでは、 「時刻 t1 に位置 x1 、 時刻 t2 に位置 x2 にある」 というような情報を基にして、 「じゃあ、時刻 t t1< t < t2 )では位置 x にいるはずだ」 という値を計算して、その位置に物体を表示させます。

このような方式とは別に、 物理シミュレーションなんかでは、 「1フレームごとに逐次的に値を更新」とかいう方式で値を計算したい場合があります。 (加速度 a を与えて、 毎時刻 x = x + v , v = v + a という更新式にしたがって値を更新したり。)

こういう「1フレームごとに処理」という処理を実現するために、 「画面がレンダリングされるタイミングを拾えるイベント」が用意されています。 それが System.Windows.Media.CompositionTarget クラスの Rendering イベント(静的イベント)です。

例として、距離に反比例する引力が働く3つの物体の運動のシミュレーションを示します。

<Window x:Class="WPFApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:c="clr-namespace:WPFApplication1"
  Title="Window1" Height="300" Width="300"
  >

  <Canvas Height="200" Width="200">
    <Canvas.Resources>
      <Style TargetType="Ellipse">
        <Setter Property="Width" Value="10"/>
        <Setter Property="Height" Value="10"/>
        <Setter Property="Fill" Value="#8080ff"/>
      </Style>
    </Canvas.Resources>
    
    <Ellipse Name="obj1" Canvas.Left="30" Canvas.Top="30"/>
    <Ellipse Name="obj2" Canvas.Left="140" Canvas.Top="50"/>
    <Ellipse Name="obj3" Canvas.Left="50" Canvas.Top="140"/>
  </Canvas>
</Window>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;
using System.Windows.Media;

namespace WPFApplication1
{
  public partial class Window1 : System.Windows.Window
  {
    Point[] x = new Point[3];
    Vector[] v = new Vector[3];
    Shape[] obj = new Shape[3];

    public Window1()
    {
      InitializeComponent();

      this.obj[0] = this.obj1;
      this.obj[1] = this.obj2;
      this.obj[2] = this.obj3;

      for (int i = 0; i < this.obj.Length; ++i)
      {
        x[i] = new Point();
        x[i].X = (double)this.obj[i].GetValue(Canvas.LeftProperty);
        x[i].Y = (double)this.obj[i].GetValue(Canvas.TopProperty);
        v[i] = new Vector();
      }

      CompositionTarget.Rendering +=
        new EventHandler(CompositionTarget_Rendering);
    }

    void CompositionTarget_Rendering(object sender, EventArgs e)
    {
      Vector a01 = x[0] - x[1];
      Vector a12 = x[1] - x[2];
      Vector a20 = x[2] - x[0];

      double abs01 = a01.Length;
      double abs12 = a12.Length;
      double abs20 = a20.Length;

      if (abs01 < 10) abs01 = 10;
      if (abs12 < 10) abs12 = 10;
      if (abs20 < 10) abs20 = 10;

      a01 /= abs01 * abs01;
      a12 /= abs12 * abs12;
      a20 /= abs20 * abs20;

      v[0] += a20 - a01;
      v[1] += a01 - a12;
      v[2] += a12 - a20;

      for (int i = 0; i < this.obj.Length; ++i)
      {
        x[i] += v[i];
        this.obj[i].SetValue(Canvas.LeftProperty, x[i].X);
        this.obj[i].SetValue(Canvas.TopProperty, x[i].Y);
      }
    }
  }
}

更新履歴

test

[雑記]

ブログ