1

In my Winforms application I'm attempting to recreate the Monte Carlo Method to approximate PI. The form itself consists of a box in which the user provides the amount of points and a panel, on which I want to draw. For this example though, let's assume the amount is going to be constant.

private int Amount = 10000;
private int InCircle = 0, Points = 0;
private Pen myPen = new Pen(Color.White);

private void DrawingPanel_Paint(object sender, PaintEventArgs e)
    {
        int w = DrawingPanel.Width, h = DrawingPanel.Height;
        e.Graphics.TranslateTransform(w / 2, h / 2);

        //drawing the square and circle in which I will display the points
        var rect = new Rectangle(-w / 2, -h / 2, w - 1, h - 5);
        e.Graphics.DrawRectangle(myPen, rect);
        e.Graphics.DrawEllipse(myPen, rect);

        double PIE;
        int X, Y;
        var random = new Random();

        for (int i = 0; i < Amount; i++)
            {
                X = random.Next(-(w / 2), (w / 2) + 1);
                Y = random.Next(-(h / 2), (h / 2) + 1);
                Points++;

                if ((X * X) + (Y * Y) < (w / 2 * h / 2))
                {
                    InCircle++;
                    e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);
                }
                else
                {
                    e.Graphics.FillRectangle(Brushes.Cyan, X, Y, 1, 1);
                }
                //just so that the points appear with a tiny delay
                Thread.Sleep(1);
            }
        PIE = 4 * ((double)InCircle/(double)Points);
    }

And this works. The visualization is great. However, now I would like to recreate this asynchronously, so that while this is being drawn in the background, the app is still responsible and the user can do something else, or even just move the window around. Initially I made a second method that does the drawing, which I call from the Event Handler:

private double Calculate(PaintEventArgs e)
    {
        int w = DrawingPanel.Width, h = DrawingPanel.Height;
        double PIE;
        int X, Y;
        var random = new Random();

        for (int i = 0; i < Amount; i++)
            {
                X = random.Next(-(w / 2), (w / 2) + 1);
                Y = random.Next(-(h / 2), (h / 2) + 1);
                Points++;

                if ((X * X) + (Y * Y) < (w / 2 * h / 2))
                {
                    InCircle++;
                    e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);
                }
                else
                {
                    e.Graphics.FillRectangle(Brushes.Cyan, X, Y, 1, 1);
                }
                Thread.Sleep(1);
            }
        PIE = 4 * ((double)InCircle/(double)Points);
        return PIE;
    }

private void DrawingPanel_Paint(object sender, PaintEventArgs e)
    {
        int w = DrawingPanel.Width, h = DrawingPanel.Height;
        e.Graphics.TranslateTransform(w / 2, h / 2);

        var rect = new Rectangle(-w / 2, -h / 2, w - 1, h - 5);
        e.Graphics.DrawRectangle(myPen, rect);
        e.Graphics.DrawEllipse(myPen, rect);

        var result = Calculate(e);
    }

And this worked fine as well. Until I made the event handler async.

private async void DrawingPanel_Paint(object sender, PaintEventArgs e) {...}

Now, when I try running the Calculate method, either through Task.Run, or when I change its return type to Task and start that, I get the error: "Parameter is not valid" in the following line:

e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);

Now the question, is it possible to draw on a panel asynchronously, so that other parts of the app are not locked? And if not, is there a way to recreate this algorithm using any other way (not necessarily a panel)? Cheers.

jazb
  • 5,498
  • 6
  • 37
  • 44
nooblet
  • 31
  • 1
  • 9
  • It's not possible to paint directly to the screen from a worker thread. –  Dec 01 '18 at 12:04
  • 1
    There are a *lot* of problems with async void event handlers. This is certainly one of them, the e.Graphics object is only valid in the first invocation. After which it is disposed and becomes invalid in the continuation. The winforms plumbing just doesn't know anything about async code, the feature was bolted on long after winforms became frozen. With no compelling reason to improve it, if it would work then it would merely flicker like a cheap motel. You must stop trying to make it async, it cannot work. – Hans Passant Dec 01 '18 at 12:04
  • You can just add `Application.DoEvents()` in the end of your `for` loop to make application responsive. – Yeldar Kurmangaliyev Dec 01 '18 at 12:14
  • @YeldarKurmangaliyev this actually does what I wanted just well enough. Thank you very much! – nooblet Dec 01 '18 at 12:27
  • But I would assume any of the possible benefits will be lost.. – TaW Dec 01 '18 at 12:28

3 Answers3

0

You must not draw in a form or control from another thread. In debugging mode, WinForms will raise an exception if you do.

The best approach would be to use a Timer component. On each tick of the timer do one step from your loop. You will have to, of course, move the look counter as a global variable.

Nick
  • 4,787
  • 2
  • 18
  • 24
0

The issues other posters raise are completely valid, but this is actually completely achievable if you alter strategy a little.

While you can't draw to a UI Graphics context on another thread, there is nothing stopping you drawing to a non-UI one. So what you could do is to have a buffer to which you draw:

private Image _buffer;

You then decide what your trigger event for beginning drawing is; let's assume here it's a button click:

private async void button1_Click(object sender, EventArgs e)
{
    if (_buffer is null)
    {
        _buffer = new Bitmap(DrawingPanel.Width, DrawingPanel.Height);
    }

    timer1.Enabled = true;
    await Task.Run(() => DrawToBuffer(_buffer));
    timer1.Enabled = false;
    DrawingPanel.Invalidate();
}

You'll see the timer there; you add a Timer to the form and set it to match the drawing refresh rate you want; so 25 frames/second would be 40ms. In the timer event, you simply invalidate the panel:

private void timer1_Tick(object sender, EventArgs e)
{
    DrawingPanel.Invalidate();
}

Most of your code just moves as-is into the DrawToBuffer() method, and you grab the Graphics from the buffer instead of from the UI element:

private void DrawToBuffer(Image image)
{
    using (Graphics graphics = Graphics.FromImage(image))
    {
        int w = image.Width;
        int h = image.Height;

        // Your code here
    }
}

Now all you need is to change the panel paint event to copy from the buffer:

private void DrawingPanel_Paint(object sender, PaintEventArgs e)
{
    if (!(_buffer is null))
    {
        e.Graphics.DrawImageUnscaled(_buffer, 0, 0);
    }
}

Copying the buffer is super-fast; probably uses BitBlt() underneath.


A caveat is you now need to be a little more careful about UI changes. E.g. if it is possible to change the size of your DrawingPanel half-way through a buffer render, you need to cater for that. Also, you need to prevent 2 buffer updates happening simultaneously.

There might be other things you need to cater for; e.g. you might need to DrawImage() instead of DrawImageUnscaled(), etc. I'm not claiming the above code is perfect, just something to give you an idea to work with.

sellotape
  • 8,034
  • 2
  • 26
  • 30
-2

You cannot make it asynchronously, because all drawings have to be made on UI thread.
What you can do is to utilize Application.DoEvents() to signal UI that it can process pending messages:

for (int i = 0; i < Amount; i++)
{
    // consider extracting it to a function and separate calculation\logic from drawing
    X = random.Next(-(w / 2), (w / 2) + 1);
    Y = random.Next(-(h / 2), (h / 2) + 1);
    Points++;

    if ((X * X) + (Y * Y) < (w / 2 * h / 2)) // and this too
    {
        InCircle++;
        e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1); 
    }
    else
    {
        // just another one suggestion - you repeat your code, only color changes
        e.Graphics.FillRectangle(Brushes.Cyan, X, Y, 1, 1); 
    }

    Thread.Sleep(1); // do you need it? :)
    Application.DoEvents(); // 
}

It will make your application responsive during drawing.

Read more about Application.DoEvents() here:
Use of Application.DoEvents()

Yeldar Kurmangaliyev
  • 33,467
  • 12
  • 59
  • 101