2

I need to draw small circles at given X,Y coordinates but could be as high as 6000 circles on a panel on a window. How it is very slow and takes about 2 to 3 seconds for 5000 circles. How could I draw this faster?

 private void drawBGA_Pins(BGAmap PinCordinates, double ExternalZoomFactor, double ExternalOffset_X, double ExternalOffset_Y)
        {
            Graphics g = this.imgBox.CreateGraphics();
            double zoomFactor = (Math.Min(Math.Abs((imgBox.Width) / PinCordinates.width), Math.Abs((imgBox.Height) / PinCordinates.height)))*92/100 * ExternalZoomFactor;
            //g.Clear(Color.Transparent); //you can choose another color for your background here.
            Pen pen = new Pen(Color.Yellow);

            foreach (var p in PinCordinates.pkgCordinates)
            {
                try
                {
                    g.DrawEllipse(pen, (float)(ExternalOffset_X + (p.X* zoomFactor)), (float)(ExternalOffset_Y + (p.Y* zoomFactor)), 10, 10);
                }
                catch
                {

                }
            }
        }
PCG
  • 2,049
  • 5
  • 24
  • 42
  • This should take no longer than a fraction of a second. Do put something in the `catch` to make sure is __isn't__ called. Exceptions __are__ slow! Don't catch them for routine situations. (Instead test to __avoid__!!) - Also: You should not use `control.CreateGraphics` as has beed explained here >10k times!! - Also you are leaking GDI resources like the pen and the Graphics object (which you shouldn't have created in the 1st place) - Only draw in the `Paint` event or pass out its `e.Graphics` object!! - For flicker-free drawing the taget must be doulebuffered. Only `PictureBox` is by default. – TaW May 11 '19 at 07:06
  • One reason to not use control.CreateGraphics is persistance but another one is speed. Doing it right speeds up output by ~100 times i my tests! – TaW May 11 '19 at 11:59

2 Answers2

2

As noted in my comment: The code you post has many issues and one of them is also the reason for the lack of speed..

Winforms Graphics rule #1 : Never use control.CreateGraphics!

Also never try to cache a Graphics object! Either draw into a Bitmap bmp using a Graphics g = Graphics.FromImage(bmp) or in the Paint event of a control, using the e.Graphics parameter..

(The only exception are graphics you actually do not want to persist like drawing rubberband rectangles.. You can test the persistence of your graphics by doing a minimize/maximize sequence..)

The correct way is to keep a list of things to draw and whenever that list changes Invalidate the control you draw on. All drawing should be in the Paint event, using e.Graphics there!

This has been discussed quite often here; but what is of even more interest here is that the correct way will be really fast compared to the wrong, non-persistent one.

Let's see:

enter image description here

The missing timings for 50k and 100k circles with the created graphics object are 5.4s (vs 0.18s) and 10.9s (vs 0.41s). (The animation file would get too long for them..)

So e.Graphics could 'draw' the circles ~30-100x faster.

How come? - Actually the 'correct' way will only prepare the surface of a double-buffered control internally but only push it to the display once when it is finished and has time to do so, whereas the wrong way will deliver each circle directly. This optimization is done by the system as well as limiting the output area..

(PicturBox is double-buffered by default; other controls can be made so as well, see here)

Here is the testbed:

int count = 5000;
List<PointF> pinCoordinates = new List<PointF>();
Random rnd = new Random(9);
float zoomFactor = 1.5f;

private void Button1_Click(object sender, EventArgs e)
{
    // init a list of points
    pinCoordinates.Clear();
    Size sz = pictureBox1.ClientSize;
    Cursor = Cursors.WaitCursor;
    for (int i = 0; i < count; i++)
    {
        pinCoordinates.Add(new PointF(rnd.Next(sz.Width), rnd.Next(sz.Height)));
    }

    // now draw in one way or the other:
    if (radioButton1.Checked)
    {
        Graphics g = pictureBox1.CreateGraphics();
        DateTime dt0 = DateTime.Now;
        foreach (var p in pinCordinates)  DoDraw(g, p);
        sayTime(dt0);
    }
    else
    {
        pictureBox1.Invalidate();
    }
    Cursor = Cursors.Default;
}

private void PictureBox1_Paint(object sender, PaintEventArgs e)
{
    DateTime dt0 = DateTime.Now;
    foreach (var p in pinCoordinates)  DoDraw(e.Graphics, p);
    sayTime(dt0);
}

void DoDraw(Graphics g, PointF p)
{
    using (Pen pen = new Pen(Color.FromArgb(rnd.Next(1234567890))))
        g.DrawEllipse(pen, p.X * zoomFactor, p.Y * zoomFactor, 10, 10);
}

void sayTime(DateTime dt)
{
    DateTime dt1 = DateTime.Now;
    label1.Text = (dt1 - dt).ToString("s\\.ffff");
}

Final note: You can expect the same speed if you draw into a Bitmap using Graphics g = Graphics.FromImage(bmp)..

TaW
  • 53,122
  • 8
  • 69
  • 111
  • Excellent, I was able to cut down the time by half with previous recommendations but this blows away all records. . . Thank you very much – PCG May 11 '19 at 22:55
0

follow the optimization sequence below until you are satisfied with the performance.

  1. Call Control.SuspendLayout and Control.ResumeLayout after frequent UI updates.

  2. check if you are calling Control.Refresh frequently, use Control.Invalidate instead of Control.Refresh

  3. paint ONLY the circles that are within the visible region (Graphics.Clip), there will be much fewer circles to be drawn then.

  4. suspend painting at Windows API level. see this post: How do I suspend painting for a control and its children?

yangching
  • 11
  • 1
  • 1
    in addition to the comment of @Taw, you should extend imgbox class, overwrite the OnPaint Method and obtain the Graphicis obejct from OnPaint parameter, see MSDN:https://learn.microsoft.com/en-us/dotnet/framework/winforms/controls/overriding-the-onpaint-method – yangching May 11 '19 at 07:27