如何使用 C# GDI+ 图形和 Windows 表格递归绘制希尔伯特曲线分形?
How can I draw a Hilbert Curve Fractal recursively using C# GDI+ Graphics and Windows Forms?
我正在开展一个项目,我需要使用递归在 C# 的 Windows Forms 应用程序中绘制希尔伯特曲线分形。我必须为此使用 GDI+ 图形,但我是 GDI+ 图形的新手。下面是我实际绘制曲线的 Form class 的完整代码。在此 post 的末尾,我附上了展示我的错误输出和预期输出的照片。
DrawRelative()
函数应该绘制从当前 [x,y] 坐标到新 [x,y] 坐标的下一条线段,这是通过添加 xDistance
和 yDistance
值传递到 DrawRelative()
函数到 xCurrent
和 yCurrent
class 属性。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace HilbertCurveFractal
{
public partial class FractalDisplay : Form
{
public int MaxDepth { get; set; }
public int CurveType { get; set; }
public int xCurrent { get; set; }
public int yCurrent { get; set; }
public int xLength { get; set; }
public int yLength { get; set; }
public FractalDisplay(int DepthValue, int SelectedCurve)
{
InitializeComponent();
MaxDepth = DepthValue;
CurveType = SelectedCurve;
xCurrent = 250;
yCurrent = 250;
xLength = 0;
yLength = 2;
}
private void FractalDisplay_Load(object sender, EventArgs e)
{
this.DoubleBuffered = true;
if (CurveType == 1) // Run the Hilbert Curve Generator
{
GenerateHilbertCurve(MaxDepth, xLength, yLength);
}
else if (CurveType == 2) // Run the Koch Curve Generator
{
}
else if (CurveType == 3) // Run the Sierpinski Curve Generator
{
}
else
{
MessageBox.Show("Error! - No Curve Type Selected. Ending Program.");
Application.Exit();
}
}
private void GenerateHilbertCurve(int depth, int xDistance, int yDistance)
{
//if (depth == 0) // Base Case
//{
// return;
//}
//else { }
if (depth > 0)
{
GenerateHilbertCurve(depth - 1, yDistance, xDistance);
}
else { }
// Draw Part of Curve Here
DrawRelative(xDistance, yDistance);
if (depth > 0)
{
GenerateHilbertCurve(depth - 1, xDistance, yDistance);
}
else { }
// Draw Part of Curve Here
DrawRelative(yDistance, xDistance);
if (depth > 0)
{
GenerateHilbertCurve(depth - 1, xDistance, yDistance);
}
else { }
// Draw Part of Curve Here
DrawRelative((- 1 * xDistance), (-1 * yDistance));
if (depth > 0)
{
GenerateHilbertCurve(depth - 1, (-1 * yDistance), (-1 * xDistance));
}
else { }
}
// Create a New Paint Event Handler
private void DrawRelative(int xDistance, int yDistance)
{
xLength = xDistance;
yLength = yDistance;
this.Paint += new PaintEventHandler(HilbertCurve_Paint);
}
// Perform the Actual Drawing
private void HilbertCurve_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
// Discover where the new X and Y points will be
int xNew, yNew;
xNew = xCurrent + xLength;
yNew = yCurrent + yLength;
// Paint from the current position of X and Y to the new positions of X and Y
e.Graphics.DrawLine(Pens.Red, xCurrent, yCurrent, xNew, yNew);
// Update the Current Location of X and Y
xCurrent = xNew;
yCurrent = yNew;
}
}
}
第一张照片(下图)是在 MaxDepth 为 1 的情况下希尔伯特曲线函数的错误输出。
第二张照片(下图)表示我应该从这组函数中得到什么(假设传入的 MaxDepth 值为 1)。
因为似乎递归算法编码正确,我怀疑我没有以正确的方式使用 GDI+ 图形,或者我的 class 属性在某处被错误地 updated/set递归调用。我能做些什么来修复我的绘图算法?提前谢谢你。
老实说,我最初并不了解您为希尔伯特曲线生成点的实现。我熟悉几种不同的方法,但都不像那样。
但是,这是一个完全不同的问题。您手头的主要问题实际上只是您不了解 Winforms 中的绘图机制是如何工作的。简而言之:有一个 Paint
事件,您的代码应该通过绘制需要绘制的内容来处理该事件。订阅 Paint
事件不会导致任何事情 发生 ;这只是一种注册方式,以便在绘图应该发生时收到通知。
通常,人们会使用设计器订阅事件,方法是导航到设计器中某个对象(例如您的表单)的“属性”窗格的 "Events" 选项卡并选择适当的事件处理程序 (或双击事件旁边的空框,让 Designer 自动插入一个空的处理程序供您填写)。您也可以在自己的对象中处理 Paint
事件时,简单地覆盖 OnPaint()
方法。
在任何一种情况下,正确的技术是建立绘图的先决条件,然后调用 Invalidate()
导致框架然后引发 Paint
事件,此时您可以实际绘制什么你想画画
请注意,在评论者 TaW 和我之间,我们提出了两种不同的绘图方法:我建议预先计算绘图所需的所有数据,然后在引发 Paint
事件时绘制它; TaW 建议从 Paint
事件处理程序调用递归方法,并在遍历递归算法时直接绘制。
这两种技术都很好,但当然各有利弊,主要与 classic 时间和 space 的权衡有关。使用前一种技术,生成曲线的成本仅在曲线参数发生变化时产生一次。绘图发生得更快,因为代码所要做的就是绘制预先存在的数据。使用后一种技术,无需存储数据,因为每个生成的新点都会立即使用,但这当然意味着每次重绘 window 时都必须重新生成所有点。
对于这个特定的应用程序,实际上我认为它并不重要。在典型的屏幕分辨率下,在开始达到要绘制的点的数据存储限制之前,您将无法确定曲线的特征。同样,算法的执行速度如此之快,以至于每次 window 需要重绘时重新计算点数也没有什么坏处。请记住,这些是您在其他情况下可能需要更仔细地判断的权衡。
好的,那是什么意思?好吧,当我将它转换为正确使用 Graphics
class 的东西时,我无法让您的实现绘制希尔伯特曲线,所以我更改了那部分代码以使用我知道的实现作品。您可以在此处找到有关此特定实现如何工作的详细讨论:希尔伯特曲线
概念与实施
下面,我提供了该特定希尔伯特曲线实现的两个不同版本,第一个使用 "retained" 方法(即生成数据,然后绘制它),第二个使用 "immediate" 方法(即每次要绘制 window 时生成数据,因为正在绘制):
"Retained"方法:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
DoubleBuffered = true;
}
private PointF[] _points;
private void FractalDisplay_Load(object sender, EventArgs e)
{
Redraw();
}
private void Redraw()
{
List<PointF> points = new List<PointF>();
GenerateHilbert(0, 0, 1, 0, 0, 1, (int)numericUpDown1.Value, points);
_points = points.ToArray();
Invalidate();
}
private void GenerateHilbert(PointF origin, float xi, float xj, float yi, float yj, int depth, List<PointF> points)
{
if (depth <= 0)
{
PointF current = origin + new SizeF((xi + yi) / 2, (xj + yj) / 2);
points.Add(current);
}
else
{
GenerateHilbert(origin, yi / 2, yj / 2, xi / 2, xj / 2, depth - 1, points);
GenerateHilbert(origin + new SizeF(xi / 2, xj / 2), xi / 2, xj / 2, yi / 2, yj / 2, depth - 1, points);
GenerateHilbert(origin + new SizeF(xi / 2 + yi / 2, xj / 2 + yj / 2), xi / 2, xj / 2, yi / 2, yj / 2, depth - 1, points);
GenerateHilbert(origin + new SizeF(xi / 2 + yi, xj / 2 + yj), -yi / 2, -yj / 2, -xi / 2, -xj / 2, depth - 1, points);
}
}
// Perform the Actual Drawing
private void HilbertCurve_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
if (_points != null)
{
float scale = Math.Min(ClientSize.Width, ClientSize.Height);
e.Graphics.ScaleTransform(scale, scale);
using (Pen pen = new Pen(Color.Red, 1 / scale))
{
e.Graphics.DrawLines(pen, _points);
}
}
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
Redraw();
}
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
Invalidate();
}
}
"Immediate"方法:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
DoubleBuffered = true;
}
private void Redraw()
{
Invalidate();
}
private PointF GenerateHilbert(PointF origin, float xi, float xj, float yi, float yj, int depth,
PointF? previous, Graphics graphics, Pen pen)
{
if (depth <= 0)
{
PointF current = origin + new SizeF((xi + yi) / 2, (xj + yj) / 2);
if (previous != null)
{
graphics.DrawLine(pen, previous.Value, current);
}
return current;
}
else
{
previous = GenerateHilbert(origin, yi / 2, yj / 2, xi / 2, xj / 2, depth - 1, previous, graphics, pen);
previous = GenerateHilbert(origin + new SizeF(xi / 2, xj / 2), xi / 2, xj / 2, yi / 2, yj / 2, depth - 1, previous, graphics, pen);
previous = GenerateHilbert(origin + new SizeF(xi / 2 + yi / 2, xj / 2 + yj / 2), xi / 2, xj / 2, yi / 2, yj / 2, depth - 1, previous, graphics, pen);
return GenerateHilbert(origin + new SizeF(xi / 2 + yi, xj / 2 + yj), -yi / 2, -yj / 2, -xi / 2, -xj / 2, depth - 1, previous, graphics, pen);
}
}
// Perform the Actual Drawing
private void HilbertCurve_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
float scale = Math.Min(ClientSize.Width, ClientSize.Height);
e.Graphics.ScaleTransform(scale, scale);
using (Pen pen = new Pen(Color.Red, 1 / scale))
{
GenerateHilbert(new PointF(), 1, 0, 0, 1, (int)numericUpDown1.Value, null, e.Graphics, pen);
}
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
Redraw();
}
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
Invalidate();
}
}
在这两个示例中,我都做了一些其他更改,这些更改并不是为了说明这些技术而严格需要的,但仍然有用:
- 曲线本身以 space 为单位计算(即边长为 1 的正方形),然后通过缩放绘图以适合 window.
来绘制
- 在有意义的情况下,各个坐标作为整个
PointF
值传递。这简化了值的重用并向 X 和 Y 值添加新的偏移量。
- 由于绘图现在已缩放到 window,因此如果 window 的大小发生变化,则会重新绘制。
- 为了简单起见,这个
Form
是自包含的,有一个NumericUpDownControl
决定递归深度。我没有包括这个控件的实例化;我想你可以在设计器中自己添加适当的控件,使上面的编译。
附录:
我有幸查看了您尝试实现的算法在 Internet 上的其他示例。现在我明白了算法的基本机制是什么,我能够修复你的版本以使其工作(主要问题是你使用实例字段来存储算法的增量,但也使用相同的字段来初始化算法,所以一旦算法 运行 一次,后续执行将不起作用)。因此,为了完整起见,这里是代码的第二个 "retained" 版本,使用您喜欢的算法而不是我上面使用的算法:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
DoubleBuffered = true;
}
private PointF _previousPoint;
private PointF[] _points;
private void FractalDisplay_Load(object sender, EventArgs e)
{
Redraw();
}
private void Redraw()
{
List<PointF> points = new List<PointF>();
// Start here, to provide a bit of margin within the client area of the window
_previousPoint = new PointF(0.025f, 0.025f);
points.Add(_previousPoint);
int depth = (int)numericUpDown1.Value;
float gridCellCount = (float)(Math.Pow(2, depth) - 1);
// Use only 95% of the available space in the client area. Scale
// the delta for drawing to fill that 95% width/height exactly,
// according to the number of grid cells the given depth will
// produce in each direction.
GenerateHilbert3(depth, 0, 0.95f / gridCellCount, points);
_points = points.ToArray();
Invalidate();
}
private void GenerateHilbert(int depth, float xDistance, float yDistance, List<PointF> points)
{
if (depth < 1)
{
return;
}
GenerateHilbert(depth - 1, yDistance, xDistance, points);
DrawRelative(xDistance, yDistance, points);
GenerateHilbert(depth - 1, xDistance, yDistance, points);
DrawRelative(yDistance, xDistance, points);
GenerateHilbert(depth - 1, xDistance, yDistance, points);
DrawRelative(-xDistance, -yDistance, points);
GenerateHilbert(depth - 1, -yDistance, -xDistance, points);
}
private void DrawRelative(float xDistance, float yDistance, List<PointF> points)
{
// Discover where the new X and Y points will be
PointF currentPoint = _previousPoint + new SizeF(xDistance, yDistance);
// Paint from the current position of X and Y to the new positions of X and Y
points.Add(currentPoint);
// Update the Current Location of X and Y
_previousPoint = currentPoint;
}
// Perform the Actual Drawing
private void HilbertCurve_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
if (_points != null)
{
float scale = Math.Min(ClientSize.Width, ClientSize.Height);
e.Graphics.ScaleTransform(scale, scale);
using (Pen pen = new Pen(Color.Red, 1 / scale))
{
e.Graphics.DrawLines(pen, _points);
}
}
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
Redraw();
}
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
Invalidate();
}
}
和以前一样,我稍微修改了您的实现,以便绘图按比例缩放以适应 window 的所有深度。这涉及绘制到单位正方形中,然后根据 window 大小适当地设置 t运行sform。
除了修复 Graphics
的基本用法以及 xLength
和 yLength
字段的问题外,我还修复了您代码中的一个小错误(您在哪里递归一层太深)并稍微清理一下递归(不需要重复深度检查……只做一次,在递归方法的开始)。
当然也可以在 "immediate" 样式中实现它。我认为在这个新代码示例和上面的 "immediate" 方法示例之间,我可以将练习留给 reader。 :)
这是我在听取@Peter Duniho 的建议后想出的分形生成器 - 显示的代码不包括实际获取用户请求的递归深度级别 (maxDepth) 的形式。
public partial class HilbertDisplay : Form
{
private int maxDepth;
private int xCurrent = 0;
private int yCurrent = 0;
private int xNew = 0;
private int yNew = 0;
public HilbertDisplay(int depthEntered)
{
InitializeComponent();
maxDepth = depthEntered;
}
private void HilbertDisplay_Load(object sender, EventArgs e)
{
this.DoubleBuffered = true;
this.Update();
}
// Perform the Drawing
private void HilbertDisplay_Paint(object sender, PaintEventArgs e)
{
// Run the Hilbert Curve Generator
// Use a line segment length of 10 for Y
GenerateHilbertCurve(maxDepth, 0, 10, e);
}
// The Recursive Hilbert Curve Generator
private void GenerateHilbertCurve(int depth, int xDistance, int yDistance, PaintEventArgs e)
{
if (depth < 1)
{
return;
}
else
{
GenerateHilbertCurve(depth - 1, yDistance, xDistance, e);
// Paint from the current position of X and Y to the new positions of X and Y
FindPointRelative(xDistance, yDistance);
e.Graphics.DrawLine(Pens.Red, xCurrent, yCurrent, xNew, yNew); // Draw Part of Curve Here
UpdateCurrentLocation();
GenerateHilbertCurve(depth - 1, xDistance, yDistance, e);
// Paint from the current position of X and Y to the new positions of X and Y
FindPointRelative(yDistance, xDistance);
e.Graphics.DrawLine(Pens.Blue, xCurrent, yCurrent, xNew, yNew); // Draw Part of Curve Here
UpdateCurrentLocation();
GenerateHilbertCurve(depth - 1, xDistance, yDistance, e);
// Paint from the current position of X and Y to the new positions of X and Y
FindPointRelative(-xDistance, -yDistance);
e.Graphics.DrawLine(Pens.Green, xCurrent, yCurrent, xNew, yNew); // Draw Part of Curve Here
UpdateCurrentLocation();
GenerateHilbertCurve(depth - 1, (-1 * yDistance), (-1 * xDistance), e);
}
}
private void FindPointRelative(int xDistance, int yDistance)
{
// Discover where the new X and Y points will be
xNew = xCurrent + xDistance;
yNew = yCurrent + yDistance;
return;
}
private void UpdateCurrentLocation()
{
// Update the Current Location of X and Y
xCurrent = xNew;
yCurrent = yNew;
return;
}
}
与@Peter Duniho 的代码不同,此代码不考虑表单的大小。这在我的笔记本电脑上描绘了递归深度为 6 或 7 的希尔伯特曲线分形(由于我的笔记本电脑屏幕 size/resolution 对 window 大小的限制)。
我知道我的解决方案不如@Peter Duniho 的解决方案优雅,但由于这是一项作业,我不想简单地复制他的代码。我根据他的建议进行了编辑,尤其是关于 Paint
事件。
我正在开展一个项目,我需要使用递归在 C# 的 Windows Forms 应用程序中绘制希尔伯特曲线分形。我必须为此使用 GDI+ 图形,但我是 GDI+ 图形的新手。下面是我实际绘制曲线的 Form class 的完整代码。在此 post 的末尾,我附上了展示我的错误输出和预期输出的照片。
DrawRelative()
函数应该绘制从当前 [x,y] 坐标到新 [x,y] 坐标的下一条线段,这是通过添加 xDistance
和 yDistance
值传递到 DrawRelative()
函数到 xCurrent
和 yCurrent
class 属性。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace HilbertCurveFractal
{
public partial class FractalDisplay : Form
{
public int MaxDepth { get; set; }
public int CurveType { get; set; }
public int xCurrent { get; set; }
public int yCurrent { get; set; }
public int xLength { get; set; }
public int yLength { get; set; }
public FractalDisplay(int DepthValue, int SelectedCurve)
{
InitializeComponent();
MaxDepth = DepthValue;
CurveType = SelectedCurve;
xCurrent = 250;
yCurrent = 250;
xLength = 0;
yLength = 2;
}
private void FractalDisplay_Load(object sender, EventArgs e)
{
this.DoubleBuffered = true;
if (CurveType == 1) // Run the Hilbert Curve Generator
{
GenerateHilbertCurve(MaxDepth, xLength, yLength);
}
else if (CurveType == 2) // Run the Koch Curve Generator
{
}
else if (CurveType == 3) // Run the Sierpinski Curve Generator
{
}
else
{
MessageBox.Show("Error! - No Curve Type Selected. Ending Program.");
Application.Exit();
}
}
private void GenerateHilbertCurve(int depth, int xDistance, int yDistance)
{
//if (depth == 0) // Base Case
//{
// return;
//}
//else { }
if (depth > 0)
{
GenerateHilbertCurve(depth - 1, yDistance, xDistance);
}
else { }
// Draw Part of Curve Here
DrawRelative(xDistance, yDistance);
if (depth > 0)
{
GenerateHilbertCurve(depth - 1, xDistance, yDistance);
}
else { }
// Draw Part of Curve Here
DrawRelative(yDistance, xDistance);
if (depth > 0)
{
GenerateHilbertCurve(depth - 1, xDistance, yDistance);
}
else { }
// Draw Part of Curve Here
DrawRelative((- 1 * xDistance), (-1 * yDistance));
if (depth > 0)
{
GenerateHilbertCurve(depth - 1, (-1 * yDistance), (-1 * xDistance));
}
else { }
}
// Create a New Paint Event Handler
private void DrawRelative(int xDistance, int yDistance)
{
xLength = xDistance;
yLength = yDistance;
this.Paint += new PaintEventHandler(HilbertCurve_Paint);
}
// Perform the Actual Drawing
private void HilbertCurve_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
// Discover where the new X and Y points will be
int xNew, yNew;
xNew = xCurrent + xLength;
yNew = yCurrent + yLength;
// Paint from the current position of X and Y to the new positions of X and Y
e.Graphics.DrawLine(Pens.Red, xCurrent, yCurrent, xNew, yNew);
// Update the Current Location of X and Y
xCurrent = xNew;
yCurrent = yNew;
}
}
}
第一张照片(下图)是在 MaxDepth 为 1 的情况下希尔伯特曲线函数的错误输出。
第二张照片(下图)表示我应该从这组函数中得到什么(假设传入的 MaxDepth 值为 1)。
因为似乎递归算法编码正确,我怀疑我没有以正确的方式使用 GDI+ 图形,或者我的 class 属性在某处被错误地 updated/set递归调用。我能做些什么来修复我的绘图算法?提前谢谢你。
老实说,我最初并不了解您为希尔伯特曲线生成点的实现。我熟悉几种不同的方法,但都不像那样。
但是,这是一个完全不同的问题。您手头的主要问题实际上只是您不了解 Winforms 中的绘图机制是如何工作的。简而言之:有一个 Paint
事件,您的代码应该通过绘制需要绘制的内容来处理该事件。订阅 Paint
事件不会导致任何事情 发生 ;这只是一种注册方式,以便在绘图应该发生时收到通知。
通常,人们会使用设计器订阅事件,方法是导航到设计器中某个对象(例如您的表单)的“属性”窗格的 "Events" 选项卡并选择适当的事件处理程序 (或双击事件旁边的空框,让 Designer 自动插入一个空的处理程序供您填写)。您也可以在自己的对象中处理 Paint
事件时,简单地覆盖 OnPaint()
方法。
在任何一种情况下,正确的技术是建立绘图的先决条件,然后调用 Invalidate()
导致框架然后引发 Paint
事件,此时您可以实际绘制什么你想画画
请注意,在评论者 TaW 和我之间,我们提出了两种不同的绘图方法:我建议预先计算绘图所需的所有数据,然后在引发 Paint
事件时绘制它; TaW 建议从 Paint
事件处理程序调用递归方法,并在遍历递归算法时直接绘制。
这两种技术都很好,但当然各有利弊,主要与 classic 时间和 space 的权衡有关。使用前一种技术,生成曲线的成本仅在曲线参数发生变化时产生一次。绘图发生得更快,因为代码所要做的就是绘制预先存在的数据。使用后一种技术,无需存储数据,因为每个生成的新点都会立即使用,但这当然意味着每次重绘 window 时都必须重新生成所有点。
对于这个特定的应用程序,实际上我认为它并不重要。在典型的屏幕分辨率下,在开始达到要绘制的点的数据存储限制之前,您将无法确定曲线的特征。同样,算法的执行速度如此之快,以至于每次 window 需要重绘时重新计算点数也没有什么坏处。请记住,这些是您在其他情况下可能需要更仔细地判断的权衡。
好的,那是什么意思?好吧,当我将它转换为正确使用 Graphics
class 的东西时,我无法让您的实现绘制希尔伯特曲线,所以我更改了那部分代码以使用我知道的实现作品。您可以在此处找到有关此特定实现如何工作的详细讨论:希尔伯特曲线
概念与实施
下面,我提供了该特定希尔伯特曲线实现的两个不同版本,第一个使用 "retained" 方法(即生成数据,然后绘制它),第二个使用 "immediate" 方法(即每次要绘制 window 时生成数据,因为正在绘制):
"Retained"方法:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
DoubleBuffered = true;
}
private PointF[] _points;
private void FractalDisplay_Load(object sender, EventArgs e)
{
Redraw();
}
private void Redraw()
{
List<PointF> points = new List<PointF>();
GenerateHilbert(0, 0, 1, 0, 0, 1, (int)numericUpDown1.Value, points);
_points = points.ToArray();
Invalidate();
}
private void GenerateHilbert(PointF origin, float xi, float xj, float yi, float yj, int depth, List<PointF> points)
{
if (depth <= 0)
{
PointF current = origin + new SizeF((xi + yi) / 2, (xj + yj) / 2);
points.Add(current);
}
else
{
GenerateHilbert(origin, yi / 2, yj / 2, xi / 2, xj / 2, depth - 1, points);
GenerateHilbert(origin + new SizeF(xi / 2, xj / 2), xi / 2, xj / 2, yi / 2, yj / 2, depth - 1, points);
GenerateHilbert(origin + new SizeF(xi / 2 + yi / 2, xj / 2 + yj / 2), xi / 2, xj / 2, yi / 2, yj / 2, depth - 1, points);
GenerateHilbert(origin + new SizeF(xi / 2 + yi, xj / 2 + yj), -yi / 2, -yj / 2, -xi / 2, -xj / 2, depth - 1, points);
}
}
// Perform the Actual Drawing
private void HilbertCurve_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
if (_points != null)
{
float scale = Math.Min(ClientSize.Width, ClientSize.Height);
e.Graphics.ScaleTransform(scale, scale);
using (Pen pen = new Pen(Color.Red, 1 / scale))
{
e.Graphics.DrawLines(pen, _points);
}
}
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
Redraw();
}
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
Invalidate();
}
}
"Immediate"方法:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
DoubleBuffered = true;
}
private void Redraw()
{
Invalidate();
}
private PointF GenerateHilbert(PointF origin, float xi, float xj, float yi, float yj, int depth,
PointF? previous, Graphics graphics, Pen pen)
{
if (depth <= 0)
{
PointF current = origin + new SizeF((xi + yi) / 2, (xj + yj) / 2);
if (previous != null)
{
graphics.DrawLine(pen, previous.Value, current);
}
return current;
}
else
{
previous = GenerateHilbert(origin, yi / 2, yj / 2, xi / 2, xj / 2, depth - 1, previous, graphics, pen);
previous = GenerateHilbert(origin + new SizeF(xi / 2, xj / 2), xi / 2, xj / 2, yi / 2, yj / 2, depth - 1, previous, graphics, pen);
previous = GenerateHilbert(origin + new SizeF(xi / 2 + yi / 2, xj / 2 + yj / 2), xi / 2, xj / 2, yi / 2, yj / 2, depth - 1, previous, graphics, pen);
return GenerateHilbert(origin + new SizeF(xi / 2 + yi, xj / 2 + yj), -yi / 2, -yj / 2, -xi / 2, -xj / 2, depth - 1, previous, graphics, pen);
}
}
// Perform the Actual Drawing
private void HilbertCurve_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
float scale = Math.Min(ClientSize.Width, ClientSize.Height);
e.Graphics.ScaleTransform(scale, scale);
using (Pen pen = new Pen(Color.Red, 1 / scale))
{
GenerateHilbert(new PointF(), 1, 0, 0, 1, (int)numericUpDown1.Value, null, e.Graphics, pen);
}
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
Redraw();
}
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
Invalidate();
}
}
在这两个示例中,我都做了一些其他更改,这些更改并不是为了说明这些技术而严格需要的,但仍然有用:
- 曲线本身以 space 为单位计算(即边长为 1 的正方形),然后通过缩放绘图以适合 window. 来绘制
- 在有意义的情况下,各个坐标作为整个
PointF
值传递。这简化了值的重用并向 X 和 Y 值添加新的偏移量。 - 由于绘图现在已缩放到 window,因此如果 window 的大小发生变化,则会重新绘制。
- 为了简单起见,这个
Form
是自包含的,有一个NumericUpDownControl
决定递归深度。我没有包括这个控件的实例化;我想你可以在设计器中自己添加适当的控件,使上面的编译。
附录:
我有幸查看了您尝试实现的算法在 Internet 上的其他示例。现在我明白了算法的基本机制是什么,我能够修复你的版本以使其工作(主要问题是你使用实例字段来存储算法的增量,但也使用相同的字段来初始化算法,所以一旦算法 运行 一次,后续执行将不起作用)。因此,为了完整起见,这里是代码的第二个 "retained" 版本,使用您喜欢的算法而不是我上面使用的算法:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
DoubleBuffered = true;
}
private PointF _previousPoint;
private PointF[] _points;
private void FractalDisplay_Load(object sender, EventArgs e)
{
Redraw();
}
private void Redraw()
{
List<PointF> points = new List<PointF>();
// Start here, to provide a bit of margin within the client area of the window
_previousPoint = new PointF(0.025f, 0.025f);
points.Add(_previousPoint);
int depth = (int)numericUpDown1.Value;
float gridCellCount = (float)(Math.Pow(2, depth) - 1);
// Use only 95% of the available space in the client area. Scale
// the delta for drawing to fill that 95% width/height exactly,
// according to the number of grid cells the given depth will
// produce in each direction.
GenerateHilbert3(depth, 0, 0.95f / gridCellCount, points);
_points = points.ToArray();
Invalidate();
}
private void GenerateHilbert(int depth, float xDistance, float yDistance, List<PointF> points)
{
if (depth < 1)
{
return;
}
GenerateHilbert(depth - 1, yDistance, xDistance, points);
DrawRelative(xDistance, yDistance, points);
GenerateHilbert(depth - 1, xDistance, yDistance, points);
DrawRelative(yDistance, xDistance, points);
GenerateHilbert(depth - 1, xDistance, yDistance, points);
DrawRelative(-xDistance, -yDistance, points);
GenerateHilbert(depth - 1, -yDistance, -xDistance, points);
}
private void DrawRelative(float xDistance, float yDistance, List<PointF> points)
{
// Discover where the new X and Y points will be
PointF currentPoint = _previousPoint + new SizeF(xDistance, yDistance);
// Paint from the current position of X and Y to the new positions of X and Y
points.Add(currentPoint);
// Update the Current Location of X and Y
_previousPoint = currentPoint;
}
// Perform the Actual Drawing
private void HilbertCurve_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
if (_points != null)
{
float scale = Math.Min(ClientSize.Width, ClientSize.Height);
e.Graphics.ScaleTransform(scale, scale);
using (Pen pen = new Pen(Color.Red, 1 / scale))
{
e.Graphics.DrawLines(pen, _points);
}
}
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
Redraw();
}
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
Invalidate();
}
}
和以前一样,我稍微修改了您的实现,以便绘图按比例缩放以适应 window 的所有深度。这涉及绘制到单位正方形中,然后根据 window 大小适当地设置 t运行sform。
除了修复 Graphics
的基本用法以及 xLength
和 yLength
字段的问题外,我还修复了您代码中的一个小错误(您在哪里递归一层太深)并稍微清理一下递归(不需要重复深度检查……只做一次,在递归方法的开始)。
当然也可以在 "immediate" 样式中实现它。我认为在这个新代码示例和上面的 "immediate" 方法示例之间,我可以将练习留给 reader。 :)
这是我在听取@Peter Duniho 的建议后想出的分形生成器 - 显示的代码不包括实际获取用户请求的递归深度级别 (maxDepth) 的形式。
public partial class HilbertDisplay : Form
{
private int maxDepth;
private int xCurrent = 0;
private int yCurrent = 0;
private int xNew = 0;
private int yNew = 0;
public HilbertDisplay(int depthEntered)
{
InitializeComponent();
maxDepth = depthEntered;
}
private void HilbertDisplay_Load(object sender, EventArgs e)
{
this.DoubleBuffered = true;
this.Update();
}
// Perform the Drawing
private void HilbertDisplay_Paint(object sender, PaintEventArgs e)
{
// Run the Hilbert Curve Generator
// Use a line segment length of 10 for Y
GenerateHilbertCurve(maxDepth, 0, 10, e);
}
// The Recursive Hilbert Curve Generator
private void GenerateHilbertCurve(int depth, int xDistance, int yDistance, PaintEventArgs e)
{
if (depth < 1)
{
return;
}
else
{
GenerateHilbertCurve(depth - 1, yDistance, xDistance, e);
// Paint from the current position of X and Y to the new positions of X and Y
FindPointRelative(xDistance, yDistance);
e.Graphics.DrawLine(Pens.Red, xCurrent, yCurrent, xNew, yNew); // Draw Part of Curve Here
UpdateCurrentLocation();
GenerateHilbertCurve(depth - 1, xDistance, yDistance, e);
// Paint from the current position of X and Y to the new positions of X and Y
FindPointRelative(yDistance, xDistance);
e.Graphics.DrawLine(Pens.Blue, xCurrent, yCurrent, xNew, yNew); // Draw Part of Curve Here
UpdateCurrentLocation();
GenerateHilbertCurve(depth - 1, xDistance, yDistance, e);
// Paint from the current position of X and Y to the new positions of X and Y
FindPointRelative(-xDistance, -yDistance);
e.Graphics.DrawLine(Pens.Green, xCurrent, yCurrent, xNew, yNew); // Draw Part of Curve Here
UpdateCurrentLocation();
GenerateHilbertCurve(depth - 1, (-1 * yDistance), (-1 * xDistance), e);
}
}
private void FindPointRelative(int xDistance, int yDistance)
{
// Discover where the new X and Y points will be
xNew = xCurrent + xDistance;
yNew = yCurrent + yDistance;
return;
}
private void UpdateCurrentLocation()
{
// Update the Current Location of X and Y
xCurrent = xNew;
yCurrent = yNew;
return;
}
}
与@Peter Duniho 的代码不同,此代码不考虑表单的大小。这在我的笔记本电脑上描绘了递归深度为 6 或 7 的希尔伯特曲线分形(由于我的笔记本电脑屏幕 size/resolution 对 window 大小的限制)。
我知道我的解决方案不如@Peter Duniho 的解决方案优雅,但由于这是一项作业,我不想简单地复制他的代码。我根据他的建议进行了编辑,尤其是关于 Paint
事件。