路径和几何图形

  Path类能够包含任何简单形状、多组形状以及更复杂的要素,如曲线。Path类提供了Data属性,该属性接受一个Geometry对象,该对象定义路径包含的一个活多个图形。不能直接创建Geometry对象,因为Geometry时抽象类,而是需要使用下表的7可派生类中的一个进行创建。

几何图形类
名称 说明
LineGeometry 代表直线,该几何图形相当于Line形状
RectangleGeometry 代表矩形(可以具有圆形拐角),该几何图形相当于Rectangle形状
EllipseGeometry 代表椭圆,该图形相当于Ellipse形状
GeometryGroup 为单个路径添加任意多个Geometry对象,使用EvenOdd活NonZero填充规则来确定要填充的区域
CombinedGeometry 将两个几何图形合并为一个形状。可使用CombineMode属性选择如何组合两个几何图形
PathGeometry 代表更复杂的由弧线、曲线以及直线构成的图形,并且既可以时闭合的,也可以不是闭合的
StreamGeometry 相当于PathGeometry的只读的轻量级类。StreamGeometry图形可节省内存,因为它不在内存中同时保存路径的所有单个分段。并且这类图形一旦创建就不能再修改

路径和几何图形之间的区别?

  几何图形定义形状,而路径用于绘制形状。因此,Geometry 对象为形状定义了坐标和尺寸等细节,而Path 对象提供了用于绘制形状的 Stroke 和 Fi 画刷。Path 类还提供了继承自 UIElement 基础架构的特性,如鼠标和键盘处理

  然而,几何图形类并不像看起来那么简单。原因之一是它们都继承自Freezable 类(通过Geometry 基类),所以支持更改通知。因此,如果使用几何图形创建路径,然后修改几何图形,就会自动被重新绘制路径。还可以使用几何图形类来定义能够通过画刷应用的图画,从而为绘制不需要 Path 类所具有的用户交互功能的复杂内容提供一种简单方法。

直线、矩形和椭圆图形

  LineGeometryRectangleGeometry以及EllipseGeometry类直接对应在LineRectangle以及Ellipse形状,例如,可将下面使用Rectangle华丽的标记:

1
2
3
4
<Rectangle Fill="Yellow"
Stroke="Blue"
Width="100"
Height="50" />

转换为下面使用Path元素的标记:

1
2
3
4
5
6
<Path Fill="Yellow"
Stroke="Blue">
<Path.Data>
<RectangleGeometry Rect="0,0 100,50" />
</Path.Data>
</Path>

  唯一的实质性区别是 Rectangle 形状使用的是 Height和 Width 值,而 RectangleGeometry 图形使用4个数值来描述矩形的尺寸和位置。前两个数值描述左上角的X和Y坐标,而后两个数值设置矩形的宽度和高度。可在(0,0)点开始绘制矩形,从而得到与普通的 Rectangle 元素相同的效果,或者使用不同的值偏移矩形。RectangleGeometry类还提供了 RadiusX和 RadiusY 属性这两个属性用于圆滑拐角。

类似地,可将下面的Line形状转变为LineGeometry图形:

1
2
3
4
5
6
7
8
9
10
11
12
<Line Stroke="Blue"
X1="0"
Y1="0"
X2="10"
Y2="100" />

<Path Stroke="Blue">
<Path.Data>
<LineGeometry StartPoint="0,0"
EndPoint="10,100" />
</Path.Data>
</Path>

类似地,可将下面的Ellipse形状转变为EllipseGeometry图形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Ellipse Fill="Yellow"
Stroke="Blue"
Width="100"
Height="50"
HorizontalAlignment="Left" />

<Path Fill="Yellow"
Stroke="Blue">
<Path.Data>
<EllipseGeometry RadiusX="50"
RadiusY="25"
Center="50,25" />
</Path.Data>
</Path>

两个半径值只是宽度和高度值的一般。还可以使用Center属性偏移椭圆的位置

使用GeometryGroup组合形状

  组合图形最简单的方法是使用 GeometryGroup对象,该对象在内部嵌套其他 Geometry 类的派生类对象。下面的示例在一个正方形的旁边放置了一个椭圆:

1
2
3
4
5
6
7
8
9
10
11
12
<Path Fill="Yellow"
Stroke="Blue"
Margin="5">
<Path.Data>
<GeometryGroup>
<RectangleGeometry Rect="0,0 100,100" />
<EllipseGeometry Center="150,50"
RadiusX="35"
RadiusY="25" />
</GeometryGroup>
</Path.Data>
</Path>

  这种做法用一个元素替代了两个元素,这一位这降低了用户界面的开销。通常,使用数量更少的较复杂几何图形元素的窗口比具有大量较简单几何图形元素的窗口的性能更高。

  当然,将多个几何图形组合成单独的 Path 元素也存在缺点–不能单独为不同的形状执行事件处理。反而,Path元素将引发所有的鼠标事件。不过,仍可以独立地控制嵌套的RectangleGeometry 和EllipseGeometry 对象,从而改变整个路径。例如,每个几何图形都提供了Transform 属性,可使用该属性拉伸、扭曲或旋转路径的相应部分。

几何图形的另一个优点时可在几个独立的Path元素中重用相同的几何图形。这不需要使用代码——只需要在Resources几何中定义几何几何图形,并使用StaticExtension活DynamicExtension标记扩展在路径中进行引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Window.Resources>
<GeometryGroup x:Key="Geometry">
<RectangleGeometry Rect="0,0 100,100" />
<EllipseGeometry Center="150,50"
RadiusX="35"
RadiusY="25" />
</GeometryGroup>
</Window.Resources>
<Grid>
<Path Fill="Yellow"
Stroke="Blue"
Margin="5"
Data="{StaticResource Geometry}" />
</Grid>

  当形状相互交叉时,GeometryGroup将更有趣。这时不能将图画简单地作为固定形状的组合对待,GeometryGroup 使用 FiRule 属性(该属性可设置为 EvenOdd 或 Nonzero)决定填充哪些形状。如果采用如下方式改变前面显示的标记,在正方形的上面放置椭圆,分析一下会出现什么情况:

1
2
3
4
5
6
7
8
9
10
11
12
<Path Fill="Yellow"
Stroke="Blue"
Margin="5">
<Path.Data>
<GeometryGroup FillRule="EvenOdd">
<RectangleGeometry Rect="0,0 100,100" />
<EllipseGeometry Center="50,50"
RadiusX="35"
RadiusY="25" />
</GeometryGroup>
</Path.Data>
</Path>

使用CombinedGeometry融合几何图形

  对于通过基本图元(矩形、椭圆和直线)构建复杂形状,GeometryGroup类是非常有价值的工具。但它也有明显的局限性。如果是绘制形状,并在其内部“减去”另一个形状来创建新的形状,GeometryGroup 类可以工作得很好。然而,如果形状的边界相互交叉,就很难得到所希望的结果了,并且如果希望移除形状的一部分,GeometryGroup 类就不能提供任何帮助了。

  CombinedGeometry 类专门用于组合重叠到一起并且不相互包含的形状。与GeometryGroup类不同,CombinedGeometry 类只使用两个几何图形,通过 Geometry1 和 Geometry2 属性提供这两个几何图形。CombinedGeometry类没有包含FilRule属性,反而具有功能更强大的GeometryCombineMode 属性,该属性可以使用4个值中的一个,下表列出了这4个值。

GeometryCombineMode枚举值
名称 说明
Union 创建包含两个几何图形所有区域的形状
Intersect 创建包含两个几何图形共有区域的形状
Xor 创建包含两个图形非共有区域的形状。换句话说,就像先合并形状(使用Union),再溢出共有部分(使用Intersect)那样
Exclude 创建的形状包含第一个几何图形的所有部分,但不包含第二个几何图形的区域

  下面的示例演示中如何使用GeometryCombineMode.Union合并两个形状,从而创建包含所有区域的形状:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Path Fill="Yellow"
Stroke="Blue"
Margin="5"
HorizontalAlignment="Center">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Union">
<CombinedGeometry.Geometry1>
<RectangleGeometry Rect="0,0 100,100" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center="85,50"
RadiusX="65"
RadiusY="35" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>

CombinedGeometry 类只能合并两个形状,这看起来可能是一个重大的局限,但实际上并非如此。可构建包含许多不同几何图形的形状–只需要使用嵌套的CombinedGeometry对象即可。例如,一个 CombinedGeometry 对象可组合另外两个CombinedGeometry 对象,而这两个CombinedGeometry对象自身可能又组合了多个几何图形。采用这种技术,可构建非常复杂的形状。

🈲禁止标志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<Path Fill="Yellow"
Stroke="Blue">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Union">
<CombinedGeometry.Geometry1>
<CombinedGeometry GeometryCombineMode="Exclude">
<CombinedGeometry.Geometry1>
<EllipseGeometry Center="50,50"
RadiusX="50"
RadiusY="50" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center="50,50"
RadiusX="40"
RadiusY="40" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<RectangleGeometry Rect="44,5 10,90">
<RectangleGeometry.Transform>
<RotateTransform Angle="45"
CenterX="50"
CenterY="50"/>
</RectangleGeometry.Transform>
</RectangleGeometry>
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>

CombinedGeometry 对象不会影响用于为形状着色的填充画刷或笔画画刷,这些细节由路径设置。因此,如果希望为路径的各部分使用不同的颜色,就需要创建彼此独立的Path 对象。

使用PathGeometry绘制曲线和直线

  PathGeometry是功能超级强大的图形,它能绘制其他所有几何图形能够绘制的内容,也能绘制其他所有几何图形所不能绘制的内容。它的唯一缺点是语法比较长(并且在某种程度上更加复杂)。

  每个 PathGeometry 对象都是由一个或多个 PathFigure 对象构建的(存储在 PathGeometry.Figures集合中)。每个 PathFigure 对象是一系列相互连接的直线和曲线,可闭合也可不闭合。如果图形中最后一条直线的终点连接到了第一条直线的起点,那么图形就是闭合的。

PathFigure属性
名称 说明
StartPoint 指示从何处开始绘制图形线条的Point对象
Segments 用于绘制图形的PathSegment对象的集合
IsClosed 如果为ture,WPF添加直线来连接起点和终点(假设它们不是同一个点)
IsFilled 如果true,就使用Path.Fill画刷填充图形内部的区域

  PathFigure 对象是由包含大量线段的不间断线条绘制的形状。然而,技巧是有几种类型的线段,"它们都继承自 PathSegment类。其中一些类比较简单,如绘制直线的 LineSegment 类。而另外一些类(如 BezierSegment 类)较为复杂,可以绘制曲线。

PathSegment类
名称 说明
LineSegment 在两点之间创建直线
ArcSegment 在两点之间创建椭圆形弧线
BezierSegment 在两点之间创建贝塞尔曲线
QuadraticBezierSegment 创建形式更简单的贝塞尔曲线,只有一个控制点而不是两个控制点,并且计算速度更快
PolyLineSegment 创建一系列直线。可使用多个LineSegment对象得到相同的效果,但使用单个PolyLineSegment对象更简明
PolyBezierSegment 创建一系列贝塞尔曲线

直线

  使用 LineSegment PathGeometry类创建简单的线条非常容易。只需要设置 StartPoint属性,并为线条中的每部分增加一条 LineSegment 直线段。LineSegment.Point 属性标识每条线段的结束点。

1
2
3
4
5
6
7
8
9
10
11
<Path Stroke="Blue">
<Path.Data>
<PathGeometry>
<PathFigure IsClosed="True"
StartPoint="10,100">
<LineSegment Point="100,100" />
<LineSegment Point="100,50" />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>

每个PathGeometry可包含任意数量的PathFigure对象。这意味着可创建几个相互独立的闭合或不闭合图形,作为同一路径的一部分

弧线

  和使用LineSegment 类时一样,使用ArcSegment.Point 属性指定弧线段终点。不过,PathFigure 从起点(或前一条线段的终点)向弧线的终点绘制一条曲线。这条弯曲的连接线实际是椭圆边缘的一部分。

  显然,为了绘制弧线,只有终点是不够的,因为有许多曲线(一些弯曲程度较缓和,另一些弯曲的程度更大)能够连接这两点。还需要指定用于绘制弧线的假想椭圆的尺寸。可使用ArcSegment.Size 属性完成该工作,该属性提供了椭圆的X半径和 Y半径。假想的椭圆越大,边缘曲线就越缓和。

对于任意两点,实际上存在最大尺寸和最小尺寸的椭圆。当创建的椭圆足够大,以至于绘制的线段看起来像直线时,这时的椭圆就具有最大尺寸。再增大尺寸就没有效果了。当椭圆足够小,以至于使用整个半圆连接两点,这时椭圆的尺寸就最小。再缩小尺寸也没有效果

1
2
3
4
5
6
7
8
9
10
11
<Path Stroke="Blue" StrokeThickness="3">
<Path.Data>
<PathGeometry>
<PathFigure IsClosed="False"
StartPoint="10,100">
<ArcSegment Point="250,150"
Size="200,300" />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>

  即使提供了起点、终点以及椭圆的尺寸,也仍不具备明确绘制弧线所需的全部信息,如果绘制椭圆上的两点,显然可以由两种方式连接它们—通过沿着短边连接两点,或沿着长边连接两点。,可通过ArcIsLargeArc.IsLargeArc属性设置弧线的方向

  即使设置了方向,也还有一点需要明确——椭圆位于何处。设想绘制一条弧线连接左边的一点和右边的一点,并使用尽可能短的弧线。连接着两可点的曲线可被向下拉伸,然后向上拉伸;也可以翻转该弧线,从而先向上弯曲,然后现在弯曲。得到的弧线依赖于定义弧线的两点的顺序以及ArcSegment.SweepDirection属性,该属性可以时Counterclockwise(逆时针 默认值)Clockwise(顺时针)

贝塞尔曲线

  贝塞尔曲线使用更复杂的数学公式连接两条线段,该公式包含的两个控制点决定了曲线的形状。实际上,贝塞尔曲线是每个矢量绘图程序都会创建的要素,因为它们非常灵活。只需要使用起点、终点和两个控制点,就可以创建出令人称奇的各种光滑曲线(包括回线(loop))。下图显示了一条经典的贝塞尔曲线。两个小圆指示控制点,而虚线将每个控制点连接到受该控制点影响最大的线条端点。

  即使不理解贝塞尔曲线的数学原理,也很容易“感觉”出贝塞尔曲线的工作原理。本质上,两个控制点时所有问题的关键。它们以两种方式影响曲线:

  • 在起点,贝塞尔曲线和从第一个控制点到起点之间的直线相切。在终点,贝塞尔曲线和连接终点与最后一个点的直线相切(在中间时曲线)
  • 弯曲程度由两个控制点的距离决定。如果一个控制点更远,该控制点会更强地“拉”贝塞尔曲线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<Canvas>
<Path Stroke="Blue"
StrokeThickness="5"
Canvas.Top="20">
<Path.Data>
<PathGeometry>
<PathFigure IsClosed="False"
StartPoint="10,10">
<BezierSegment Point1="130,30"
Point2="40,140"
Point3="150,150" />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>

<Path Stroke="Green"
StrokeThickness="2"
StrokeDashArray="5 2"
Canvas.Top="20">
<Path.Data>
<GeometryGroup>
<LineGeometry StartPoint="10,10"
EndPoint="130,30" />
<LineGeometry StartPoint="40,140"
EndPoint="150,150" />
</GeometryGroup>
</Path.Data>
</Path>

<Path Stroke="Red"
Fill="Red"
StrokeThickness="8"
Canvas.Top="20">
<Path.Data>
<GeometryGroup>
<EllipseGeometry Center="130,30" />
<EllipseGeometry Center="40,140" />
</GeometryGroup>
</Path.Data>
</Path>

</Canvas>

微语言几何图形

  到目前为止看到的几何图形都比较简明,只用了少数几个点。更复杂的几何图形在概念上与此相同,只不过动辄就需要几百条线段。在复杂路径中定义每条直线、弧线以及曲线非常繁琐而且不是必需的–毕竟,复杂曲线可能由设计工具生成,而不是通过手工编写,所以保持标记的清晰性并不是最重要的。为此,WPF 创作人员为定义几何图形增加了一种更简明的替换语法,通过该语法可用更少的标记表示详细的图形。这种语法通常称为图形微语言(geometrymini-language),并且由于应用于 Path 元素,因此有时也称为路径微语言

  为理解微语言,需要认识到它在本质上是包含一系列命令的长字符串。这些命令由类型转换器读取,然后创建相应的几何图形。每个命令都是单独的字母,后面可选地跟随一些由空格分隔的数字信息(如x和Y坐标)。每个命令也使用空格与前面的命令隔开

1
2
<Path Stroke="Blue"
Data="M 10,100 L 100,100 L 100,50 Z" />

  这个路径使用一个包含4个命令的命令序列。第一个命令(M)创建PathFigure,并将起点设置为(10,100)。接下来的两个命令(L)创建线段。最后一个命令(Z)结束 PathFigure,并将 IsClosed属性设置为true。这个字符串中的逗号是可选的,同样,命令及其参数之间的空格也是可选的但在相邻的两个参数之间以及命令之间至少要保留一个空格。这意味着可以进一步精简语法,形成下面这种更难读的形式:

1
2
<Path Stroke="Blue"
Data="M10 100 L100 100 L100 50 Z" />
微语言图形命令
名称 说明
F value (FillRule) 设置Geometry.FillRule属性。0表示EvenOdd,1表示NonZero。如果决定使用该命令,就必须将该命令放在字符串的开头
M x,y (Move) 为几何图形创建新的PathFigure对象,并设置其起点。该命令必须在其他命令之前使用,F命令除外。然而,也可在绘制序列期间使用该命令移动坐标系统的原点
L x,y (Line) 创建一条到指定点的LineSegment几何图形
H x (HorizontalLine) 使用指定的X值创建一条水平的LineSegment几何图形,并保持Y值不变
V x (VerticalLine) 使用指定的Y值创建一条垂直的LineSegment几何图形,并保持X值不变
A radiusX,radiusY,degress,isLargeArc,isClockwise,x,y 创建一条到指定点的ArgSegment线段。指定描述弧线的椭圆半径、弧线旋转的度数,以及用原图设置IsLargeArc和SweepDirection属性的布尔标志
C x1,y1,x2,y2,x,y 创建到指定点的贝塞尔曲线,使用点(x1,y1)和(x2,y2)作为控制点
Q x1,y1,x,y 创建到指定点的二次贝塞尔曲线,使用一个控制点(x1,y1)
S x2,y2,x,y 通过将前一条贝塞尔曲线的第二个控制点作为新建贝塞尔曲线的第一个和控制点,从而创建一条光滑的贝塞尔曲线
Z 结束当前 PathFigure对象,并将IsClosed属性设置为true。如果不希望将IsClosed属性设置为tue,就不必使用该命令–如果希望开始一个新的 PathFigure 对象或结束字符串,只需使用M命令

在微语言几何图形中另有一个技巧。如果希望命令的参数值相对于前一个点,而不是使用绝对坐标进行计算,可使用小写的命令。

使用几何图形进行裁剪

  正如您所看到的,几何图形是创建形状的最强大方法。然而,几何图形不仅可用于 Path元素,也可为任何需要的地方提供抽象的图形定义(而不是在窗口中绘制真实的具体形状)。
  几何图形的另一个用途是用于设置Clip属性,所有元素都提供了该属性。可以通过Clip属性约束元素的外边界以符合特定的几何图形。可使用Cip 属性创建大量的特殊效果。尽管该属性通常用于修剪 1mage 元素中的图像内容,但也可将 Clip 属性应用于任何元素。唯一的限制是,如果确实希望看到一些内容–而不仅是用处不大的单独曲线和线段,需要使用闭合的几何图形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<Window.Resources>
<GeometryGroup x:Key="clipGeometry"
FillRule="Nonzero">
<EllipseGeometry RadiusX="75"
RadiusY="50"
Center="100,150" />
<EllipseGeometry RadiusX="100"
RadiusY="25"
Center="200,150" />
<EllipseGeometry RadiusX="75"
RadiusY="130"
Center="140,140" />
</GeometryGroup>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>

<Button Clip="{StaticResource clipGeometry}">A button</Button>

<Image Grid.Column="1"
Clip="{StaticResource clipGeometry}"
Source="Images/happyface.png" />
</Grid>

  使用剪裁存在限制。设置的剪裁不会考虑元素的尺寸。换句话说,当改变窗口尺寸时,不管上图显示的按钮变大还是变小,剪裁区域仍保留原样,并显示按钮的不同部分。一种可能的解决方案是在 Viewbox 控件中封装元素,以便提供自动重新缩放功能。但这会导致所有内容都按比例地改变尺寸,包括希望改变尺寸的一些细节(剪裁区域和按钮表面)以及那些可能不希望改变的内容(按钮文本和绘制按钮边框的线条)。

图画

  Geometry抽象类表示形状或路径。Drawing抽象类扮演了互补的角色,他表示2D图画(drawing)—换句话说,他包含了显示矢量图形活位图需要的所有信息。

图画类
说明 属性
GeometryDrawing 封装一个几何图形,该几何图形具有填充它的画刷和绘制其边框的画笔 Geometry、Brush、Pen
ImageDrawing 封装一幅图像(通常是基于文件的位图图像),该图像具有定义图像边界的矩形 ImageSource、Rect
VideoDrawing 结合用于播放视频文件的媒体播放器和定义其边界的矩形 Player、Rect
GlyphRunDrawing 封装低级文本对象,即所谓的具有绘制用画刷的GlyphRun对象 GlyphRun、ForgroundBrush
DrawingGroup 组合各种类型的Drawing对象的集合。可使用DrawingGroup创建混合图画,并可使用它的一个属性一次为整个集合应用效果 BitmapEffect、BitmapEffectInput、Children、ClipGeometry、GuidelineSet、Opacity、OpacityMask、Transform

显示图画

  因为继承自 Drawing 的类不是元素,所以不能将它们放置到用户界面中。为了显示图画需要使用下表中列出的三个类中的一个。

用于显示图画的类
父类 说明
DrawingImage ImageSource 允许在Image元素中驻留图画
DrawingBrush Brush 允许使用画刷封装图画,之后就可以用画刷绘制任何表面
DrawingVisual Visual 允许在低级的格式化对象中放置图画。可视化对象并不具有真正元素的开销,但是如果实现了需要的基础结构,那么仍可以实现可视化对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Button>
<Button.Background>
<DrawingBrush>
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="Yellow">
<GeometryDrawing.Pen>
<Pen Brush="Blue"
Thickness="3" />
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0 100,100" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Button.Background>
</Button>