《C# GDI+ 破境之道》:第一境 GDI+基础 —— 第一节:画直线
今天正式开一本新书,《C# GDI+ 破镜之道》,同样是破镜之道系列丛书的一分子。
关于GDI+呢,官方的解释是这样的:
GDI+ 是 Microsoft Windows 操作系统的窗体子系统应用程序编程接口 (API)。 GDI+ 是负责在屏幕和打印机上显示的信息。 顾名思义,GDI+ 是包含 GDI 与早期版本的 Windows 图形设备接口的后续版本。
好,两个关键信息:
- 窗体子系统应用的编程接口
- 图形设备接口
充分说明了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+基础 —— 第一节:画直线的更多相关文章
- 《C# GDI+ 破境之道》:第一境 GDI+基础 —— 第二节:画矩形
有了上一节画线的基础,画矩形的各种边线就特别好理解了,所以,本节在矩形边线上,就不做过多的讲解了,关注一下画“随机矩形”的具体实现就好.与画线相比较,画矩形稍微复杂的一点就是在于它多了很多填充的样式. ...
- 《C# GDI+ 破境之道》:第一境 GDI+基础 —— 第三节:画圆形
有了上一节画矩形的基础,画圆形就不要太轻松+EZ:)所以,本节在画边线及填充上,就不做过多的讲解了,关注一下画“随机椭圆”.“正圆”.“路径填充”的具体实现就好.与画矩形相比较,画椭圆与之完全一致,没 ...
- 《ASP.NET MVC 5 破境之道》:第一境 ASP.Net MVC5项目初探 — 第三节:View层简单改造
第一境 ASP.Net MVC5项目初探 — 第三节:View层简单改造 MVC默认模板的视觉设计从MVC1到MVC3都没有改变,比较陈旧了:在MVC4中做了升级,好看些,在不同的分辨率下,也能工作得 ...
- 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第一节:HTTP协议数据采集
首先欢迎您来到本书的第二境,本境,我们将全力打造一个实际生产环境可用的爬虫应用了.虽然只是刚开始,虽然路漫漫其修远,不过还是有点小鸡冻:P 本境打算针对几大派生类做进一步深耕,包括与应用的结合.对比它 ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第六节:第一境尾声
在第一境中,我们主要了解了爬虫的一些基本原理,说原理也行,说基础知识也罢,结果就是已经知道一个小爬虫是如何诞生的了~那么现在,请默默回想一下,在第一境中,您都掌握了哪些内容?哪些还比较模糊?如果还有什 ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第五节:数据流处理的那些事儿
为什么说到数据流了呢,因为上一节中介绍了一下异步发送请求.同样,在数据流的处理上,C#也为我们提供几个有用的异步处理方法.而且,爬虫这生物,处理数据流是基础本能,比较重要.本着这个原则,就聊一聊吧. ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第二节:WebRequest
本节主要来介绍一下,在C#中制造爬虫,最为常见.常用.实用的基础类 ------ WebRequest.WebResponse. 先来看一个示例 [1.2.1]: using System; usin ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第一节:整体思路
在构建本章节内容的时候,笔者也在想一个问题,究竟什么样的采集器框架,才能算得上是一个“全能”的呢?就我自己以往项目经历而言,可以归纳以下几个大的分类: 根据通讯协议:HTTP的.HTTPS的.TCP的 ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第四节:同步与异步请求方式
前两节,我们对WebRequest和WebResponse这两个类做了介绍,但两者还相对独立.本节,我们来说说如何将两者结合起来,方式有哪些,有什么不同. 1.4.1 说结合,无非就是我们如何发送一个 ...
随机推荐
- Java List集合的介绍与常用方法
List接口的介绍 List接口简介: java.util.List接口继承自Collection接口,是单列集合的一个重要分支,习惯性地会将实现了List接口的对象称为List集合. 在List集合 ...
- wireshark使用过程中,卡死未响应
原因 : 未知 处理办法:1.重装wireshark ----------无效果 2.卸载有道词典-----------成功,wireshark正常运行,重装有道词典该问题必现,所以问题由有道词典引起 ...
- Spirng Boot2 系列教程(二十二)| 启动原理
一个读者,也是我的好朋友投稿的一篇关于 SpringBoot 启动原理的文章,才大二就如此优秀,未来可期. 我一直想了解一下 SpirngBoot 的是如何启动的,我想就来写一篇关于 SpirngBo ...
- DP-直线分割递推
在 DP 里有一类是直线分割平面的问题 , 也是属于递推 类的 . 一 . 直线分割平面的问题 先考虑第一个小问题 : n 条直线最多可以将平面分割成几部分 ? 想想 最优的分割方法是怎样的呢 ? ...
- PTA - 拓扑排序
一个项目由若干个任务组成,任务之间有先后依赖顺序.项目经理需要设置一系列里程碑,在每个里程碑节点处检查任务的完成情况,并启动后续的任务.现给定一个项目中各个任务之间的关系,请你计算出这个项目的最早完工 ...
- 关于爬虫的日常复习(17)——scrapy系列1
- 如何用rflask快速初始化Flask Restful项目
如何用rflask快速初始化Flask Restful项目 说明 多啰嗦两句 我们在创建flask项目的时候,使用pycharm创建出来的项目比较简陋,而且随着项目的功能完善,项目目录结构会比较多,多 ...
- [转载收藏]C#基础知识梳理系列十一:垃圾回收机制
摘 要 基于.NET平台的开发语言中,最让开发人员爽的一点就是垃圾回收处理机制,在编码过程中,终于可以解放你的双手来关注更重要的事情.很多的资料中在讲到.NET中的垃圾回收机制时都说"CLR ...
- Java反射的常见用法
反射的常见用法有三类,第一类是“查看”,比如输入某个类的属性方法等信息,第二类是“装载“,比如装载指定的类到内存里,第三类是“调用”,比如通过传入参数,调用指定的方法. 1 查看属性的修饰符.类型和名 ...
- python实例:自动爬取豆瓣读书短评,分析短评内容
思路: 1.打开书本“更多”短评,复制链接 2.脚本分析链接,通过获取短评数,计算出页码数 3.通过页码数,循环爬取当页短评 4.短评写入到txt文本 5.读取txt文本,处理文本,输出出现频率最高的 ...

