如何使用 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] 坐标的下一条线段,这是通过添加 xDistanceyDistance 值传递到 DrawRelative() 函数到 xCurrentyCurrent 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 的基本用法以及 xLengthyLength 字段的问题外,我还修复了您代码中的一个小错误(您在哪里递归一层太深)并稍微清理一下递归(不需要重复深度检查……只做一次,在递归方法的开始)。

当然也可以在 "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 事件。