WPFでパイプ流体の流れをシミュレート - パスアニメーション

WPFでパイプ流体の流れをシミュレート - パスアニメーション

WPFの大きな特徴の一つはアニメーションシステムです。アニメーションを使用することで、WinFormでは実現が難しい多くの効果を実現できます。

最終更新 2023/01/15 12:46
ludewig
読了目安 9 分
カテゴリ
WPF
テーマ
WPF UIデザイン
タグ
.NET C# WPF Winform

WPF の大きな特徴の 1 つはアニメーション システムです。アニメーションを使用すると、WinForm では実現が難しかった多くの効果を実現できます。最近、ネットで偶然、WPF アニメーションを使用してオブジェクトを特定のパスに沿って正方向または逆方向に移動させるデモを見つけたので、参考にして自分で試してみることにしました。

1. シンプルなパスアニメーション

まずは、最もシンプルなパスアニメーションです。四角形と線分を用意し、四角形を線分の始点から終点まで移動させます。フロントエンドのコードは次のとおりです。

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="80"></RowDefinition>
    <RowDefinition></RowDefinition>
  </Grid.RowDefinitions>
  <WrapPanel VerticalAlignment="Center" HorizontalAlignment="Center">
    <button x:Name="btnAnimo" Click="btnAnimo_Click" Margin="0,0,10,0">
      開始
    </button>
  </WrapPanel>
  <Grid Grid.Row="1">
    <canvas x:Name="cvsMain">
      <Path
        x:Name="path1"
        Data="M100,100 L300,100 400,200 500,200"
        Stroke="LightGreen"
        StrokeThickness="20"
        StrokeLineJoin="Round"
      ></Path>
    </canvas>
  </Grid>
</Grid>

バックエンドのロジックコードは次のとおりです。

private void btnAnimo_Click(object sender, RoutedEventArgs e)
{
    AnimationByPath(cvsMain, path1,path1.StrokeThickness);
}

/// <summary>
/// パスアニメーション
/// </summary>
/// <param name="cvs">キャンバス</param>
/// <param name="path">パス</param>
/// <param name="target">アニメーションオブジェクト</param>
/// <param name="duration">時間</param>
private void AnimationByPath(Canvas cvs, Path path,double targetWidth, int duration = 5)
{
    #region アニメーションオブジェクトの作成
    Rectangle target = new Rectangle();
    target.Width = targetWidth;
    target.Height = targetWidth;
    target.Fill = new SolidColorBrush(Colors.Orange);
    cvs.Children.Add(target);
    Canvas.SetLeft(target, -targetWidth / 2);
    Canvas.SetTop(target, -targetWidth / 2);
    target.RenderTransformOrigin = new Point(0.5, 0.5);
    #endregion

    MatrixTransform matrix = new MatrixTransform();
    TransformGroup groups = new TransformGroup();
    groups.Children.Add(matrix);
    target.RenderTransform = groups;
    string registname = "matrix" + Guid.NewGuid().ToString().Replace("-", "");
    this.RegisterName(registname, matrix);
    MatrixAnimationUsingPath matrixAnimation = new MatrixAnimationUsingPath();
    matrixAnimation.PathGeometry = PathGeometry.CreateFromGeometry(Geometry.Parse(path.Data.ToString()));
    matrixAnimation.Duration = new Duration(TimeSpan.FromSeconds(duration));
    matrixAnimation.DoesRotateWithTangent = true;// パスに沿って回転
    matrixAnimation.RepeatBehavior = RepeatBehavior.Forever;// ループ
    Storyboard story = new Storyboard();
    story.Children.Add(matrixAnimation);
    Storyboard.SetTargetName(matrixAnimation, registname);
    Storyboard.SetTargetProperty(matrixAnimation, new PropertyPath(MatrixTransform.MatrixProperty));

    story.FillBehavior = FillBehavior.Stop;
    story.Begin(target, true);
}

ここでのポイントは、アニメーションオブジェクトとして動的に Rectangle の正方形を作成し、その幅と高さをパスの太さと同じに設定し、変換原点を中心(RenderTransformOrigin ="0.5,0.5")に設定することです。これにより、正方形がパスに沿って移動するときにパスに沿って回転もするようになります。最終的な効果は次のとおりです。

2. 逆方向のパスアニメーション

前の例を基に、線分を複数の連続した線分に変更したり、弧線を追加しても問題はなく、小さな正方形はパスに沿って移動し続けます。パスには始点と終点があり、通常はアニメーションオブジェクトが始点から終点へ移動しますが、オブジェクトを終点から始点へ移動させることはできるでしょうか?

考え方を変えて、元のパスを反転させ、始点と終点を入れ替えれば、元のパスと同じ外観だがデータが逆のパスが得られます。アニメーションオブジェクトを反転したパスに沿って移動させれば、視覚的には終点から始点へ移動しているように見えます。

この問題を解決する鍵は、パスデータの変換にあります。

private string ConvertPathData(string data)
{
    data = data.Replace("M", "");
    Regex regex = new Regex("[a-z]", RegexOptions.IgnoreCase);
    MatchCollection mc = regex.Matches(data);
    // item1 前の位置から現在の位置までの文字列 (match.Index=元の文字列でキャプチャされた部分文字列の最初の文字の位置)
    // item2 現在見つかった記号 (L C Z M)
    List<Tuple<string, string>> tmps = new List<Tuple<string, string>>();
    int index = 0;
    for (int i = 0; i < mc.Count; i++)
    {
        Match match = mc[i];
        if (match.Index != index)
        {
            string str = data.Substring(index, match.Index - index);
            tmps.Add(new Tuple<string, string>(str, match.Value));
        }
        index = match.Index + match.Length;
        if (i + 1 == mc.Count)// 最後
        {
            tmps.Add(new Tuple<string, string>(data.Substring(index), match.Value));
        }
    }
    List<string[]> arrys = new List<string[]>();
    Regex regexnum = new Regex(@"(\-?\d+\.?\d*)", RegexOptions.IgnoreCase);
    for (int i = 0; i < tmps.Count; i++)
    {
        MatchCollection childMcs = regexnum.Matches(tmps[i].Item1);
        if (childMcs.Count % 2 != 0)
        {
            continue;
        }
        int groups = childMcs.Count / 2;
        var strTmp = new string[groups];
        for (int j = 0; j < groups; j++)
        {
            string cdatas = childMcs[j * 2] + "," + childMcs[j * 2 + 1];// データを再構成
            strTmp[j] = cdatas;
        }
        arrys.Add(strTmp);
    }

    List<string> result = new List<string>();
    for (int i = arrys.Count - 1; i >= 0; i--)
    {
        string[] clist = arrys[i];
        for (int j = clist.Length - 1; j >= 0; j--)
        {
            if (j == clist.Length - 2 && i > 0)// 2 番目の要素に L または C の識別子を追加
            {
                var pointWord = tmps[i - 1].Item2;// 識別子を取得
                result.Add(pointWord + clist[j]);
            }
            else
            {
                result.Add(clist[j]);
                if (clist.Length == 1 && i > 0)// 要素が 1 つだけの場合 ex L44.679973,69.679973
                {
                    result.Add(tmps[i - 1].Item2);
                }
            }
        }
    }
    return "M" + string.Join(" ", result);

}

また、アニメーションオブジェクトとしての正方形は任意のコントロールに置き換えることができます。わかりやすくするために、正方形を矢印に置き換えます。また、正方向と逆方向のアニメーションを区別するために、パスも異なる色に設定します。修正後のコードは次のとおりです。

/// <summary>
/// 正方向
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAnimo_Click(object sender, RoutedEventArgs e)
{
    AnimationByPath(cvsMain, path1,path1.StrokeThickness,false,3);
}
/// <summary>
/// 逆方向
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnReback_Click(object sender, RoutedEventArgs e)
{
    AnimationByPath(cvsMain, path1, path1.StrokeThickness, true, 3);
}



/// <summary>
/// パスアニメーション
/// </summary>
/// <param name="cvs">キャンバス</param>
/// <param name="path">パス</param>
/// <param name="targetWidth">アニメーションオブジェクトの幅・高さ</param>
/// <param name="isInverse">反転するかどうか</param>
/// <param name="duration">アニメーション時間</param>
private void AnimationByPath(Canvas cvs, Path path, double targetWidth, bool isInverse = false, int duration = 5)
{
    Polygon target = new Polygon();
    target.Points = new PointCollection()
    {
        new Point(0,0),
        new Point(targetWidth/2,0),
        new Point(targetWidth,targetWidth/2),
        new Point(targetWidth/2,targetWidth),
        new Point(0,targetWidth),
        new Point(targetWidth/2,targetWidth/2)
    };

    if (isInverse)// 反転
    {
        target.Fill = new SolidColorBrush(Colors.DeepSkyBlue);
    }
    else// 正方向
    {
        target.Fill = new SolidColorBrush(Colors.Orange);
    }

    cvs.Children.Add(target);
    Canvas.SetLeft(target, -targetWidth / 2);
    Canvas.SetTop(target, -targetWidth / 2);
    target.RenderTransformOrigin = new Point(0.5, 0.5);

    MatrixTransform matrix = new MatrixTransform();
    TransformGroup groups = new TransformGroup();
    groups.Children.Add(matrix);
    target.RenderTransform = groups;
    string registname = "matrix" + Guid.NewGuid().ToString().Replace("-", "");
    this.RegisterName(registname, matrix);
    MatrixAnimationUsingPath matrixAnimation = new MatrixAnimationUsingPath();
    if (!isInverse)// 正方向
    {
        matrixAnimation.PathGeometry = PathGeometry.CreateFromGeometry(Geometry.Parse(path.Data.ToString()));
    }
    else// 反転
    {
        string data = ConvertPathData(path.Data.ToString());
        matrixAnimation.PathGeometry = PathGeometry.CreateFromGeometry(Geometry.Parse(data));
    }
    matrixAnimation.Duration = new Duration(TimeSpan.FromSeconds(duration));
    matrixAnimation.DoesRotateWithTangent = true;// 回転
    matrixAnimation.RepeatBehavior = RepeatBehavior.Forever;
    Storyboard story = new Storyboard();
    story.Children.Add(matrixAnimation);
    Storyboard.SetTargetName(matrixAnimation, registname);
    Storyboard.SetTargetProperty(matrixAnimation, new PropertyPath(MatrixTransform.MatrixProperty));

    story.FillBehavior = FillBehavior.Stop;
    story.Begin(target, true);
}

効果は次のようになります。なかなか面白いですね。

3. パイプ内の流体アニメーションのシミュレーション

以上の基礎があるので、改良して水道管の中を水が流れるようなアニメーション効果を作ってみます。配管はもちろん 1 本ではなく複数本あり、管径も異なります。さらにポンプを追加し、ポンプが回転すると水が流れ、ポンプが逆回転すると水が逆流するようにします。前の手順で最も核心的な問題は解決済みなので、今回はキーフレームアニメーションを追加してアニメーションオブジェクトの回転を制御するだけです。

フロントエンドコードを次のように変更します。

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="80"></RowDefinition>
    <RowDefinition></RowDefinition>
  </Grid.RowDefinitions>
  <WrapPanel VerticalAlignment="Center" HorizontalAlignment="Center">
    <button x:Name="btnAnimo" Click="btnAnimo_Click" Margin="0,0,10,0">
      正回転
    </button>
    <button x:Name="btnReback" Click="btnReback_Click" Margin="0,0,10,0">
      逆回転
    </button>
  </WrapPanel>
  <Grid Grid.Row="1">
    <canvas x:Name="cvsMain">
      <Path
        x:Name="path1"
        Data="M100,100 L300,100 300,200 400,200"
        Stroke="LightGreen"
        StrokeThickness="20"
        StrokeLineJoin="Round"
      ></Path>
      <Path
        x:Name="path2"
        Data="M200,300 L350,300 350,200"
        Stroke="LightGreen"
        StrokeThickness="12"
        StrokeLineJoin="Round"
      ></Path>
      <Path
        x:Name="path3"
        Data="M450,223 L550,223 650,100 750,100 800,150"
        Stroke="LightGreen"
        StrokeThickness="16"
        StrokeLineJoin="Round"
      ></Path>
      <image
        Source="fan.png"
        Width="50"
        Height="50"
        Canvas.Left="400"
        Canvas.Top="185"
      ></image>
      <image
        x:Name="imgFan"
        Source="fan-inner.png"
        Width="24"
        Height="24"
        Canvas.Left="410"
        Canvas.Top="197"
        RenderTransformOrigin="0.5,0.5"
      ></image>
    </canvas>
  </Grid>
</Grid>

バックエンドコードを次のように変更します。

/// <summary>
/// 正回転
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAnimo_Click(object sender, RoutedEventArgs e)
{
    // 原文の第 3 引数は this.path[x].Width ですが、実際は this.path[x].StrokeThickness です
    AnimationByPath(this.cvsMain, this.path1, this.path1.StrokeThickness,false, 3);
    AnimationByPath(this.cvsMain, this.path2, this.path2.StrokeThickness,false, 3);
    AnimationByPath(this.cvsMain, this.path3, this.path3.StrokeThickness,false, 3);

    StoryByOrient(this.imgFan,0, 3);
}
/// <summary>
/// 逆回転
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnReback_Click(object sender, RoutedEventArgs e)
{
    // 原文の第 3 引数は this.path[x].Width ですが、実際は this.path[x].StrokeThickness です
    AnimationByPath(this.cvsMain, this.path1, this.path1.StrokeThickness, true, 3);
    AnimationByPath(this.cvsMain, this.path2, this.path2.StrokeThickness, true, 3);
    AnimationByPath(this.cvsMain, this.path3, this.path3.StrokeThickness, true, 3);

    StoryByOrient(this.imgFan, 1, 3);
}

/// <summary>
/// 回転アニメーション
/// </summary>
/// <param name="img">アニメーションオブジェクト</param>
/// <param name="orientation">時計回り/反時計回り</param>
/// <param name="duration"></param>
private void StoryByOrient(Image img, int orientation, int duration = 5)
{
    Storyboard storyboard = new Storyboard();// ストーリーボードを作成
    DoubleAnimation doubleAnimation = new DoubleAnimation();// Double 型のアニメーションをインスタンス化
    RotateTransform rotate = new RotateTransform();// 回転変換のインスタンス
    img.RenderTransform = rotate;// 画像コントロールに変換のインスタンスを設定
    storyboard.RepeatBehavior = RepeatBehavior.Forever;// 繰り返しを永久に設定
    storyboard.SpeedRatio = 2;// 再生速度
    // 0 から 360 度の回転を設定
    doubleAnimation.From = 0;
    if (orientation==0)// 時計回り
    {
        doubleAnimation.To = 360;
    }
    else// 反時計回り
    {
        doubleAnimation.To = -360;
    }
    doubleAnimation.Duration = new Duration(TimeSpan.FromSeconds(duration));// 再生時間を 2 秒に設定
    Storyboard.SetTarget(doubleAnimation, img);// アニメーションにオブジェクトを指定
    Storyboard.SetTargetProperty(doubleAnimation,
new PropertyPath("RenderTransform.Angle"));// アニメーションに依存プロパティを指定
    storyboard.Children.Add(doubleAnimation);// アニメーションをストーリーボードに追加
    storyboard.Begin(img);// アニメーションを開始
}

最終的な効果を見てみましょう。

なかなかそれらしく見えますね。

注:3 番目のケースのコードには画像が不足しています。サイト管理者は元の記事の Gif 画像から一部を切り取り、パラメーターを設定して上図のような動作を確認できるようにしています:https://github.com/dotnet9/TerminalMACS.ManagerForWPF/tree/master/src/Demo/PathAnimationDemo

プログラミングはとても面白く、決して諦めません。

本記事は転載です。

著者:ludewig

原文タイトル:WPF 随笔(九)--使用路径动画模拟管道流体流向

原文リンク:https://blog.csdn.net/lordwish/article/details/85007867

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2022/04/04

すごい開発者が WinUI 3 と WPF で作ったオープンソースの動的壁紙ソフトウェア

動的壁紙ソフトと言えば、多くの人が `Wallpaper Engine` を聞いたことがあるか、使ったことがあるでしょう。これは Steam Store で高評価の壁紙ソフトで、価格は `18` 元です。私も以前から使っていましたが、今日紹介するのは別の壁紙ソフト `Lively Wallpaper` です。

続きを読む
同じカテゴリ / 同じタグ 2021/04/07

WPFとWinformsの違いをご存知ですか?

Windowsデスクトップアプリケーションを開発するための2つの方法の主な違いを紹介します。これらは、最新のシステム開発においてより効果的に機能します。

続きを読む
同じカテゴリ / 同じタグ 2025/09/13

WPF から Avalonia への移行シリーズ:なぜ WPF プログラムを Avalonia に移行しなければならないのか

過去数年間、当社の上位機ソフトウェアは主に WPF と WinForm で開発されてきました。これらの技術は Windows プラットフォームで非常に便利であり、小規模試作から現在の規模拡大による納品まで、私たちを支えてきました。しかし、ビジネスの発展や顧客ニーズの変化に伴い、単一の Windows テクノロジースタックは私たちが必ず乗り越えなければならない壁となってきました。

続きを読む