c# - 如何使用C#GDI +图形和Windows窗体递归绘制希尔伯特曲线分形?

标签 c# winforms recursion gdi+ fractals

我正在一个项目中,我需要在C#的Windows Forms应用程序中使用递归来绘制希尔伯特曲线分形。为此,我必须使用GDI +图形,但是我是GDI +图形的新手。下面是我实际绘制曲线的Form类的全部代码。在这篇文章的结尾,我包括了一些照片,这些照片证明了我的错误输出和预期的输出。

DrawRelative()函数应该从当前[x,y]坐标到新的[x,y]坐标绘制下一个线段,这是通过将xDistanceyDistance值添加到< cc>函数添加到DrawRelative()xCurrent类属性。

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,第一张照片(下图)是希尔伯特曲线函数的错误输出。

Incorrect Output from Hilbert Curve Function Given a Depth of 1

第二张照片(下图)表示我应该从这套函数中得到的结果(传入的MaxDepth值为1)。
Correct Demonstration of Hilbert Curve Function Given a Depth of 1

因为似乎递归算法编码正确,所以我怀疑我没有以正确的方式使用GDI +图形,或者我的类属性在递归调用中某处被错误地更新/设置。我该如何解决我的绘图算法?先感谢您。

最佳答案

老实说,我最初不了解生成希尔伯特曲线点的实现方法。我熟悉几种不同的方法,但都不是那样。

但是,这是一个完全不同的问题。您面临的主要问题实际上就是您不了解Winforms中的绘制机制是如何工作的。简要地说:有一个Paint事件,您的代码应通过绘制需要绘制的内容来处理该事件。订阅Paint事件不会导致任何事情发生。它只是一种在应该发生图纸时进行通知的注册方式。

通常,通过导航到“设计器”中某个对象(例如,窗体)的“属性”窗格的“事件”选项卡,然后选择适当的事件处理程序(或在空白处双击),可以使用设计器来订阅事件。事件旁边的框,以使设计器自动插入一个空处理程序供您填写)。您还可以在自己的对象中处理Paint事件时,只需覆盖OnPaint()方法。

无论哪种情况,正确的技术都是确定绘图的先决条件,然后调用Invalidate(),这将导致框架引发Paint事件,此时您可以实际绘制要绘制的内容。

请注意,在评论者TaW和我之间,我们提出了两种不同的绘制方法:我建议对计算所需的所有数据进行预计算,然后在引发Paint事件时进行绘制。 TaW建议从Paint事件处理程序中调用递归方法,并在遍历递归算法时直接进行绘制。

两种技术都很好,但是两者都各有利弊,这主要与时间和空间的经典权衡有关。使用前一种技术,当曲线的参数更改时,生成曲线的成本仅产生一次。绘制速度更快,因为所有代码所需要做的就是绘制预先存在的数据。使用后一种技术时,无需存储数据,因为会立即使用生成的每个新点,但这当然意味着每次重新绘制窗口时都必须重新生成所有点。

对于这个特定的应用程序,实际上我认为它并不重要。在典型的屏幕分辨率下,在开始达到要绘制的点的数据存储限制之前,您将无法分辨出曲线的特征。同样,算法的执行速度如此之快,以至于每次需要重绘窗口时,重新计算点实际上都没有害处。请记住,这些是您在其他情况下可能需要更仔细地判断的折衷方案。


好吧,那是什么意思呢?好吧,当我将其转换为正确使用Graphics类的东西时,我无法获得您的实现来绘制希尔伯特曲线,因此我更改了代码部分以使用我知道的实现。您可以在此处找到有关此特定实现方式的详细讨论:Hilbert Curve Concepts & Implementation

下面,我提供了该特定希尔伯特曲线实现的两种不同版本,第一种使用“保留”方法(即生成数据,然后绘制数据),第二种使用“立即”方法(即每次生成数据)您要绘制窗口,因为绘制正在进行中):

“保留”方法:

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();
    }
}


“立即”方法:

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();
    }
}


在两个示例中,我都进行了一些其他更改,这些更改对于说明这些技术不是严格必需的,但仍然有用:


曲线本身是在单位空间(即边长为1的正方形)中计算的,然后通过缩放绘图以适合窗口来绘制。
在有意义的情况下,将单独的坐标传递为整个PointF值。这简化了值的重用,并为X和Y值添加了新的偏移量。
由于现在将图形缩放到窗口,因此如果窗口的大小发生更改,则会重新绘制窗口。
为简单起见,此Form是自包含的,带有确定递归深度的NumericUpDownControl。我没有包含此控件的实例化;我想您可以在Designer中自己添加适当的控件,以进行上述编译。



附录:

我有机会在Internet上查看您尝试实现的算法的其他示例。现在,我了解了算法的基本机制是什么,我可以修复您的版本,使其起作用(主要问题是,您正在使用实例字段存储算法的增量,还使用了相同的字段来初始化算法,因此一旦算法运行一次,后续执行将无法执行。因此,为了完整起见,这是代码的第二个“保留”版本,使用的是您喜欢的算法,而不是我上面使用的算法:

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();
    }
}


和以前一样,我对您的实现进行了一些修改,以便缩放图形以适合所有深度的窗口。这涉及绘制成单位正方形,然后根据窗口大小适当地设置变换。

除了修复Graphics的基本用法以及xLengthyLength字段的问题之外,我还修复了代码中的一个小错误(递归的层次太深了)并清理了递归一点点(不需要重复深度检查……在递归方法开始时只需重复一次)。

当然也可以以“立即”的方式实现。我认为在这个新代码示例和上面的“立即”方法示例之间,我可以将练习留给读者。 :)

关于c# - 如何使用C#GDI +图形和Windows窗体递归绘制希尔伯特曲线分形?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36784146/

相关文章:

c# - ClearSelection 后选中的 Datagridview 单元格

python - 来自 N 个非连续重复的 M 个元素的组合

java - java中如何将递归函数转换为迭代函数?

c# - 获取 Windows 应用程序和 ASP.NET 应用程序的应用程序路径的统一方法

c# - "k += c += k += c;"中是否有内联运算符的解释?

c# - 为什么在所有初始订阅者断开连接后 RefCount 不工作? (减少)

c# - 使用静态类来收集公共(public)事件处理程序是否可行?

c# - 如何找出 datagridview 中的最后一行并将其写入文本文件

php - 带有递归的 ArrayCollection::forAll

c# - 列表类的类结构?