【问题标题】:C# - asynchronous drawing on a panelC# - 面板上的异步绘图
【发布时间】:2019-05-03 08:45:21
【问题描述】:

在我的 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 并启动它时,我收到以下行中的错误:“Parameter is not valid”:

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

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

【问题讨论】:

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

标签: c# winforms asynchronous system.drawing


【解决方案1】:

您不得从另一个线程中绘制表单或控件。在调试模式下,如果你这样做,WinForms 会引发异常。

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

【讨论】:

    【解决方案2】:

    其他发帖者提出的问题是完全正确的,但如果你稍微改变一下策略,这实际上是完全可以实现的。

    虽然您无法在另一个线程上绘制到 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 帧/秒将是 40 毫秒。在计时器事件中,您只需使面板无效:

    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() 等等。我并不是说上面的代码是完美的,只是给你一个想法。

    【讨论】:

      【解决方案3】:

      您不能异步制作,因为所有绘图都必须在 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()

      【讨论】:

        猜你喜欢
        • 2011-07-27
        • 1970-01-01
        • 2015-11-19
        • 2013-05-06
        • 2011-10-17
        • 1970-01-01
        • 2019-07-15
        相关资源
        最近更新 更多