—前面一片博文,我们详细介绍了绘图坐标的内容《绘图坐标的理解和应用》,但我们在绘图时都只是在用一个单一的绘图对象Graphics,如果我们想对图形来一些局部的操作,比如移动,旋转,变形等,那最终会造成整个图像跟着变化。
很明显,这不是我们的初中,所以,这里我们介绍的内容将告诉我们,如何将绘制的内容分成更小的“团队”来管理。这些小的团队分别是Graphicspath、region、figure,可以说给他们将给我们的绘图提供了更广阔的空间。
文章原出处: https://haigear.blog.csdn.net/article/details/129020502
一、Graphics
首先,我们还是回顾一下Graphics,我们前面的绘图基本上都是基于这个对象的,我们常用的都是Graphics提供的方法,他们主要分为两类,即绘制轮廓类draw和填充类fill,如下表(我按照使用难易和频率排序):
函数名称 | 函数说明 |
---|---|
DrawLine | 画线 |
DrawString | 绘制文字 |
DrawRectangle | 画矩形 |
DrawArc | 画弧 |
DrawEllipse | 画椭圆 |
DrawImage | 画图像 |
DrawPolygon | 画多边形 |
DrawPath | 通过路径画线和曲线 |
DrawPie | 画饼形 |
DrawBezier | 画立体的贝尔塞曲线 |
DrawBeziers | 画连续立体的贝尔塞曲线 |
DrawClosedCurve | 画闭合曲线 |
DrawCurve | 画曲线 |
FillEllipse | 填充椭圆 |
FillPath | 填充路径,注意:路径如果没有封闭时默认将起点和终点连接起来填充 |
FillPie | 填充饼图 |
FillPolygon | 填充多边形 |
FillRectangle | 填充矩形 |
FillRectangles | 填充矩形组 |
FillRegion | 填充区域,区域自身就带有填充的方法 |
那么这里面有两个很特别的对象,他们就是Path和Region,尤其在填充部分我们可以看到FillPath和FillRegion两个方法。下面我们来介绍他们。
二、Graphicspath
首先,你也许会有一个疑问,有了graphic对象后,为什么还要一个graphicpath呢?path能够起到什么作用呢?
我们仔细看graphic的两类方法可以发现,除了path和region外都是绘制的比较规则的图形,我们要绘制不规则图形那就无计可施了,所以,graphicpath的作用就是绘制不规则图形。
1、构造函数和属性
(1) 属性
属性 | 说明 |
---|---|
FillMode | 如果填充路径的内部区域,这个属性表示填充的方式。其值取自于FiIIMode枚举(它有两个值Alternate和 Winding)。可以获取和设置这个属性的值,参见下面的说明 |
PathData | 它会获取一个PathData对象,该对象包含了如何构建路径的信息(PathData对象有两个属性Points和Types,它们给出了与这个表中PathPoints和PathTypes属性所描述的相同的数组) |
PathPoints | 以数组的形式给出路径上的点 |
PathTypes | GraphicsPath中的每个点都有一个类型(例如Start、Line、Bezier、CloseSubpath)。这个属性获取路径中点的类型,其形式是一个数组。注意所有可能的点类型都列在PathPointType枚举中,该枚举system.Drawing.Drawing2D枚举的一部分 |
PointCount | 它获取由PathPoints 或PathTypes属性返回的数组中的元素个数 |
(2)构造函数
函数重载方式 | 说明 |
---|---|
GraphicsPath() | 用 GraphicsPath 的 FillMode 值初始化 Alternate 类的新实例。 |
GraphicsPath(FillMode) | 用指定的 GraphicsPath 枚举初始化 FillMode 类的新实例。 |
GraphicsPath(Point[], Byte[]) | 使用指定的 GraphicsPath 和 PathPointType 数组初始化 Point 类的新实例。 |
GraphicsPath(Point[], Byte[], FillMode) | 使用指定的 GraphicsPath 和 PathPointType 数组以及指定的 Point 枚举元素初始化 FillMode 类的新实例。 |
GraphicsPath(PointF[], Byte[]) | 使用指定的 GraphicsPath 和 PathPointType 数组初始化 PointF 数组的新实例。 |
GraphicsPath(PointF[], Byte[], FillMode) | 使用指定的 GraphicsPath 和 PathPointType 数组以及指定的 PointF 枚举元素初始化 FillMode 数组的新实例。 |
(3)方法
至于graphicpath的方法,数量太多,这里就不一一列出了,基本和graphic对象的类似,只不过graphic是最终的绘制者,而path这里只是数据准备着,所以方法都以add开头,addline,addlines,addArc,addRectangle等等,但有两个方法我们必须提出来,后面我们将单独介绍他们。
这里貌似冒出来了一个新概念figure,实际它是配合graphicpath来使用的,这里给出它的几个方法是使用说明。
方法名 | 说明 |
---|---|
StartFigure() | 不闭合当前图形即开始一个新图形。 后面添加到该路径的所有点都被添加到此新图形中。 |
CloseAllFigures() | 闭合此路径中所有开放的图形并开始一个新图形。 它通过连接一条从图形的终结点到起始点的直线,闭合每一开放的图形。 |
CloseFigure() | 闭合当前图形并开始新的图形。 如果当前图形包含一系列相互连接的直线和曲线,该方法通过连接一条从终结点到起始点的直线,闭合该环回。 |
有一个明显的特征这里要指出,所有的方法中没有一个是Fill或者draw开头,这说明它就是一个地地道道的“图形数据准备者“”,要详细了解可以查看官网GraphicsPath。
以上了解了构造函数、属性、方法,那这个类我们也了解得差不多了,反正它就是为绘制不规则的图像轮廓而生。既然这样,我们就来绘制一个不规则图形吧!
2、GraphicPath的创建
可以说GraphicsPath就是一组图形序列,创建GraphicPath自然是使用其构造函数,因为目前没有说有一个功能可以将一个图形反过来编程图形序列(可能是缺少拆分图形序列的标准)。
(1)、构造函数
既然是构造函数,那我们自然可以进到GraphicsPath的类中去看看,目前支持6中构造函数重载:
public GraphicsPath();
public GraphicsPath(FillMode fillMode);
public GraphicsPath(PointF[] pts, byte[] types);
public GraphicsPath(Point[] pts, byte[] types);
public GraphicsPath(PointF[] pts, byte[] types, FillMode fillMode);
public GraphicsPath(Point[] pts, byte[] types, FillMode fillMode);
获取graphicpath的放其实还有fromRectangle
(2)、Addxxx()系列方法
很显然,既然GraphicsPath是图形序列,那这个序列的构成就如前面我们学习过的数组、列表、词典、哈希表一样,必须一个个将图形元素添加进去(我们可以理解为GraphicsPath是图形绘制中的专用的数据结构),这里我们就不一一介绍这一系列的添加图形元素的函数了。
(3)、 getBound
我们都知道,图形绘制中几乎从来都离不开矩形Rectangle,所以这里也提供了一个返回矩形的方法。
public RectangleF GetBounds(Matrix? matrix);
public RectangleF GetBounds();
public RectangleF GetBounds(Matrix? matrix, Pen? pen);
当然,我们也直接可以将一个控件的bounds属性赋值给一个声明好的Rectangle。
(4)、变形
对路径来说,需要改变位置、方向、大小、已经其中的文字的大小,色彩,透明度都需要依靠我们的变形。
public void Transform(Matrix matrix);
从Transfrom函数的参数看,我们知道,要使用它首先必须创建一个Matrix
这举例说明:
GraphicsPath myPath = new GraphicsPath();
myPath.AddEllipse(0, 0, 100, 200);
Matrix translateMatrix = new Matrix(); //新建一个矩阵
translateMatrix.Translate(100, 0); //对矩阵使用translate位置变换()
myPath.Transform(translateMatrix); //最后调用GraphicPath的Tansform方法(以矩阵为参数)实现位置变换
3、 figure
figure我们没有单独使用的时候,都是为graphicpath服务的。一个graphicpath可以隐式的进行一个figure,但可以必须显示的结束一个figure,使用closefigure。
(1)startFigure方法
一般我们在开启一个新的开放图形的时候就使用它,但如果一个graphicpath刚刚创建或我们开启一个新封闭图形完全没有必要使用它,默认系统就会调用它,官方给出的说法叫做隐式开启一个图形。当然我们调用这个函数那就叫做显式开启一个新path图形。
public void StartFigure();
```### (2)closeFigure方法
我们要结束上一个图形,我们必须调用它,如果不调用它,上一个图形和下一个你想要与前者独立的图形就会被链接在一起,中间就会有一根线。如下图,我们绘制两根线,下面的代码不会有线连接,因为我们使用了closeFigure(也叫作显式关闭一个图形):
```csharp
Graphics g = e.Graphics;
GraphicsPath gp = new GraphicsPath();
gp.StartFigure();
gp.AddLine(300,300, 400, 300);
gp.CloseFigure();
gp.AddLine(300, 320, 400, 320);
g.DrawPath(Pens.Black, gp);
下面我们不使用Closefigure或者把Closefigure放在第二根线后,那么两根线之间就会出现连接线
对于封闭图形则不会出现连接线,有不有figure都无所谓了,我们只能看到开放图形之间如果不使用closefigure会有连接线(系统不知道你的图形是否结束),代码如下:
Graphics g = e.Graphics;
GraphicsPath gp = new GraphicsPath();
gp.StartFigure();
gp.AddEllipse(200, 100, 100, 100);
gp.AddRectangle(new Rectangle(300, 200, 100, 50));
gp.AddEllipse(400, 100, 100, 100);
gp.AddLine(300,300, 400, 300);
gp.AddLine(300, 320, 400, 320);
gp.CloseFigure();
g.DrawPath(Pens.Black, gp);
4、Marker
(1) setMarkers
标题应给出了官网的链接,这里摘抄它的说明如下:
SetMarkers使用 方法在路径中设置标记。 标记用于创建子路径组。 一个或多个子路径可以介于两个标记之间。
此方法在路径上创建一个标记,该标记可用于分隔路径的各个部分。 然后 NextMarker ,可以使用方法循环访问路径中的标记。
public void SetMarkers();
(2) NextMarker
public int NextMarker (System.Drawing.Drawing2D.GraphicsPath path);
这个函数是GraphicsPathIterator的方法,不是单独的,所以其作用也就很明显了,他的作用就是获取我们前面用setMarkes在GraphicPath中所设置的分段(或者说设了标记)的图形数据集。
详细情况,可参考官方给出的实例
三、 region
region类在控件的编写时使用比较频繁,我们查看源码就可以发现,最明显的是每个控件都有一个Region属性。
区域的创建都是依靠Region类的重载,我们来看看它的构造重载函数,他们很显然都有一个显著的特点,那就是参数都提供了一个封闭图形的对象(path,rectangel等,当然,如果是离散的点,或者线的话,只能理解为一些离散的封闭图形了):
public Region(GraphicsPath path);
public Region(RegionData rgnData);
public Region(Rectangle rect);
public Region(RectangleF rect);
1.从路径中创建:上述示例
一般的情形,我们都是通过一个封闭图形来获取一个区域region的,如下代码:
GraphicPath path=new GraphicPath();
//path.AddArc
//path.AddLine
Region rg=new Region(path);
2.从各类图形中创建
除了典型的path可以提供封闭图形,那最明显的能够提供封闭图形的就是rectangle了(我个人觉得椭圆也是不错的选项,当时构造函数中没有它)。
Region reg = new Region(new Rectangle(10, 10, 80, 20));
3.从一个区域中创建另一个区域
从一个区域中创建另一个区域我们需要用到RegionData ,是不是这个RegionData 会使我们想起GraphicPath中的PathData?是的,他们都有共同的特点,那就是他们可以为创建相应的对象提供数据。
Region r1 = new Region(new Rectangle(10, 10, 80, 20));
RegionData r1Data = r1.GetRegionData();//区域数据
Region r2 = new Region(r1Data); //构造函数不接受区域对象本身,可以接受区域数据数组。
4、region和控件刷新的密切关系
控件的刷新函数中有一个invalidate,这个函数里面的一个参数就是region,这个我们必须注意:
重载 | 说明 |
---|---|
Invalidate(Region, Boolean) | 使控件的指定区域无效(将其添加到控件的更新区域,下次绘制操作时将重新绘制更新区域),并向控件发送绘制消息。 还可以使分配给该控件的子控件无效。 |
Invalidate(Rectangle, Boolean) | 使控件的指定区域无效(将其添加到控件的更新区域,下次绘制操作时将重新绘制更新区域),并向控件发送绘制消息。 还可以使分配给该控件的子控件无效。 |
Invalidate(Region) | 使控件的指定区域无效(将其添加到控件的更新区域,下次绘制操作时将重新绘制更新区域),并向控件发送绘制消息。 |
Invalidate(Boolean) | 使控件的特定区域无效并向控件发送绘制消息。 还可以使分配给该控件的子控件无效。 |
除了上述invalidate的刷新函数外,我们每个控件都有一个region的属性,在这里就可以指定控件绘制的区域,如我们在OnPaint函数中可以做个测试,我们可以绘制一个矩形,:
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
Graphics graphics = pe.Graphics;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
graphics.FillRectangle(Brushes.Red, new Rectangle(0,0,Width,Height));
graphics.Dispose();
}
在控件的构造函数中给Region指定一个路径,则显示的图一个圆形,而不是上面的矩形了。
GraphicsPath gp1 = new GraphicsPath();
gp1.AddEllipse(new Rectangle(0, 0, Width, Height));
Region = new Region(gp1);
效果如下:
这样,两个控件叠加在一起,很显然区Region域外的像素就不会被显示出来,我们也常常用这种思路来实现叠加控件之间的透明像素不遮挡,也就是是我们常说的控件叠加透明(常规情况下设置控件透明只能透明到父容器,而控件叠加时无法透明过去),当然最常见的异形窗口也是靠这个原理来实现。但region实现透明有个最要命的地方,那就是无法反锯齿,要实现反锯齿恐怕需要对每个像素进行相应的滤波平滑处理了,这是后话放下不说。
4.区域的并集和交集
方法 | 逻辑 | 举例 | 操作 |
---|---|---|---|
Intersect | 交 | r1.Intersect(r2) | 把r1更新为r1和r2之间的交集(即同时包含在r1和r2中的部分) |
Union | 并 | r1.Union(r2) | 把rl更新为r1和r2之间的并集(即包含在r1或r2中的部分,或同时包含在r1和r2中的部分) |
Xor | 异并 | r1.Xor(r2) | 把r更新为r和r2之间的异并集(即包含在r1或r2中、但没有同时包含在两者中的部分) |
Complement | r1.Complement(r2) | 更新r1,使之包含位于r2中的部分,但不包含最初位于r1中的部分 | |
Exclude | 排除 | r1.Exclude(r2) | 更新r1,使之不包含任何也位于r2中的部分 |
这里我们来分别测试一下他们的效果,
上述图形实例的代码如下:
enum RegionOperation
{
Union=0,
Complement=1,
Intersect=2,
Exclude=3,
Xor=4
}
void DrawRegionOperation(RegionOperation rgnOperation ,Graphics g)
{
g = this.CreateGraphics();
Rectangle rect1 = new Rectangle(100, 100, 120, 120);
Rectangle rect2 = new Rectangle(70, 70, 120, 120);
Region rgn1 = new Region(rect1);
Region rgn2 = new Region(rect2);
g.DrawRectangle(Pens.Blue, rect1);
g.DrawRectangle(Pens.Red, rect2);
switch (rgnOperation)
{
case RegionOperation.Union:
rgn1.Union(rgn2);
break;
case RegionOperation.Complement:
rgn1.Complement(rgn2);
break;
case RegionOperation.Intersect:
rgn1.Intersect(rgn2);
break;
case RegionOperation.Exclude:
rgn1.Exclude(rgn2);
break;
case RegionOperation.Xor:
rgn1.Xor(rgn2);
break;
default:
break;
}
g.FillRegion(Brushes.Tomato, rgn1);
g.Dispose();
}
四、总结
region着重于图形区域填充,path着重于图形轮廓绘制。但region有个很中要的作用,它可以限定绘图的区域,不在这个绘图区域则不绘制(留空,保持透明),所以这个特性可以用来实现控件与控件叠加时的透明显示,当然,其他的透明显示也可以使用它来实现。后面我们会专门介绍GDI+绘图时透明的实现。有兴趣的童鞋可以继续关注。
文章随时可能更新,请关注原出处:https://haigear.blog.csdn.net/article/details/129020502