今天正式開一本新書,《C# GDI+ 破鏡之道》,同樣是破鏡之道系列叢書的一分子。
關於GDI+呢,官方的解釋是這樣的:
GDI+ 是 Microsoft Windows 操作系統的窗體子系統應用程序編程接口 (API)。 GDI+ 是負責在屏幕和打印機上顯示的信息。 顧名思義,GDI+ 是包含 GDI 與早期版本的 Windows 圖形設備接口的后續版本。
好,兩個關鍵信息:
- 窗體子系統應用的編程接口
- 圖形設備接口
充分說明了GDI+的應用場景與用途。需要了解更多呢,就去查閱一下吧。
本書的開始,不打算去解釋一些枯燥的概念,比如什么是Graphics、Brush、Pen甚至是Color;第一境畢竟是基礎,我打算先帶大家玩兒,等玩兒開了、玩兒嗨了,咱們再來總結這些概念,就會相當好理解了。咱們就先從最基本的畫元素開始吧:)
本節,主要是說道一下如何使用GDI+畫直線。體育老師說了,兩點確定一條直線,那么,畫直線的關鍵呢,就是確定兩個點了。音樂老師也說了,直線呢,是向兩邊無限延長的,木有盡頭。那我們還是別挑戰無極限了,所以,咱們在這里說的畫直線呢,其實是畫線段。
這是我建立的一個簡單的WinForm窗體(FormDrawLines)。 擺了幾個按鈕,用來繪制各種不同的線條以及展示不同線條的特性。
兩個輔助按鈕,用來切換線條的顏色和窗體是否使用雙緩沖。

1 using System; 2 using System.Collections.Generic; 3 using System.Drawing; 4 using System.Drawing.Drawing2D; 5 using System.Windows.Forms; 6 7 public partial class FormDrawLines : Form 8 { 9 private Random random = null; 10 private Color penColor = Color.Transparent; 11 private Point lastMouseDownLocation = Point.Empty; 12 private bool startDrawPointToPointLine = false; 13 private bool startDrawFollowMouseLine = false; 14 15 public FormDrawLines() 16 { 17 InitializeComponent(); 18 random = new Random(DateTime.Now.Millisecond); 19 penColor = Color.White; 20 } 21 22 …… 23 24 }
幾個輔助方法,不是本節重點,這里簡單說明一下用途,一筆帶過:P
1、獲取畫布中的一個隨機點

1 private Point GetRandomPoint() 2 { 3 return new Point(random.Next(0, ClientRectangle.Width), random.Next(0, ClientRectangle.Height - pnlToolbox.Height)); 4 }
2、顯示信息,其中,lblInformation為一個Label控件。

1 private void ShowInformation(string message) 2 { 3 lblInformation.Text = message; 4 }
3、切換線條顏色,其中,colors為ColorDialog組件。

1 private void btnChangePenColor_Click(object sender, EventArgs e) 2 { 3 if (colors.ShowDialog(this) == DialogResult.OK) 4 { 5 penColor = colors.Color; 6 } 7 }
4、切換是否使用雙緩沖

1 private void btnSwitchDoubleBuffered_Click(object sender, EventArgs e) 2 { 3 DoubleBuffered = !DoubleBuffered; 4 5 ShowInformation($"二級緩沖:{DoubleBuffered}。"); 6 }
下面是本節的重點:
1、隨機畫線

1 private void btnDrawRandomLine_Click(object sender, EventArgs e) 2 { 3 var pointA = GetRandomPoint(); 4 var pointB = GetRandomPoint(); 5 6 using (var g = CreateGraphics()) 7 using (var pen = new Pen(penColor, 2f)) 8 { 9 g.Clear(SystemColors.AppWorkspace); 10 g.DrawLine(pen, pointA, pointB); 11 } 12 13 ShowInformation($"畫隨機線,{pointA}->{pointB}。"); 14 }
g.Clear(SystemColors.AppWorkspace); 是用來清屏的。
關鍵方法是

// // Summary: // Draws a line connecting two System.Drawing.Point structures. // // Parameters: // pen: // System.Drawing.Pen that determines the color, width, and style of the line. // // pt1: // System.Drawing.Point structure that represents the first point to connect. // // pt2: // System.Drawing.Point structure that represents the second point to connect. // // Exceptions: // T:System.ArgumentNullException: // pen is null. public void DrawLine(Pen pen, Point pt1, Point pt2);
這是畫線的最基礎方法,給一根筆、兩個點,就可以在畫布上作畫了:)
- 筆,決定了線的顏色及粗細;
- 兩點,決定了線的位置及長度;
應該不難理解。
2、消除鋸齒
通過“隨機畫線”,我們發現,畫出的線邊緣鋸齒狀嚴重,垂直和水平線還好,帶點角度就慘不忍睹了。還好,GDI+為我們提供了一系列消除鋸齒的選項,雖然有時也很難差強人意,不過總的來說還是可以接受的。

1 private void btnDrawSmoothLine_Click(object sender, EventArgs e) 2 { 3 var pointA = GetRandomPoint(); 4 var pointB = GetRandomPoint(); 5 var mode = (SmoothingMode)(random.Next(0, 5)); 6 7 using (var g = CreateGraphics()) 8 using (var pen = new Pen(penColor, 2f)) 9 { 10 g.Clear(SystemColors.AppWorkspace); 11 g.SmoothingMode = mode; 12 g.DrawLine(pen, pointA, pointB); 13 } 14 15 ShowInformation($"消除鋸齒,{pointA}->{pointB},模式:{mode.ToString()}。"); 16 }
關鍵點在於g.SmoothingMode = mode;為了盡量多的展示平滑模式帶來的效果,mode來自於System.Drawing.Drawing2D.SmoothingMode的隨機取值;

1 // 2 // Summary: 3 // Specifies whether smoothing (antialiasing) is applied to lines and curves and 4 // the edges of filled areas. 5 public enum SmoothingMode 6 { 7 // 8 // Summary: 9 // Specifies an invalid mode. 10 Invalid = -1, 11 // 12 // Summary: 13 // Specifies no antialiasing. 14 Default = 0, 15 // 16 // Summary: 17 // Specifies no antialiasing. 18 HighSpeed = 1, 19 // 20 // Summary: 21 // Specifies antialiased rendering. 22 HighQuality = 2, 23 // 24 // Summary: 25 // Specifies no antialiasing. 26 None = 3, 27 // 28 // Summary: 29 // Specifies antialiased rendering. 30 AntiAlias = 4 31 }
嚴格來講,消除鋸齒並不屬於畫線的范疇,在畫其他圖形元素時同樣有效,他歸屬於GDI+的2D渲染質量,指定是否將平滑(抗鋸齒)應用於直線和曲線以及填充區域的邊緣。這一點,需要明確。
3、畫虛線

1 private void btnDrawDashLine_Click(object sender, EventArgs e) 2 { 3 var pointA = GetRandomPoint(); 4 var pointB = GetRandomPoint(); 5 var style = (DashStyle)(random.Next(1, 5)); 6 7 using (var g = CreateGraphics()) 8 using (var pen = new Pen(penColor, 2f)) 9 { 10 g.Clear(SystemColors.AppWorkspace); 11 g.SmoothingMode = SmoothingMode.HighQuality; 12 pen.DashStyle = style; 13 g.DrawLine(pen, pointA, pointB); 14 } 15 16 ShowInformation($"畫虛線,{pointA}->{pointB},樣式:{style.ToString()}。"); 17 }
畫虛線的關鍵點在於制定筆的樣式:pen.DashStyle = style;同樣,為了多展示集中樣式,style取了枚舉的隨機值;

// // Summary: // Specifies the style of dashed lines drawn with a System.Drawing.Pen object. public enum DashStyle { // // Summary: // Specifies a solid line. Solid = 0, // // Summary: // Specifies a line consisting of dashes. Dash = 1, // // Summary: // Specifies a line consisting of dots. Dot = 2, // // Summary: // Specifies a line consisting of a repeating pattern of dash-dot. DashDot = 3, // // Summary: // Specifies a line consisting of a repeating pattern of dash-dot-dot. DashDotDot = 4, // // Summary: // Specifies a user-defined custom dash style. Custom = 5 }
沒有取0和5,Solid = 0為實線,Custom = 5為自定義樣式,我們在第一境里先不介紹這類自定義的用法,容易玩兒不嗨……
4、畫線冒
這是一個很朴實的需求,比如我想畫一個連接線,一頭帶箭頭,或者兩頭都帶箭頭,又或者一頭是圓點另一頭是箭頭等。經常被問到,其實在GDI+中,非常容易實現,甚至還可以指定虛線的線冒,可愛:)

1 private void btnDrawLineCap_Click(object sender, EventArgs e) 2 { 3 var pointA = GetRandomPoint(); 4 var pointB = GetRandomPoint(); 5 var style = (DashStyle)(random.Next(0, 6)); 6 var lineCaps = new List<int> { 0, 1, 2, 3, 16, 17, 18, 19, 20, 240 }; 7 var dashCaps = new List<int> { 0, 2, 3 }; 8 var startCap = (LineCap)lineCaps[random.Next(0, 10)]; 9 var endCap = (LineCap)lineCaps[random.Next(0, 10)]; 10 var dashCap = (DashCap)dashCaps[random.Next(0, 3)]; 11 12 using (var g = CreateGraphics()) 13 using (var pen = new Pen(penColor, 4f)) 14 { 15 g.Clear(SystemColors.AppWorkspace); 16 g.SmoothingMode = SmoothingMode.HighQuality; 17 pen.DashStyle = style; 18 pen.SetLineCap(startCap, endCap, dashCap); 19 g.DrawLine(pen, pointA, pointB); 20 } 21 22 ShowInformation($"畫線冒,{pointA}->{pointB},起點線冒:{startCap.ToString()},終點線冒:{endCap.ToString()},虛線冒:{dashCap.ToString()},線條樣式:{style.ToString()}。"); 23 }
關鍵點在於pen.SetLineCap(startCap, endCap, dashCap);同樣,startCap, endCap分別取了System.Drawing.Drawing2D.LineCap的隨機值;dashCap取了System.Drawing.Drawing2D.DashCap的隨機值;

// // Summary: // Specifies the available cap styles with which a System.Drawing.Pen object can // end a line. public enum LineCap { // // Summary: // Specifies a flat line cap. Flat = 0, // // Summary: // Specifies a square line cap. Square = 1, // // Summary: // Specifies a round line cap. Round = 2, // // Summary: // Specifies a triangular line cap. Triangle = 3, // // Summary: // Specifies no anchor. NoAnchor = 16, // // Summary: // Specifies a square anchor line cap. SquareAnchor = 17, // // Summary: // Specifies a round anchor cap. RoundAnchor = 18, // // Summary: // Specifies a diamond anchor cap. DiamondAnchor = 19, // // Summary: // Specifies an arrow-shaped anchor cap. ArrowAnchor = 20, // // Summary: // Specifies a mask used to check whether a line cap is an anchor cap. AnchorMask = 240, // // Summary: // Specifies a custom line cap. Custom = 255 }

// // Summary: // Specifies the type of graphic shape to use on both ends of each dash in a dashed // line. public enum DashCap { // // Summary: // Specifies a square cap that squares off both ends of each dash. Flat = 0, // // Summary: // Specifies a circular cap that rounds off both ends of each dash. Round = 2, // // Summary: // Specifies a triangular cap that points both ends of each dash. Triangle = 3 }
同樣,我們也可以通過分別設置pen的StartCap、EndCap、DashCap屬性來達到相同目的;
好了,到這里呢,關於線的基本畫法就已經全部介紹完了,感覺有點EZ? BORED?那么我們就來利用現有的知識,耍個花活?
5、點點連線
這里比簡單的畫線,稍微復雜一點點,需要兩個事件配合:

1 private void btnDrawPointToPointLine_Click(object sender, EventArgs e) 2 { 3 startDrawPointToPointLine = true; 4 lastMouseDownLocation = Point.Empty; 5 6 using (var g = CreateGraphics()) 7 { 8 g.Clear(SystemColors.AppWorkspace); 9 } 10 11 ShowInformation($"點點連線,等待起點(鼠標單擊畫布內任意位置)。"); 12 }

1 private void FormDrawLines_MouseDown(object sender, MouseEventArgs e) 2 { 3 if (startDrawPointToPointLine) 4 { 5 if (Point.Empty.Equals(lastMouseDownLocation)) 6 { 7 lastMouseDownLocation = e.Location; 8 ShowInformation($"點點連線,起點:{lastMouseDownLocation},等待終點(鼠標單擊畫布內任意位置)。"); 9 } 10 else 11 { 12 using (var g = CreateGraphics()) 13 using (var pen = new Pen(penColor, 2f)) 14 { 15 g.Clear(SystemColors.AppWorkspace); 16 g.SmoothingMode = SmoothingMode.HighQuality; 17 g.DrawLine(pen, lastMouseDownLocation, e.Location); 18 } 19 20 ShowInformation($"點點連線,{lastMouseDownLocation}->{e.Location}。"); 21 22 startDrawPointToPointLine = false; 23 lastMouseDownLocation = Point.Empty; 24 } 25 } 26 }
原理很簡單,當我們點擊“點點連線”按鈕的時候,激活標記位startDrawPointToPointLine、歸位lastMouseDownLocation,並提示需要鼠標操作,選擇一個起始點;
當我們在畫布區域內單擊一個下,就觸發了FormDrawLines_MouseDown事件, 它會判斷,當startDrawPointToPointLine處於激活狀態並且lastMouseDownLocation處於原位時,它就把鼠標的當前位置賦值給lastMouseDownLocation,作為線段的起始點位置,並提示需要鼠標操作,選擇一個終點;
當我們再次在畫布區域內單擊一個下,就又觸發了FormDrawLines_MouseDown事件, 它會判斷,當startDrawPointToPointLine處於激活狀態並且lastMouseDownLocation不處於原位時,它就把鼠標的當前位置作為線段的終點位置,並畫出線段;然后就是恢復startDrawPointToPointLine為未激活狀態,並歸位 lastMouseDownLocation;
恐怕要非常適應這種多事件配合的方式了,因為鼠標跟隨也是多事件配合一起玩兒的:P
6、鼠標跟隨
在點點連線的基礎上,我們把標記位換成了startDrawFollowMouseLine;同時,增加了FormDrawLines_MouseMove事件;

1 private void FormDrawLines_MouseMove(object sender, MouseEventArgs e) 2 { 3 if (startDrawFollowMouseLine && !Point.Empty.Equals(lastMouseDownLocation)) 4 { 5 using (var g = CreateGraphics()) 6 using (var pen = new Pen(penColor, 2f)) 7 { 8 g.Clear(SystemColors.AppWorkspace); 9 g.SmoothingMode = SmoothingMode.HighQuality; 10 g.DrawLine(pen, lastMouseDownLocation, e.Location); 11 } 12 13 ShowInformation($"鼠標跟隨,{lastMouseDownLocation}->{e.Location}。"); 14 } 15 }
原理也不難,就是在選了起點以后,鼠標的移動事件會把鼠標的當前位置作為終點,重繪線段,以達到跟隨的效果;由於截圖也看不出動態效果,就不上圖了,有興趣的童鞋可以Run代碼看看效果:)
Okay,關於GDI+畫線的部分,我們就到此告一段落了。
篇外話
這里涉及了坐標系,美術老師說:
橫坐標,坐標原點左為負,坐標原點右為正,從左到右越來越大;
縱坐標,坐標原點下為負,坐標原點上為正,從下到上越來越大;
但是在GDI+的世界坐標系里,縱坐標的描述正好相反;並且坐標原點初始時在畫布的左上角,而不是畫布的中央; 用心體會一下:)
喜歡本系列叢書的朋友,可以點擊鏈接加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑問的時候可以及時給我個反饋。同時,也算是給各位志同道合的朋友提供一個交流的平台。
需要源碼的童鞋,也可以在群文件中獲取最新源代碼。