我应该如何缓冲绘制的矩形以提高性能 (C#/.NET/WinForms/GDI+)

How should I buffer drawn rectangles to improve performance (C#/.NET/WinForms/GDI+)

我在做什么

我正在开发一个 C#/.NET 4.7.2/WinForms 应用程序,该应用程序使用 Graphics.FillRectangle 在表单上绘制大量填充矩形。

目前,矩形是在表单的 Paint 事件中绘制的。绘制完所有矩形后,根据鼠标位置绘制十字准线。

只要鼠标移动,就会在窗体上调用 Invalidate 以强制重新绘制,以便十字准线出现在新位置。

问题

这是低效的,因为矩形不会改变,只有十字线位置,但每次都会重新绘制矩形。 CPU 鼠标移动期间的使用很重要。

下一步

我相信解决这个问题的方法是先将矩形绘制到缓冲区(在 Paint 事件之外)。然后,Paint 事件只需要渲染缓冲区并在顶部绘制十字准线。

由于我是 GDI+ 和手动缓冲的新手,所以我很担心误入歧途。 Google 搜索显示大量关于手动缓冲的文章,但每篇文章似乎采用不同的方法,这增加了我的困惑。

如果有利于简单性和效率的建议方法,我将不胜感激。如果有一种惯用的 .NET 方法可以做到这一点 — 意味着 的方式 — 我很想知道。

这是一个不需要任何缓冲的快速简单的解决方案。要复制它,请从一个新的 Windows Forms 项目开始。我只画了两个矩形,你想画多少就画多少。

如果您使用这两个成员变量和这两个处理程序创建一个新的 WinForms 项目,您将获得一个工作示例。

首先,为您的表单添加几个成员变量:

private bool _started = false;
private Point _lastPoint;

第一次移动鼠标后,started 标志将变为 true_lastPoint 字段将跟踪最后绘制十字准线的点(这主要是 _started 存在的原因)。

Paint 处理程序每​​次调用时都会绘制十字准线(您会明白为什么 MouseMove 处理程序可以这样做):

private void Form1_Paint(object sender, PaintEventArgs e)
{
    var graphics = e.Graphics;
    var clientRectangle = this.ClientRectangle;
    //draw a couple of rectangles
    var firstRectangle = clientRectangle;
    firstRectangle.Inflate(-20, -40);
    graphics.FillRectangle(Brushes.Aqua, firstRectangle);
    var secondRectangle = clientRectangle;
    secondRectangle.Inflate(-100, -4);
    graphics.FillRectangle(Brushes.Red, secondRectangle);

    //draw Cross-Hairs
    if (_started)
    {
        //horizontal
        graphics.DrawLine(Pens.LightGray, new Point(clientRectangle.X, _lastPoint.Y),
            new Point(ClientRectangle.Width + clientRectangle.X, _lastPoint.Y));
        //vertical
        graphics.DrawLine(Pens.LightGray, new Point(_lastPoint.X, clientRectangle.Y),
            new Point(_lastPoint.X, ClientRectangle.Height + clientRectangle.Y));
    }
}

现在是 MouseMove 处理程序。这就是魔法发生的地方。

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    var clientRectangle = this.ClientRectangle;
    var position = e.Location;
    if (clientRectangle.Contains(position))
    {
        Rectangle horizontalInvalidationRect;
        Rectangle verticalInvalidationRect;
        if (_started)
        {
            horizontalInvalidationRect = new Rectangle(clientRectangle.X,
                Math.Max(_lastPoint.Y - 1, clientRectangle.Y), clientRectangle.Width, 3);
            verticalInvalidationRect = new Rectangle(Math.Max(_lastPoint.X - 1, clientRectangle.X),
                clientRectangle.Y, 3, clientRectangle.Height);
            Invalidate(horizontalInvalidationRect);
            Invalidate(verticalInvalidationRect);
        }

        _started = true;
        _lastPoint = position;

        horizontalInvalidationRect = new Rectangle(clientRectangle.X,
            Math.Max(_lastPoint.Y - 1, clientRectangle.Y), clientRectangle.Width, 3);
        verticalInvalidationRect = new Rectangle(Math.Max(_lastPoint.X, clientRectangle.X - 1),
            clientRectangle.Y, 3, clientRectangle.Height);
        Invalidate(horizontalInvalidationRect);
        Invalidate(verticalInvalidationRect);

    }
}

如果光标在表格内,我会做很多工作。首先,我声明了两个我将用于无效的矩形。水平的将是一个填充客户端矩形宽度的矩形,但只有 3 个像素高,以我想要无效的区域的 Y 坐标为中心。垂直的与客户端矩形一样高,但只有 3 像素宽。它以我要无效的区域的 X 坐标为中心。

当 Paint 处理程序运行时,它实际上会绘制整个客户区,但实际上只有整个无效区域中的像素才会绘制在屏幕上。无效区域之外的任何东西都被保留。

因此,当鼠标移动时,我创建了两个矩形(一个垂直,一个水平)围绕着最后一组十字准线所在的位置(以便在绘制这些矩形中的像素时(包括背景) ,旧的十字准线被有效地擦除)然后我在当前十字准线应该去的地方创建两个新的矩形(导致背景和新的十字准线被绘制)。

如果您有一个复杂的绘图应用程序,您将想要了解失效矩形。例如,当窗体调整大小时,您要做的只是使新出现的矩形无效,这样就不需要渲染整个图形。

这可行,但是为十字准线选择一种颜色(或画笔)以使其始终显示可能很困难。使用我的另一个建议(你画线两次(一次擦除,一次绘制)使用 INVERT(即 XOR)画笔更快,并且它总是显示。