C#绘图双缓冲 C#双缓冲解释: 简单说就是当我们在进行画图操作时,系统并不是直接把内容呈现到屏幕上,而是先在内存中保存,然后一次性把结果输出来,如果没用双缓冲的话,你会发现在画图过程中屏幕会闪的很厉害,因为后台一直在刷新,而如果等用户画完之后再输出就不会出现这种情况,具体的做法,其实也就是先创建一个位图对象,然后把内容保存在里面,最后把图呈现出来。 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、设置显示图元控件的几个属性: 必须要设置,否则效果不是很明显! this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.AllPaintingInWmPaint, true); 2、窗口刷新一次的过程中,让所有图元同时显示到窗口。 可以通过以下几种方式实现,这几种方式都涉及到Graphics对象的创建方式。 具体实现 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; 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对象),在其中绘制完成后,再一次性显示。 完整代码如下: 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)); 另外一个例子,差不多 Graphics对象的创建方式: a、在内存上创建一块和显示控件相同大小的画布,在这块画布上创建Graphics对象。 接着所有的图元都在这块画布上绘制,绘制完成以后再使用该画布覆盖显示控件的背景,从而达到“显示一次仅刷新一次”的效果! 实现代码(在OnPaint方法中): Rectangle rect = e.ClipRectangle; Bitmap bufferimage = new Bitmap(this.Width, this.Height); Graphics g = Graphics.FromImage(bufferimage); g.Clear(this.BackColor); g.SmoothingMode = SmoothingMode.HighQuality; //高质量 g.PixelOffsetMode = PixelOffsetMode.HighQuality; //高像素偏移质量 foreach (IShape drawobject in doc.drawObjectList) { if (rect.IntersectsWith(drawobject.Rect)) { drawobject.Draw(g); if (drawobject.TrackerState == config.Module.Core.TrackerState.Selected && this.CurrentOperator == Enum.Operator.Transfrom)//仅当编辑节点操作时显示图元热点 { drawobject.DrawTracker(g); } } } using (Graphics tg = e.Graphics) { tg.DrawImage(bufferimage, 0, 0); //把画布贴到画面上 } b、直接在内存上创建Graphics对象: Rectangle rect = e.ClipRectangle; BufferedGraphicsContext currentContext = BufferedGraphicsManager.Current; BufferedGraphics myBuffer = currentContext.Allocate(e.Graphics, e.ClipRectangle); Graphics g = myBuffer.Graphics; g.SmoothingMode = SmoothingMode.HighQuality; g.PixelOffsetMode = PixelOffsetMode.HighSpeed; g.Clear(this.BackColor); foreach (IShape drawobject in doc.drawObjectList) { if (rect.IntersectsWith(drawobject.Rect)) { drawobject.Draw(g); if (drawobject.TrackerState == config.Module.Core.TrackerState.Selected && this.CurrentOperator == Enum.Operator.Transfrom)//仅当编辑节点操作时显示图元热点 { drawobject.DrawTracker(g); } } } myBuffer.Render(e.Graphics); g.Dispose(); myBuffer.Dispose();//释放资源 至此,双缓冲问题解决,两种方式的实现效果都一样,但最后一种方式的占有的内存很少,不会出现内存泄露! 接下来是对acdsee拖动图片效果的实现。开始不懂双缓冲,以为双缓冲可以解决这个问题,结果发现使用了双缓冲没啥效果,请教了高人,然后修改了些代码,完成这个效果。 图片是在pictureBox1里。 Bitmap currentMap; bool first = true; private void pictureBox1_MouseDown(object sender, MouseEventArgs e) { if (zoom == 0) { if (e.Button == MouseButtons.Left) //dragging mousedrag = e.Location; Image myImage = myMap.GetMap(); currentMap = new Bitmap(myImage); first = false; } } private void pictureBox1_MouseMove(object sender, MouseEventArgs e) { if (zoom == 0&&!first) { Image img = new Bitmap(Size.Width, Size.Height); Graphics g = Graphics.FromImage(img); g.Clear(Color.Transparent);//图片移动后显示的底色 g.SmoothingMode = SmoothingMode.HighQuality; //高质量 g.PixelOffsetMode = PixelOffsetMode.HighQuality; //高像素偏移质量 g.DrawImageUnscaled(currentMap, new System.Drawing.Point(e.Location.X - mousedrag.X, e.Location.Y - mousedrag.Y));//在g中移动图片,原图在(0,0)画的,所以直接用new System.Drawing.Point(e.Location.X - mousedrag.X, e.Location.Y - mousedrag.Y)就好。 g.Dispose(); pictureBox1.Image = img;//img是在鼠标这个位置时生成被移动后的暂时的图片 } } private void pictureBox1_MouseUp(object sender, MouseEventArgs e) { if (zoom == 0) { System.Drawing.Point pnt = new System.Drawing.Point(Width / 2 + (mousedrag.X - e.Location.X), Height / 2 + (mousedrag.Y - e.Location.Y)); myMap.Center = myMap.ImageToWorld(pnt); pictureBox1.Image = myMap.GetMap(); first = true; } } 说说思路,在鼠标点下时创建一个bitmap,currentMap,用它来存放当前图像。鼠标移动时,根据鼠标位置画图,最后,鼠标up时,重新画图。