不闪烁地绘制多层






4.87/5 (11投票s)
在 .NET 中使用图层提高绘图速度。
引言
在绘制自定义控件时,如果想要避免闪烁并最大化速度,应该开始使用缓冲区。.NET 2.0 引入了 BufferedGraphicsContext
对象来简化该过程。通常,用户控件的设计涉及不同的图层,从下到上排序。背景图层是变化最少的那个。
有时,绘图可能是一项昂贵(缓慢)的操作,因为:
- 您需要访问数据库中的数据(对于网格)或二进制格式的数据(对于地图)。访问和过滤数据很慢。
- 您需要绘制复杂的形状(抗锯齿等)。
- 绘图涉及大量笔、刷子、文本、位图等的创建。
因此,如果图像的一部分在控件使用期间变化不大,最好将其缓冲并仅在更改时重新绘制。
工作原理
这个想法是使用 BufferedGraphicsContext
数组,每个图层一个。每个缓冲区都包含先前图层的绘图。例如,对于网格:
Buffer1: Background
Buffer2: Background and Fixed columns
Buffer3: Background and Fixed columns and rows and cells
Buffer4: Background and Fixed columns and cells and user selection
这样,例如,当用户在网格中选择多个单元格时,您只需要将 buffer3 复制到 buffer4 并在 buffer4 上绘制用户选择。最后,将 buffer4 复制到屏幕。
当用户滚动时,您将 buffer2 复制到 buffer3 并在 buffer3 上绘制单元格,将其复制到 buffer4,在 buffer4 上绘制用户选择,最后将 buffer4 复制到屏幕。
当用户什么都不做(只是 Alt Tab)时,您只需将 buffer4 复制到屏幕。对于某些应用程序,速度的提升在这里最明显。
当然,这必须根据用户控件的要求来使用。例如,如果背景是纯色,则绘图操作并不昂贵,并且复制缓冲区会更昂贵。但是,如果您想绘制纹理或压缩图像 (JPG) 呢?这同样适用于固定列。如果您在它们上面绘制图像并使用抗锯齿绘制文本,那么将其保存在缓冲区中并仅在用户调整大小时重新绘制会更快。
为了使其正确工作,需要一个变量来指示从哪里绘制。例如,当您滚动时,该状态是 buffer3。当您更改背景时,必须从 buffer1 开始。当用户正在选择行时,您可以从 buffer4 开始。
代码
首先,您声明一个 BufferedGraphics
数组,一个具有图层的枚举,以及 "from" 变量。
BufferedGraphics[] buffergraphs;
enum TypeOfChange
{
TChangeBackGround,
TChangeRectangles,
TChangeDotWidth,
TChangeSelCircle,
TChangeNone,
};
const int NumLayers = 4;
TypeOfChange vartchange;
然后创建控件。您需要设置控件,以便所有绘图都在 paint 事件中完成并禁用双缓冲。您还需要创建缓冲区数组并初始化变量。
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
SetStyle(ControlStyles.Opaque, true);
buffergraphs = new BufferedGraphics[NumLayers];
vartchange = TypeOfChange.TChangeBackGround;
然后是绘图方法。
private void DrawFirstLayer(Graphics g, Rectangle r);
private void DrawSecondLayer(Graphics g, Rectangle r);
private void DrawThirdLayer(Graphics g, Rectangle r);
private void DrawFourthLayer(Graphics g, Rectangle r);
然后是 paint 方法。
private void UsrCtlBufferDrawing_Paint(Object sender, PaintEventArgs e)
{
if (vartchange == TypeOfChange.TChangeBackGround)
{
DrawFirstLayer(buffergraphs[(int)TypeOfChange.TChangeBackGround].Graphics,
e.ClipRectangle);
buffergraphs[(int)TypeOfChange.TChangeBackGround].Render(
buffergraphs[(int)TypeOfChange.TChangeRectangles].Graphics);
}
if (vartchange == TypeOfChange.TChangeRectangles)
buffergraphs[(int)TypeOfChange.TChangeBackGround].Render(
buffergraphs[(int)TypeOfChange.TChangeRectangles].Graphics);
if (vartchange == TypeOfChange.TChangeBackGround || vartchange ==
TypeOfChange.TChangeRectangles)
{
DrawSecondLayer(buffergraphs[(int)TypeOfChange.TChangeRectangles].Graphics,
e.ClipRectangle);
buffergraphs[(int)TypeOfChange.TChangeRectangles].Render(
buffergraphs[(int)TypeOfChange.TChangeDotWidth].Graphics);
}
if (vartchange == TypeOfChange.TChangeDotWidth)
buffergraphs[(int)TypeOfChange.TChangeRectangles].Render(
buffergraphs[(int)TypeOfChange.TChangeDotWidth].Graphics);
if (vartchange == TypeOfChange.TChangeBackGround || vartchange ==
TypeOfChange.TChangeRectangles || vartchange == TypeOfChange.TChangeDotWidth)
{
DrawThirdLayer(buffergraphs[(int)TypeOfChange.TChangeDotWidth].Graphics,
e.ClipRectangle);
buffergraphs[(int)TypeOfChange.TChangeDotWidth].Render(
buffergraphs[(int)TypeOfChange.TChangeSelCircle].Graphics);
}
if (vartchange == TypeOfChange.TChangeSelCircle)
buffergraphs[(int)TypeOfChange.TChangeDotWidth].Render(
buffergraphs[(int)TypeOfChange.TChangeSelCircle].Graphics);
if (vartchange != TypeOfChange.TChangeNone)
{
DrawFourthLayer(buffergraphs[(int)TypeOfChange.TChangeSelCircle].Graphics,
e.ClipRectangle);
buffergraphs[(int)TypeOfChange.TChangeSelCircle].Render(e.Graphics);
}
if (vartchange == TypeOfChange.TChangeNone)
buffergraphs[(int)TypeOfChange.TChangeSelCircle].Render(e.Graphics);
vartchange = TypeOfChange.TChangeNone;
}
当用户调整大小时,这是一个创建缓冲区的好时机(因为它们的大小取决于控件的大小)。
private void UsrCtlBufferDrawing_Resize(Object sender, EventArgs e)
{
BufferedGraphicsContext context;
context = BufferedGraphicsManager.Current;
context.MaximumBuffer = new System.Drawing.Size(this.Width + 1, this.Height + 1 );
for (int i = 0; i < NumLayers; i++)
buffergraphs[i] = context.Allocate(this.CreateGraphics(),
new System.Drawing.Rectangle(0,0, this.Width, this.Height ));
vartchange = TypeOfChange.TChangeBackGround;
Invalidate();
}
最后,您必须捕获用户更改内容以触发重绘。
public Color ColorBackGround
{
get
{
return m_ColorBackGround;
}
set
{
m_ColorBackGround = value;
vartchange = TypeOfChange.TChangeBackGround;
Invalidate();
}
}
public Color ColorRectangle
{
get
{
return m_ColorRectangle;
}
set
{
m_ColorRectangle = value;
vartchange = TypeOfChange.TChangeRectangles;
Invalidate();
}
}
private void UsrCtlBufferDrawing_MouseDown(Object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
mousestartpoint.X = e.X;
mousestartpoint.Y = e.Y;
vartchange = TypeOfChange.TChangeSelCircle;
Invalidate();
}
}
private void UsrCtlBufferDrawing_MouseUp(Object sender, MouseEventArgs e)
{
if (mousestartpoint.X != -1)
{
ResetSelectPtos();
vartchange = TypeOfChange.TChangeDotWidth;
mousestartpoint.X = -1;
Invalidate();
}
}
其他用途
这个想法可以应用于 MFC (CMemDC
) 和 Win32。使用 CreateCompatibleBitmap
和 CBitmap
代替 BufferedGraphics
,使用 Bitblt 代替 render。
示例
示例代码有点基本,只是为了了解这个想法是如何工作的。它使用计时器来更改背景。即使您在绘制一个圆,并且背景同时发生变化,应用程序也不会闪烁。
改进
我添加了一个方法来在控件退出时释放数组。在设计器中。
this.HandleDestroyed += new System.EventHandler(this.UsrCtlBufferDrawingDestroy);
private void UsrCtlBufferDrawingDestroy(Object sender, EventArgs e)
{
for (int i = 0; i < NumLayers; i++)
{
if (buffergraphs[i] != null)
buffergraphs[i].Dispose();
}
}
我还添加了一些指标来查看它与简单的双缓冲相比有多快。结果是:
在没有独立显卡的慢速机器上运行:
- 如果您正在绘制线条和形状,那么仅使用双缓冲会更快。
- 如果您正在绘制纯色渐变,则双缓冲和图层的速度相同。
- 如果您正在绘制带有渐变的文本,或者只是大量的文本,那么使用图层会更快(快三倍以上)。
使用示例源代码(绘制渐变和渐变文本),我得到的双缓冲值为 200,图层值为 60(快三倍以上)。
在带有独立显卡的快速机器上运行:
使用图层要快得多。我认为这是因为缓冲区管理是在显卡内部完成的。使用示例源代码(绘制渐变和渐变文本),我得到的双缓冲值为 110,图层值为 13(快近 10 倍)。