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();
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章