幸運的是當編寫一個典型的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框架的繪制技術及其應用。