为完成PaintBoardDemo(本人设计的一个基于.NET Framework的WinForm的画图程序),过程中遇到的技术难点之一就是就是要显示任何图形绘制过程中的轨迹,也即需要在pictureBox控件的MouseMove事件中添加相应的Graphics对象的DrawXX Methods.
在设计之初,仅仅能够在MouseMove事件中写出一行代码:g.DrawLine(new Pen(Color.Red),ptLast.X,ptLast.Y,e.X,e.Y)(ptLast为Point类型的全局变量,用以保存MouseDown事件的坐标)。可想而知,鼠标的每一次Move就将触发MouseMove事件,即执行一次g.DrawLine(画一条直线)。因此程序运行的结果就是,画板上显示了绘制过程中的所有轨迹,如下图,显然,这是与我们的设计目的相不符的。
经过对Bitmap与Graphics的学习及经过同学开导,终于解决了不显示绘制过程中多余轨迹的问题。基本实现的思想可以总结如下:
1.建立一个位图对象,即Bitmap _bitmapTemp,和一个Graphics对象用来实现一系列绘制操作,即Graphics g(_bitmapTemp,g均为全局变量)。
2.在主窗体的Shown事件中,对_bitmapTemp和g进行初始化:
_bitmapTemp=new Bitmap(pictureBox.Width,pictureBox.Height);//初始化位图大小和pictureBox一致
Graphics.FromImage(_bitmapTemp).FillRectangle(Brushes.White,0,0,pictureBox.Width,pictureBox.Height);//并且将位图_bitmapTemp以白色填充全部区域,以作绘制之用。
g=pictureBox.CreateGraphics();//实例化g对象,表明以后对g的所有绘制操作均在pictureBox之上。
pictureBox.Image=_bitmapTemp;//“引用传递”,将pictureBox.Image&_bitmapTemp指向内存中同一块位图,实现在pictureBox显示实时的绘制结果。
3.pictureBox的MouseMove事件中执行绘制操作:
g.DrawImage(_bitmapTemp,0,0);//在第N次绘制时,将第N-1次绘制所得的结果画到pictureBox中,既实现了绘制结果的同步显示,也消除了第N次绘制过程中产生的轨迹。
g.DrawLine(new Pen(Color.Red),ptLast.X,ptLast.Y,e.X,e.Y);//绘制直线。
4.pictureBox的MouseUp事件中将绘制结果绘制到_bitmapTemp,保证绘制结果的正确性(所绘的图形均被保留):
Graphics.FromImage(_bitmapTemp).DrawLine(new Pen(Color.Red),ptLast.X,ptLast.Y,e.X,e.Y);
至此,就可以在pictureBox中绘制各类图形了(调用Graphics类的DrawXX Methods即可)。但是会发现整个绘制过程中,图形的闪动极其厉害。为什么呢?回过头来再看代码,发现问题出在g.DrawImage(_bitmapTemp,0,0);每次MouseMove,就在pictureBox中将之前的图全部重新绘制一次,往往绘制过程中触发的MouseMove事件是不计其数的,频繁的重绘全图,不仅效率低下,更带来了直观的闪动问题。但是也不可能把这行代码去掉,它可是解决绘制轨迹问题的核心代码。
百度了关于闪动优化的方法,发现不少都提到了“双缓冲"技术,参照着网上的方法,发现闪动问题确实得到了解决。看来有必要对这个双缓冲技术进行一下学习。
双缓冲解决闪动问题的原理主要是由于当启用双缓冲时,所有绘制操作均呈现到内存缓冲区,而不是在屏幕上绘制。所有绘制完成之后,内存缓冲区直接复制到与其关联的绘制区域。因为在屏幕上只执行了一次图形操作,所以消除了由复杂操作造成的图像闪烁。
【默认双缓冲】
在应用程序中启用双缓冲的最简便的方法是使用.NET Framework为窗体和控件提供的默认双缓冲。通过将DoubleBuffered属性设置为True或者使用SetStyle方法可以为Windows窗体和所创作的Windows控件启用默认双缓冲。
public void EnableDoubleBuffering()
{
// Set the value of the double-buffering style bits to true.
this.SetStyle(ControlStyles.DoubleBuffer |
ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint,
true);
this.UpdateStyles();
}
【管理和显示缓冲的图形】
.NET中负责单独分配和管理图形缓冲区的类是BufferedGraphicsContext类。每个应用程序都有自己的默认BufferedGraphicsContext实例来管理此应用程序的所有默认双缓冲。大多数情况下,每个应用程序只有一个应用程序域,所以每一个应用程序通常只有一个BufferedGraphicsContext。默认 BufferedGraphicsContext 实例由 BufferedGraphicsManager 类管理。通过调用 BufferedGraphicsManager.Current 属性可以检索对默认 BufferedGraphicsContext 实例的引用。还可以创建一个专用的 BufferedGraphicsContext 实例以提高图形密集型应用程序的性能。介绍一下所需用到的几个类。
BufferedGraphicsContext类提供创建图形缓冲区的方法,该缓冲区可以用于双缓冲,创建用于绘制缓冲图形的 BufferedGraphics 实例。
BufferedGraphicsManager可以实现图形的自定义双缓冲,提供对应用程序域的主缓冲图形上下文对象的访问。此类有一个静态属性Current,该属性返回当前应用程序域的主BufferGraphicsContext
BufferedGraphics类用于绘制缓冲图形,它没有公共的构造函数,必须有应用程序域的BufferedGraphicsContext对象使用其Allocate方法创建。提供图形缓冲区的包装,以及可用于写入缓冲区和将其内容呈现到输出设备的方法。通过Graphics属性提供对Graphics对象的访问(即访问一系列的Draw方法,即可在缓冲区中进行绘制操作),Render()方法用来将图形缓冲区的内容写入到与缓冲区关联的区域。
因此实现在缓冲区绘制图形并且最终显示到屏幕上的目标控件中可通过如下代码:
//获得分配和管理图形的缓冲区(该应用程序域的主缓冲区)
BufferedGraphicsContext current = BufferedGraphicsManager.Current;
//创建用于缓冲区绘制的对象myBuffer,像素格式为pictureBox.CreateGraphics,大小和pictureBox一致
//这里我们假设缓冲区图像最终是显示到pictureBox中的
BufferedGraphics myBuffer = current.Allocate(pictureBox.CreateGraphics(),pictureBox.DisplayReactangle);
//和之前的想法一致,DrawImage用以消除绘制过程中的多余轨迹,DrawLine进行绘制操作(以直线为例)
myBuffer.Graphics.DrawImage(_bitmapTemp,0,0);
myBuffer.Graphics.DrawLine(new Pen(Color.Red),ptLast.X,ptLast.Y,e.X,e.Y);
//将缓冲区的图片绑定至pictureBox,最终显示在屏幕上
myBuffer.Render(pictureBox.CreateGraphics());
//释放系统资源
myBuffer.Dispose();