说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。
本节主要介绍用于生成诸如正方形或矩形等2D图形的Shape类与更基础的用来生成几何对象的Geometry类。
首先来介绍Shape类,这些支持2D图形绘制的Shape类主要是位于System.Windows.Shapes命名空间下,Shape类是这些图形类的基类。Shape派生自FrameworkElement,用于绘制基本的二维图画,它内部包装了Geometry,Pen和Brush,并且可以直接作为一个可视化元素呈现出来而无须其它机制。
WPF中内置了6种Shape,都派生自System.Windows.Shapes.Shape这个抽象类:
- Rectangle:绘制一个矩形
- Ellipse:绘制一个椭圆或圆
- Line:绘制一条线段
- Polyline:绘制一个相连的由直线组成的图形
- Polygon:绘制一个封闭的由若干线段相连的图形
- Path:绘制一系列相连的线段,可以控制线段曲度
后文将逐一详细介绍。
Shape类定义了许多属性用于控制子类的外观,其中最重要的两个是Fill与Stroke(均为Brush类型),分别用于填充内部区域和绘制轮廓,这两个属性的作用相当于GeometryDrawing中Brush和Pen属性的作用。Shape内部封装了Pen对象,并通过包括Stroke及StrokeStartLineCap,StrokeThickness在内的8个属性来控制内部的Pen对象,从而简化代码编写。
DrawingVisual。
注意:对于Shape,其Stroke和Fill属性默认被设为null,只有显式设置这些值才会看到Shape的效果。
Shape的原理
Shape类(主要指其子类)内部重写了UIElement的OnRender方法,使用DrawingContext方法来绘制Geometry。
以Ellipse为例,其内部实现的代码原型如下:
1 public class Shape:UIElement 2 { 3 protected override void OnRender(DrawingContext drawingContext) 4 { 5 Pen pen = ...; //根据StrokeXXX属性构造Pen对象 6 Rect rect = ...; // 根据矩形的大小决定布局 7 drawingContext.DrawGeometry(this.Fill,pen,new EllipseGeometry(rect)); 8 } 9 }
上面代码包含了与布局系统交互的管道,详见后文。
1. Rectangle:绘制一个矩形
Width与Height属性定义矩形的宽和高,当两个值相同时,所绘图形为正方形。包括描边填充等效果与Ellipse相同。我们来看一些比较特殊的属性,RadiusX与RadiusY两个double类型的属性分别用来设置横向与纵向转角的半径,这些点与RectangleGeometry中的同名属性作用相同,RadiusX和RadiusY的最大有效值分别为Width与Height的一半,虽然可以设置为超过这些值的值,但不会有效果;这两个属性的默认值均为0.0,表示没有转角。
在没有给Rectangle显式指定Width与Height的情况下,Rectangle会使用继承自FrameworkElement的Width和Height值。也可以用Canvas.Left和Canvas.Top这样的依赖属性来控制位置。
下面的例子演示了独立设置这两个属性得到的矩形的转角效果,可以看到横向转角更为平滑:
1 <Rectangle Fill="Pink" RadiusX="50" RadiusY="25" Width="200" Height="128" />
2. Ellipse:绘制一个椭圆或圆。
虽然通过Rectangle可以得到一个Ellipse(将RadiusX与RadiusY设为最大的有效值),通过Ellipse可以更方便的绘制椭圆。Width与Height属性定义椭圆的形状,相同时得到一个圆。Center属性用来定义椭圆的中心位置。形状的描边使用Stroke属性定义,填充使用Fill属性定义。可以参见介绍画刷的章节。注意,与EllipseGeometry不同,Ellipse没有提供RadiusX,RadiusY和Center属性。
3. Line:绘制一条线段
最基本的X1,Y1属性定义起点,X2,Y2定义终点,所采用的值是相对值,是Line相对于上一级布局控件给其的空间的位置而来。下面是一个很好的说明相对位置的例子:
1 <StackPanel>
2 <Line X1="0" Y1="0" X2="100" Y2="100" Stroke="Pink" StrokeThickness="8" Margin="4" />
3 <Line X1="0" Y1="0" X2="100" Y2="0" Stroke="Pink" StrokeThickness="8" Margin="4" />
4 <Line X1="0" Y1="100" X2="100" Y2="0" Stroke="Pink" StrokeThickness="8" Margin="4" />
5 </StackPanel>
效果图:
另外之所以没有为起点和终点定义两个Point对象,是为了数据绑定更容易。
通常我们使用Canvas.Top和Canvas.Left属性来定义线段左上角的位置。另外由于是线段所以Fill对与这种图形就没有作用,但可以使用Stroke及其相关属性来实现对线段的描边效果。
下面这个例子展示一下Canvas.Top与Canvas.Left对线段位置的影响:
1 <Canvas>
2 <Line X1="20" Y1="20" X2="80" Y2="80" Stroke="Pink" StrokeThickness="5" />
3 <Line X1="20" Y1="20" X2="80" Y2="80" Canvas.Top="50" Stroke="GreenYellow" StrokeThickness="5"/>
4 </Canvas>
由设计时截图可以看出,X1,X2,Y1,Y2是基于Canvas.Top与Canvas.Left定位后的位置来绘制线段。
4. Polyline
Polyline通过Point对象的集合表示一组线段。设置Points属性可以使用如下这样的格式"0,0 100,100 150,200"(逗号也可省略),表示(0,0),(100,100),(150,200)这样三个点。通过Fill属性可以很方便的设置填充,系统会自动找到闭合区域,这是因为Polyline内部使用了PathGeometry。另外对于Polyline的FillRule属性的设置会直接设置给内部的PathGeometry的FillRule属性。
5. Polygon多边形
Polygon的Points属性用于定义多边形的各个顶点。WPF会将起点与终点自动封闭,这是Polygon与Polyline唯一的区别,其本质是将内部的PathGeometry的IsClose设置为true。同Polyline,对于Polygon的FillRule属性的设置会直接设置给内部的PathGeometry的FillRule属性。
6. Path:绘制相连接的线段或曲线
Path用于绘制相连的包含弯曲线段的对象,这是一个功能很强大的类,几乎所有的几何图形都使用这个类来定义。正如所有的Geometry都可以用PathGeometry表示一样。Path的IsClosed属性用于定义是否将线的起点与终点相连接。在Path的Data属性是Path添加给Shape唯一的属性,其可以设置为一个Geometry的实例。Path.Data常以属性元素的方式使用。Data中定义的几何形状使用Geometry来定义(Path 也只能使用几何形状Geometry来定义)。因此Path是将任意Geometry嵌入到用户界面最简单的方法。有关Geometry的话题见补充。这里我们给出几个简单的例子来演示使用Geometry定义Path的方式:
XAML:
1 <Path Stroke="Pink" StrokeThickness="5">
2 <Path.Data>
3 <EllipseGeometry RadiusX="100" RadiusY="80" Center="100,80"/>
4 </Path.Data>
5 </Path>
这段代码中使用了EllipseGeometry来演示,对于这个类的使用方法后文有详细介绍。代码的效果如下所示:
<Path.Data>中除了可以定义单独的几何图形外还可以定义几何组<GeometryGroup>,几何组中可以定义如PathGemeotry,EllipseGemeotry等标签。同样我们也给出一个小示例:
XAML:
1 <Path Stroke="Pink" StrokeThickness="5">
2 <Path.Data>
3 <GeometryGroup>
4 <LineGeometry StartPoint="10,10" EndPoint="90,90"/>
5 <RectangleGeometry Rect="10,10,80,80"/>
6 <PathGeometry>
7 <PathFigure StartPoint="10,90">
8 <LineSegment Point="90,10"/>
9 <LineSegment Point="180,90"/>
10 </PathFigure>
11 </PathGeometry>
12 </GeometryGroup>
13 </Path.Data>
14 </Path>
这段代码的效果:
另外,Data属性也支持Geometry类型转换器,我们可以直接使用Path语言设置该属性。
详解Geometry
Geometry是一种对形状和路径尽可能简单的抽象表示,通过这个类提供的函数可以得到路径区域或路径是否相交等信息。Geometry的子类可以被分为两类,基本几何体和聚合几何体。
我们依次来研究一下内置的几何图形(Geometry) 对象:
- LineGeometry
- RectangleGeometry
- EllipseGeometry
- PathGeometry
- GeometryGroup
基本几何体
1. LineGeometry
这个类用来定义单一的一条线段,我们使用StartPiont定义起始点,使用EndPoint定义终点。
2. RectangleGeometry
这个类定义了一个矩形的路径,最主要的属性是Rect属性,其中定义了矩形的尺寸,这个属性的值包含4个字符串,分别表示矩形左上角的位置,及矩形的宽度和高度。所以如果定义Rect="20,20,30,35"表示一个左上角在(20,20)处,宽为30,高为30的矩形。另外两个重要的属性是RadiusX和RadiusY分别用于定义圆角的X,Y轴半径。
3. EllipseGeometry
这个类用来生成一个椭圆形,EllipseGeometry使用RadiusX和RadiusY分别定义X轴与Y轴方向上的半径,Center属性可以定义中心的位置。前文有代码演示了这个类的使用。
4. PathGeometry
我们把PathGemeotry放在最后讲解,这是一个很复杂同时功能强大的通用的Geometry。通过PathGemeotry可以定义一系列复杂的形状,包括弧形,贝塞尔曲线等。相比之下前面3个几何体只是方便用户使用而提供的单独实现,通过PathGeometry可以很容易实现相同的对象。
PathGeometry的Figures属性存储对形状的定义(包含了一组PathFigure对象的几何),当然一般要使用<PathGeometry.Figures>这种形式定义,或者更简单的,由于Figures可以作为内容属性,可以直接在PathGeometry元素中来定义PathFigure对象(即直接省略<PathGeometry.Figures>这个元素),而线段(Segment)即定义在PathFigure元素中。PathGeometry中可以定义多个PathFigure元素。而PathFigure中一个很重的属性是表示线段(PathSegment)起始位置的StartPoint属性。当一个PathFigure元素中存在多个PathSegment的定义(定义在PathFigure的Segments内容属性中,PathSegment仅仅是一条直线或曲线,后文将详细介绍WPF内置的PathSegment。)第一个之后的PathSegment将以前一个Segment的终点作为起点(见下面第一个例子)。如果存在多组PathFigure,每组的第一个Segment都使用相应的PathFigure中的StartPoint的定义(见下面第二个例子)。
下面给一个例子:
XAML:
1 <Image>
2 <Image.Source>
3 <DrawingImage>
4 <DrawingImage.Drawing>
5 <GeometryDrawing>
6 <GeometryDrawing.Pen>
7 <Pen Brush="Pink" Thickness="5"/>
8 </GeometryDrawing.Pen>
9 <GeometryDrawing.Geometry>
10 <PathGeometry>
11 <PathGeometry.Figures>
12 <PathFigure StartPoint="10,90">
13 <LineSegment Point="90,10"/>
14 <ArcSegment Point="180,90" Size="90,80" RotationAngle="50" SweepDirection="Counterclockwise"/>
15 </PathFigure>
16 </PathGeometry.Figures>
17 </PathGeometry>
18 </GeometryDrawing.Geometry>
19 </GeometryDrawing>
20 </DrawingImage.Drawing>
21 </DrawingImage>
22 </Image.Source>
23 </Image>
斜体部分即可以省略的<PathGeometry.Figures>属性。另外正如绘图概览一文所述,要想最终得到输出,需要把GeometryDrawing放在DrawingImage这样的类中。
效果图如下:
在这个例子的基础上,如果我们给<GeometryDrawing>的Brush属性指定一个值,像这样:
1 <GeometryDrawing Brush="Azure">
填充效果如下所示:
另外为了让这个图形闭合,我们可以在PathFigure中手工添加一条PathSegment,或者更简单的直接将PathFigure的IsClosed属性指定为true,即:
1 <PathFigure StartPoint="10,90" IsClosed="True">
这样会产生如下效果:
另外如果想让PathSegment的交叉点不是尖角而是圆滑的拐角,可以通过将PathSegment对象(这个例子中是LineSegment与ArcSegment)的IsSmoothJoin属性设置为true:
1 <LineSegment Point="90,10" IsSmoothJoin="True"/>
2 <ArcSegment Point="180,90" Size="90,80" RotationAngle="50" SweepDirection="Counterclockwise" IsSmoothJoin="True"/>
效果图:
下面是另一个例子,其中定义了多组<PathFigure>:
1 <GeometryDrawing Brush="Azure">
2 <GeometryDrawing.Pen>
3 <Pen Brush="Pink" Thickness="5"/>
4 </GeometryDrawing.Pen>
5 <GeometryDrawing.Geometry>
6 <PathGeometry>
7 <!-- 三角形1 -->
8 <!-- 不指定StartPoint,默认为0,0 -->
9 <PathFigure IsClosed="True">
10 <LineSegment Point="0,100"/>
11 <LineSegment Point="100,100"/>
12 </PathFigure>
13 <!-- 三角形2 -->
14 <PathFigure StartPoint="70,0" IsClosed="True">
15 <LineSegment Point="0,100"/>
16 <LineSegment Point="100,100"/>
17 </PathFigure>
18 </PathGeometry>
19 </GeometryDrawing.Geometry>
20 </GeometryDrawing>
效果图如下:
上面的效果图中我们看到了重叠几何体默认的填充效果,PathGeometry的FillRule属性可以控制填充效果,该类型为FillRule枚举类型,其提供两个值:
- EvenOdd:这是默认值,效果如我们看到的那样。MSDN给出的说明是:
确定一个点是否位于填充区域内的规则,具体方法是从该点沿任意方向画一条无限长的射线,然后计算该射线在给定形状中因交叉而形成的路径段数。 如果此数目为奇数,则该点在内部;如果是偶数,则该点在外部。
以我们上面的例子为例:
如图,填充区域的橙色射线形成奇数个线段,而非填充区域出发的紫色射线形成偶数个线段。
- Nonzero:MSDN说明如下
确定一个点是否位于路径填充区域内的规则,具体方法是从该点沿任意方向画一条无限长的射线,然后检查形状段与该射线的交点。 从零开始计数,每当线段从左向右穿过该射线时加 1,而每当路径段从右向左穿过该射线时减 1。 计算交点的数目后,如果结果为零,则说明该点在路径外部。 否则,说明该点位于路径内部。
使用:
1 <PathGeometry FillRule="Nonzero">
我们可以将前面的例子的填充模式改为Nonzero,填充同效果为:
从分析图看见,黄色射线与紫色射线切割,所计算出的值分别为1和2,所以两个区域都在填充区域内部。
出现有不同填充效果一般是Geometry中含有交叉点,这可能是由一个PathFigure中多个重叠的PathSegment形成,也能是多个PathFigure形成,无论如何在判断填充效果时采用的规则是一致的。
StreamGeometry
StreamGeometry与PathGeometry的使用很相似,但是StreamGeometry只能用于程序代码来绘制几何体。对于某些一次建立而不用修改的复杂几何体,使用StreamGeometry会提高性能。
下面的代码展示了使用StreamGeometry建立与上文一样的重叠三角的图案:
1 StreamGeometry g = new StreamGeometry(); 2 using (StreamGeometryContext context = g.Open()) 3 { 4 //三角形 1 5 context.BeginFigure(new Point(0, 0), true, true); 6 context.LineTo(new Point(0,100), true,true ); 7 context.LineTo(new Point(100,100),true,true ); 8 9 //三角形 2 10 context.BeginFigure(new Point(70,0),true,true ); 11 context.LineTo(new Point(0, 100), true, true); 12 context.LineTo(new Point(100, 100), true, true); 13 } 14 15 //将该Geometry应用到存在的GeometryDrawing上 16 geometryDrawing.Geometry = g;
如代码中所示,我们使用了LineTo构建了一条LineSegment,另外还可以使用ArcTo和BezierTo这样的函数建立ArgSegment与BezierSegment。
聚合几何体
WPF中提供了两种聚合Geometry类 – GeometryGroup与CombinedGeometry,它们都派生自Geometry,可以用于所有可以使用Geometry的地方。前两者与后者的关系类似于TransformGroup之于Transform及DrawingGroup之于Drawing,它们之间还是存在一定明显差异的。
5. GeometryGroup
GeometryGroup同其它如LineGeometry,PathGeometry等Geometry,也是用来设置接收Geometry的地方(如Path的Data属性)。最大的不同是GeometryGroup用于将其它LineGeometry等对象组合在一起,一个GeometryGroup中可以设置多个Geometry对象,使用起来非常简单这里直接给出一个例子:
1 <GeometryDrawing Brush="Azure">
2 <GeometryDrawing.Pen>
3 <Pen Brush="Pink" Thickness="5"/>
4 </GeometryDrawing.Pen>
5 <GeometryDrawing.Geometry>
6 <GeometryGroup>
7 <!-- 三角形1 -->
8 <PathGeometry>
9 <PathFigure IsClosed="True">
10 <LineSegment Point="0,100"/>
11 <LineSegment Point="100,100"/>
12 </PathFigure>
13 </PathGeometry>
14 <!-- 三角形2 -->
15 <PathGeometry>
16
17 <PathFigure StartPoint="70,0" IsClosed="True">
18 <LineSegment Point="0,100"/>
19 <LineSegment Point="100,100"/>
20 </PathFigure>
21 </PathGeometry>
22 </GeometryGroup>
23 </GeometryDrawing.Geometry>
24 </GeometryDrawing>
最终而成的效果与之前PathGeemetry中完全一样。
特别注意,GeometryGroup拥有和PathGeometry类一样的FillRule属性,默认值也是EvenOdd,而且其上该属性的优先级高于任意子类的该属性的设定。
选择使用GeometryGroup而不是通过PathGeometry包含多个PathFigure绘制图形的一个理由是,我们可以独立控制GeometryGroup中每一个Geometry来设置它们的属性(如Transform)。我们把前文的例子进行扩展,来展示这里所描述的原因:
1 <GeometryGroup>
2 <!-- 三角形1 -->
3 <PathGeometry>
4 <PathGeometry.Transform>
5 <RotateTransform Angle="25"/>
6 </PathGeometry.Transform>
7 <PathFigure IsClosed="True">
8 <LineSegment Point="0,100"/>
9 <LineSegment Point="100,100"/>
10 </PathFigure>
11 </PathGeometry>
12 <!-- 三角形2 -->
13 <PathGeometry>
14 <PathFigure StartPoint="70,0" IsClosed="True">
15 <LineSegment Point="0,100"/>
16 <LineSegment Point="100,100"/>
17 </PathFigure>
18 </PathGeometry>
19 </GeometryGroup>
如代码所示,我们很方便的为其中一个<PathGeometry>增加了一个变换。
提示:由于Brush和Pen定义在Drawing中,可以通过在DrawingGroup中放置多个Drawing(可以有也可以没有Geometry)来将多个具有不同填充或轮廓的图形放在一起。
提示:Geometry与Path的实例可以被共享(虽然UIElement只能有一个父类),在某些情况下(尤其是对于复杂几何体),这样做可以提高性能。
下面是一个例子,原型还是前文那两个三角形,其中一个可以通过在另一个的基础上通过变换得到,下面将展示在资源中定义PathFigure并复用的方法
首先我们在父容器Canvas一级定义Resource:
1 <Canvas.Resources> 2 <PathFigure x:Key="figure" IsClosed="True"> 3 <LineSegment Point="0,100"/> 4 <LineSegment Point="100,100"/> 5 </PathFigure> 6 </Canvas.Resources>
注意,那个加粗展示的ResouceKey很重要,下面的代码中需要通过这个值来引用PathFigure:
1 <GeometryGroup> 2 <!-- 三角形1 --> 3 <PathGeometry> 4 <StaticResource ResourceKey="figure" /> 5 </PathGeometry> 6 <!-- 三角形2 --> 7 <PathGeometry> 8 <PathGeometry.Transform> 9 <SkewTransform CenterY="100" AngleX="-30" /> 10 </PathGeometry.Transform> 11 <StaticResource ResourceKey="figure" /> 12 </PathGeometry> 13 </GeometryGroup>
这样我们就得到与之前示例一样的效果,但这次我们只定义了一次PathFigure并复用它。
6. CombinedGeometry
CombinedGeometry可以组合两个Geometry,分别定义于Geometry1和Geometry2两个属性中。另外GeometryCombineMode枚举用来设置合并两个几何体的方式,该枚举有如下四种值:
- Union:默认值,将两个几何体的全部作为合并后的几何体
- Intersect:将两个几何体的相交部分作为合并后的几何体
- Xor:将两个几何体的不相交部分作为合并后的几何体
- Exclude:将第一个几何体的独有部分作为合并后的几何体
我们继续以前文的两个三角形作为示例:
1 <GeometryDrawing Brush="Azure">
2 <GeometryDrawing.Pen>
3 <Pen Brush="Pink" Thickness="5"/>
4 </GeometryDrawing.Pen>
5 <GeometryDrawing.Geometry>
6 <CombinedGeometry GeometryCombineMode="Union">
7 <CombinedGeometry.Geometry1>
8 <!-- 三角形1 -->
9 <PathGeometry>
10 <PathFigure IsClosed="True">
11 <LineSegment Point="0,100"/>
12 <LineSegment Point="100,100"/>
13 </PathFigure>
14 </PathGeometry>
15 </CombinedGeometry.Geometry1>
16 <CombinedGeometry.Geometry2>
17 <!-- 三角形2 -->
18 <PathGeometry>
19 <PathFigure StartPoint="70,0" IsClosed="True">
20 <LineSegment Point="0,100"/>
21 <LineSegment Point="100,100"/>
22 </PathFigure>
23 </PathGeometry>
24 </CombinedGeometry.Geometry2>
25 </CombinedGeometry>
26 </GeometryDrawing.Geometry>
27 </GeometryDrawing>
代码中斜体部分就是我们可以控制的合并方式,在几种不同的GeometryCombineMode下,得到的不同效果如下:
Union | Intersect | Xor | Exclude |
PathSegment详解
我们把所有可以用于定义PathFigure的线段类列在下方并逐一介绍,这些类都派生自PathSegment类:
- LineSegment,表示线段
- PolyLineSegment,表示多个线段顺序连接的快捷实现
- ArcSegment,表示椭圆形上的一段曲线段
- BezierSegment,三次贝塞尔曲线
- PolyBezierSegment,表示多个三次贝塞尔曲线顺序
- QuadraticBezierSegment,二次贝塞尔曲线
- PolyQuadraticBezierSegment,表示多个二次贝塞尔曲线顺序
1. LineSegment
这个类的使用非常简单,Point属性定义了线段的终点,结合PathFigure中定义的起点就可绘制出单一一条线段。如果存在多组LineSegment,则第二组以后的LineSegment的起点为前一组的终点(前一组Point的值)。
2. PolyLineSegment
PolyLineSegment通过定义中间点来定义多段折线,中间点数据定义于Points属性中,格式为逗号分隔的浮点数,每两个为一组表示一个中间点,如Points="90,10,180,90,270,10"的设置最终会得到如下效果(StartPoint设置为10,90):
3. ArcSegment
ArcSegment用于定义两点之间的一段拱形(椭圆形)线段。下面的这些属性用于控制这段弧线的形状:
- Point:设置椭圆弧的终点位置(同LineSegment,起始位置在PathFigure中定义)
- Size:定义弧线所在椭圆的X轴与Y轴的半径
- RotationAngle:定义了相对于X轴旋转的角度
- IsLargeArc:决定弧线形状,当此属性为True时,使用弧形为椭圆长的一条边
- SweepDirection:设置拱形的方向,Clockwise 与 Counterclockwise两种值分别表示顺时针与逆时针
4. BezierSegment(贝塞尔曲线),
贝塞尔曲线的一个基本特点是除了同普通线段有两个端点以外,还有一个或多个控制点。不同次贝塞尔曲线的控制点不同,如马上要介绍的三次贝塞尔曲线有两个控制点。这使得线段具有弯曲的特性,最终效果中控制点是不可见的,一个形象的比喻是,控制点处如存在引力一般把线段拉了过去。
BezierSegment用于在两点之间生成一条三次贝塞尔曲线。三次贝塞尔曲线基于4个点,起点,终点与两个控制点,同前面几个Segment起点使用父类PathFigure的StartPoint指定,Point1,Point2属性定义两个控制点,属性Point3定义终点(如果只定义了Point1与Point2两个点,则Point2作为终点,此时只有Point1这一个控制点)。
5. PolyBazierSegement
通过点的集合绘制多条贝塞尔曲线,每一条曲线需要三个点:两个控制点,一个终点,后一条曲线的起点取决于上一条曲线的终点。这些点依次定义于PolyBezierSegement的Points属性中,同前,第一条曲线的起点定义于PathFigure类的StartPoint属性中。
PolyBezierSegment的Points属性定义方式有两种:
第一种是类似PolyLineSegment中Points的定义方式:
1 <PathFigure StartPoint="10,90">
2 <PolyBezierSegment Points="20,20,30,30,90,10,
3 55,55,35,35,180,90"/>
4 </PathFigure>
第二种方式是使用属性元素的语法定义Points属性:
1 <PathFigure StartPoint="10,90">
2 <PolyBezierSegment>
3 <PolyBezierSegment.Points>
4 <Point X="20" Y="20" />
5 <Point X="30" Y="30" />
6 <Point X="90" Y="10" />
7 <Point X="55" Y="55" />
8 <Point X="35" Y="35" />
9 <Point X="180" Y="90" />
10 </PolyBezierSegment.Points>
11 </PolyBezierSegment>
12 </PathFigure>
这两种语法的效果完全相同,我们先来看一下效果图:
代码中定义了2条贝塞尔曲线,(20,20),(30,30),(90,10)为一组定义第一条贝塞尔曲线,其中前两个点为控制点,最后一个为终点。(55,55),(35,35),(180,90)为第二组定义第二条贝塞尔曲线,同样前两个点为控制点,最后一个为终点。
6. QuadraticBezierSegment
由名称来看,QuadraticBezierSegment比BezierSegment更长,但实际上QuadraticBezierSegment比BezierSegment简单,前者用于表示一条只有一个控制点的二次贝塞尔曲线。所以前者绘制的二次贝塞尔曲线(QuadraticBezierSegment)只能给出一条U形曲线(某些情况下会变成直线),但后者绘制的三次贝塞尔曲线(BezierSegment)可以给出S形曲线。
如上所述,QuadraticBezierSegment用于定义一条二次贝塞尔曲线,其中第一个控制点用于确定二次贝塞尔曲线的形状,第二个控制点作为终点。如果只定义了一个点,这个点将被作为终点所得的形状为一直线段。
下面给出一个示例:
1 <PathFigure>
2 <QuadraticBezierSegment Point1="200,0" Point2="300,100"/>
3 </PathFigure>
由于在PathFigure中没有显示指定StartPoint将使用默认值0,0,得到的二次贝塞尔曲线为:
7. PolyQuadraticBezierSegment
类似于PolyBezierSegment,PolyQuadraticBezierSegment描述了一组相连的二次贝塞尔曲线,下面的例子使用的在PolyBezierSegment部分中展示的第一种定义方式:
1 <PathFigure StartPoint="100,100">
2 <PolyQuadraticBezierSegment Points="50,50,150,150,250,250,
3 100,200,200,100,300,300">
4 </PolyQuadraticBezierSegment>
5 </PathFigure>
有前面的分析我们可以知道,这段XAML中定义了3条贝塞尔曲线组成的一组线段,第一条线段由(100,100)到(150,150),使用(50,50)为控制点,由于控制点与起止点斜率相同这段线段为直线,第二段线段从(150,150)到(100,200),控制点为(250,250)。第三段从(100,200)开始到(300,300),控制带为(200,100)。最终图形为:
Path语言
WPF提供了名为GeometryConvert的类型转换器,支持使用字符串的方式定义PathGeometry,这种字符串即Path语言。在过程代码中通过Geometry的Parse静态方法可以将Path语言转换为Geometry的实例。使用Path语言的一个好处是当表示复杂几何图形时使用Path语言可以缩短代码长度(如使用Expression Design将AI等格式的矢量图导出XAML字典时,均使用Path语言描述图形)。Path语言的语法使用关键字加空格加逗号分隔数字数组(如坐标)等来定义图形。我们由浅入深先看一个简单的例子:
下面的XAML是使用传统方式表示我们前文见过的三角形之一:
1 <DrawingImage>
2 <DrawingImage.Drawing>
3 <GeometryDrawing Brush="Azure">
4 <GeometryDrawing.Pen>
5 <Pen Brush="Pink" Thickness="5"/>
6 </GeometryDrawing.Pen>
7 <GeometryDrawing.Geometry>
8 <GeometryGroup>
9 <PathGeometry>
10 <PathFigure IsClosed="True">
11 <LineSegment Point="0,100"/>
12 <LineSegment Point="100,100"/>
13 </PathFigure>
14 </PathGeometry>
15 </GeometryGroup>
16 </GeometryDrawing.Geometry>
17 </GeometryDrawing>
18 </DrawingImage.Drawing>
19 </DrawingImage>
使用Path语言后的简化代码为:
1 <GeometryDrawing Brush="Azure" Geometry="M 0,0 L 0,100 L 100,100 Z">
2 <GeometryDrawing.Pen>
3 <Pen Brush="Pink" Thickness="5"/>
4 </GeometryDrawing.Pen>
5 </GeometryDrawing>
如果要表示前文两个重叠的三角形,Path语言字符串会更复杂:
1 <GeometryDrawing Brush="Azure" Geometry="M 0,0 L 0,100 L 100,100 Z M 70,0 L 0,100 L 100,100 Z">
2 <GeometryDrawing.Pen>
3 <Pen Brush="Pink" Thickness="5"/>
4 </GeometryDrawing.Pen>
5 </GeometryDrawing>
最后是一个使用通过Path语言表示的Geometry设置Path的Data属性的例子:
1 <Path Stroke="Pink" StrokeThickness="5" Data="M 100,100 L 200,200" />
加粗部分即Path语言,M关键字(Move的缩写),将起始点移动到(100,100),其不实际绘制内容,L关键字(Line的缩写)定义了一条线段,(200,200)表示终止点。
这些命令既可以是大写,也可以是小写。通过这些关键字,可以控制PathGeometry及PathFigure属性,也可以控制PathFigure中的PathSegment。
所有支持的命令关键字分类列于下方,它们语法上很简单,但功能很强大:
命名 | 说明 |
PathGeometry与PathFigure属性 | |
F n | 设置FillRule,0表示EvenOdd,1表示NonZero。如果使用该命令,需要将其放在字符串开头。 |
M x,y | M表示Move,作用是开始一个PathFigure并设置StartPoint为(x,y)。该命令必须用在除了F以外的其它任何命令之前。 |
Z | 结束当前PathFigure并设置IsClosed为true。如果不想让PathFigure闭合,可以直接省略该命令。在这之后可以紧接着使用M命令由指定点开始一个新的PathFigure,或者直接使用其它命令由当前点开始一个新的PathFigure。 |
PathSegment | |
L x,y | 绘制一个当前点到目的点(x,y)的线段。 |
A rx,ry d f1 f2 x,y | 以rx,ry为长短半径,旋转d度来建立一条到(x,y)的椭圆线。f1和f2是两个bool值,分别用于控制ArgSegment的IsLargeArc与Clockwise属性。 |
C x1,y1 x2,y2 x,y | 以(x1,y1)与(x2,y2)为控制点,绘制一条当前点为起点,到(x,y)的三次贝塞尔曲线 |
Q x1,y1 x,y | 以(x1,y1)为控制点,绘制一条当前点为起点,到(x,y)的二次贝塞尔曲线 |
其它快捷方式 | |
H x | 绘制一个从当前点开始,距离为参数指定值的一条水平直线(即目标点为(x,y)的线段,其中y即当前点y值) |
V y | 绘制一个从当前点开始,距离为参数指定值的一条垂直直线(即目标点为(x,y)的线段,其中x即当前点x值) |
S x2,y2 x,y | 绘制平滑的三次贝塞尔曲线。以(x1,y1)与(x2,y2)为控制点,绘制一条当前点为起点,到(x,y)的三次贝塞尔曲线。其中(x1,y1)会由系统自动计算以保证平滑性。(如果上一段是贝塞尔曲线,则该点是上一段曲线的第二控制点,否则,该点就是就是上段线段的当前点) |
T | 绘制平滑的二次贝塞尔曲线 |
特别注意,任何命令都可以使用小写表示,对于除了F、M和Z之外的其它命令,使用小写表示参数坐标是对当前坐标的相对值而不是绝对坐标。
另外,命令和参数间的空格和逗号是可选的,但属性间必须有一个空格或逗号。
关于输入命中测试
继承自UIElement的元素,如Shape,都支持输入命中测试,这种命中测试只支持命中任何坐标上最顶部的元素,并且只有元素的IsEnabled和IsVisible都为true时元素才能被命中。
要执行输入命中测试,只需要在想要测试的UIElement实例上调用InputHitTest函数就可以了。该函数接受一个Point,返回一个InputElement实例。
由于UIElement一些事件,如KeyDown,KeyUp,MouseEnter等事件的存在,使得输入命中测试很少被用到。另外,当输入命中测试没法满足要求时,仍然可以在UIElement上使用可视命中测试。
提示
输入命中测试本质上是可视命中测试的一个特例。InputHitTest实现中调用了VisualTreeHelper.HitTest并且使用一个预置的回调函数来过滤并处理结果。过滤过程去除了被禁用和不可见的UIElement,并且处理过程在遇到第一个合适的结果之后就会停止。
本文完