0

Sorry if this sounds a bit of a dim question - I'm completely new to this. I'm trying to get a new ellipse to appear on each tick of a timer. So far, I have:

using System;
using System.Drawing;
using System.Windows.Forms;

namespace WindowsFormsApplication6
{
    public partial class Form1 : Form
    {
        private Bitmap DrawingArea;
        int numberofcircles = 0;
        int[] narrary = new int[30];
        int newcircle;
        Random rnd = new Random();
        public Form1()
        {
            InitializeComponent();
            Invalidate();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            numberofcircles = numberofcircles + 1;
            newcircle = (rnd.Next(15) * 6) + 76;
            narrary[numberofcircles] = newcircle;
            Invalidate();
            timer1.Start();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            DrawingArea = new Bitmap(this.ClientRectangle.Width, this.ClientRectangle.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            Pen pen = new Pen(Color.Black);
            using (var canvas = Graphics.FromImage(DrawingArea))
            { canvas.Clear(Color.Transparent);
                canvas.DrawLine(pen, 100, 100, 700, 100);
                for (int i = 1; i <= numberofcircles; i++)
                {
                    canvas.DrawEllipse(pen, 180 + (30 * i), narrary[i], 8, 6);
                }
            }
            this.Invalidate();
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            Pen pen = new Pen(Color.Red);
            for (int i = 1; i <= numberofcircles; i++)
            {
                canvas.DrawEllipse(pen, 180 + (30 * i), narrary[i], 8, 6);
            }
                e.Graphics.DrawImage(DrawingArea, 0, 0, DrawingArea.Width, DrawingArea.Height);
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            numberofcircles = numberofcircles + 1;
            newcircle = (rnd.Next(15) * 6) + 76;
            narrary[numberofcircles] = newcircle;
            for (int i = 1; i <= numberofcircles; i++)
            {
                canvas.DrawEllipse(pen, 180 + (30 * i), narrary[i], 8, 6);
            }
            Invalidate();
        }
    }
}

"canvas" and "pen" references are flagging up as errors in the Form1_Paint and timer1_Tick sections ("The name 'canvas' does not exist in the current context"). I'm sure I must be referencing them wrong, but I'm afraid I don't have the basic C# knowledge to be able to sort this out!

I'd be very grateful for any help.

Richard
  • 55
  • 6
  • 1
    Sections of code can only "see" variables that are declared in the same scope which is everything between brackets `{ }`. To get your objects visible to the event handlers you can declare them at the top of the class, e.g., right above `int numberofcircles = 0` you can put `Graphics canvas;`, then you can change the using line to: `using (canvas = Graphics.FromImage(DrawingArea))` (I got rid of `var`). And likewise with `pen.` `using` disposes of the object when it's done, so your `Form1_Paint` method will probably throw an exception as `canvas` has been disposed; maybe just reopen it. – Quantic Oct 10 '16 at 21:29
  • Thankyou - that was really helpful. Much appreciated. – Richard Oct 11 '16 at 23:25

1 Answers1

0

There are a number of concepts it looks like you need explaining.

First, as noted in the comments, you need to pay attention to "scoping". In C# (and most other languages), variables have a well-defined scope that prevent them from being visible except where they are relevant. The variable you're having trouble with are "local variables", and are valid only in the method in which they are declared.

If you want those variables to be visible to other methods, they need to be declared somewhere that they are visible to all methods. For example, you could declare them as instance variables.

But in this case, that would be the wrong thing to do. Another concept you seem to have trouble with is how drawing in a Winforms program works. You should only draw to the screen in the Paint event handler. There are a couple of ways to approach this:

  1. Keep track of the data that is the basis of what you're drawing, and then redraw everything any time the Paint event is raised.
  2. Maintain an off-screen bitmap, drawing new data to it as needed, and then draw this bitmap to the screen when the Paint event is raised.

Either way, you cause the Paint event to be raised by calling Invalidate().

Here is an example of the first approach, based on your original code:

public partial class Form1 : Form
{
    const int maxCircleCount = 30;
    List<int> circles = new List<int>();
    Random rnd = new Random();

    public Form1()
    {
        InitializeComponent();
        DoubleBuffered = true;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        // Arguably, you only need to start the timer and can skip these first two lines
        circles.Add(rnd.Next(15) * 6 + 76);
        Invalidate();
        timer1.Start();
    }

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        e.Graphics.DrawLine(Pens.Black, 100, 100, 700, 100);
        for (int i = 0; i < circles.Count; i++)
        {
            e.Graphics.DrawEllipse(Pens.Red, 180 + (30 * i), circles[i], 8, 6);
        }
    }

    private void timer1_Tick(object sender, EventArgs e)
    {
        if (circles.Count < maxCircleCount)
        {
            circles.Add(rnd.Next(15) * 6 + 76);
            Invalidate();
        }
        else
        {
            timer1.Stop();
        }
    }
}

The above will draw 30 circles after you click the button, one per timer tick, and then stop the timer. Since in your code example, when the Load event handler is called you don't have any circles yet, it wasn't clear to me what your intent with that code was. So I did not bother to draw any black circles.

Other changes include:

  1. Setting DoubleBuffered to true, to avoid flickering when Invalidate() is called.
  2. Using the stock Pens objects instead of creating them (note that your code, which did create new Pen objects, should have disposed the Pen objects it created…that it did not was also a bug).
  3. Use a List<int> for the circles. This encapsulates the storage and the count in a single object.

Note that an improvement I didn't bother to make would be to consolidate the circles.Add() and Invalidate() calls into a separate method that can be called by any place it needs to be.

Hopefully the above gets you back on track. There are lots of other questions on Stack Overflow discussing the various nuances of how to draw in a Winforms program, whether from raw data or by caching to an off-screen bitmap. You should be able to use those posts to refine your techniques.

See also e.g. Force Form To Redraw? and How do I call paint event?, which include some answers that elaborate on the basic "call Invalidate()" concept.

Community
  • 1
  • 1
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136