今天正式开一本新书,《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. Flask 作者 Armin Ronacher:我不觉得有异步压力

    英文 | I'm not feeling the async pressure[1] 原作 | Armin Ronacher,2020.01.01 译者 | 豌豆花下猫@Python猫 声明 :本翻译 ...

  2. nginx负载均衡动态自动更新(微博开源模块nginx-upsync-module使用)

    这几天项目有个需求:负载要求能根据节点健康状态动态的增减.nginx自带的upstram已经很强大,而且基于Nginx Upstream配置动态更新已经有很多开源方案,大多数都是基于生成配置文件后进行 ...

  3. python报错: invalid syntax

    invalid syntax: 无效的语法. 解决办法:查看当前语句中的  , 如果当前行没找到错误,依次往上找,往上找时可以利用是否有输出进行快速查找. 原因:python语法很严格,少了左括号.右 ...

  4. SPSS 相关性的选择

    在SPSS中导入数据,analyze-correlate-bivariate-选择变量 OK 输出的是相关系数矩阵 相关系数下面的Sig.是显著性检验结果的P值,越接近0越显著. 同样的数据,我们接着 ...

  5. GeoGebra学习-lesson1

    我曾经想过学习数学的时候总觉得不太直观,希望通过直观的图形来直观的学习数学.庆幸的是,在B站看视频时看到了妈咪叔使用了这款数学软件,很好用的样子.就去简单的了解了一下,下面是摘抄自<Geogeb ...

  6. 玩转Django2.0---Django笔记建站基础八(admin后台系统)

    第八章 admin后台系统 admin后台系统也成为网站后台管理系统,主要用于对网站前台的信息进行管理,如文字.图片.影音和其他日常使用文件的发布.更新.删除等操作,也包括功能信息的统计和管理,如用户 ...

  7. 软件发布!DOTA2统计学

    更新日志: 1.2 增加DOTABUFF作为数据源,可以与DOTAMAX切换 1.1 增加关于对话框 增加版本信息 修复列表框头的错误   受到 http://tieba.baidu.com/p/38 ...

  8. SpringBoot实现简单的CRUD

    CRUD-员工列表 实验要求: 1).RestfulCRUD:CRUD满足Rest风格: URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作 2).实验的请求架构; 3).员工列表 ...

  9. 【WPF学习】第二十一章 特殊容器

    内容控件不仅包括基本控件,如标签.按钮以及工具提示:它们还包含特殊容器,这些容器可用于构造用户界面中比较大的部分区域. 首先介绍ScrollViewer控件,该控件直接继承自ContentContro ...

  10. 爬虫之pyspider 安装

    解决方法: 利用wheel安装 S1: pip install wheelS2: 进入www.lfd.uci.edu/~gohlke/pythonlibs/,Ctrl + F查找pycurl S3:这 ...