C#發現之旅第五講 圖形開發基礎篇
袁永福 2008-5-15
系列課程說明
為了讓大家更深入的瞭解和使用C#,我們將開始這一系列的主題為“C#發現之旅”的技術講座。考慮到各位大多是進行WEB資料庫開發的,而所謂發現就是發現我們所不熟悉的領域,因此本系列講座內容將是C#在WEB資料庫開發以外的應用。目前規劃的主要內容是圖形開發和XML開發,並計劃編排了多個課程。在未來的C#發現之旅中,我們按照由淺入深,循序漸進的步驟,一起探索和發現C#的其他未知的領域,更深入的理解和掌握使用C#進行軟體開發,拓寬我們的視野,增強我們的軟體開發綜合能力。
本系列課程配套的示範代碼為 http://files.cnblogs.com/xdesigner/cs_discovery.zip 。其中的EllipseButtonLib.zip 就是本課程的示範代碼。
本系列課程發行的文章有
C#發現之旅第一講 C#-XML開發
C#發現之旅第二講 C#-XSLT開發
C#發現之旅第三講 使用C#開發基於XSLT的代碼產生器
C#發現之旅第四講 Windows圖形開發入門
C#發現之旅第五講 圖形開發基礎篇
C#發現之旅第六講 C#圖形開發中級篇
C#發現之旅第七講 C#圖形開發進階篇
C#發現之旅第八講 ASP.NET圖形開發帶超連結的餅圖
C#發現之旅第九講 ASP.NET驗證碼技術
C#發現之旅第十講 文件物件模型
課程說明
經過上次Windows圖形開發基本原理的課程,大家對Windows圖形開發有著一些感性的認識,但還可能對此不甚瞭解,還有一些迷茫,在本次課程中,我們將用C#從零開始開發一個比較簡單的橢圓形按鈕的圖形軟體,和大家一起開始探索C#圖形開發。
功能需求
在本次快速軟體開發中,首先是確定軟體功能需求。
現有一個客戶,需要一個軟體,其功能要求如下
- 實現一個橢圓形的按鈕。可置中顯示一段單行文本。
- 滑鼠離開按鈕和進入這個按鈕時,按鈕邊框和背景色需要變化。
- 滑鼠點擊按鈕會觸發一個 Click 事件。
最後產生的軟體的使用者介面
軟體設計
根據功能需求,本軟體設計如下
- 橢圓形按鈕是從UserControl 派生的一種自訂控制項。
- 控制項內部重寫OnPaint事件來繪製按鈕介面。
- 重寫OnMouseMove,OnMouseEnter,OnMouseLeave事件來實現按鈕的動態效果。
- 重寫OnClick事件來觸發 Click 事件。
軟體開發過程
經過簡單的設計,我們開始來開發這個軟體了。
建立C# WinForm.NET工程
開啟VS.NET2003整合式開發環境。建立立一個C#WinForm.NET程式。客戶最終需要一個組件,但此處為了調試方便,開始使用WinForm.NET應用程式工程模式,開發完畢後可以設定它為DLL工程模式提交給客戶。
要進行圖形開發,C#工程必須引用 System.Drawing.dll,在新增WinForm.NET過程時,會自動添加該引用,而新增其他類型的工程時可能不會預設添加該引用,此時需要手動添加該引用。圖形編程需要頻繁引用System.Drawing名稱空間中的類型,因此在代碼的開頭需要添加 using System.Drawing ; 不過很多時候VS.NET會自動添加這個代碼,若不自動添加則需要手動添加。
新增控制項
新增一個名稱為EllipseButton 的使用者控制項。
首先是定義控制項的一些屬性,主要有邊框色,按鈕背景色,滑鼠懸浮時邊框色和按鈕背景色。
定義一個滑鼠移至上方標誌變數。 bool bolMouseHoverFlag = false ;
繪製控制項使用者介面
重寫控制項的OnPaint方法,繪製橢圓形按鈕,其代碼在示範程式中可以看到。在開發自訂的控制項時,可以相應控制項的Paint事件,也可以重寫OnPaint方法,這裡為了代碼結構簡單,此處重寫了OnPaint方法,在重寫該方法時一定要調用基類的 base.OnPaint 方法。
在重寫的OnPaint 方法中,具有一個類型為 PaintEventArgs 的參數,該參數有若干個成員,其中最重要的就是Graphics成員和ClipRectangle成員,Graphics成員是圖形繪製對象,可以看作一個空白的畫布,可以任意繪製圖形;ClipRectangle成員就是繪製地區剪下矩形。
在C#圖形開發中,Graphics類型是最重要的類型,它表示一個畫布對象,任何圖形操作都是輸出到這個畫布上。這個類型提供了很多屬性和方法,可以設定某些圖形輸出品質,還提供了一系列的以Draw開頭的方法來繪製圖形,以Fill開頭的方法來填充圖形。此外還提供方法和屬性進行座標轉換。
ClipRectangle表示剪下矩形,一般情況下,控制項重新繪製內容時是不需要重寫所有的內容,而是繪製一部分內容,該參數就指明控制項中那個部分是需要重新繪製的,該地區以外的介面是不需要繪製,因此該參數是最佳化圖形介面軟體效能的基礎,在這裡,由於橢圓形按鈕繪製的內容少,介面結構簡單,因此不需要最佳化,不需要使用ClipRectangle參數。
我們重寫的OnPaint函數代碼如下
protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); // 建立橢圓路徑 using( System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath()) { path.AddEllipse( 0 , 0 , this.ClientSize.Width -1 , this.ClientSize.Height -1 ); // 填充背景色 using( SolidBrush b = new SolidBrush( bolMouseHoverFlag ? this.HoverBackColor : this.ButtonBackColor )) { e.Graphics.FillPath( b , path ); } // 繪製邊框 using( Pen p = new Pen( bolMouseHoverFlag ? this.HoverBorderColor : this.BorderColor , 2 )) { e.Graphics.DrawPath( p , path ); } } if( this.Caption != null ) { // 繪製文本 using( StringFormat f = new StringFormat()) { // 水平置中對齊 f.Alignment = System.Drawing.StringAlignment.Center ; // 垂直置中對齊 f.LineAlignment = System.Drawing.StringAlignment.Center ; // 設定為單行文本 f.FormatFlags = System.Drawing.StringFormatFlags.NoWrap ; // 繪製文本 using( SolidBrush b = new SolidBrush( this.ForeColor )) { e.Graphics.DrawString( this.Caption , this.Font , b , new System.Drawing.RectangleF( 0 , 0 , this.ClientSize.Width , this.ClientSize.Height ) , f ); } } } }//protected override void OnPaint(PaintEventArgs e) |
在這個方法中,我們首先建立了一個 GraphicsPath 對象,這個對象表示一個路徑,所謂路徑就是若干個直線和曲線的組合。我們可以向路徑對象中添加各種直線段或曲線。在這裡我們調用它的 AddEllipse 方法向路徑中添加了一個橢圓曲線,這是一個封閉曲線。AddEllipse 方法的參數表示一個橢圓的外切矩形。在這裡外切矩形就是控制項的用戶端區域。
所謂客戶區就是控制項內部可以自訂繪製圖形的地區。某些Windows控制項具有邊框,比如文本輸入框,邊框上面是不能繪製圖形的,因此若控制項有邊框則它的客戶區大小不等於控制項大小,此時需要使用控制項的 ClientSize 屬性獲得控制項客戶區大小,當然若控制項沒有邊框,則它的客戶區大小等於控制項大小,為了編程方便,建議大家以後繪製控制項內容時都使用 ClientSize 屬性獲得可繪製地區的大小。
建立了一個橢圓路徑後,我們可以使用繪製橢圓形了,首先是建立一個 SolidBrush 對象,然後調用圖形繪製對象的FillPath方法來填充路徑。然後建立 Pen 對象,使用Graphics的DrawPath方法來繪製路徑。這裡要注意順序不能搞反。若先繪製邊框然後填充橢圓,則會導致後面的操作覆蓋掉前面的操作成果。
圖形編程有一個很明顯的特點,那就是各種圖形操作是要注意順序的,因為後一個圖形操作很容易覆蓋掉前面的圖形操作結果,這造成了圖形開發中調試困難,很多時候需要對代碼進行非常仔細的靜態檢查。
很多圖形編程對象,例如SolidBrush,Pen,GraphisPath等等,都實現了System.IDisposable介面,其內部都使用了非託管資源,在不使用的時候要銷毀這些對象,因此在代碼中使用了 using 文法結構來處理這些對象。
這裡我們使用滑鼠移至上方標誌變數 bolMouseHoverFlag ,使得滑鼠移至上方和不懸停時按鈕的背景色和邊框色有所不同。
繪製出橢圓地區後,我們就可以繪製按鈕文本。首先建立一個 StringFormat 對象,這個對象用於控制繪製文本時的樣式。我們設定文字格式設定為水平置中對齊,垂直置中對齊樣式,而且還不能換行,只能顯示單行文本。
我們根據文本顏色建立一個SolidBrush對象,然後繪製文本,然後調用圖形繪製對象的 DrawString 方法來繪製字串。這個函數第一個參數是常值內容,第二個是字型,第三個就是繪製文本使用的畫刷對象,第四個就是包含文本顯示地區的矩形地區,第5個就是文字格式設定控制。
完成了OnPaint方法後,我們就獲得了一個具有橢圓形外觀的使用者控制項,我們編譯器,然後進入一個表單設計器,在工具箱的“我的使用者控制項”欄目,上面可以看到已經有一個 EllipseButton 項目,按下這個項目就可以在表單上放置一個橢圓形的按鈕了,你可以在屬性列表中設定它的文本。然後運行程式,可以看到啟動並執行表單上顯示了一個橢圓形的按鈕,但這個按鈕就像圖片一樣,毫無生機,我們還需要改進這個控制項來實現動態效果。
響應事件,實現動態效果
開啟這個按鈕控制項的代碼,開始添加代碼來實現滑鼠移至上方的動態效果。首先編寫一個 CheckMouseHover 函數,該函數用於判斷滑鼠是否懸停到按鈕上面,由於按鈕是橢圓形,控制項上有部分內容不屬於按鈕地區,因此即使滑鼠在控制項上面,也要判斷滑鼠游標是否在橢圓形地區中。CheckMouseHover函數代碼如下
/// <summary> /// 檢測釋放發生滑鼠移至上方狀態發生改變,若發生改變則重寫繪製控制項 /// </summary> /// <param name="x">測試點X座標</param> /// <param name="y">測試點Y座標</param> /// <returns>測試點是否在橢圓地區中</returns> private bool CheckMouseHover( int x , int y ) { using( System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath()) { path.AddEllipse( 0 , 0 , this.ClientSize.Width -1 , this.ClientSize.Height -1 ); bool flag = path.IsVisible( x , y ); if( flag != bolMouseHoverFlag ) { bolMouseHoverFlag = flag ; // 控制項整體無效,準備重新繪製,但不立即繪製使用者介面. this.Invalidate(); //this.Refresh(); // 強制立即繪製使用者介面. } return flag ; } } |
我們建立一個路徑對象,向該路徑添加橢圓地區,然後調用路徑的 IsVisible 函數判斷指定點是否包含在這個路徑中,若不包含在路徑中,則該點不在橢圓形按鈕上面。若這次判斷的結果和上次判斷的結果不相同,則設定滑鼠移至上方狀態變數,然後重新繪製按鈕。
代碼中重新繪製控制項具有兩種選擇,一個是調用控制項的 Invalidate 方法,另外可調用 Refresh 方法。兩者都能重新繪製使用者介面,但是有差別的。Invalidate方法是聲明控制項使用者介面一部分或全部無效,但不會導致立即重新繪製使用者介面,而是延遲一段時間後才真正的重新繪製使用者介面,可以看作是一種非同步作業;而Refresh則是立即重新繪製使用者介面,繪製完畢後才結束Refresh方法,是一種同步操作。
在一般情況下Invalidate函數導致的延遲時間很短暫,人類無法察覺,此時應當調用Invalidate方法;但在少數情況下使用Invalidate會導致明顯的可察覺的延遲,則需要使用 Refresh 方法。Invalidate導致的延遲時間的長短和Windows底層訊息驅動機制有關,這裡看出比較精細的圖形編程和Windows底層是有關聯的,Invalidate方法是Win32API函數InvalidateRect的.NET封裝,而Refresh方法是Win32API函數UpdateWindow的封裝。查閱MSND中關於這兩個API函數的說明就可以理解為什麼會出現這種情況。
微軟提出.NET架構目的是讓開發人員脫離Windows底層API來進行快速軟體開發,這個目標在ASP.NET中得到的相當好的實現,因此常規的Web資料庫開發中是不會用到Win32API的。但在圖形開發中,.NET架構仍然很大程度的依賴Win32API函數,.NET圖形相關類庫中有很多部分是Win32API的封裝,這方面和VC的MFC架構有點類似,VC的MFC個人認為是傻大黑粗,功能是強大,可是使用很不方便,而.NET架構中包含了一個充滿靈性的MFC,使用方便,功能也不弱,但仍然是基於Win32API的。因此要很深入的學習.NET圖形編程,就要求對Win32API有所瞭解,這也加大了.NET圖形編程的學習難度。當然比較簡單的.NET圖形編程是不需要瞭解Win32API的。
在這裡也反映出圖形開發中對使用者體驗的一些特殊要求。圖形軟體需要在電腦螢幕上繪製圖形,而人類由於其生理特點,各種感覺器官和運動器官的速度是不同的,大腦思維反應最遲鈍,手操作鍵盤和滑鼠速度一般,而人眼的反映速度是很快的,能感知螢幕上幾十毫秒內發生的變化,由於人眼具有很高的反應速度,因此對圖形軟體的圖形繪製代碼運行速度有很高的要求。
重寫控制項的OnMouseMove 方法,處理滑鼠移動事件,該事件處理中,只是簡單的調用CheckMouseHover 成員,參數就使用滑鼠游標位置。
控制項提供了一系列的以OnMouse開頭的方法都是處理滑鼠事件的,該方法有一個類型為 MouseEventArgs 的參數,該參數具有一些屬性,列出了發生滑鼠事件時的滑鼠按鍵狀態,滑鼠滾輪計數和滑鼠游標在控制項客戶區中的位置。
控制項還重寫 OnMouseLeave 方法,處理滑鼠離開控制項客戶區的事件,取消控制項的滑鼠移至上方狀態。
觸發Click事件
客戶要求滑鼠按下這個橢圓形按鈕需要觸發一個事件,我們選擇了控制項本身具有的Click事件作為按鈕點擊事件,於是我們重寫了OnClick函數,該函數代碼為
/// <summary> /// 處理按一下滑鼠事件 /// </summary> /// <param name="e"></param> protected override void OnClick(EventArgs e) { //base.OnClick (e); Point p = System.Windows.Forms.Control.MousePosition ; p = base.PointToClient( p ); if( CheckMouseHover( p.X , p.Y )) { base.OnClick( e ); } } |
由於按鈕是橢圓形的,當使用者滑鼠點擊控制項時,要判斷點擊點是否在橢圓形地區中,從而要判斷是否需要觸發Click事件。因此我們重寫 OnClick 方法來處理控制項的 Click 事件。
OnClick方法的參數沒有指明滑鼠游標位置,因此我們自己計算滑鼠游標在客戶區中的位置,我們使用Control類型的MousePosition靜態屬性,獲得滑鼠游標在電腦螢幕中的位置,然後使用控制項的PointToClient函數將這個座標從電腦螢幕座標轉換為控制項客戶區座標,然後調用CheckMouseHover函數判斷這個座標是否在橢圓形地區中,若滑鼠在橢圓形地區中,則調用base.OnClick方法,觸發Click事件。
測試控制項
重新編譯器,建立一個表單,開啟表單設計器,在工具箱的我的使用者控制項頁面中可以看到有一個EllipiseButton的使用者控制項,若沒有則滑鼠右擊工具箱,選擇功能表項目“添加/移除項目”。在對話方塊中點擊瀏覽選擇剛剛編譯產生的EXE或DLL檔案,然後選中EllipiseButton即可在工具箱上新增EllipseButton項目。選中橢圓形按鈕,設定屬性列表為顯示控制項事件,雙擊添加控制項的Click事件,在該事件中顯示一個訊息框,然後編譯運行即可看到一個具有動態效果的橢圓形按鈕。如此這個按鈕控制項編寫完畢。
我們設定工程類型為DLL樣式,重新編譯,得到一個DLL檔案,這個DLL檔案就可以提交給客戶使用了。
小結
在本次課程中,我們使用了C#開發了一個很簡單的具有動態效果的橢圓形按鈕的小工具,示範了C#圖形開發的基本過程,使得大家能對C#圖形開發有一個初步的印象。從這個小程式可以看出,代碼是不多的,但所需的基本知識是比較多的,軟體的設計,開發和WEB資料庫開發有著很大的不同。最後我希望大家能在今天的程式的基礎上,實現一個三角型的按鈕控制項。
在下一次課程中,我們繼續使用C#開發一個稍微複雜的圖形軟體,從而更深入的進行C#圖形開發的探索。