通过 WPF 绘制分形






4.86/5 (7投票s)
在 WPF 中,C# 的递归是如何良好工作的。
在 WPF 中,C# 的递归是如何良好工作的
本文旨在说明如何使用 WPF 绘制二叉树和雪花。这两个例子看起来都没有实际价值,但它们是并行计算中的热门话题:分形。您可以将二叉树定义为递归地连接到分支的树干。分支连接到较小的分支,较小的分支连接到更小的分支,依此类推。也就是说,我们将编写一个程序,该程序将继续绘制越来越小的分支,直到新分支的长度小于一像素。此时,程序将停止。首先要从布局管理开始。在 WPF 中,Canvas
控件允许使用固定坐标绝对定位元素。这种布局容器最类似于传统的 Windows Forms,但它不提供锚定或停靠功能。StackPanel
控件将元素放置在水平或垂直堆栈中。这种布局容器通常用于更大、更复杂的窗口的小区域。在构建 WPF 应用程序时,两者都用于布局管理。单击开始按钮控件后,视图如下:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Project.BinaryTree"
x:Name="Window"
Title="BinaryTree"
Width="345"
Height="300">
<Window.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop
Color="Black" Offset="0"/>
<GradientStop
Color="#FF0016FF" Offset="1"/>
</LinearGradientBrush>
</Window.Background>
<Viewbox Stretch="Uniform">
<StackPanel>
<StackPanel Orientation="Horizontal"
Margin="5,5,5,0">
<Button Name="btnStart" Click="btnStart_Click"
Width="98.502" Content="Start"
FontFamily="Times New Roman"
FontWeight="Bold" FontSize="16"/>
<TextBlock Name="tbLabel" Margin="20,5,0,0"/>
</StackPanel>
<Canvas Name="canvas1" Width="300"
Height="300" Margin="5" Background="#FFBDFF00"/>
</StackPanel>
</Viewbox>
</Window>
随着整个树通过添加连续较小的分支而生长,每一笔都变得越来越小。
要构建此项目而不包含解决方案文件,请启动 Visual Studio 2008(2010)或 Expression Blend,创建一个名为 Project 的新 C# WPF 项目。右键单击项目图标,然后选择“添加新项”以选择一个新窗口,您将其命名为 BinaryTree
。右键单击 MainWindow.xaml 文件,然后选择“从项目移除”。您只会看到一个带有按钮的窗口。由于我们将在相应的代码隐藏文件中编写代码,因此它将绘制一棵树。
这是代码隐藏文件。请注意递归的使用。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Project
{
public partial class BinaryTree : Window
{
private int II = 0;
private int i = 0;
public BinaryTree()
{
InitializeComponent();
}
private void btnStart_Click(object sender, RoutedEventArgs e)
{
canvas1.Children.Clear();
tbLabel.Text = "";
i = 0;
II = 1;
CompositionTarget.Rendering += StartAnimation;
}
private void StartAnimation(object sender, EventArgs e)
{
i += 1;
if (i % 60 == 0)
{
DrawBinaryTree(canvas1, II,
new Point(canvas1.Width / 2,
0.83 * canvas1.Height),
0.2 * canvas1.Width, -Math.PI / 2);
string str = "Binary Tree - Depth = " +
II.ToString();
tbLabel.Text = str;
II += 1;
if (II > 10)
{
tbLabel.Text = "Binary Tree - Depth = 10. Finished";
CompositionTarget.Rendering -=
StartAnimation;
}
}
}
private double lengthScale = 0.75;
private double deltaTheta = Math.PI / 5;
private void DrawBinaryTree(Canvas canvas,
int depth, Point pt, double length, double theta)
{
double x1 = pt.X + length * Math.Cos(theta);
double y1 = pt.Y + length * Math.Sin(theta);
Line line = new Line();
line.Stroke = Brushes.Blue;
line.X1 = pt.X;
line.Y1 = pt.Y;
line.X2 = x1;
line.Y2 = y1;
canvas.Children.Add(line);
if (depth > 1)
{
DrawBinaryTree(canvas, depth - 1,
new Point(x1, y1),
length * lengthScale, theta + deltaTheta);
DrawBinaryTree(canvas, depth - 1,
new Point(x1, y1),
length * lengthScale, theta - deltaTheta);
}
else
return;
}
}
}
这个例子包含一种系统概念。如果我们考虑输出到控制台屏幕的文本和图形,系统必须将它们写入屏幕。也许另一个 API 使文本和/或对象对人眼来说是可理解的。话虽如此,请考虑这条被三等分的线段。我们在中间线段上形成一个等边三角形。
雪花始于一个等边三角形。程序将三角形的每个边替换为适当缩放和旋转的基本单元版本。然后,程序将新图形中的每个直线段替换为基本单元的较小版本。它用越来越小的基本单元版本替换较新的直线段,直到雪花达到所需的深度。顺便说一句,在此和二叉树应用程序中看到的线性渐变只是为了使外观更具吸引力。带有白色背景的窗口绰绰有余。因此,现在,向我们称为 Project 的项目中添加一个新的 WPF 窗口。从项目中移除二叉树 XAML 文件。将新窗口命名为 SnowFlake
。这是在单击按钮之前和之后的视图。
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Project.SnowFlake"
x:Name="Window"
Title="SnowFlake"
Width="350"
Height="300">
<Window.Background>
<LinearGradientBrush EndPoint="0.5,1"
StartPoint="0.5,0">
<GradientStop Color="Black" Offset="0"/>
<GradientStop
Color="#FFFF0006" Offset="1"/>
</LinearGradientBrush>
</Window.Background>
<Viewbox Stretch="Uniform">
<StackPanel>
<StackPanel Orientation="Horizontal"
Margin="5,5,5,0" Background="#FFA6FF00">
<Button Name="btnStart" Click="btnStart_Click"
Width="95.949" Content="Start" Height="29.618"
FontFamily="Times New Roman"
FontWeight="Bold" FontSize="16"/>
<TextBlock Name="tbLabel" Margin="20,5,0,0"/>
</StackPanel>
<Canvas Name="canvas1" Width="300"
Height="300" Margin="5">
</Canvas>
</StackPanel>
</Viewbox>
</Window>
这是相应的代码隐藏文件。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Project
{
public partial class SnowFlake : Window
{
private double distanceScale = 1.0 / 3;
double[] dTheta = new double[4] { 0, Math.PI / 3,
-2 * Math.PI / 3, Math.PI / 3 };
Polyline pl = new Polyline();
private Point SnowflakePoint = new Point();
private double SnowflakeSize;
private int II = 0;
private int i = 0;
public SnowFlake()
{
InitializeComponent();
// determine the size of the snowflake:
double ysize = 0.8 * canvas1.Height /
(Math.Sqrt(3) * 4 / 3);
double xsize = 0.8 * canvas1.Width / 2;
double size = 0;
if (ysize < xsize)
size = ysize;
else
size = xsize;
SnowflakeSize = 2 * size;
pl.Stroke = Brushes.Blue;
}
private void btnStart_Click(object sender, RoutedEventArgs e)
{
canvas1.Children.Clear();
tbLabel.Text = "";
i = 0;
II = 0;
canvas1.Children.Add(pl);
CompositionTarget.Rendering += StartAnimation;
}
private void StartAnimation(object sender, EventArgs e)
{
i += 1;
if (i % 60 == 0)
{
pl.Points.Clear();
DrawSnowFlake(canvas1, SnowflakeSize, II);
string str = "Snow Flake - Depth = " +
II.ToString();
tbLabel.Text = str;
II += 1;
if (II > 5)
{
tbLabel.Text = "Snow Flake - Depth = 5. Finished";
CompositionTarget.Rendering -=
StartAnimation;
}
}
}
private void SnowFlakeEdge(Canvas canvas,
int depth, double theta, double distance)
{
Point pt = new Point();
if (depth <= 0)
{
pt.X = SnowflakePoint.X +
distance * Math.Cos(theta);
pt.Y = SnowflakePoint.Y +
distance * Math.Sin(theta);
pl.Points.Add(pt);
SnowflakePoint = pt;
return;
}
distance *= distanceScale;
for (int j = 0; j < 4; j++)
{
theta += dTheta[j];
SnowFlakeEdge(canvas, depth - 1,
theta, distance);
}
}
private void DrawSnowFlake(Canvas canvas, double length, int depth)
{
double xmid = canvas.Width / 2;
double ymid = canvas.Height / 2;
Point[] pta = new Point[4];
pta[0] = new Point(xmid, ymid + length / 2 *
Math.Sqrt(3) * 2 / 3);
pta[1] = new Point(xmid + length / 2,
ymid - length / 2 * Math.Sqrt(3) / 3);
pta[2] = new Point(xmid - length / 2,
ymid - length / 2 * Math.Sqrt(3) / 3);
pta[3] = pta[0];
pl.Points.Add(pta[0]);
for (int j = 1; j < pta.Length; j++)
{
double x1 = pta[j - 1].X;
double y1 = pta[j - 1].Y;
double x2 = pta[j].X;
double y2 = pta[j].Y;
double dx = x2 - x1;
double dy = y2 - y1;
double theta = Math.Atan2(dy, dx);
SnowflakePoint = new Point(x1, y1);
SnowFlakeEdge(canvas, depth, theta, length);
}
}
}
}
当程序递归地绘制线段时,它首先绘制该线段长度的三分之一,沿当前方向。然后它转动 60 度,绘制另一段长度为三分之一的线段。接下来,它转动 -120 度,绘制另一段长度为三分之一的线段。最后,它再次转动 60 度,并绘制原始线段长度的又一个三分之一。这就是为什么您以这种形式定义 dTheta
。
double[] dTheta = new double[4] { 0, Math.PI / 3, -2 * Math.PI / 3, Math.PI / 3 };
从基本微积分中可以回顾到,当 y = f(x) 时,因变量 y 是自变量 x 的函数。Y-prime,或 y',或 f'(x),是方程等于 f(x) 的导数。如果 y = f(x) = x 的 3 次方,则 f'(x) 为 2x 平方。将递归函数视为调用自身的函数可能也有帮助。SnowFlakeEdge
方法递归地绘制一条线段(通过将点添加到 polyline 的点集合中),该线段从 snowflakePoint
开始,并以长度 distance 沿 theta 方向移动。完成后,它会留下 snowflakePoint
的值来指示线段的终点。这使得连续执行所有必要的递归调用更加容易。DrawSnowFlake
方法调用 SnowFlakeEndge
方法来绘制初始三角形的每个边。在此方法中,Atan2
函数接受参数 dy
和 dx
,它们分别是线段 Y 和 X 坐标的变化量。它返回 dy/dx 的正切角。现在,如果按下“开始”按钮,程序将开始绘制雪花。
本文内容包含 Jack Xu 的作品中的参考资料,Jack Xu 是 .NET 高级图形和 WPF 开发领域的领导者。