C# GDI+高效绘图(转载)
汇总利用双缓冲技术在C#中实现GDI高效绘图
双缓冲是将图片在显示到DC前,现在要内存建一个DC,也就是用于存储这张图片的内存区,然后在将这部分update到你要显示的地方
这样,可以防止画面抖动很大
这样和你说吧,如果要实现你要的效果,你必须用指针访问内存
比如,把程序声明成unsafe的,然后按照上面的操作进行
this.clear(this.BackColor)不行的 invalidate(),闪的厉害 所以不行
我再来详细解释一下刚才实现双缓冲的具体步骤:
1、 在内存中建立一块“虚拟画布”:
Bitmap bmp = new Bitmap(600, 600);
2、 获取这块内存画布的Graphics引用:
Graphics g = Graphics.FromImage(bmp);
3、 在这块内存画布上绘图:
g.FillEllipse(brush, i * 10, j * 10, 10, 10);
4、将内存画布画到窗口中
this.CreateGraphics().DrawImage(bmp, 0, 0);
重点:
现在的cpu飞快,其实数学计算一般很快,cpu大部分时间是在处理绘图,而绘图有三种境界:1>每次重绘整体Invalidate()
2>每次局部绘制Invalidate(Rect);
3>有选择的局部绘制。
不能说,一定是第三种方式好,得视情况,境界高程序肯定就复杂,如果对效率要求不高或者绘图量小当然直接用第一种方式。然而,稍微专业点的绘图程序,第一第二种方式肯定满足不了要求,必须选用第三种方式。而第三种方式的手段多样,也得根据实际情况拿相应的解决之道。这里讲解一般的三种手段,他们可以联合使用。
1. 缓存——Bitmap或者DoubleBuffer。缓存就是先把绘制的图形绘制到一张内存位图上,然后在一次性的贴位图,他可以提高绘图速度,也能避免闪烁。DoubleBuffer=true是C#窗体的属性,设置了此属性估计系统本身会起用无效区的内存位图缓存,而不需要程序员Bitmap处理。
2. 合理利用无效区域。无效区域就是系统保存当前变化需要重绘的区域,可以在OnPaint()中,e.ClipRectangle(e.ClipRectangle.X)直接获得,也可以通过其他方式获得。Windows系统只会重绘无效区域内的绘图信息,然而我们用户的绘制代码一般是绘制整个区域的,很多时候无效区域只是一小部分区域,虽然执行了所有的绘图代码,但是Windows系统只会重新更新无效区域内的绘图。这里有两个利用点:
1>用户请求重绘时,只请求重绘指定区域的,而不是整个区域,如Invalidate(Rect);
2>在用户绘图代码
Graphics g; g.DrawLine\g.DrawString\g.FillRectangle…前,先判断绘图的内容是否在无效区域,如果不是就不直接g.Draw…绘图代码。
3. 直接贴图。一般绘图或者重绘是Windows根据无效区域绘制的,如果在鼠标移动时需要重绘通过Windows系统处理Paint消息,有时满足不了要求,
比如①鼠标移动绘制十字测量线就得用异或线而不是Paint消息,
又比如②鼠标移动绘制跟随的信息提示框需要频繁擦除上次覆盖的背景,
又比如③台球滚动时台球与球桌背景的关系。
类似的这些问题如何解决?首先肯定不能利用Windows原来的绘图机制。其中一种解决方式是,不断的帧间变化区域贴内存位图——②中的信息框每次鼠标位置变化时可以重新g.Draw…或者贴早生成的信息框内存位图,②中被信息框覆盖的背景应该把本来的大背景截取此需要擦除区域的位置大小位图贴回来就是擦除背景了。由于每次大背景发生变化时,都应会重新生成大背景内存位图,所以可以是变化的背景。
这三种方式可以一起使用,应该可以解决中等的绘图项目的效率问题。中大型的绘图,必须记住两点1>只绘制电脑屏幕能
显示的部分;2>只绘制变化的部分。
C#GDI+双缓冲高效绘图
Rectangle rectangle = e.ClipRectangle;//取出次窗体或者画布的有效区的矩形区域
BufferedGraphicsContext GraphicsContext = BufferedGraphicsManager.Current;//获取程序住缓冲区域的BufferedGraphicsContext(双缓存类,此类用于提供双缓冲的功能)对象
BufferedGraphics myBuffer = GraphicsContext.Allocate(e.Graphics, e.ClipRectangle);//获取缓冲区
Graphics g = myBuffer.Graphics;
指定在呈现期间像素偏移的方式。
g.PixelOffsetMode = PixelOffsetMode.HighQuality;//高质量低速度呈现
指定是否将平滑处理(消除锯齿)应用于直线、曲线和已填充区域的边缘。
g.SmoothingMode = SmoothingMode.HighQuality;// 指定高质量、低速度呈现。
g.Clear(BackColor);//或者使用invalidate方法==有效区的擦除
Pen bluePen2 = new Pen(Color.Blue);
LineDrawRoutine(g, bluePen2);
myBuffer.Render(e.Graphics); //将图形缓冲区的内容写入指定的 Graphics 对象。
g.Dispose();
myBuffer.Dispose();
其实在C#里如果是在Form中绘图的话直接把Form的DoubleBuffered = true就可以了(利用winfrom窗体的默认双缓冲)
把所有的绘图放在一个picturebox里面绘制,
不要直接再在form里面绘
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.Selectable, true);
如果你在Form中绘图的话,不论是不是采用的双缓存,都会看到图片在更新的时候都会不断地闪烁,解决方法就是在这个窗体的构造函数中增加以下三行代码:
请在构造函数里面底下加上如下几行:
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true); // 禁止擦除背景.
SetStyle(ControlStyles.DoubleBuffer, true); // 双缓冲
参数说明:
UserPaint
如果为true,控件将自行绘制,而不是通过操作系统来绘制。此样式仅适用于派生自 Control的类。
AllPaintingInWmPaint
如果为true,控件将忽略 WM_ERASEBKGND窗口消息以减少闪烁。仅当UserPaint 位设置为true时,才应当应用该样式。
DoubleBuffer
如果为true,则绘制在缓冲区中进行,完成后将结果输出到屏幕上。双重缓冲区可防止由控件重绘引起的闪烁。要完全启用双重缓冲,还必须将UserPaint和AllPaintingInWmPaint样式位设置为 true。
GDI+的双缓冲问题
我想有很多搞图形方面的朋友都会用到双缓冲技术的时候,而且有的时候她的确是个头疼的问题。最近我也要用双缓冲技术,程序怎么调试都不合适,当要对图形进行移动时,总是会出现闪烁抖动。在网上找了些资料,说得都不清不楚的,折腾了一晚上也没弄出来。第二天觉定自己研究一下。现在把自己的一些想法拿出来跟大家分享一下。
双缓冲的基本原理:
一直以来的误区:.net1.1 和 .net 2.0 在处理控件双缓冲上是有区别的。
.net 1.1中,使用:this.SetStyle(ControlStyles.DoubleBuffer, true);
.net 2.0中,使用:this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
要知道,图元无闪烁的实现和图元的绘制方法没有多少关系,只是绘制方法可以控制图元的刷新区域,使双缓冲性能更优!
导致画面闪烁的关键原因分析:
一、绘制窗口由于大小位置状态改变进行重绘操作时
绘图窗口内容或大小每改变一次,都要调用Paint事件进行重绘操作,该操作会使画面重新刷新一次以维持窗口正常显示。刷新过程中会导致所有图元重新绘制,而各个图元的重绘操作并不会导致Paint事件发生,因此窗口的每一次刷新只会调用Paint事件一次。窗口刷新一次的过程中,每一个图元的重绘都会立即显示到窗口,因此整个窗口中,只要是图元所在的位置,都在刷新,而刷新的时间是有差别的,闪烁现象自然会出现。所以说,此时导致窗口闪烁现象的关键因素并不在于Paint事件调用的次数多少,而在于各个图元的重绘。
根据以上分析可知,当图元数目不多时,窗口刷新的位置也不多,窗口闪烁效果并不严重;当图元数目较多时,绘图窗口进行重绘的图元数量增加,绘图窗口每一次刷新都会导致较多的图元重新绘制,窗口的较多位置都在刷新,闪烁现象自然就会越来越严重。特别是图元比较大绘制时间比较长时,闪烁问题会更加严重,因为时间延迟会更长。
解决上述问题的关键在于:窗口刷新一次的过程中,让所有图元同时显示到窗口。
二、进行鼠标跟踪绘制操作或者对图元进行变形操作时
当进行鼠标跟踪绘制操作或者对图元进行变形操作时,Paint事件会频繁发生,这会使窗口的刷新次数大大增加。虽然窗口刷新一次的过程中所有图元同时显示到窗口,但也会有时间延迟,因为此时窗口刷新的时间间隔远小于图元每一次显示到窗口所用的时间。因此闪烁现象并不能完全消除!所以说,此时导致窗口闪烁现象的关键因素在于Paint事件发生的次数多少。
解决此问题的关键在于:设置窗体或控件的几个关键属性。
下面讲具体的实现方法:(转)
1、在内存中建立一块“虚拟画布”:Bitmap bmp = new Bitmap(600, 600);
2、获取这块内存画布的Graphics引用:Graphics g = Graphics.FromImage(bmp);
3、在这块内存画布上绘图:如画线g.DrawLine(添加参数);
4、将内存画布画到窗口中:this.CreateGraphics().DrawImage(bmp, 0, 0);
在构造函数中加如下代码
代码一:
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true); // 禁止擦除背景.
SetStyle(ControlStyles.DoubleBuffer, true); // 双缓冲
或代码二:
this.SetStyle(ControlStyles.DoubleBuffer | ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint, true);
this.UpdateStyles();
上述方式适合直接在窗体上绘制图形,并且很容易做到。但有时我们需要在某个控件上绘制图形,那该怎么办呢?原理跟直接在窗体上绘制图形采用双缓冲是一样的,也要在控件的构造函数里设置上述代码一或代码二。那么又怎么设置呢?
在Microsoft Visual Studio 2005环境下的,用的C#语言,并采用GDI+。目标是实现简单的鼠标拖动画线,并且要把之前画过的线都重新画出来。
整个程序使用了三个控件:一个SplitContainer控件、一个自定义的Panel控件和一个VS自带的Panel控件。SplitContainer控件的大小设置成窗体宽、半窗体高并定位在窗体的下半部分。自定义的Panel控件和VS自带的Panel控件都是通过设置它们的Dock属性使它们绑定到SplitContainer控件的Panel1和Panel2上。附录中会说到自定义的Panel控件是怎么定义的。
窗体的上半部分采用双缓冲。自定义的Panel控件采用了双缓冲,是通过在自定义Panel控件时设置样式来做到的(设置方法与窗体的双缓冲设置方法一样,如下面三条语句),这不能够在面板的Paint方法里直接设置,因为SetStyle()在Panel类中不是public方法。VS自带的Panel控件没有采用双缓冲。
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
把三者结合
有两种方式来创建Graphics对象:第一是在内存上创建一块和显示区域或控件相同大小的画布,在这块画布上创建Graphics对象。接着所有的图元都在这块画布上绘制,绘制完成以后再使用该画布覆盖显示控件的背景,从而达到“显示一次仅刷新一次”的效果!第二是直接在内存上创建Graphics对象。
使用双缓冲的图形可以减少或消除重绘显示图面时产生的闪烁。使用双缓冲时,更新的图形首先被绘制到内存的缓冲区中,然后,此缓冲区的内容被迅速写入某些或所有显示的图面中。显示图形的重写相对简短,这通常可以减少或消除有时在更新图形时出现的闪烁。
双缓冲技术(C# GDI)
c#如何实现防窗体闪烁的功能。大家都会想到运用双缓冲技术,那么在c#中是如何做的?
1、 利用默认双缓冲
(1)在应用程序中使用双缓冲的最简便的方法是使用 .NET Framework 为窗体和控件提供的默认双缓冲。通过将 DoubleBuffered 属性设置为 true。
this.DoubleBuffered=true;
(2)使用 SetStyle 方法可以为 Windows 窗体和所创作的 Windows 控件启用默认双缓冲。
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
2、 手工设置双缓冲
.netframework提供了一个类BufferedGraphicsContext负责单独分配和管理图形缓冲区。每个应用程序域都有自己的默认 BufferedGraphicsContext 实例来管理此应用程序的所有默认双缓冲。大多数情况下,每个应用程序只有一个应用程序域,所以每个应用程序通常只有一个默认 BufferedGraphicsContext。默认 BufferedGraphicsContext 实例由 BufferedGraphicsManager 类管理。通过管理BufferedGraphicsContext实现双缓冲的步骤如下:
(1)获得对 BufferedGraphicsContext 类的实例的引用。
(2)通过调用 BufferedGraphicsContext.Allocate 方法创建 BufferedGraphics 类的实例。
(3)通过设置 BufferedGraphics.Graphics 属性将图形绘制到图形缓冲区。
(4)当完成所有图形缓冲区中的绘制操作时,可调用 BufferedGraphics.Render 方法将缓冲区的内容呈现到与该缓冲区关联的绘图图面或者指定的绘图图面。
(5)完成呈现图形之后,对 BufferedGraphics 实例调用释放系统资源的 Dispose 方法。
完整的例子,在一个400*400的矩形框内绘制10000个随机生成的小圆。
BufferedGraphicsContext current = BufferedGraphicsManager.Current; //(1)
BufferedGraphics bg = current.Allocate(this.CreateGraphics(),this.DisplayRectangle); //(2)
Graphics g = bg.Graphics;//(3)
//随机 宽400 高400
System.Random rnd = new Random();
int x,y,w,h,r,i;
for (i = 0; i < 10000; i++)
{
x = rnd.Next(400);
y = rnd.Next(400);
r = rnd.Next(20);
w = rnd.Next(10);
h = rnd.Next(10);
g.DrawEllipse(Pens.Blue, x, y, w, h);
}
bg.Render();//(4)
//bg.Render(this.CreateGraphics());
bg.Dispose();//(5)
BufferedGraphicsContext current = BufferedGraphicsManager.Current; //(1)
BufferedGraphics bg;
bg = current.Allocate(this.CreateGraphics(),this.DisplayRectangle); //(2)
Graphics g = bg.Graphics;//(3)
//随机 宽400 高400
System.Random rnd = new Random();
int x,y,w,h,r,i;
for (i = 0; i < 10000; i++)
{
x = rnd.Next(400);
y = rnd.Next(400);
r = rnd.Next(20);
w = rnd.Next(10);
h = rnd.Next(10);
g.DrawEllipse(Pens.Blue, x, y, w, h);
}
bg.Render();//(4)
//bg.Render(this.CreateGraphics());
bg.Dispose();//(5)
3、 自己开辟一个缓冲区(如一个不显示的Bitmap对象),在其中绘制完成后,再一次性显示。
完整代码如下:
view plaincopy to clipboardprint?
Bitmap bt = new Bitmap(400, 400);
Graphics bg = Graphics.FromImage(bt);
System.Random rnd = new Random();
int x, y, w, h, r, i;
for (i = 0; i < 10000; i++)
{
x = rnd.Next(400);
y = rnd.Next(400);
r = rnd.Next(20);
w = rnd.Next(10);
h = rnd.Next(10);
bg.DrawEllipse(Pens.Blue, x, y, w, h);
}
this.CreateGraphics().DrawImage(bt, new Point(0, 0));
BufferedGraphicsContext的构造函数
- BufferedGraphicsContext
初始化BufferedGraphicsContext 类的新实例。
BufferedGraphicsContext的方法
- Allocate(Graphics, Rectangle)
使用指定的Graphics 的像素格式,创建指定大小的图形缓冲区。
- Allocate(IntPtr, Rectangle)
使用指定的Graphics 的像素格式,创建指定大小的图形缓冲区。
- Dispose
Releases all resources used by theBufferedGraphicsContext.
- Equals(Object)
确定指定的Object 是否等于当前的Object。(继承自Object。)
- Finalize
允许Object 在“垃圾回收”回收Object 之前尝试释放资源并执行其他清理操作。(继承自Object。)
- GetHashCode
用作特定类型的哈希函数。 (继承自Object。)
- GetType
获取当前实例的Type。(继承自Object。)
- Invalidate
如果某个缓冲区已被分配但尚未释放,则释放当前的图形缓冲区。
- MemberwiseClone
创建当前Object 的浅表副本。(继承自Object。)
- ToString
返回表示当前Object 的String。(继承自Object。)
BufferedGraphicsContext的属性
- MaximumBuffer
获取或设置要使用的缓冲区的最大大小。
双缓冲技术绘图
本文主要介绍 .Net 框架的基本绘图技术。通过简要的介绍和示例程序来探讨绘图技术的
优势、劣势以及其它相关注意事项。
简介
幸运的是当编写一个典型的Windows 窗体程序时,窗体和控件的绘制、效果等操作是
不需要特别加以考虑的。这是为什么呢?因为通过使用 .Net 框架,开发人员可以拖动一
系列的控件到窗体上,并书写一些简单的与事件相关联的代码然后在IDE中按F5,一个完完
全全的窗体程序就诞生了!所有控件都将自己绘制自己,窗体或者控件的大小和缩放都调
整自如。在这里经常会用到的,且需要引起一点注意的就是控件效果。游戏,自定义图表
控件以及屏幕保护程序的编写会需要程序员额外撰写用于响应 Paint 事件的代码。
本文针对那些Windows 窗体开发人员并有助于他们在应用程序编制过程中使用简单的
绘图技术。首先,我们会讨论一些基本的绘图概念。到底谁在负责进行绘制操作?Window
s 窗体程序是如何知道何时该进行绘制的?那些绘制代码究竟被放置在哪里?之后,还将
介绍图像绘制的双重缓冲区技术,你将会看到它是怎样工作的,怎样通过一个方法来实现
缓存和实际显示的图像间的交替。最后,我们将会探讨”智能无效区域”,实际就是仅仅
重绘或者清除应用程序窗体上的无效部分,加快程序的显示和响应速度。希望这些概念和
技术能够引导读者阅读完本文,并且有助于更快和更有效的开发Windows 窗体程序。
Windows 窗体使用GDI+图像引擎,在本文中的所有绘图代码都会涉及使用托管的.Net
框架来操纵和使用Windows GDI+图像引擎。
尽管本文用于基本的窗体绘图操作,但是它同样提供了快速的、有效的且有助于提高
程序性能的技术和方法。所以,在通读本文之前建议读者对.Net框架有个基本的了解,包
括Windows 窗体事件处理、简单的GDI+对象譬如Line,Pen和Brush等。熟悉Visual Basic
.Net或者C#编程语言。
概念
Windows 应用程序是自己负责绘制的,当一个窗体”不干净”了,也就是说窗体改变
了大小,或者部分被其它程序窗体遮盖,或者从最小化状态恢复时,程序都会收到需要绘
制的信息。Windows把这种”不干净”状态称为”无效的(Invalidated)”状态,我们理解为:需要重绘,当Windows 窗体程序需要重绘窗体时它会从Windows消息队列中获取绘制的信息。这个信息经过.Net框架封装然后传递到窗体的 PaintBackground 和 Paint 事件中去,在上述事件中适当的书写专门用于绘制的代码即可。
简单的绘图示例如下:
using System;
using System.Drawing;
using System.Windows.Forms;
public class BasicX : Form {
public BasicX() {
InitializeComponent();
}
private void BasicX_Paint(object sender, PaintEventArgs e) {
Graphics g = e.Graphics;
Pen p = new Pen(Color.Red);
int width = ClientRectangle.Width;
int height= ClientRectangle.Height;
g.DrawLine(p, 0,0, width, height);
g.DrawLine(p, 0, height, width, 0);
p.Dispose();
}
private void InitializeComponent() {
this.SetStyle(ControlStyles.ResizeRedraw, true);
this.ClientSize = new System.Drawing.Size(300, 300);
this.Text = “BasicX”;
this.Paint += new PaintEventHandler(this.BasicX_Paint);
}
[System.STAThreadAttribute()]
public static void Main() {
Application.Run(new BasicX());
}
}
上述代码分成两个基本的步骤来创建示例程序。首先 InitializeComponent 方法包含
一些属性的设置和附加窗体 Paint 事件的处理过程。注意,在方法中控件的样式也同时被
设置,设置控件的样式也是自定义Windows 窗体及控件行为的一种有效途径,譬如:控件
的”ResizeRedraw”属性指示当窗体的大小变化发生以后需要对其完全进行重绘,也就是说
重绘时总是需要对整个窗体的客户区域进行重绘。窗体的“客户区域”是指除了标题栏和
边框的所有窗体区域。可以进行一个有趣的试验,取消该控件的属性然后再运行程序,我
们可以很明显的看出为什么该属性会被经常的设置,因为窗体调整大小后的无效区域根本
不会被重绘。
好了,我们需要注意一下BasicX_Paint方法,正如先前所提到的,Paint 事件在程序
需要重绘时被激活,程序窗体利用Paint事件来负责回应需要重绘的系统消息,BasicX_Pa
int方法的调用需要一个对象 sender 和一个PaintEventArgs类型的变量,PaintEventArg
s类的实例或称之为变量 e 封装了两个重要的数据,第一个就是窗体的 Graphics 对象,
该对象表示窗体可绘制的表面也称之为画布用于绘制诸如线、文本以及图像等,第二个数
据就是ClipRectangle,该Rectangle对象表示窗体上无效的的矩形范围,或者说就是窗体
需要重绘的区域。记住,当窗体的ResizeRedDraw设置后,调整大小后该ClipRectangle的
大小实际就等于窗体整个客户区域的大小,或者是被其它程序窗体遮盖的那部分剪切区域
。关于部分剪切区域的用处我们会在智能重绘章节作更详细的阐述。
双重缓冲区绘图技术
双重缓冲区技术能够使程序的绘图更加快速和平滑,有效减少绘制时的图像闪烁。该
技术的基本原理是先将图像绘制到内存中的一块画布上,一旦所有的绘制操作都完成了,
再将内存中的画布推到窗体的或者控件的表面将其显示出来。通过这种操作后的程序能使
用户感觉其更加快速和美观。
下面提供的示例程序能够阐明双重缓冲区的概念和实现方法,这个示例所包含的功能
已相当完整,且完全可以在实际应用中使用。在该章节后面还会提及该技术应该配合控件
的一些属性设置才能达到更好的效果。
要想领略双重缓冲区绘图技术所带来的好处就请运行SpiderWeb示例程序吧。程序启动
并运行后对窗口大小进行调整,你会发现使用这种绘图算法的效率不高,并且在调整大小
的过程中有大量的闪烁出现。
纵观程序的源码你会发现在程序Paint事件激活后是通过调用LineDrawRoutine方法来实现
线的绘制的。LineDrawRoutine方法有两个参数,第一个是Graphics对象是用于绘制线条的
地方,第二个是绘图工具Pen对象用来画线条。代码相当简单,一个循环语句,LINEFREQ常量等,程序从窗体表面的左下一直划线到其右上。请注意,程序使用浮点数来计算在窗体
上的绘制位置,这样做的好处就是当窗体的大小发生变化时位置数据会更加精确。
private void LineDrawRoutine(Graphics g, Pen p) {
float width = ClientRectangle.Width;
float height = ClientRectangle.Height;
float xDelta = width / LINEFREQ;
float yDelta = height / LINEFREQ;
for (int i = 0; i < LINEFREQ; i++) {
g.DrawLine(p, 0, height - (yDelta * i), xDelta * i, 0);
}
}
撰写很简单的用于响应Paint事件SpiderWeb_Paint的代码,正如前面所提到的,Grap
hics对象就是从Paint事件参数PaintEventArgs对象中提取出来的表示窗体的绘制表面。这
个Graphics对象连同新创建Pen对象一起传递给LineDrawRoutine方法来画出蜘蛛网似的线
条,使用完Graphics对象和Pen对象后释放其占用的资源,那么整个绘制操作就完成了。
private void SpiderWeb_Paint(object sender, PaintEventArgs e) {
Graphics g = e.Graphics;
Pen redPen = new Pen(Color.Red);
//call our isolated drawing routing
LineDrawRoutine(g, redPen);
redPen.Dispose();
g.Dispose();
}
那么到底作怎么样的改动才能使上面的SpiderWeb程序实现简单的双重缓冲区技术呢?
原理其实相当简单,就是将应该画到窗体表面的绘制操作改成先画到内存中的位图上,Li
neDrawRoutine向这个在内存中隐藏的画布执行同样的蜘蛛网绘制操作,等到绘制完毕再通
过调用Graphics.DrawImage方法将隐藏的画布上内容推到窗体表面来显示出来,最后,再
加上一些小的改动一个高性能的绘图窗体程序就完成了。
比较下面双重缓冲区绘图事件与前面介绍的简单绘图事件间的区别:
private void SpiderWeb_DblBuff_Paint(object sender, PaintEventArgs e) {
Graphics g = e.Graphics;
Pen bluePen = new Pen(Color.Blue);
//create our offscreen bitmap
Bitmap localBitmap = new Bitmap(ClientRectangle.Width,ClientRectangle.Height
);
Graphics bitmapGraphics = Graphics.FromImage(localBitmap);
//call our isolated drawing routing
LineDrawRoutine(bitmapGraphics, bluePen);
//push our bitmap forward to the screen
g.DrawImage(localBitmap, 0, 0);
bitmapGraphics.Dispose();
bluePen.Dispose();
localBitmap.Dispose();
g.Dispose();
}
上面的示例代码创建了内存位图对象,它的大小等于窗体的客户区域(就是绘图表面)
的大小,通过调用Graphics.FromImage将内存中位图的引用传递给Graphics对象,也就是
说后面所有对该Graphics对象的操作实际上都是对内存中的位图进行操作的,该操作在C+
+中等同于将位图对象的指针复制给Graphics对象,两个对象使用的是同一块内存地址。现
在Graphics对象表示的是屏幕后方的一块画布,而它在双重缓冲区技术中起到至关重要的
作用。所有的线条绘制操作都已经针对于内存中的位图对象,下一步就通过调用DrawImag
e方法将该位图复制到窗体,蜘蛛网的线条就会立刻显示在窗体的绘制表面而且丝毫没有闪
烁出现。
这一系列的操作完成后还不是特别有效,因为我们先前提到了,控件的样式也是定义Wind
ows 窗体程序行为的一条途径,为了更好的实现双重缓冲区必须设置控件的Opaque属性,
这个属性指明窗体是不负责在后台绘制自己的,换句话说,如果这个属性设置了,那么必
须为清除和重绘操作添加相关的代码。具备双重缓冲区版本的SpiderWeb程序通过以上的设
置在每一次需要重绘时都表现良好,窗体表面用其自己的背景色进行清除,这样就更加减
少了闪烁的出现。
public SpiderWeb_DblBuff() {
SetStyle(ControlStyles.ResizeRedraw | ControlStyles.Opaque, true);
}
private void SpiderWeb_DblBuff_Paint(object sender, PaintEventArgs e) {
//create our offscreen bitmap
Bitmap localBitmap = new Bitmap(ClientRectangle.Width, ClientRectangle.Heigh
t);
Graphics bitmapGraphics = Graphics.FromImage(localBitmap);
bitmapGraphics.Clear(BackColor);
//call our isolated drawing routing
LineDrawRoutine(bitmapGraphics, bluePen);
}
结果怎么样?图像的绘制平滑多了。从内存中将蜘蛛网的线条推到前台以显示出来是
完全没有闪烁的,但是我们还是稍微停顿一下,先将内存中的位图修整一下再显示出来,
可以添加一行代码以便使线条看上去更加平坦。
bitmapGraphics.SmoothingMode = SmoothingMode.AntiAlias;
在将内存中的位图对象赋给Graphics后通过放置这行代码,我们在画布上所画的每一
个线条都使用了反锯齿,使凹凸不平的线条显得更加平坦。
完成了简单的双重缓冲区应用后有两个问题需要向读者阐明,.Net中的某些控件例如
:Button、PictureBox、Label还有PropertyGrid都已经很好的利用了该技术!这些控件在
默认状态下会自动启用双重缓冲区技术,用户可以通过对“DoubleBuffer”属性的设置来
就可以实现双重缓冲区技术。所以,用户若使用PictureBox来绘制蜘蛛网将会更有效率一
些,而且也使程序变得更加简单了。
我们在这里讨论的双重缓冲区技术既不是完全被优化但也没有什么太大的负面影响。
双重缓冲区技术是减少Windows 窗体绘制时闪烁的一条重要途径,但是它也确实消耗不少
内存,因为它将会使用双倍的内存空间:应用程序所显示的图像和屏幕后方内存中的图像
。每次Paint事件被激活时都会动态的创建位图对象,这种机制会相当耗费内存。而自带双
重缓冲区技术的控件在使用DoubleBuffer属性后执行起来的优化程度则会更好一些。
使用GDI+的DIB(与设备无关的位图)对象来实现这种画面以外的内存缓冲,自带双重缓
冲区机制的控件则能好的利用该位图对象。DIB是底层Win32的对象用于高效的屏幕绘制。
同样,值得注意的是GDI+的第一个版本GDI中仅与硬件加速有关以及一些简单功能可以直接
使用,由于这样的限制,像反锯齿和半透明等屏幕绘制方法执行起来的速度则相当慢。尽
管双重缓冲区机制消耗了一些内存但是它的使用不容置疑的增强了程序的执行性能。
智能重绘,在绘制前需要斟酌一下
“智能无效”(智能重绘)就是在暗示程序员应该明白仅应对程序中无效的区域进行重
绘,对Regions对象所对应的无效区域进行重绘可以提高绘制性能,使用Regions对象你可
以仅排除或绘制控件和窗体的部分区域已获得更好的性能。我们现在就开始来看一下Basi
cClip示例程序,这个程序使用保存在PaintEventArgs对象的ClipRectangle对象,之前我
们已经提及,无论何时当程序的大小发生变化时Paint事件都会被激活。BasicClip示例程
序用红和蓝两种颜色填充剪切的矩形区域,利用不同的速度调整窗体的大小几次以后,你
会发现绘制的矩形区域其实就是窗体的无效区域(包括大于原始窗体大小的区域部分和缩少
了的区域部分),示例程序的Paint事件代码如下:
private void BasicClip_Paint(object sender, PaintEventArgs e) {
Graphics g = e.Graphics;
//swap colors
if (currentBrush.Color == Color.Red)
currentBrush.Color = Color.Blue;
else
currentBrush.Color = Color.Red;
g.FillRectangle(currentBrush, e.ClipRectangle);
g.Dispose();
}
该示例程序的唯一目的就是演示怎样仅针对部分区域进行图形绘制。
Regions是一种被用来定义Windows 窗体或者控件区域的对象,调整窗体大小后所获得
的Regions就是窗体重绘的最小区域。当程序需要进行绘制的时候仅绘制感兴趣的特殊区域
,这样绘制更小的区域就会使程序的运行速度更快。
为了更好的演示Regions的用法,请查看TextCliping示例程序。该程序重载了OnPain
tBackground和OnPaint方法,直接重载这些方法比侦听事件更能保证代码在其它的绘制操
作之前被调用,而且对于自定义控件的绘制也更加有效。为了清楚起见,示例程序提供了
一个Setup方法,该方法定义了全局的Graphics对象。
private void Setup() {
GraphicsPath textPath = new GraphicsPath();
textPath.AddString(displayString, FontFamily.GenericSerif,
0, 75, new Point(10, 50), new StringFormat());
textRegion = new Region(textPath);
backgroundBrush = new TextureBrush(new Bitmap(“CoffeeBeanSmall.jpg”),
WrapMode.Tile);
foregroundBrush = new SolidBrush(Color.Red);
}
上面的Setup方法首先定义一个空的GraphicsPath对象变量textPath,下一步字符串“
Windows Forms”的边界被添加到该路径中,围绕这个轮廓创建Region。这样,一个被绘制
在窗体表面的以字符串轮廓为区域的Region就被创建了。最后,Setup方法创建以材质刷子
为背景和以实色刷子为前景来绘制窗体。
protected override void OnPaintBackground(PaintEventArgs e) {
base.OnPaintBackground(e);
Graphics bgGraphics = e.Graphics;
bgGraphics.SetClip(textRegion, CombineMode.Exclude);
bgGraphics.FillRectangle(backgroundBrush, e.ClipRectangle);
bgGraphics.Dispose();
}
上面定义的OnPaintBackground方法先立刻调用基类方法,这能够保证所有底层绘制的
代码都能够被执行。下一步,从PaintEventArgs中获得Graphics对象,再将Graphics对象
的剪切区域定义为textRegion对象。通过指定CombineMode.Exclude参数,明确无论在哪里
绘制或怎样绘制Graphics对象都不绘制textRegion区域内部。
protected override void OnPaint(PaintEventArgs e) {
base.OnPaint(e);
Graphics fgGraphics = e.Graphics;
fgGraphics.FillRegion(foregroundBrush, textRegion);
fgGraphics.Dispose();
}
最后,OnPaint事件负责精确的绘制出字符串。可以很容易的通过调用Graphics的Fil
lRegion方法来实现。通过指定的前景刷子foregroundBrush和textRegion且仅是该区域被
绘制。结果,Windows 窗体程序在运行之前确实“思考”该怎样进行绘制。
TextClipping示例程序,通过Region定义的Windows Forms字符串。能够使程序在绘制时避
开一个区域。
适当的组合使用区域和智能重绘你可以编写出运行速度快且不会引起闪烁的绘制代码
,并且比单独使用双重缓冲区绘制还要节省内存的消耗。
结论
如果你的程序确定要进行绘制操作,使用几种技术可以增强绘制性能。确保争取设置
控件属性以及适当的Paint事件处理是编写健壮程序的开始。在权衡好利弊后可以使用双重
缓冲区技术产生非常“保护视力”的结果。最后,在实际绘制前进行思考到底哪些客户区
域或Region需要被绘制将非常有益。