【问题标题】:Moving within a designated circle in C#在 C# 中的指定圆圈内移动
【发布时间】:2015-04-21 03:43:10
【问题描述】:

我正在尝试用 C# 制作一个简单的项目,但是由于我是新手,所以遇到了很多麻烦。 我想做的是模拟苍蝇在一个圆圈内的运动。 它应该与鼠标点击一起工作——第一次点击,你选择了一个圆的中心。第二次单击,您选择了圆的半径并绘制它。在第三次单击时(在圆圈内),您应该画一只苍蝇(我们现在将其设为一个小的填充椭圆),它应该立即开始四处移动。

我已经尝试完成这项工作一个多星期了,但几乎没有任何结果。

我使用面板作为画布。

这是我目前所拥有的:

 private int start_x = 0;
    private int start_y = 0;
    private int end_x = 0;
    private int end_y = 0;
    private int fly_x = 0;
    private int fly_y = 0;
    private int clicks = 0;
    private int distance = 0;


    public Form1()
    {
        InitializeComponent();
    }

    private void panel1_MouseDown(object sender, MouseEventArgs e)
    {
        clicks++;
        if (clicks == 1)
        {
            start_x = e.X;
            start_y = e.Y;
        }
        else if (clicks == 2)
        {
            end_x = e.X;
            end_y = e.Y;
        }
        else if (clicks == 3)
        {
            fly_x = e.X;
            fly_y = e.Y;
        }
        else if (clicks == 4)
        {

        }
        this.panel1.Refresh();

    }

    private void panel1_Paint(object sender, PaintEventArgs e)
    {
        Random r = new Random();
        Thread t = new Thread(new ThreadStart(Fly));

        if (clicks == 1)
        {
            e.Graphics.DrawEllipse(Pens.Black, start_x - 2, start_y - 2, 4, 4);
        }
        else if (clicks == 2)
        {
            distance = Distance(start_x, start_y, end_x, end_y);
            e.Graphics.DrawEllipse(Pens.Black, start_x - 2, start_y - 2, 4, 4);
            e.Graphics.DrawEllipse(Pens.Black, start_x - distance, start_y - distance, distance * 2, distance * 2);             
        }
        else if (clicks == 3)
        {
            t.Start();
        }
        else if (clicks == 4)
        {
            t.Abort();
            clicks = 0;
        }
    }

    private int Distance(int a_x, int a_y, int b_x, int b_y)
    {
        int a, b;
        a = a_x - b_x;
        b = a_y - b_y;

        return (Convert.ToInt32(Math.Sqrt(a * a + b * b)));
    }

    private void Fly()
    {
        Random r = new Random();
        Graphics e = CreateGraphics();
        distance = Distance(start_x, start_y, end_x, end_y);

        while (clicks < 4)
        {             
            e.DrawEllipse(Pens.Black, start_x - 2, start_y - 2, 4, 4);
            e.DrawEllipse(Pens.Black, start_x - distance, start_y - distance, distance * 2, distance * 2);
            e.FillEllipse(Brushes.Red, fly_x - 3, fly_y - 3, 6, 6);
            fly_x = fly_x - 5 + r.Next(1, 11);
            fly_y = fly_y - 5 + r.Next(1, 11);
            Invalidate();
            if (Distance(start_x, start_y, fly_x, fly_y) > distance - 1)
            {
                if (fly_x < start_x)
                {
                    fly_x = fly_x + 5;
                }
                else
                {
                    fly_x = fly_x - 5;
                }
                if (fly_y < start_y)
                {
                    fly_y = fly_y + 5;
                }
                else
                {
                    fly_y = fly_y - 5;
                }
            }
        }
    }

但是,显然,它根本不起作用。我可以画圆圈,但是一旦我再次点击面板,圆圈就会消失。你能指出我正确的方向吗?我的意思是,我做错了什么? 我试图在不使用线程的情况下做到这一点,甚至可以画出苍蝇,但在那一刻,整个事情都冻结了。 编辑: 我删除了线程并放了一个计时器:

 private void panel1_Paint(object sender, PaintEventArgs e)
    {
        Random r = new Random();
        this.DoubleBuffered = true;

        if (clicks == 1)
        {
            e.Graphics.DrawEllipse(Pens.Black, start_x - 2, start_y - 2, 4, 4);
        }
        else if (clicks == 2)
        {
            distance = Distance(start_x, start_y, end_x, end_y);
            e.Graphics.DrawEllipse(Pens.Black, start_x - 2, start_y - 2, 4, 4);
            e.Graphics.DrawEllipse(Pens.Black, start_x - distance, start_y - distance, distance * 2, distance * 2);             
        }
        else if (clicks == 3)
        {
            e.Graphics.DrawEllipse(Pens.Black, start_x - 2, start_y - 2, 4, 4);
            e.Graphics.DrawEllipse(Pens.Black, start_x - distance, start_y - distance, distance * 2, distance * 2);
            e.Graphics.FillEllipse(Brushes.Red, fly_x - 3, fly_y - 3, 6, 6);
        }
        else if (clicks == 4)
        {
            clicks = 0;
        }
    }

      private void timer1_Tick(object sender, EventArgs e)
    {
        Random r = new Random();
        fly_x = fly_x - 5 + r.Next(1, 11);
        fly_y = fly_y - 5 + r.Next(1, 11);
        if (Distance(start_x, start_y, fly_x, fly_y) > distance - 1)
        {
            if (fly_x < start_x)
            {
                fly_x = fly_x + 5;
            }
            else
            {
                fly_x = fly_x - 5;
            }
            if (fly_y < start_y)
            {
                fly_y = fly_y + 5;
            }
            else
            {
                fly_y = fly_y - 5;
            }
        }
        panel1.Invalidate();
    }

现在它可以工作了,但图像仍然闪烁很多,即使我使用了双缓冲。我从我的同事那里得到了一个解决方案,不知何故,他的任何方式都没有闪烁。除了双缓冲还有其他方法可以解决这个问题吗?

【问题讨论】:

  • 你不需要线程你需要一个计时器。
  • 您可以使用计时器,或者您可以在 Paint 方法的末尾调用 panel.Invalidate() 以确保它不断被重绘。我还建议您使用自定义控件而不是面板。这将允许您在构造函数中启用 OptimizedDoubleBuffer 样式。没有它,您会看到一些难看的闪烁,因为 Windows 在每次调用 Paint 之前都会使用背景颜色擦除您的绘图。
  • 我确实尝试使用计时器,但它没有做任何事情。然而,很有可能是我用错了。使用定时器有什么需要注意的吗?
  • 您的计时器应该在每个滴答声中调用 panel.Invalidate()。此外,请确保您已通过设置 Enabled = true 或调用 Start() 方法来启动计时器。我建议滴答间隔不超过 50 毫秒。
  • 只需在 Paint 结束时调用 panel.Invalidate() 不,这将导致循环&crash|freeze!使用计时器并将 Intervall (ms) 设置为您想要的速度。将随机变量移动到类级别也是一个好习惯。

标签: c# drawing


【解决方案1】:

在 Winforms 中避免闪烁并不难,但它确实需要持久性。 Winforms 没有任何类型的合成引擎——它是非托管 Windows user32.dll API 之上的一个薄层,窗口中的每个控件本身就是一个单独绘制的窗口——因此每个可能闪烁的控件都需要被双缓冲。

这是您的代码版本,闪烁:

public partial class Form1 : Form
{
    private readonly Random r = new Random();

    private int start_x = 0;
    private int start_y = 0;
    private int end_x = 0;
    private int end_y = 0;
    private int fly_x = 0;
    private int fly_y = 0;
    private int clicks = 0;
    private int distance = 0;

    public Form1()
    {
        InitializeComponent();
        DoubleBuffered = true;
        label1.Text = "clicks: 0";
    }

    protected override void OnMouseDown(MouseEventArgs e)
    {
        base.OnMouseDown(e);

        clicks++;
        if (clicks == 1)
        {
            start_x = e.X;
            start_y = e.Y;
        }
        else if (clicks == 2)
        {
            end_x = e.X;
            end_y = e.Y;
        }
        else if (clicks == 3)
        {
            fly_x = e.X;
            fly_y = e.Y;
            timer1.Start();
        }
        else if (clicks == 4)
        {
            timer1.Stop();
            clicks = 0;
        }
        label1.Text = "clicks: " + clicks;
        Invalidate();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        if (clicks == 1)
        {
            e.Graphics.DrawEllipse(Pens.Black, start_x - 2, start_y - 2, 4, 4);
        }
        else if (clicks == 2)
        {
            distance = Distance(start_x, start_y, end_x, end_y);
            e.Graphics.DrawEllipse(Pens.Black, start_x - 2, start_y - 2, 4, 4);
            e.Graphics.DrawEllipse(Pens.Black, start_x - distance, start_y - distance, distance * 2, distance * 2);
        }
        else if (clicks == 3)
        {
            e.Graphics.DrawEllipse(Pens.Black, start_x - 2, start_y - 2, 4, 4);
            e.Graphics.DrawEllipse(Pens.Black, start_x - distance, start_y - distance, distance * 2, distance * 2);
            e.Graphics.FillEllipse(Brushes.Red, fly_x - 3, fly_y - 3, 6, 6);
        }
    }

    private int Distance(int a_x, int a_y, int b_x, int b_y)
    {
        int a, b;
        a = a_x - b_x;
        b = a_y - b_y;

        return (Convert.ToInt32(Math.Sqrt(a * a + b * b)));
    }

    private void timer1_Tick(object sender, EventArgs e)
    {
        fly_x = fly_x + r.Next(-4, 5);
        fly_y = fly_y + r.Next(-4, 5);
        if (Distance(start_x, start_y, fly_x, fly_y) > distance - 1)
        {
            if (fly_x < start_x)
            {
                fly_x = fly_x + 5;
            }
            else
            {
                fly_x = fly_x - 5;
            }
            if (fly_y < start_y)
            {
                fly_y = fly_y + 5;
            }
            else
            {
                fly_y = fly_y - 5;
            }
        }

        Invalidate();
    }
}

请注意,我在这里没有绘制到 Panel 对象,而是使用了 Form1 对象本身。您可以将相同的基本技术应用于Panel 或其他自定义控件来完成相同的基本结果,但这意味着创建自己的Panel 子类(或常规自定义控件,继承Control)并将所有将上面的代码放入那个而不是表单中。 IE。您仍将在OnMouseDown()OnPaint() 方法上使用override,而不是附加事件处理程序;您仍然可以使用设计器将Timer 对象拖放到您的自定义类中。

关键是实际绘制的控件需要将其DoubleBuffered 属性设置为true(默认为false)。由于该属性不是public,因此正确的做法是对类型进行子类化并在构造函数中设置它(您可以在普通的Panel 对象上使用反射,但这很丑陋且不明显)。在上面,Form1 类已经是我可以控制的子类,因此在此处设置属性比为此创建新的自定义控件更容易。

关于代码的其他几点:

  • 您可能需要考虑使用MouseClick 事件而不是MouseDown 进行鼠标交互。这种行为可能更符合用户的期望;两个主要区别是,事件发生在鼠标按下而不是鼠标按下时,如果用户在释放鼠标按钮之前从控件中拖出,它根本不会发生(即为用户提供一种改变他们的方法注意并避免点击)。
  • 我注意到你的随机游走在我看来是一个错误。您生成的值介于 -4 和 +5 之间,这当然会导致“苍蝇”移动到圆圈的右下角。我更改了您的随机化代码,以便它生成从 -4 到 4 的值,从而在各个方向上均匀分布。我在上面的代码示例中修复了这个问题,只需为 Next() 方法重载提供正确的值,并从计算中删除不需要的 - 5
  • 最重要的是,您的随机化代码中有一个严重但常见的错误:每次您想要一个新值时,您都会创建一个新的Random 对象。这会产生 非随机 结果,因为默认情况下,随机数生成器是使用系统时钟播种的。在最坏的情况下,代码选择值的速度非常快,以至于每个新的Random 对象都以相同的方式初始化,因此返回相同的值;即使在最好的情况下,随机值之间的代码延迟,返回的值仍然与时钟相关,而不是真正的随机分布。我在上面的代码示例中通过将 Random 对象移动到带有初始化程序的私有字段来解决此问题,以便生成良好的随机值序列(技术上,“伪随机”)。

【讨论】:

  • 仅供参考:PictureBox 是一种简单的方法来获取默认情况下启用 DoubleBuffering 的控件。当只有一个小错误在移动时,我也会使用 Invalidate(Rectangle) 重载;-)
  • @TaW:确实如此,但恕我直言,这是为了设置标志而携带的很多额外的“重量”(可能不是运行时,但至少在概念上)。您是对的,仅使更改的区域无效更有效;但是,根据我的经验,学习事件驱动渲染模型的人很难正确确定要失效的矩形。恕我直言,这作为一个“中间”主题更好,尽管在这里,绘图案例是如此简单,它可能不会分散主要的注意力。
  • 谢谢@Peter Duniho。这是一个非常详细的答案,尽管我主要是初学者,但我仍然理解其中的大部分内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多