我想有很多搞圖形方面的朋友都會用到雙緩沖技術的時候,而且有的時候她的確是個頭疼的問題。最近我也要用雙緩沖技術,程序怎么調試都不合適,當要對圖形進行移動時,總是會出現閃爍抖動。在網上找了些資料,說得都不清不楚的,折騰了一晚上也沒弄出來。第二天覺定自己研究一下。現在把自己的一些想法拿出來跟大家分享一下。
雙緩沖的基本原理:(轉)
一直以來的誤區:.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控件,我想其它兩個控件的添加是比較容易的,這就不贅述了,有問題請留言。
原文鏈接:C# GDI+雙緩沖技術
其它參考鏈接: