A major feature of WPF is the animation system. Using animation can achieve many effects that are difficult to achieve in WinForm. Recently, I accidentally saw on the Internet that God used WPF animation to achieve the effect of objects moving forward or backward along a specific path, so I wanted to refer to it and give it a try.
1. Simple path animation
Let's start with the simplest path animation. Add a line segment to a square, and let the square move from the starting point of the line segment to the ending point of the line segment. The front desk page code is as follows:
<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>
The background logic code is as follows:
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. Reverse path animation
Based on the previous example, changing the line segment to multiple continuous line segments and even adding arcs will not affect the effect, and the small squares will move along the path. For a path, there is a starting point and an ending point. Under normal circumstances, animated objects move from the starting point to the ending point. Can we make the object move from the ending point to the starting point?
It is to think differently, reverse the original path, switch the starting point and ending point, and then you can get a path that looks the same as the original path but has the opposite data? Moving the animated object along the reversed path visually means moving from the end point to the starting point.
The key to solving this problem lies in the conversion of path data.
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)//last
{
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)//对于第二个元素增加 L或者C的标识
{
var pointWord = tmps[i - 1].Item2;//获取标识
result.Add(pointWord + clist[j]);
}
else
{
result.Add(clist[j]);
if (clist.Length == 1 && i > 0)//说明只有一个元素 ex L44.679973,69.679973
{
result.Add(tmps[i - 1].Item2);
}
}
}
}
return "M" + string.Join(" ", result);
}
In addition, the cube used as an animation object can be replaced with any control object. In order to visualize points, the cube can be replaced with arrows; at the same time, in order to distinguish forward and reverse animations, the path is also set to different colors. The revised code is as follows:
/// <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);
}
The effect becomes the following. Isn't it interesting:

3. Simulate pipe fluid animation
With the above foundation, consider improving the animation effect that simulates the flow of water in the water pipe. Of course, there is more than one pipe, but there are more than one pipe, and the pipe diameters are different; add a water pump, and the water will flow when the pump is started, and when the water pump is reversed, the water will flow backwards. Because the core problem has been solved in the previous step, this step just adds a keyframe animation to control the rotation of the animated object.
Change the front desk code to:
<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>
Modify the background code to:
/// <summary>
/// 正转
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAnimo_Click(object sender, RoutedEventArgs e)
{
// 原文第三个参数传的是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)
{
// 原文第三个参数传的是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);//启动动画
}
Let's take a look at the final effect:

It's quite like that.
Note: The third case code is missing pictures. The webmaster intercepted part of it based on the original Gif diagram and set parameters. The effect of the above picture can be run: github.com/dotnet9/TerminalMACS.ManagerForWPF/tree/master/src/Demo/PathAnimationDemo
Programming is fun and never give up for a moment.
This article is reproduced.
Author: ludewig
Original title: WPF Essay (9)-Using path animation to simulate fluid flow direction in pipes
Original link: blog.csdn.net/lordwish/article/details/85007867