今天正式开一本新书,《C# GDI+ 破镜之道》,同样是破镜之道系列丛书的一分子。

关于GDI+呢,官方的解释是这样的:

GDI+ 是 Microsoft Windows 操作系统的窗体子系统应用程序编程接口 (API)。 GDI+ 是负责在屏幕和打印机上显示的信息。 顾名思义,GDI+ 是包含 GDI 与早期版本的 Windows 图形设备接口的后续版本。

好,两个关键信息:

  1. 窗体子系统应用的编程接口
  2. 图形设备接口

充分说明了GDI+的应用场景与用途。需要了解更多呢,就去查阅一下吧。

本书的开始,不打算去解释一些枯燥的概念,比如什么是Graphics、Brush、Pen甚至是Color;第一境毕竟是基础,我打算先带大家玩儿,等玩儿开了、玩儿嗨了,咱们再来总结这些概念,就会相当好理解了。咱们就先从最基本的画元素开始吧:)

本节,主要是说道一下如何使用GDI+画直线。体育老师说了,两点确定一条直线,那么,画直线的关键呢,就是确定两个点了。音乐老师也说了,直线呢,是向两边无限延长的,木有尽头。那我们还是别挑战无极限了,所以,咱们在这里说的画直线呢,其实是画线段。

这是我建立的一个简单的WinForm窗体(FormDrawLines)。 摆了几个按钮,用来绘制各种不同的线条以及展示不同线条的特性。

两个辅助按钮,用来切换线条的颜色和窗体是否使用双缓冲。

 using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms; public partial class FormDrawLines : Form
{
private Random random = null;
private Color penColor = Color.Transparent;
private Point lastMouseDownLocation = Point.Empty;
private bool startDrawPointToPointLine = false;
private bool startDrawFollowMouseLine = false; public FormDrawLines()
{
InitializeComponent();
random = new Random(DateTime.Now.Millisecond);
penColor = Color.White;
} …… }

命名空间引用、私有变量及构造函数

几个辅助方法,不是本节重点,这里简单说明一下用途,一笔带过:P

1、获取画布中的一个随机点

 private Point GetRandomPoint()
{
return new Point(random.Next(, ClientRectangle.Width), random.Next(, ClientRectangle.Height - pnlToolbox.Height));
}

获取随机点 —— GetRandomPoint

2、显示信息,其中,lblInformation为一个Label控件。

 private void ShowInformation(string message)
{
lblInformation.Text = message;
}

显示信息 —— ShowInformation

3、切换线条颜色,其中,colors为ColorDialog组件。

 private void btnChangePenColor_Click(object sender, EventArgs e)
{
if (colors.ShowDialog(this) == DialogResult.OK)
{
penColor = colors.Color;
}
}

切换线条颜色 —— btnChangePenColor_Click

4、切换是否使用双缓冲

 private void btnSwitchDoubleBuffered_Click(object sender, EventArgs e)
{
DoubleBuffered = !DoubleBuffered; ShowInformation($"二级缓冲:{DoubleBuffered}。");
}

切换是否使用双缓冲 —— btnSwitchDoubleBuffered_Click

下面是本节的重点:

1、随机画线

 private void btnDrawRandomLine_Click(object sender, EventArgs e)
{
var pointA = GetRandomPoint();
var pointB = GetRandomPoint(); using (var g = CreateGraphics())
using (var pen = new Pen(penColor, 2f))
{
g.Clear(SystemColors.AppWorkspace);
g.DrawLine(pen, pointA, pointB);
} ShowInformation($"画随机线,{pointA}->{pointB}。");
}

随机画线 —— 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+为我们提供了一系列消除锯齿的选项,虽然有时也很难差强人意,不过总的来说还是可以接受的。

 private void btnDrawSmoothLine_Click(object sender, EventArgs e)
{
var pointA = GetRandomPoint();
var pointB = GetRandomPoint();
var mode = (SmoothingMode)(random.Next(, )); using (var g = CreateGraphics())
using (var pen = new Pen(penColor, 2f))
{
g.Clear(SystemColors.AppWorkspace);
g.SmoothingMode = mode;
g.DrawLine(pen, pointA, pointB);
} ShowInformation($"消除锯齿,{pointA}->{pointB},模式:{mode.ToString()}。");
}

消除锯齿 —— btnDrawSmoothLine_Click

关键点在于g.SmoothingMode = mode;为了尽量多的展示平滑模式带来的效果,mode来自于System.Drawing.Drawing2D.SmoothingMode的随机取值;

 //
// Summary:
// Specifies whether smoothing (antialiasing) is applied to lines and curves and
// the edges of filled areas.
public enum SmoothingMode
{
//
// Summary:
// Specifies an invalid mode.
Invalid = -,
//
// Summary:
// Specifies no antialiasing.
Default = ,
//
// Summary:
// Specifies no antialiasing.
HighSpeed = ,
//
// Summary:
// Specifies antialiased rendering.
HighQuality = ,
//
// Summary:
// Specifies no antialiasing.
None = ,
//
// Summary:
// Specifies antialiased rendering.
AntiAlias =
}

System.Drawing.Drawing2D.SmoothingMode

严格来讲,消除锯齿并不属于画线的范畴,在画其他图形元素时同样有效,他归属于GDI+的2D渲染质量,指定是否将平滑(抗锯齿)应用于直线和曲线以及填充区域的边缘。这一点,需要明确。

3、画虚线

 private void btnDrawDashLine_Click(object sender, EventArgs e)
{
var pointA = GetRandomPoint();
var pointB = GetRandomPoint();
var style = (DashStyle)(random.Next(, )); using (var g = CreateGraphics())
using (var pen = new Pen(penColor, 2f))
{
g.Clear(SystemColors.AppWorkspace);
g.SmoothingMode = SmoothingMode.HighQuality;
pen.DashStyle = style;
g.DrawLine(pen, pointA, pointB);
} ShowInformation($"画虚线,{pointA}->{pointB},样式:{style.ToString()}。");
}

画虚线 —— 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 = ,
//
// Summary:
// Specifies a line consisting of dashes.
Dash = ,
//
// Summary:
// Specifies a line consisting of dots.
Dot = ,
//
// Summary:
// Specifies a line consisting of a repeating pattern of dash-dot.
DashDot = ,
//
// Summary:
// Specifies a line consisting of a repeating pattern of dash-dot-dot.
DashDotDot = ,
//
// Summary:
// Specifies a user-defined custom dash style.
Custom =
}

System.Drawing.Drawing2D.DashStyle

没有取0和5,Solid = 0为实线,Custom = 5为自定义样式,我们在第一境里先不介绍这类自定义的用法,容易玩儿不嗨……

4、画线冒

这是一个很朴实的需求,比如我想画一个连接线,一头带箭头,或者两头都带箭头,又或者一头是圆点另一头是箭头等。经常被问到,其实在GDI+中,非常容易实现,甚至还可以指定虚线的线冒,可爱:)

 private void btnDrawLineCap_Click(object sender, EventArgs e)
{
var pointA = GetRandomPoint();
var pointB = GetRandomPoint();
var style = (DashStyle)(random.Next(, ));
var lineCaps = new List<int> { , , , , , , , , , };
var dashCaps = new List<int> { , , };
var startCap = (LineCap)lineCaps[random.Next(, )];
var endCap = (LineCap)lineCaps[random.Next(, )];
var dashCap = (DashCap)dashCaps[random.Next(, )]; using (var g = CreateGraphics())
using (var pen = new Pen(penColor, 4f))
{
g.Clear(SystemColors.AppWorkspace);
g.SmoothingMode = SmoothingMode.HighQuality;
pen.DashStyle = style;
pen.SetLineCap(startCap, endCap, dashCap);
g.DrawLine(pen, pointA, pointB);
} ShowInformation($"画线冒,{pointA}->{pointB},起点线冒:{startCap.ToString()},终点线冒:{endCap.ToString()},虚线冒:{dashCap.ToString()},线条样式:{style.ToString()}。");
}

画线冒 —— 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 = ,
//
// Summary:
// Specifies a square line cap.
Square = ,
//
// Summary:
// Specifies a round line cap.
Round = ,
//
// Summary:
// Specifies a triangular line cap.
Triangle = ,
//
// Summary:
// Specifies no anchor.
NoAnchor = ,
//
// Summary:
// Specifies a square anchor line cap.
SquareAnchor = ,
//
// Summary:
// Specifies a round anchor cap.
RoundAnchor = ,
//
// Summary:
// Specifies a diamond anchor cap.
DiamondAnchor = ,
//
// Summary:
// Specifies an arrow-shaped anchor cap.
ArrowAnchor = ,
//
// Summary:
// Specifies a mask used to check whether a line cap is an anchor cap.
AnchorMask = ,
//
// Summary:
// Specifies a custom line cap.
Custom =
}

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 = ,
//
// Summary:
// Specifies a circular cap that rounds off both ends of each dash.
Round = ,
//
// Summary:
// Specifies a triangular cap that points both ends of each dash.
Triangle =
}

System.Drawing.Drawing2D.DashCap

同样,我们也可以通过分别设置pen的StartCap、EndCap、DashCap属性来达到相同目的;

好了,到这里呢,关于线的基本画法就已经全部介绍完了,感觉有点EZ? BORED?那么我们就来利用现有的知识,耍个花活?

5、点点连线

这里比简单的画线,稍微复杂一点点,需要两个事件配合:

 private void btnDrawPointToPointLine_Click(object sender, EventArgs e)
{
startDrawPointToPointLine = true;
lastMouseDownLocation = Point.Empty; using (var g = CreateGraphics())
{
g.Clear(SystemColors.AppWorkspace);
} ShowInformation($"点点连线,等待起点(鼠标单击画布内任意位置)。");
}

点点连线 —— btnDrawPointToPointLine_Click

 private void FormDrawLines_MouseDown(object sender, MouseEventArgs e)
{
if (startDrawPointToPointLine)
{
if (Point.Empty.Equals(lastMouseDownLocation))
{
lastMouseDownLocation = e.Location;
ShowInformation($"点点连线,起点:{lastMouseDownLocation},等待终点(鼠标单击画布内任意位置)。");
}
else
{
using (var g = CreateGraphics())
using (var pen = new Pen(penColor, 2f))
{
g.Clear(SystemColors.AppWorkspace);
g.SmoothingMode = SmoothingMode.HighQuality;
g.DrawLine(pen, lastMouseDownLocation, e.Location);
} ShowInformation($"点点连线,{lastMouseDownLocation}->{e.Location}。"); startDrawPointToPointLine = false;
lastMouseDownLocation = Point.Empty;
}
}
}

点点连线 —— FormDrawLines_MouseDown

原理很简单,当我们点击“点点连线”按钮的时候,激活标记位startDrawPointToPointLine、归位lastMouseDownLocation,并提示需要鼠标操作,选择一个起始点;

当我们在画布区域内单击一个下,就触发了FormDrawLines_MouseDown事件, 它会判断,当startDrawPointToPointLine处于激活状态并且lastMouseDownLocation处于原位时,它就把鼠标的当前位置赋值给lastMouseDownLocation,作为线段的起始点位置,并提示需要鼠标操作,选择一个终点;

当我们再次在画布区域内单击一个下,就又触发了FormDrawLines_MouseDown事件, 它会判断,当startDrawPointToPointLine处于激活状态并且lastMouseDownLocation不处于原位时,它就把鼠标的当前位置作为线段的终点位置,并画出线段;然后就是恢复startDrawPointToPointLine为未激活状态,并归位 lastMouseDownLocation;

恐怕要非常适应这种多事件配合的方式了,因为鼠标跟随也是多事件配合一起玩儿的:P

6、鼠标跟随

在点点连线的基础上,我们把标记位换成了startDrawFollowMouseLine;同时,增加了FormDrawLines_MouseMove事件;

 private void FormDrawLines_MouseMove(object sender, MouseEventArgs e)
{
if (startDrawFollowMouseLine && !Point.Empty.Equals(lastMouseDownLocation))
{
using (var g = CreateGraphics())
using (var pen = new Pen(penColor, 2f))
{
g.Clear(SystemColors.AppWorkspace);
g.SmoothingMode = SmoothingMode.HighQuality;
g.DrawLine(pen, lastMouseDownLocation, e.Location);
} ShowInformation($"鼠标跟随,{lastMouseDownLocation}->{e.Location}。");
}
}

鼠标跟随 —— FormDrawLines_MouseMove

原理也不难,就是在选了起点以后,鼠标的移动事件会把鼠标的当前位置作为终点,重绘线段,以达到跟随的效果;由于截图也看不出动态效果,就不上图了,有兴趣的童鞋可以Run代码看看效果:)

Okay,关于GDI+画线的部分,我们就到此告一段落了。

篇外话

这里涉及了坐标系,美术老师说:

横坐标,坐标原点左为负,坐标原点右为正,从左到右越来越大;

纵坐标,坐标原点下为负,坐标原点上为正,从下到上越来越大;

但是在GDI+的世界坐标系里,纵坐标的描述正好相反;并且坐标原点初始时在画布的左上角,而不是画布的中央; 用心体会一下:)

喜欢本系列丛书的朋友,可以点击链接加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑问的时候可以及时给我个反馈。同时,也算是给各位志同道合的朋友提供一个交流的平台。
需要源码的童鞋,也可以在群文件中获取最新源代码。

《C# GDI+ 破境之道》:第一境 GDI+基础 —— 第一节:画直线的更多相关文章

  1. 《C# GDI+ 破境之道》:第一境 GDI+基础 —— 第二节:画矩形

    有了上一节画线的基础,画矩形的各种边线就特别好理解了,所以,本节在矩形边线上,就不做过多的讲解了,关注一下画“随机矩形”的具体实现就好.与画线相比较,画矩形稍微复杂的一点就是在于它多了很多填充的样式. ...

  2. 《C# GDI+ 破境之道》:第一境 GDI+基础 —— 第三节:画圆形

    有了上一节画矩形的基础,画圆形就不要太轻松+EZ:)所以,本节在画边线及填充上,就不做过多的讲解了,关注一下画“随机椭圆”.“正圆”.“路径填充”的具体实现就好.与画矩形相比较,画椭圆与之完全一致,没 ...

  3. 《ASP.NET MVC 5 破境之道》:第一境 ASP.Net MVC5项目初探 — 第三节:View层简单改造

    第一境 ASP.Net MVC5项目初探 — 第三节:View层简单改造 MVC默认模板的视觉设计从MVC1到MVC3都没有改变,比较陈旧了:在MVC4中做了升级,好看些,在不同的分辨率下,也能工作得 ...

  4. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第一节:HTTP协议数据采集

    首先欢迎您来到本书的第二境,本境,我们将全力打造一个实际生产环境可用的爬虫应用了.虽然只是刚开始,虽然路漫漫其修远,不过还是有点小鸡冻:P 本境打算针对几大派生类做进一步深耕,包括与应用的结合.对比它 ...

  5. 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第六节:第一境尾声

    在第一境中,我们主要了解了爬虫的一些基本原理,说原理也行,说基础知识也罢,结果就是已经知道一个小爬虫是如何诞生的了~那么现在,请默默回想一下,在第一境中,您都掌握了哪些内容?哪些还比较模糊?如果还有什 ...

  6. 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第五节:数据流处理的那些事儿

    为什么说到数据流了呢,因为上一节中介绍了一下异步发送请求.同样,在数据流的处理上,C#也为我们提供几个有用的异步处理方法.而且,爬虫这生物,处理数据流是基础本能,比较重要.本着这个原则,就聊一聊吧. ...

  7. 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第二节:WebRequest

    本节主要来介绍一下,在C#中制造爬虫,最为常见.常用.实用的基础类 ------ WebRequest.WebResponse. 先来看一个示例 [1.2.1]: using System; usin ...

  8. 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第一节:整体思路

    在构建本章节内容的时候,笔者也在想一个问题,究竟什么样的采集器框架,才能算得上是一个“全能”的呢?就我自己以往项目经历而言,可以归纳以下几个大的分类: 根据通讯协议:HTTP的.HTTPS的.TCP的 ...

  9. 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第四节:同步与异步请求方式

    前两节,我们对WebRequest和WebResponse这两个类做了介绍,但两者还相对独立.本节,我们来说说如何将两者结合起来,方式有哪些,有什么不同. 1.4.1 说结合,无非就是我们如何发送一个 ...

随机推荐

  1. 小小TODO标识,你用对了吗?

    前言 有时,您需要标记部分代码以供将来参考,比如: 优化,改进,可能的更改,要讨论的问题等. 通常我们会在代码中加入如下的标记表示待办: //TODO 我将要在这里做 xxx 你这样做,别人也会这样做 ...

  2. 【Java基础总结】泛型

    泛型实现了参数化类型的概念,使代码可以应用于多种类型. 1. 泛型类 声明的泛型类型静态方法不能使用 class Tools<T>{ private T t; public void se ...

  3. a:visited不起作用的原因解析

    目前大多数的网站都很少会出现"一个超链接被点击后改变其颜色"的需求,但也还是有一部分网站有这样的实际需求,也是为了能够让用户更容易区分哪些标题是被点击访问过了,这类需求大多在新闻类 ...

  4. postman传递当前时间戳

    有时我们在请求接口时,需要带上当前时间戳这种动态参数,那么postman能不能自动的填充上呢. 1请求动态参数(例如时间戳) 直接在参数值写 {{$timestamp}} 如下: 我们也可以使用pos ...

  5. (2)MongoDB副本集自动故障转移原理

    前文我们搭建MongoDB三成员副本集,了解集群基本特性,今天我们围绕下图聊一聊背后的细节. 默认搭建的replica set均在主节点读写,辅助节点冗余部署,形成高可用和备份, 具备自动故障转移的能 ...

  6. max_element( )

    直接用这个函数 , 会比自己写个for 判断快的多了 . position=max_element(a,a+n)-a; position  代表找到最大元素的位置 , max_element( ) 的 ...

  7. 公文流转系统v0.1

    河北金力集团公文流转系统 1.项目需求: 河北金力集团是我省机械加工的龙头企业,主要从事矿山机械制造及各种机械零部件加工.企业有3个厂区,主厂区位于省高新技术开发区,3个分厂分别在保定.邢台和唐山.为 ...

  8. python property()函数:定义属性

    正常情况下,类包含的属性应该是隐藏的,只允许通过类提供的方法来间接的实现对类属性的访问和操作. class Person: #构造函数 def __init__(self, name): self.n ...

  9. 洛谷P3335 [ZJOI2013]蚂蚁寻路

    题目描述 在一个 n*m 的棋盘上,每个格子有一个权值,初始时,在某个格子的顶点处一只面朝北的蚂蚁,我们只知道它的行走路线是如何转弯,却不知道每次转弯前走了多长. 蚂蚁转弯是有一定特点的,即它的转弯序 ...

  10. 【WPF学习】第十九章 控件类

    WPF窗口充满了各种元素,但这些元素中只有一部分是控件.在WPF领域,控件通常被描述为与用户交互的元素——能接收焦点并接受键盘或鼠标输入的元素.明显的例子包括文本框和按钮.然而,这个区别有时有些模糊. ...