標籤:style http color 使用 os io strong 資料
當編寫一個典型的Windows 表單程式時,表單和控制項的繪製、效果等操作是不需要特別加以考慮的。這是為什麼呢?因為通過使用 .Net 架構,開發人員可以拖動一系列的控制項到表單上,並書寫一些簡單的與事件相關聯的代碼然後在IDE中按F5,一個完完全全的表單程式就誕生了!所有控制項都將自己繪製自己,表單或者控制項的大小和縮放都調整自如。在這裡經常會用到的,且需要引起一點注意的就是控制項效果。遊戲,自訂圖表控制項以及螢幕保護裝置程式的編寫會需要程式員額外撰寫用於響應 Paint 事件的代碼。
本文針對那些Windows 表單開發人員並有助於他們在應用程式編製過程中使用簡單的繪圖技術。首先,我們會討論一些基本的繪圖概念。到底誰在負責進行繪製操作?Windows 表單程式是如何知道何時該進行繪製的?那些繪製代碼究竟被放置在哪裡?之後,還將介紹映像繪製的雙重緩衝區技術,你將會看到它是怎樣工作的,怎樣通過一個方法來實現緩衝和實際顯示的映像間的交替。最後,我們將會探討”智能無效地區”,實際就是僅僅重繪或者清除應用程式表單上的無效部分,加快程式的顯示和響應速度。希望這些概念和技術能夠引導讀者閱讀完本文,並且有助於更快和更有效開發Windows 表單程式。
Windows 表單使用GDI+映像引擎,在本文中的所有繪圖代碼都會涉及使用託管的.Net 架構來操縱和使用Windows GDI+映像引擎。
儘管本文用於基本的表單繪圖操作,但是它同樣提供了快速的、有效且有助於提高程式效能的技術和方法。所以,在通讀本文之前建議讀者對.Net架構有個基本的瞭解,包括Windows 表單事件處理、簡單的GDI+對象譬如Line,Pen和Brush等。熟悉Visual Basic .Net或者C#程式設計語言。
概念
Windows 應用程式是自己負責繪製的,當一個表單”不乾淨”了,也就是說表單改變了大小,或者部分被其它程式表單遮蓋,或者從最小化狀態恢複時,程式都會收到需要繪製的資訊。Windows把這種”不乾淨”狀態稱為”無效的(Invalidated)”狀態,我們理解為:需要重繪,當Windows 表單程式需要重繪表單時它會從Windows訊息佇列中擷取繪製的資訊。這個資訊經過.Net架構封裝然後傳遞到表單的 PaintBackground 和 Paint 事件中去,在上述事件中適當的書寫專門用於繪製的代碼即可。
簡單的繪圖樣本如下:
以下是引用片段: using System; using System.Drawing; using System.Windows.Forms; public class BasicX : Form { public BasicX() { InitializeComponent(); } private void BasicX_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; Pen p = new Pen(Color.Red); int width = ClientRectangle.Width; int height= ClientRectangle.Height; g.DrawLine(p, 0,0, width, height); g.DrawLine(p, 0, height, width, 0); p.Dispose(); } private void InitializeComponent() { this.SetStyle(ControlStyles.ResizeRedraw, true); this.ClientSize = new System.Drawing.Size(300, 300); this.Text = "BasicX"; this.Paint += new PaintEventHandler(this.BasicX_Paint); } [System.STAThreadAttribute()] public static void Main() { Application.Run(new BasicX()); } } |
上述代碼分成兩個基本的步驟來建立樣本程式。首先 InitializeComponent 方法包含一些屬性的設定和附加表單 Paint 事件的處理過程。注意,在方法中控制項的樣式也同時被設定,設定控制項的樣式也是自訂Windows 表單及控制項行為的一種有效途徑,譬如:控制項的"ResizeRedraw"屬性指示當表單的大小變化發生以後需要對其完全進行重繪,也就是說重繪時總是需要對整個表單的用戶端區域進行重繪。表單的“用戶端區域”是指除了標題列和邊框的所有表單地區。可以進行一個有趣的實驗,取消該控制項的屬性然後再運行程式,我們可以很明顯的看出為什麼該屬性會被經常的設定,因為表單調整大小後的無效地區根本不會被重繪。
好了,我們需要注意一下BasicX_Paint方法,正如先前所提到的,Paint 事件在程式需要重繪時被啟用,程式表單利用Paint事件來負責回應需要重繪的系統訊息,BasicX_Paint方法的調用需要一個對象 sender 和一個PaintEventArgs類型的變數,PaintEventArgs類的執行個體或稱之為變數 e 封裝了兩個重要的資料,第一個就是表單的 Graphics 對象,該對象表示表單可繪製的表面也稱之為畫布用於繪製諸如線、文本以及映像等,第二個資料就是ClipRectangle,該Rectangle對象表示表單上無效的的矩形範圍,或者說就是表單需要重繪的地區。記住,當表單的ResizeRedDraw設定後,調整大小後該ClipRectangle的大小實際就等於表單整個用戶端區域的大小,或者是被其它程式表單遮蓋的那部分剪下地區。關於部分剪下地區的用處我們會在智能重繪章節作更詳細的闡述。
雙重緩衝區繪圖技術
雙重緩衝區技術能夠使程式的繪圖更加快速和平滑,有效減少繪製時的映像閃爍。該技術的基本原理是先將映像繪製到記憶體中的一塊畫布上,一旦所有的繪製操作都完成了,再將記憶體中的畫布推到表單的或者控制項的表面將其顯示出來。通過這種操作後的程式能使使用者感覺其更加快速和美觀。
下面提供的樣本程式能夠闡明雙重緩衝區的概念和實現方法,這個樣本所包含的功能已相當完整,且完全可以在實際應用中使用。在該章節後面還會提及該技術應該配合控制項的一些屬性設定才能達到更好的效果。
要想領略雙重緩衝區繪圖技術所帶來的好處就請運行SpiderWeb樣本程式吧。程式啟動並運行後對視窗大小進行調整,你會發現使用這種繪圖演算法的效率不高,並且在調整大小的過程中有大量的閃爍出現。
不具備雙重緩衝區技術的SpiderWeb樣本程式
縱觀程式的源碼你會發現在程式Paint事件啟用後是通過調用LineDrawRoutine方法來實現線的繪製的。LineDrawRoutine方法有兩個參數,第一個是Graphics對象是用於繪製線條的地方,第二個是繪圖工具Pen對象用來畫線條。代碼相當簡單,一個迴圈語句,LINEFREQ常量等,程式從表單表面的左下一直劃線到其右上。請注意,程式使用浮點數來計算在表單上的繪製位置,這樣做的好處就是當表單的大小發生變化時位置資料會更加精確。
以下是引用片段: private void LineDrawRoutine(Graphics g, Pen p) { float width = ClientRectangle.Width; float height = ClientRectangle.Height; float xDelta = width / LINEFREQ; float yDelta = height / LINEFREQ; for (int i = 0; i < LINEFREQ; i++) { g.DrawLine(p, 0, height - (yDelta * i), xDelta * i, 0); } } |
撰寫很簡單的用於響應Paint事件SpiderWeb_Paint的代碼,正如前面所提到的,Graphics對象就是從Paint事件參數PaintEventArgs對象中提取出來的表示表單的繪製表面。這個Graphics對象連同新建立Pen對象一起傳遞給LineDrawRoutine方法來畫出蜘蛛網似的線條,使用完Graphics對象和Pen對象後釋放其佔用的資源,那麼整個繪製操作就完成了。
以下是引用片段: private void SpiderWeb_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; Pen redPen = new Pen(Color.Red); LineDrawRoutine(g, redPen); redPen.Dispose(); g.Dispose(); } |
那麼到底作怎麼樣的改動才能使上面的SpiderWeb程式實現簡單的雙重緩衝區技術呢?原理其實相當簡單,就是將應該畫到表單表面的繪製操作改成先畫到記憶體中的位元影像上,LineDrawRoutine向這個在記憶體中隱藏的畫布執行同樣的蜘蛛網繪製操作,等到繪製完畢再通過調用Graphics.DrawImage方法將隱藏的畫布上內容推到表單表面來顯示出來,最後,再加上一些小的改動一個高效能的繪圖表單程式就完成了。
請比較下面雙重緩衝區繪圖事件與前面介紹的簡單繪圖事件間的區別:
以下是引用片段: private void SpiderWeb_DblBuff_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; Pen bluePen = new Pen(Color.Blue); Bitmap localBitmap = new Bitmap(ClientRectangle.Width,ClientRectangle.Height); Graphics bitmapGraphics = Graphics.FromImage(localBitmap); LineDrawRoutine(bitmapGraphics, bluePen); //把在記憶體裡處理的bitmap推向前台並顯示 g.DrawImage(localBitmap, 0, 0); bitmapGraphics.Dispose(); bluePen.Dispose(); localBitmap.Dispose(); g.Dispose(); } |
上面的範例程式碼建立了記憶體位元影像對象,它的大小等於表單的用戶端區域(就是繪圖表面)的大小,通過調用Graphics.FromImage將記憶體中位元影像的引用傳遞給Graphics對象,也就是說後面所有對該Graphics對象的操作實際上都是對記憶體中的位元影像進行操作的,該操作在C++中等同於將位元影像對象的指標複製給Graphics對象,兩個對象使用的是同一塊記憶體位址。現在Graphics對象表示的是螢幕後方的一塊畫布,而它在雙重緩衝區技術中起到至關重要的作用。所有的線條繪製操作都已經針對於記憶體中的位元影像對象,下一步就通過調用DrawImage方法將該位元影像複製到表單,蜘蛛網的線條就會立刻顯示在表單的繪製表面而且絲毫沒有閃爍出現。
這一系列的操作完成後還不是特別有效,因為我們先前提到了,控制項的樣式也是定義Windows 表單程式行為的一條途徑,為了更好的實現雙重緩衝區必須設定控制項的Opaque屬性,這個屬性指明表單是不負責在後台繪製自己的,換句話說,如果這個屬性設定了,那麼必須為清除和重繪操作添加相關的代碼。具備雙重緩衝區版本的SpiderWeb程式通過以上的設定在每一次需要重繪時都表現良好,表單表面用其自己的背景色進行清除,這樣就更加減少了閃爍的出現。
以下是引用片段: public SpiderWeb_DblBuff() { SetStyle(ControlStyles.ResizeRedraw | ControlStyles.Opaque, true); } private void SpiderWeb_DblBuff_Paint(object sender, PaintEventArgs e) { Bitmap localBitmap = new Bitmap(ClientRectangle.Width, ClientRectangle.Height); Graphics bitmapGraphics = Graphics.FromImage(localBitmap); bitmapGraphics.Clear(BackColor); LineDrawRoutine(bitmapGraphics, bluePen); } |
結果怎麼樣?映像的繪製平滑多了。從記憶體中將蜘蛛網的線條推到前台以顯示出來是完全沒有閃爍的,但是我們還是稍微停頓一下,先將記憶體中的位元影像修整一下再顯示出來,可以添加一行代碼以便使線條看上去更加平坦。
以下是引用片段: bitmapGraphics.SmoothingMode = SmoothingMode.AntiAlias; |
在將記憶體中的位元影像對象賦給Graphics後通過放置這行代碼,我們在畫布上所畫的每一個線條都使用了反鋸齒,使凹凸不平的線條顯得更加平坦。
具備雙重緩衝區技術的且使用AntiAliasing(反鋸齒)屬性的SpiderWeb_DblBuff樣本程式
完成了簡單的雙重緩衝區應用後有兩個問題需要向讀者闡明,.Net中的某些控制項例如:Button、PictureBox、Label還有PropertyGrid都已經很好的利用了該技術!這些控制項在預設狀態下會自動啟用雙重緩衝區技術,使用者可以通過對“DoubleBuffer”屬性的設定來就可以實現雙重緩衝區技術。所以,使用者若使用PictureBox來繪製蜘蛛網將會更有效率一些,而且也使程式變得更加簡單了。
我們在這裡討論的雙重緩衝區技術既不是完全被最佳化但也沒有什麼太大的負面影響。雙重緩衝區技術是減少Windows 表單繪製時閃爍的一條重要途徑,但是它也確實消耗不少記憶體,因為它將會使用雙倍的記憶體空間:應用程式所顯示的映像和螢幕後方記憶體中的映像。每次Paint事件被啟用時都會動態建立位元影像對象,這種機制會相當耗費記憶體。而內建雙重緩衝區技術的控制項在使用DoubleBuffer屬性後執行起來的最佳化程度則會更好一些。
使用GDI+的DIB(與裝置無關的位元影像)對象來實現這種畫面以外的記憶體緩衝,內建雙重緩衝區機制的控制項則能好的利用該位元影像對象。DIB是底層Win32的對象用於高效的螢幕繪製。同樣,值得注意的是GDI+的第一個版本GDI中僅與硬體加速有關以及一些簡易功能可以直接使用,由於這樣的限制,像反鋸齒和半透明等螢幕繪製方法執行起來的速度則相當慢。儘管雙重緩衝區機制消耗了一些記憶體但是它的使用不容置疑的增強了程式的執行效能。
智能重繪,在繪製前需要斟酌一下
“智能無效”(智能重繪)就是在暗示程式員應該明白僅應對程式中無效的地區進行重繪,對Regions對象所對應的無效地區進行重繪可以提高繪製效能,使用Regions對象你可以僅排除或繪製控制項和表單的部分地區已獲得更好的效能。我們現在就開始來看一下BasicClip樣本程式,這個程式使用儲存在PaintEventArgs對象的ClipRectangle對象,之前我們已經提及,無論何時當程式的大小發生變化時Paint事件都會被啟用。BasicClip樣本程式用紅和藍兩種顏色填充剪下的矩形地區,利用不同的速度調整表單的大小几次以後,你會發現繪製的矩形地區其實就是表單的無效地區(包括大於原始表單大小的地區部分和縮少了的地區部分),樣本程式的Paint事件代碼如下:
以下是引用片段: private void BasicClip_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; if (currentBrush.Color == Color.Red) currentBrush.Color = Color.Blue; else currentBrush.Color = Color.Red; g.FillRectangle(currentBrush, e.ClipRectangle); g.Dispose(); } |
該樣本程式的唯一目的就是示範怎樣僅針對部分地區進行圖形繪製。
BasicClip樣本程式中的彩色矩形地區就是表示表單的下方和右側的無效地區。
Regions是一種被用來定義Windows 表單或者控制項地區的對象,調整表單大小後所獲得的Regions就是表單重繪的最小地區。當程式需要進行繪製的時候僅繪製感興趣的特殊地區,這樣繪製更小的地區就會使程式的運行速度更快。
為了更好的示範Regions的用法,請查看TextCliping樣本程式。該程式重載了OnPaintBackground和OnPaint方法,直接重載這些方法比偵聽事件更能保證代碼在其它的繪製操作之前被調用,而且對於自訂控制項的繪製也更加有效。為了清楚起見,樣本程式提供了一個Setup方法,該方法定義了全域的Graphics對象。
以下是引用片段: private void Setup() { GraphicsPath textPath = new GraphicsPath(); textPath.AddString(displayString, FontFamily.GenericSerif, 0, 75, new Point(10, 50), new StringFormat()); textRegion = new Region(textPath); backgroundBrush = new TextureBrush(new Bitmap("CoffeeBeanSmall.jpg"), WrapMode.Tile); foregroundBrush = new SolidBrush(Color.Red); } |
上面的Setup方法首先定義一個空的GraphicsPath物件變數textPath,下一步字串“Windows Forms”的邊界被添加到該路徑中,圍繞這個輪廓建立Region。這樣,一個被繪製在表單表面的以字串輪廓為地區的Region就被建立了。最後,Setup方法建立以材質刷子為背景和以實色刷子為前景來繪製表單。
以下是引用片段: protected override void OnPaintBackground(PaintEventArgs e) { base.OnPaintBackground(e); Graphics bgGraphics = e.Graphics; bgGraphics.SetClip(textRegion, CombineMode.Exclude); bgGraphics.FillRectangle(backgroundBrush, e.ClipRectangle); bgGraphics.Dispose(); } |
上面定義的OnPaintBackground方法先立刻調用基類方法,這能夠保證所有底層繪製的代碼都能夠被執行。下一步,從PaintEventArgs中獲得Graphics對象,再將Graphics對象的剪下地區定義為textRegion對象。通過指定CombineMode.Exclude參數,明確無論在哪裡繪製或怎樣繪製Graphics對象都不繪製textRegion地區內部。
以下是引用片段: protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics fgGraphics = e.Graphics; fgGraphics.FillRegion(foregroundBrush, textRegion); fgGraphics.Dispose(); } |
最後,OnPaint事件負責精確的繪製出字串。可以很容易的通過調用Graphics的FillRegion方法來實現。通過指定的前景刷子foregroundBrush和textRegion且僅是該地區被繪製。結果,Windows 表單程式在運行之前確實“思考”該怎樣進行繪製。
TextClipping樣本程式,通過Region定義的Windows Forms字串。能夠使程式在繪製時避開一個地區。
適當的組合使用地區和智能重繪你可以編寫出運行速度快且不會引起閃爍的繪製代碼,並且比單獨使用雙重緩衝區繪製還要節省記憶體的消耗。
結論
如果你的程式確定要進行繪製操作,使用幾種技術可以增強繪製效能。確保爭取設定控制項屬性以及適當的Paint事件處理是編寫健壯程式的開始。在權衡好利弊後可以使用雙重緩衝區技術產生非常“保護視力”的結果。最後,在實際繪製前進行思考到底哪些用戶端區域或Region需要被繪製將非常有益。
希望通過這篇文章能夠使讀者更好的理解關於.net架構的繪製技術及其應用。