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

时间:2016-04-22 03:11:41

标签: c# winforms recursion gdi+ fractals

我正在开发一个项目,我需要使用递归在C#中的Windows窗体应用程序中绘制希尔伯特曲线分形。我必须使用GDI +图形,但我是GDI +图形的新手。下面是我实际绘制曲线的Form类的完整代码。在这篇文章的最后,我已经包含了证明我的错误输出和预期输出的照片。

@Override public int getCount() { return 1; } 函数应该将当前[x,y]坐标的下一个线段绘制到新的[x,y]坐标,这些坐标是通过添加DrawRelative()和{来计算的。传递到xDistance函数的{1}}值到yDistanceDrawRelative()类属性。

xCurrent

第一张照片(下图)是希尔伯特曲线函数输出错误,给出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 +图形,或者我的类属性在递归调用中的某处更新/设置不正确。我该怎么做才能修复绘图算法?提前谢谢。

2 个答案:

答案 0 :(得分:2)

说实话,我最初并不了解你为Hilbert曲线生成点的实现。我熟悉几种不同的方法,两种方法都不一样。

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

通常,人们可以使用Designer订阅活动,方法是导航到"事件" Designer中对象的“属性”窗格的选项卡(例如,您的表单)并选择适当的事件处理程序(或双击事件旁边的空框以使Designer自动插入空的处理程序以供您填写) 。您还可以在处理自己对象中的Paint事件时,只需覆盖OnPaint()方法。

在任何一种情况下,正确的技术是建立绘图的先决条件,然后调用Invalidate()导致框架然后引发Paint事件,此时你可以实际绘制你想要的画画。

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

这两种技术都很好,但当然也有优点和缺点,主要与经典的时间和空间权衡有关。使用前一种技术,当曲线的参数发生变化时,生成曲线的成本仅产生一次。绘图发生得更快,因为所有代码都要绘制预先存在的数据。使用后一种技术,不需要存储数据,因为生成的每个新点都是立即使用的,但当然这意味着每次重绘窗口时都必须重新生成所有点。

对于这个特殊应用,在实践中我并不认为这很重要。在典型的屏幕分辨率下,在开始达到要绘制的点的数据存储限制之前,您很快就无法确定曲线的特征。类似地,算法的执行速度非常快,以至于每次需要重绘窗口时重新计算点都没有坏处。请记住,这些是您可能需要在其他情况下更密切判断的权衡。


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

下面,我提供了两个不同版本的特定希尔伯特曲线实现,第一个使用"保留"方法(即生成数据,然后绘制数据),第二个使用"立即"方法(即每当你想要绘制窗口时生成数据,如图所示):

<强>&#34;保留&#34;方法

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

<强>&#34;立即&#34;方法

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中自己添加适当的控件,以进行上述编译。


附录:

我有机会查看您尝试实施的算法在互联网上的其他示例。现在我了解算法的基本机制是什么,我能够修复你的版本以便它可以工作(主要的问题是你使用实例字段来存储算法的增量,但也使用相同的字段来初始化算法,因此一旦算法运行一次,后续执行就不起作用了。所以为了完整起见,这里有第二个&#34;保留&#34;代码的版本,使用您首选的算法而不是我上面使用的算法:

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字段的问题之外,我还修复了代码中的一个小错误(您在其中复制了一个错误)水平太深了)并稍微清理了递归(没有必要重复深度检查......只需在递归方法的开头做一次)。

当然可以在&#34;立即&#34;中实现这一点。风格也是如此。我想在这个新的代码示例和&#34; immediate&#34;上面的方法示例,我可以把这个练习留给读者。 :)

答案 1 :(得分:0)

这是我听到@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不同,此代码不考虑表单的大小。这描绘了一个Hilbert曲线分形,在我的笔记本电脑上的递归深度为6或7(由于我的笔记本电脑屏幕尺寸/分辨率对窗口尺寸的限制)。

我知道我的解决方案并不像@Peter Duniho那样优雅,但由于这是一项任务,我不想简单地复制他的代码。我根据他的建议进行了编辑,特别是关于Paint事件。