1. 程式人生 > >《C# GDI+ 破境之道》:第一境 GDI+基礎 —— 第一節:畫直線

《C# GDI+ 破境之道》:第一境 GDI+基礎 —— 第一節:畫直線

今天正式開一本新書,《C# GDI+ 破鏡之道》,同樣是破鏡之道系列叢書的一分子。

關於GDI+呢,官方的解釋是這樣的:

GDI+ 是 Microsoft Windows 作業系統的窗體子系統應用程式程式設計介面 (API)。 GDI+ 是負責在螢幕和印表機上顯示的資訊。 顧名思義,GDI+ 是包含 GDI 與早期版本的 Windows 圖形裝置介面的後續版本。

 好,兩個關鍵資訊:

  1. 窗體子系統應用的程式設計介面
  2. 圖形裝置介面

充分說明了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 }
獲取隨機點 —— GetRandomPoint

2、顯示資訊,其中,lblInformation為一個Label控制元件。

1 private void ShowInformation(string message)
2 {
3     lblInformation.Text = message;
4 }
顯示資訊 —— ShowInformation

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 }
切換線條顏色 —— btnChangePenColor_Click

4、切換是否使用雙緩衝

1 private void btnSwitchDoubleBuffered_Click(object sender, EventArgs e)
2 {
3     DoubleBuffered = !DoubleBuffered;
4 
5     ShowInformation($"二級緩衝:{DoubleBuffered}。");
6 }
切換是否使用雙緩衝 —— btnSwitchDoubleBuffered_Click

下面是本節的重點:

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 }
隨機畫線 —— btnDrawRandomLine_Click

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);
Graphics.DrawLine 方法原型

這是畫線的最基礎方法,給一根筆、兩個點,就可以在畫布上作畫了:)

  • 筆,決定了線的顏色及粗細;
  • 兩點,決定了線的位置及長度;

應該不難理解。

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 }
消除鋸齒 —— btnDrawSmoothLine_Click

關鍵點在於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 }
System.Drawing.Drawing2D.SmoothingMode

嚴格來講,消除鋸齒並不屬於畫線的範疇,在畫其他圖形元素時同樣有效,他歸屬於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 }
畫虛線 —— btnDrawDashLine_Click

畫虛線的關鍵點在於制定筆的樣式: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
}
System.Drawing.Drawing2D.DashStyle

沒有取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 }
畫線冒 —— btnDrawLineCap_Click

關鍵點在於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
}
System.Drawing.Drawing2D.LineCap
//
// 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
}
System.Drawing.Drawing2D.DashCap

同樣,我們也可以通過分別設定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 }
點點連線 —— btnDrawPointToPointLine_Click
 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 }
點點連線 —— FormDrawLines_MouseDown

原理很簡單,當我們點選“點點連線”按鈕的時候,啟用標記位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 }
滑鼠跟隨 —— FormDrawLines_MouseMove

原理也不難,就是在選了起點以後,滑鼠的移動事件會把滑鼠的當前位置作為終點,重繪線段,以達到跟隨的效果;由於截圖也看不出動態效果,就不上圖了,有興趣的童鞋可以Run程式碼看看效果:)

 

Okay,關於GDI+畫線的部分,我們就到此告一段落了。

 

篇外話

這裡涉及了座標系,美術老師說:

橫座標,座標原點左為負,座標原點右為正,從左到右越來越大;

縱座標,座標原點下為負,座標原點上為正,從下到上越來越大;

但是在GDI+的世界座標系裡,縱座標的描述正好相反;並且座標原點初始時在畫布的左上角,而不是畫布的中央; 用心體會一下:)

 

 

喜歡本系列叢書的朋友,可以點選連結加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑問的時候可以及時給我個反饋。同時,也算是給各位志同道合的朋友提供一個交流的平臺。
需要原始碼的童鞋,也可以在群檔案中獲取最新原始碼。