一、 畫面閃爍問題與雙緩沖技術
1.1 導致畫面閃爍的關鍵原因分析:
1 繪制窗口由於大小位置狀態改變進行重繪操作時
繪圖窗口內容或大小每改變一次,都要調用Paint事件進行重繪操作,該操作會使畫面重新刷新一次以維持窗口正常顯示。刷新過程中會導致所有圖元重新繪制,
而各個圖元的重繪操作並不會導致Paint事件發生,因此窗口的每一次刷新只會調用Paint事件一次。窗口刷新一次的過程中,每一個圖元的重繪都會立即顯示到窗口,
因此整個窗口中,只要是圖元所在的位置,都在刷新,而刷新的時間是有差別的,閃爍現象自然會出現。
所以說,此時導致窗口閃爍現象的關鍵因素並不在於Paint事件調用的次數多少,而在於各個圖元的重繪。
根據以上分析可知,當圖數目不多時,窗口刷新的位置也不多,窗口閃爍效果並不嚴重;當圖元數目較多時,繪圖窗口進行重繪的圖元數量增加,繪圖窗口每一次刷新
都會導致較多的圖元重新繪制,窗口的較多位置都在刷新,閃爍現象自然就會越來越嚴重。特別是圖元比較大繪制時間比較長時,閃爍問題會更加嚴重,因為時間延遲會更長。
解決上述問題的關鍵在於:窗口刷新一次的過程中,讓所有圖元同時顯示到窗口。
2、進行鼠標跟蹤繪制操作或者對圖元進行變形操作時
當進行鼠標跟蹤繪制操作或者對圖元進行變形操作時,Paint事件會頻繁發生,這會使窗口的刷新次數大大增加。雖然窗口刷新一次的過程中所有圖元同時顯示到窗口,但
也會有時間延遲,因為此時窗口刷新的時間間隔遠小於圖元每一次顯示到窗口所用的時間。因此閃爍現象並不能完全消除!
所以說,此時導致窗口閃爍現象的關鍵因素在於Paint事件發生的次數多少。
解決此問題的關鍵在於:設置窗體或控件的幾個關鍵屬性。
1.2 雙緩沖的關鍵技術
1、設置顯示圖元控件的幾個屬性,這樣可以使效果更加明顯。
this.SetStyle(ControlStyles.OptimizedDoubleBuffer|ControlStyles.ResizeRedraw|ControlStyles.AllPaintingInWmPaint,true);
2、窗口刷新一次的過程中,讓所有圖元同時顯示到窗口。
Bitmap bmp=null; Graphics g_bmp=null; bmp=new Bitmap(this.Width,this.Height); g_bmp=Graphics.FromImage(bmp); g_bmp.Clear(this.BackColor); g_bmp.DrawString("重繪",this.Font,new SolidBrush(this.ForeColor),this.Location.X+1,this.Location.Y+1); this.Refresh(); //在OnPaint方法中實現下面代碼 private void this_Paint(object sender,PaintEventArgs e) { Graphics g=e.Graphics; if(g==null) return; if(g_bmp!=null) { g.DrawImage((Image)bmp,0,0); } }
1.3 窗口刷新一次的過程中,讓所有圖元同時顯示到窗口
可以通過以下幾種方式實現,這幾種方式都涉及到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); myBuffer.Dispose();//釋放資源
至此,雙緩沖問題解決,兩種方式的實現效果都一樣,但最后一種方式的占有的內存很少,不會出現內存泄露!
1.4 對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時,重新畫圖。
二、示例1
在使用gdi技術繪圖時,有時會發現圖形線條不夠流暢,或者在改變窗體大小時會閃爍不斷的現象.(Use DoubleBuffer to solve it!)
1.線條不流暢:窗體在重繪時自身重繪與圖形重繪之間存在時間差,導致二者圖像顯示不協調
2.改變窗體大小不流暢:重繪時自身背景顏色與圖形顏色頻繁交替,造成視覺上的閃爍
下面,用四個圖形例子解決這個問題 :貝塞爾曲線,圓形,矩形,不規則圖形
思路:首先用 width 定義位圖的寬度; height 定義位圖的高度
//創建一個與窗體工作區大小相同的位圖實例 // image:Image類的子類的實例引用 Bitmap localBitmap=new Bitmap(CilentRectangle.Width,CilentRectangle.Height) //創建位圖實例 // image:要繪制的圖像 x:繪制的圖像的左上角 x坐標 y:左上角y坐標 Graphics g=e.Graphics;//獲取窗體畫布 g.DrawImage(localBitmap,0,0); //在窗體中繪制出內存中的圖像
實現:由於Paint被 .net隱藏,我們需要在窗體代碼中加上自己的Paint事件中繪制窗口
this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);
private void InitializeComponent() { this.SuspendLayout(); // // Form1 // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(388, 325); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "Form1"; this.Text = "雙緩沖技術繪圖"; this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint); this.ResumeLayout(false); }
源代碼:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Drawing.Drawing2D; namespace DoubleBuffer { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Paint(object sender, PaintEventArgs e) { Bitmap localBitmap = new Bitmap(ClientRectangle.Width, ClientRectangle.Height); //創建位圖實例 Graphics g_bmp= Graphics.FromImage(localBitmap); g_bmp.Clear(BackColor); g_bmp.SmoothingMode = SmoothingMode.AntiAlias; PaintImage(g_bmp);
Graphics g = e.Graphics;//獲取窗體畫布 g.DrawImage(localBitmap, 0, 0); //在窗體的畫布中繪畫出內存中的圖像 g_bmp.Dispose(); localBitmap.Dispose(); g.Dispose(); } private void PaintImage(Graphics g) { //繪圖 GraphicsPath path = new GraphicsPath(new Point[]{ new Point(100,60),new Point(350,200),new Point(105,225),new Point(190,ClientRectangle.Bottom), new Point(50,ClientRectangle.Bottom),new Point(50,180)}, new byte[]{ (byte)PathPointType.Start, (byte)PathPointType.Bezier, (byte)PathPointType.Bezier, (byte)PathPointType.Bezier, (byte)PathPointType.Line, (byte)PathPointType.Line}); PathGradientBrush pgb = new PathGradientBrush(path); pgb.SurroundColors = new Color[] { Color.Green, Color.Yellow, Color.Red, Color.Blue, Color.Orange, Color.LightBlue }; g.FillPath(pgb, path); g.DrawString("雙緩沖繪圖", new Font("宋體", 18, FontStyle.Bold), new SolidBrush(Color.Red), new PointF(110, 20)); g.DrawBeziers(new Pen(new SolidBrush(Color.Green),2),new Point[] {new Point(120,100),new Point(120,120),new Point(120,100),new Point(120,150)}); g.DrawArc(new Pen(new SolidBrush(Color.Blue), 5), new Rectangle(new Point(120, 170), new Size(60, 60)), 0, 360); g.DrawRectangle(new Pen(new SolidBrush(Color.Orange), 3), new Rectangle(new Point(240, 260), new Size(90, 50))); } } }
// Form 設計
namespace DoubleBuffer { partial class Form1 { /// <summary> /// 必需的設計器變量。 /// </summary> private System.ComponentModel.IContainer components = null; /// <summary> /// 清理所有正在使用的資源。 /// </summary> /// <param name="disposing">如果應釋放托管資源,為 true;否則為 false。</param> protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows 窗體設計器生成的代碼 /// <summary> /// 設計器支持所需的方法 - 不要 /// 使用代碼編輯器修改此方法的內容。 /// </summary> private void InitializeComponent() { this.SuspendLayout(); // // Form1 // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(388, 325); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "Form1"; this.Text = "雙緩沖技術繪圖"; this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint); this.ResumeLayout(false); } #endregion } }

當變化窗體時,會導致圖像出現變形,可把窗體屬性中的ResizeRedraw 設置為 true
增加繪制隨機圖形功能的動畫效果如下:

現在將源碼貢獻自此,讓不太懂雙緩沖繪圖的有一個大致的了解,以便少走筆者學習的彎路。如有問題,歡迎詢問評論。
參考文章
ShinePans , C#-gdi繪圖,雙緩沖繪圖,Paint事件的觸發
一個年輕人, c# 雙緩沖 技術與例子(解決應用程序閃爍問題)
