C# GDI 绘图闪烁的解决方案

原文链接:https://www.cnblogs.com/1175429393wljblog/p/5676741.html

C#画图解决闪烁问题
导致画面闪烁的关键原因分析:
一、绘制窗口由于大小位置状态改变进行重绘操作时,绘图窗口内容或大小每改变一次,都要调用Paint事件进行重绘操作,该操作会使画面重新刷新一次以维持窗口正常显示。刷新过程中会导致所有图元重新绘制,而各个图元的重绘操作并不会导致Paint事件发生,因此窗口的每一次刷新只会调用Paint事件一次。窗口刷新一次的过程中,每一个图元的重绘都会立即显示到窗口,因此整个窗口中,只要是图元所在的位置,都在刷新,而刷新的时间是有差别的,闪烁现象自然会出现。所以说,此时导致窗口闪烁现象的关键因素并不在于Paint事件调用的次数多少,而在于各个图元的重绘。
根据以上分析可知,当图元数目不多时,窗口刷新的位置也不多,窗口闪烁效果并不严重;当图元数目较多时,绘图窗口进行重绘的图元数量增加,绘图窗口每一次刷新都会导致较多的图元重新绘制,窗口的较多位置都在刷新,闪烁现象自然就会越来越严重。特别是图元比较大绘制时间比较长时,闪烁问题会更加严重,因为时间延迟会更长。
解决上述问题的关键在于:窗口刷新一次的过程中,让所有图元同时显示到窗口。
二、进行鼠标跟踪绘制操作或者对图元进行变形操作时,当进行鼠标跟踪绘制操作或者对图元进行变形操作时,Paint事件会频繁发生,这会使窗口的刷新次数大大增加。虽然窗口刷新一次的过程中所有图元同时显示到窗口,但也会有时间延迟,因为此时窗口刷新的时间间隔远小于图元每一次显示到窗口所用的时间。因此闪烁现象并不能完全消除!所以说,此时导致窗口闪烁现象的关键因素在于Paint事件发生的次数多少。
解决此问题的关键在于:设置窗体或控件的几个关键属性。

解决双缓冲的关键技术:
1、设置显示图元控件的几个属性: 必须要设置,否则效果不是很明显!
this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw |ControlStyles.AllPaintingInWmPaint, true);
2、窗口刷新一次的过程中,让所有图元同时显示到窗口。
可以通过以下几种方式实现,这几种方式都涉及到Graphics对象的创建方式。
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();//释放资源
至此,双缓冲问题解决,两种方式的实现效果都一样,但最后一种方式的占有的内存很少,不会出现内存泄露!

或者:
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);

还有的方式
在构造函数中加如下代码

代码一:
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true); // 禁止擦除背景.
SetStyle(ControlStyles.DoubleBuffer, true); // 双缓冲

代码二:
this.SetStyle(ControlStyles.DoubleBuffer | ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);
this.UpdateStyles();

=======================================================================

使用 GDI+ 双缓冲 解决绘图闪烁问题
现在的问题是很多人不知道怎么怎么使用GDI+ 双缓冲

public partial class Form1 : Form
{
//记录矩形位置的变量
Point p = Point .Empty ;
Point location = new Point(0, 0);
int x = 0;
int y = 0;

    public Form1()
    {
        InitializeComponent();
        //采用双缓冲技术的控件必需的设置
        this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
        this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
        this.SetStyle(ControlStyles.UserPaint, true);
    }
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        Graphics g = e.Graphics;
        g.FillRectangle(Brushes.Black, x, y, 200, 200);
    }
    private void Form1_MouseDown(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Right) return;
        p = e.Location;
    }
    private void Form1_MouseUp(object sender, MouseEventArgs e)
    {
        if (e.Button == MouseButtons.Right) return;
        location.X += e.X - p.X;
        location.Y += e.Y - p.Y;
        p = Point.Empty;
    }
    private void Form1_MouseMove(object sender, MouseEventArgs e)
    {
     if (p == Point.Empty) return;
        x = e.X - p.X + location.X;
        y = e.Y - p.Y + location.Y;
        this.Invalidate(true);//触发Paint事件
    }
 }

这个简单的例子实现了用鼠标拖动窗口中矩形,利用双缓冲技术使动画过程不会产生闪烁.
在这个例子上我犯的错误:
在 OnPaint(PaintEventArgs e)中,我使用下面两种方法获取graphics 对象
Graphics g = this.CreateGraphics();
Graphics g = Graphics.FromHwnd(this.Handle);
这都使双缓冲失效.
获得graphics 对象还有两种方法是
Graphics g = Graphics.FromImage(image); //后面将用此方法实现双缓冲
Graphics g = e.Graphics; //这是唯一好使的方法

上面是在Form窗口直接绘制图形,那么如何在控件上(比如Panel)利用双缓冲技术绘图呢?
在窗口窗创建一个Panel , 并修改一下代码
private void panel1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.FillRectangle(Brushes.Black, x, y, 200, 200);
}
运行后发现拖动在panel1上绘制的图形依然有闪烁,那么是不是应该这样设置
panel1.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);//这样并不行,
因为SetStyle()在Panel类中不是public方法

使用从Panel类继承的MyPanel类 的构造函数中设置双缓冲
public class MyPanel:Panel
{
public MyPanel()
{
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.UserPaint, true);
}
}
不管怎么说这个方法的确好用,不过注意以后你使用的是MyPanel类,而不是Panel.
把自己定义MyPanel从工具栏里拖到窗口上和Panle一样使用.

注意:在控件上绘制就必须设置该控件的DoubleBuffer,而不是Form的DoubleBuffer.

在此之前我采用自己的方法实现双缓冲而不是控件自身的DoubleBuffer
先在内存里绘制图形,包括清除旧画面和绘制新画面,然后将内存的图形绘制到屏幕上
public void Draw(System.Windows.Forms.Panel _panel, float _x, float _y)
{
Graphics g = Graphics.FromHwnd(_panel.Handle);
try{
//在内存创建一块和panel一大小的区域
Bitmap bitmap = new Bitmap(_panel.ClientSize.Width, _panel.ClientSize.Height);
using (Graphics buffer = Graphics.FromImage(bitmap))
{
//buffer中绘图
buffer.Clear(_panel.BackColor); //用背景色填充画面
buffer.Transform = matrix;
buffer.DrawImage(source, _x/Scale , _y/Scale ); //绘制新画面
//屏幕绘图
g.DrawImage(bitmap, 0, 0); //将buffer绘制到屏幕上
}
}
finally
{
g.Dispose();
}
}
使用上面方法不需要任何设置.

总结一下
与绘图有关的ControlStyles
enum ControlStyles{
AllPainingInWmPaint, //将绘制阶段折叠入Paint事件
DoubleBuffer, //直到Paint返回,再显示绘制对象
UserPaint, //用于自身有着特别绘制的控件
Opaque, //忽略OnPaintBackground,Paint事件绘制整个区域
ResizeRedraw,//当调整控件大小时使整个工作区无效
SupportsTransparentBackColor,//模拟透明控件
...
}

1.在OnPaint(PaintEventArgs e)或Paint中 使用e获取graphics,我之所以费了很大周折就是因为在网上找到一篇实现双缓冲文章介绍,不要使用e获取graphics,而用this.CreateGraphics(),还有的文章介绍了奇怪的方法居然最终也好使.

2.在继承了Form和control 的控件上利用双缓冲绘制的时候,可以在控件的构造函数里设置双缓冲属性,而在窗口Form 里设置doublebuffer,只针对窗口的绘制起作用.

3.使用自己的方法,双缓冲的原理都是一样的.

示例:

    private void DrawRectBackImage()
    {
        Graphics g = Graphics.FromHwnd(_currentChart.Handle);
        try
        {
            //在内存创建一块和panel一大小的区域
            Bitmap bitmap = new Bitmap(_currentChart.ClientSize.Width, _currentChart.ClientSize.Height);
            using (Graphics buffer = Graphics.FromImage(bitmap))
            {
                buffer.Transform =new  Matrix();               

                //要画的背景图片

                buffer.DrawImage(curChartImage, _currentChart.ClientRectangle);

               //背景图片上的内容
                DrawFitAdjustRect(buffer);
                //屏幕绘图
                g.DrawImage(bitmap, 0, 0); //将buffer绘制到屏幕上
            }
        }
        finally
        {
            g.Dispose();
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章