C#-面板上的异步绘图

时间:2018-12-01 11:27:44

标签: c# winforms asynchronous system.drawing

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

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

这有效。可视化效果很棒。 但是,现在我想异步地重新创建它,以便在后台绘制它时,该应用程序仍然负责,用户可以执行其他操作,甚至只是移动窗口。 最初,我制作了第二种方法来绘制图形,并从事件处理程序中调用该方法:

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

这也很好。直到我使事件处理程序异步。

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

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

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

现在的问题是,是否可以异步绘制面板,从而不锁定应用程序的其他部分?如果没有,是否有办法使用其他任何方式(不一定是面板)来重新创建此算法?干杯。

3 个答案:

答案 0 :(得分:0)

您不得从其他线程绘制表格或控件。在调试模式下,WinForms将引发异常。

最好的方法是使用Timer组件。在计时器的每个刻度上,从循环执行一步。当然,您必须将外观计数器移动为全局变量。

答案 1 :(得分:0)

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

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

private Image _buffer;

然后,您决定开始绘图的触发事件是什么;让我们假设这是一个按钮单击:

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。在计时器事件中,您只需使面板无效:

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

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

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

        // Your code here
    }
}

现在您所需要做的就是更改面板绘制事件以从缓冲区复制:

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

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


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

您可能还需要满足其他条件;例如您可能需要使用DrawImage()而不是DrawImageUnscaled(),依此类推。我并不是说上面的代码是完美的,只是可以给您一个想法提供帮助。

答案 2 :(得分:-1)

您不能异步制作它,因为所有图形都必须在UI线程上制作。
您可以做的是利用Application.DoEvents()向UI发出信号,表明它可以处理未决消息:

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

它将使您的应用程序在绘制过程中具有响应能力。

在此处详细了解Application.DoEvents()
Use of Application.DoEvents()