C# C-面板上的异步绘图

C# C-面板上的异步绘图,c#,winforms,asynchronous,system.drawing,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, Pain

在我的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);
    }
现在,当我尝试通过Task.Run运行Calculate方法时,或者当我将其返回类型更改为Task并启动该方法时,我在以下行中得到错误:参数无效:

e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 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的更多信息:

您不能从另一个线程绘制窗体或控件。在调试模式下,如果您这样做,WinForms将引发异常


最好的方法是使用计时器组件。在计时器的每个滴答声中,从循环中执行一步。当然,您必须将look counter作为一个全局变量移动。

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

虽然不能在另一个线程上绘制UI图形上下文,但没有任何东西可以阻止您绘制非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();
}
你会在那里看到计时器;向窗体添加计时器,并将其设置为与所需的图形刷新率相匹配;因此,25帧/秒等于40毫秒。在计时器事件中,您只需使面板无效:

private void timer1_Tick(object sender, EventArgs e)
{
    DrawingPanel.Invalidate();
}
大多数代码只是按原样移动到DrawToBuffer方法中,从缓冲区而不是从UI元素获取图形:

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

        // Your code here
    }
}
现在,您只需将panel paint事件更改为从缓冲区复制:

private void DrawingPanel_Paint(object sender, PaintEventArgs e)
{
    if (!(_buffer is null))
    {
        e.Graphics.DrawImageUnscaled(_buffer, 0, 0);
    }
}
复制缓冲区的速度非常快;可能在下面使用BitBlt

需要注意的是,您现在需要更加小心地对待UI更改。例如,如果可以通过缓冲区渲染中途更改DrawingPanel的大小,则需要满足这一要求。此外,还需要防止同时发生两次缓冲区更新


你可能还需要满足其他方面的需求;e、 g.您可能需要DrawImage,而不是DrawImageUnscaled,等等。我不是说上面的代码是完美的,只是想让您了解如何使用这些代码。

不可能从工作线程直接绘制到屏幕上。异步void事件处理程序存在很多问题。这当然是其中之一,e.Graphics对象仅在第一次调用中有效。之后,它将被释放,并在继续中变为无效。winforms管道对异步代码一无所知,在winforms冻结很久之后,该功能就被固定下来了。没有令人信服的理由来改进它,如果它能工作,那么它只会像廉价的汽车旅馆一样忽隐忽现。您必须停止尝试使其异步,因为它无法工作。您只需在for循环的末尾添加Application.DoEvents,即可使应用程序响应。@YeldarKurmangaliyev这实际上很好地满足了我的要求。非常感谢你!但我认为任何可能的好处都会失去。。
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);
    }
}