C#-面板上的异步绘图 [英] C# - asynchronous drawing on a panel

查看:78
本文介绍了C#-面板上的异步绘图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在Winforms应用程序中,我试图重新创建蒙特卡洛方法"以近似PI.表单本身包含一个供用户提供点数的框和一个要在其上绘制的面板.不过,对于此示例,我们假设金额将保持不变.

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) {...}

现在,当我尝试通过Task.Run运行Calculate方法时,或者将其返回类型更改为Task并启动它时,在以下行中出现错误:参数无效":

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.

推荐答案

其他张贴者提出的问题是完全有效的,但是如果您稍微改变策略,这实际上是可以实现的.

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

虽然您无法在另一个线程上绘制UI Graphics 上下文,但是没有什么可以阻止您绘制到非UI的图形.因此,您可以做的就是绘制一个缓冲区:

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

您将在那里看到计时器;您将 Timer 添加到表单中,并将其设置为与所需的图形刷新率相匹配;因此25帧/秒将是40ms.在计时器事件中,您只需使面板无效:

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

大多数代码只是按原样移到 DrawToBuffer()方法中,并且您从缓冲区而不是从UI元素中获取 Graphics :

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

复制缓冲区非常快;可能在下面使用 BitBlt().

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

一个警告是,您现在需要对UI更改多加注意.例如.如果可以在缓冲区渲染的中途更改 DrawingPanel 的大小,则需要满足此要求.另外,您需要防止同时发生2个缓冲区更新.

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.

您可能还需要满足其他条件;例如您可能需要 DrawImage()而不是 DrawImageUnscaled()

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.

这篇关于C#-面板上的异步绘图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆