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();
(转载结束)
上述方式适合直接在窗体上绘制图形,并且很容易做到。但有时我们需要在某个控件上绘制图形,那该怎么办呢?原理跟直接在窗体上绘制图形采用双缓冲是一样的,也要在控件的构造函数里设置上述代码一或代码二。那么又怎么设置呢?我是通过阅读MSDN,找到自定义控件的方法,并在控件的构造函数里设置。在后面的附录里,我会说明怎么做。
在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对象。
第一种方式具体代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
namespace PanelLib2Test
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
//激活窗体的双缓冲技术,可以注释掉看看是什么效果
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}
private Point startP, endP, curP = new Point();//定义线段的起始点,终止点,鼠标当前位置
//用在自定义面板上的变量,采用双缓冲
private bool mdFlag = false;//标志左键是否按下
private Point[][] lines = new Point[1000][];//存储已经画过的线段
private int k = 0;//线段数组当前下标,表示已经画过的线段数
//用在系统面板上的变量,不采用双缓冲
private bool mdFlag2 = false;
private Point[][] lines2 = new Point[1000][];
private int k2 = 0;
//用在自定义面板上的变量,采用双缓冲
private bool mdFlag3 = false;
private Point[][] lines3 = new Point[1000][];
private int k3 = 0;
private void Form1_Load(object sender, EventArgs e)
{
//初始化各线段数组
for (int i = 0; i < 1000; i++)
{
lines[i] = new Point[2];
}
for (int i = 0; i < 1000; i++)
{
lines2[i] = new Point[2];
}
for (int i = 0; i < 1000; i++)
{
lines3[i] = new Point[2];
}
//将分割控件定位在窗体的下半部分
splitContainer1.Location = new Point(0, ClientSize.Height / 2);
splitContainer1.Size = new Size(ClientSize.Width, ClientSize.Height / 2);
}
private void panelLib21_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
//Graphics g = panelLib21.CreateGraphics();用这个不行,可以试试是什么效果
Bitmap bmp = new Bitmap(panelLib21.Width, panelLib21.Height);
Graphics bufg = Graphics .FromImage(bmp);
//g.Clear(panelLib21.BackColor);//可用可不用,视具体情况
//bufg.Clear(panelLib21.BackColor);//可用可不用
bufg.DrawLine(new Pen(Color.Black, 1), startP, endP);
for (int i = 0; i <= k; i++)
{
bufg.DrawLine(new Pen(Color.Black, 1), lines[i][0], lines[i][1]);
}
bufg.DrawString("Custom Panel DoubleBufferd", new Font("verdana", 16), new
SolidBrush(Color.Red), 0, 0);
g.DrawImage(bmp, 0, 0);
bufg.Dispose();
bmp.Dispose();
//g.Dispose();//注意这个地方的绘图面g是窗体的Paint事件中的,不能被释放,否则出现”
Application.Run(new Form1());“参数无效错误
}
private void panelLib21_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
startP = e.Location;
endP = e.Location;
mdFlag = true;
panelLib21.Capture = true;//捕获panelLib21上鼠标按下
}
}
private void panelLib21_MouseMove(object sender, MouseEventArgs e)
{
curP = e.Location;
if (mdFlag)
{
if (endP.X != curP.X || endP.Y != curP.Y)
{
endP = curP;
panelLib21.Invalidate();//刷新panelLib21
}
}
}
private void panelLib21_MouseUp(object sender, MouseEventArgs e)
{
lines[k][0] = startP;
lines[k][1] = endP;
k++;
mdFlag = false;
panelLib21.Capture = false;//释放panelLib21上鼠标按下
}
private void panel1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
// Graphics g = this.CreateGraphics();
Bitmap bmp = new Bitmap(panelLib21.Width, panelLib21.Height);
Graphics bufg = Graphics.FromImage(bmp);
//g.Clear(this.BackColor);//可用可不用,视具体情况
//bufg.Clear(this.BackColor);//可用可不用
bufg.DrawLine(new Pen(Color.Black, 1), startP, endP);
for (int i = 0; i <= k2; i++)
{
bufg.DrawLine(new Pen(Color.Black, 1), lines2[i][0], lines2[i][1]);
}
bufg.DrawString("System Panel Un-DoubleBufferd", new Font("verdana", 16), new
SolidBrush(Color.Red), 0, 0);
g.DrawImage(bmp, 0, 0);
bufg.Dispose();
bmp.Dispose();
}
private void panel1_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
startP = e.Location;
endP = e.Location;
mdFlag2 = true;
panel1.Capture = true;
}
}
private void panel1_MouseMove(object sender, MouseEventArgs e)
{
curP = e.Location;
if (mdFlag2)
{
if (endP.X != curP.X || endP.Y != curP.Y)
{
endP = curP;
panel1.Invalidate();
}
}
}
private void panel1_MouseUp(object sender, MouseEventArgs e)
{
lines2[k2][0] = startP;
lines2[k2][1] = endP;
k2++;
mdFlag2 = false;
panel1.Capture = false;
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
// Graphics g = this.CreateGraphics();
Bitmap bmp = new Bitmap(ClientSize .Width, ClientSize .Height - splitContainer1
.Height );//定义窗体宽、半窗体高的缓冲位图
Graphics bufg = Graphics.FromImage(bmp);
//g.Clear(this.BackColor);//可用可不用,视具体情况
//bufg.Clear(this.BackColor);//可用可不用
bufg.DrawLine(new Pen(Color.Black, 1), startP, endP);
for (int i = 0; i <= k3; i++)
{
bufg.DrawLine(new Pen(Color.Black, 1), lines3[i][0], lines3[i][1]);
}
bufg.DrawString("From DoubleBufferd", new Font("verdana", 16), new SolidBrush
(Color.Red), 0, 0);
g.DrawImage(bmp, 0, 0);
bufg.Dispose();
bmp.Dispose();
}
private void Form1_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
startP = e.Location;
endP = e.Location;
mdFlag3 = true;
Capture = true;
}
}
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
curP = e.Location;
if (mdFlag3)
{
if (endP.X != curP.X || endP.Y != curP.Y)
{
endP = curP;
Invalidate(new Rectangle(0, 0, ClientSize.Width, ClientSize.Height -
splitContainer1.Height));//只刷新窗体的上半部分
}
}
}
private void Form1_MouseUp(object sender, MouseEventArgs e)
{
lines3[k3][0] = startP;
lines3[k3][1] = endP;
k3++;
mdFlag3 = false;
Capture = false;
}
private void Form1_SizeChanged(object sender, EventArgs e)
{
splitContainer1.Location = new Point(0, ClientSize.Height / 2);
splitContainer1.Size = new Size(ClientSize.Width, ClientSize.Height / 2);
}
}
}
第二种方式只需将Form1_Paint方法里改成如下代码:
private void Form1_Paint(object sender, PaintEventArgs e)
{
BufferedGraphicsContext currentContext = BufferedGraphicsManager.Current;
BufferedGraphics bufg = currentContext .Allocate (e.Graphics, new Rectangle(0, 0,
ClientSize.Width, ClientSize.Height - splitContainer1.Height));
Graphics g = bufg.Graphics;
g.Clear(this.BackColor);
g.DrawLine(new Pen(Color.Black, 1), startP, endP);
for (int i = 0; i <= k3; i++)
{
g.DrawLine(new Pen(Color.Black, 1), lines3[i][0], lines3[i][1]);
}
g.DrawString("From DoubleBufferd", new Font("verdana", 16), new SolidBrush
(Color.Red), 0, 0);
bufg.Render(e.Graphics);
g.Dispose();
bufg .Dispose ();
}
你可以在panel1_Paint方法中设置如下代码来证明SetStyle的那三句话的重要性:
private void panel1_Paint(object sender, PaintEventArgs e)
{
BufferedGraphicsContext currentContext = BufferedGraphicsManager.Current;
BufferedGraphics bufg = currentContext.Allocate(e.Graphics, new Rectangle(0, 0,
panel1.Width, panel1.Height));
Graphics g = bufg.Graphics;
g.Clear(panel1.BackColor);
g.DrawLine(new Pen(Color.Black, 1), startP, endP);
for (int i = 0; i <= k2; i++)
{
g.DrawLine(new Pen(Color.Black, 1), lines2[i][0], lines2[i][1]);
}
g.DrawString("System Panel Un-DoubleBufferd", new Font("verdana", 16), new
SolidBrush(Color.Red), 0, 0);
bufg.Render(e.Graphics);
g.Dispose();
bufg.Dispose();
}
附录:自定义设置双缓冲的Panel面板控件
1.创建项目
创建新项目时,应指定其名称以便设置根命名空间、程序集名称和项目名称,并确保默认组件位于正确的命名空间中。
创建 panelLib2 控件库和 panelLib2 控件在“文件”菜单上,指向“新建”,然后单击“项目”打开“新建项目”对话框。
在 Visual C# 项目列表中,选择“Windows 控件库”项目模板,然后在“名称”框中键入 panelLib2。
在“解决方案资源管理器”中右击“UserControl1.cs”,再从快捷菜单中选择“重命名”。将文件名更改为 panelLib2.cs。系统询问是否要重命名对“UserControl1”代码元素的所有引用时,单击“是”按钮。
在“解决方案资源管理器”中右击“panelLib2.cs”,再选择“查看代码”。
找到 class 语句行 public partial class panelLib2,并将此控件继承的类型从 UserControl 更改为Panel。这允许您所继承的控件继承 Panel 控件的所有功能。
在“解决方案资源管理器”中打开“panelLib2.cs”节点,以显示设计器生成的代码文件“panelLib2.Designer.cs”。在“代码编辑器”中打开此文件。
找到 InitializeComponent 方法并删除分配 AutoScaleMode 属性的行。在 Panel 控件中并不存在此属性。
从“文件”菜单中,选择“全部保存”来保存项目。
注意
可视化设计器不再可用。由于 Panel 控件自行绘制,因此您无法在设计器中修改其外观。除非在代码中进行修改,否则它的可视化表示形式将与它所继承的类(即 Panel)的可视化表示形式完全一样。但您仍然可以向设计器图面添加不含 UI 元素的组件。
2.将属性添加到继承的控件中
继承的 Windows 窗体控件的可能用途之一是创建与标准 Windows 窗体控件的外观相同、但公开自定义属性的控件。在本节中,您将向控件中设置双缓冲属性。
在“解决方案资源管理器”中,右击“panelLib2.cs”,然后从快捷菜单中单击“查看代码”。
找到 class 语句。紧接在 { 后面键入下列代码:
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
从“文件”菜单中,选择“全部保存”来保存项目。
3.测试控件
控件不是独立的项目,它们必须寄宿在某个容器中。若要测试控件,必须提供一个运行该控件的测试项目。还必须通过生成(编译)该控件使其可由测试项目访问。在本节中,将生成控件并在 Windows 窗体中测试它。
4.生成控件
在“生成”菜单上单击“生成解决方案”。生成应该成功,没有任何编译器错误或警告。
5.创建测试项目
在“文件”菜单上,指向“添加”,然后单击“新项目”打开“添加新项目”对话框。
在“Visual C#”节点下选择“Windows”节点,再单击“Windows 应用程序”。
在“名称”框中键入 panelLib2Test。
在“解决方案资源管理器”中,右击测试项目的“引用”节点,然后从快捷菜单上选择“添加引用”以显示“添加引用”对话框。
单击标记为“项目”的选项卡。panelLib2 项目将在“项目名称”下列出。双击项目以添加对测试项目的引用。
在“解决方案资源管理器”中右击“测试”,再选择“生成”。
6.将控件添加到窗体
在“解决方案资源管理器”中,右击“Form1.cs”,然后从快捷菜单中选择“视图设计器”。
在“工具箱”中单击“panelLib2 组件”。双击“panelLib2”。窗体上显示一个“panelLib2”。
在“解决方案资源管理器”中,右击“测试”,然后从快捷菜单中选择“设为启动项目”。
从“调试”菜单中选择“启动调试”。将显示 Form1。
以上只是说了如何自定义一个设置为双缓冲属性的Panel控件,我想其它两个控件的添加是比较容易的,这就不赘述了,有问题请留言。