《Programming WPF》翻译 第7章 2.图形
2015-11-13 18:02
375 查看
原文:《Programming WPF》翻译 第7章 2.图形图形时绘图的基础,代表用户界面树的元素。WPF支持多种不同的形状,并为它们每一个都提供了元素类型。
7.2.1基本图形类
在这一节列出的所有元素,派生于一个共同的抽象基类Shape。虽然你不能直接使用这个类,知道它还是有帮助的,因为它定义了一组共同的特性——你可以在任何形状上使用。这些共同的属性都被连接到形状的内部和外部被绘制的地方。
Fill属性详细指出了Brush要用于填充内部。Line和Polyline这些类没有内部,所以它们没有Fill属性。(这比通过有独立的Shape和FilledShape基类使继承层次复杂化的发式要简单的多)Stroke属性详细指出了用来画形状轮廓的Brush。
如果为你的形状没有详细指出它的Fill或Stroke属性,这将是不可见的,因为这两种属性默认都是透明的。
这看起来特殊——Stroke属性是Brush类型。正如我们早时看到的,WPF定义了Pen类来详细指明一个线条的厚度、dsah样式以及样子,因此如果Stroke属性是Brush类型的,这将是更有意义的。WPF实际上确实在内部使用了Pen来绘制形状的边框。Stroke属性为Brush类型主要是因为它的便利。所有的Pen样式通过独立的Shape属性对外暴露,正如表7-1所示。这详细指明了该场景的标记——在你乐于使用默认的钢笔设置的地方,你不需要提供一个完整的Pen定义仅仅是设置边框颜色。
Table 7-1. Shape Stroke properties and Pen equivalents
笔刷和钢笔都详细描述在“Brushes and Pens”一节,在本章的后面。
7.2.2矩形
Rectangle实现了它的名称所示的。无论任何形状,它可以被填充来绘制,作为一个边框。或者全部。不但绘制一个正常的矩形,它还能画一个圆角矩形。
Rectangle不提供任何属性用于设置它的大小和位置。它依赖于同样的外观机制如其它UI元素。位置由面板容器决定。长和宽都有它的父一级自动设置,或者使用标准外观属性来显示地设置Width和Height。
示例7-6显示了Canvas面板上的一个Rectangle。这里,Width和Height都被显示地设置,以及外观被详细的指定——通过使用附属的Canvas.Left和Canvas.Top属性。
示例7-6
<Canvas>
<Rectangle Fill="Yellow" Stroke="Black"
Canvas.Left="30" Canvas.Top="10"
Width="100" Height="20" />
</Canvas>
示例7-7显示了另一种方法。没有一个矩形有其显示设置的外观和大小。取代的,它们依赖于Grid容器。图7-10显示了结果。
图7-10
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Rectangle Grid.Column="0" Grid.Row="0" Fill="LightGray" />
<Rectangle Grid.Column="1" Grid.Row="0" Fill="Black" />
<Rectangle Grid.Column="0" Grid.Row="1" Fill="DarkGray" />
<Rectangle Grid.Column="1" Grid.Row="1" Fill="White" />
</Grid>
Rectangle通常使用其父面板的坐标系统来排列。这意味着它的边缘将通常是垂直的或水平的,即使父一级面板被旋转过了,Rectangle也当然会随着它一起旋转。如果你想要旋转一个Rectangle相对于它的容器面板,你可以使用有效的RenderTransform属性在所有的用户界面元素上,正如示例7-8所证明的,这个示例说明了RenderTransform的使用来旋转一系列矩形。结果如图7-11所示。
[b]示例7-8
<Canvas>
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Indigo" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Violet" RenderTransform="rotate 45" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Blue" RenderTransform="rotate 90" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Cyan" RenderTransform="rotate 135" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Green" RenderTransform="rotate 180" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Yellow" RenderTransform="rotate 225" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Orange" RenderTransform="rotate 270" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Red" RenderTransform="rotate 315" />
</Canvas>
图7-11
<Rectangle Width="100" Height="50" Fill="Black" RadiusX="30" RadiusY="40" />
图7-12[/b]
<Ellipse Width="100" Height="50" Fill="Yellow" Stroke="Black" />
图7-13[/b]
<StackPanel Orientation="Vertical">
<TextBlock Background="LightGray">Foo</TextBlock>
<Line Stroke="Green" X1="20" Y1="10" X2="100" Y2="40" />
<TextBlock Background="LightGray">Bar</TextBlock>
<Line Stroke="Green" X1="0" Y1="10" X2="100" Y2="0" />
</StackPanel>
示例7-11使用了垂直的StackPanel来排列TextBlock和Line元素的交错序列。TextBlock元素有灰色的背景使之易于看到每个元素垂直的区域。结果如图7-14所示。
图7-14
<Polyline Stroke="Blue"
Points="0,30 10,30 15,0 18,60 23,30 35,30 40,0 43,60 48,30 160,30" />
图7-15
<StackPanel Orientation="Horizontal">
<Polyline Fill="Yellow" Stroke="Blue" Points="40,10 70,50 10,50" />
<Polygon Fill="Yellow" Stroke="Blue" Points="40,10 70,50 10,50" />
</StackPanel>
正如你在图7-16中看到的,Polyline忽略了Fill属性。这个形状并没有它的内部,它被置为敞开的。Polygon,另一方面,通过在首尾两条线段之间绘制一条额外的线段而关闭了形状,同时,它绘制了形状的内部。
图7-16
<StackPanel Orientation="Horizontal">
<Polygon Fill="Yellow" Stroke="Blue" FillRule="EvenOdd"
Points="50,30 13,41 36,11 36,49 14,18" />
<Polygon Fill="Yellow" Stroke="Blue" FillRule="Nonzero"
Points="50,30 13,41 36,11 36,49 14,18" />
</StackPanel>
默认规则是EvenOdd,这用于图7-17中的左边的Polygon。这是能理解的最简单规则。为了决定一个特定的围绕区域是在形状的内部和外部,EvenOdd规则对线段的数量进行计数——你不得不跨越从一个端点到另一个完全在形状外的端点。如果这个数量是奇数的,这个点就是在形状中;反之,就是在外面。
第二个填充规则,Nonzero,是非常精细的。从图7-17中,你可以已经想到任何密闭的区域被认为是在图形中的,但是这并不是那样十分简单的。Nonzero规则表现了一个和Nonzero类似的过程,而不是简单的对线段的数量计数。考虑到线段运行位置的方向——它增长或减少的每条线段跨越的数量,依赖于这个方向。如果在末端总计为nonzero,这个点被认为是在形状内部。
在图7-17中右边的Polygon,Nonzero规则导致了所有的密闭区域作为内部的一部分。然而,如果形状的轮廓沿着一条较轻微盘旋的路径,结果可能有一点更混合,如示例7-15所示。
示例7-15
<Polygon Fill="Yellow" Stroke="Blue" FillRule="Nonzero"
Points="10,10 60,10 60,25 20,25 20,40 40,40 40,18 50,18 50,50 10,50" />
示例7-15的结果显示在图7-18中。这说明了Nonzero规则并不是像第一次看到的那样直接。
图7-18
<Rectangle Fill=”Blue” Width=”40” Height=”80”>
是有效的Path速记:
<Path Fill=”Blue”>
<Path.Data>
<RectangleGeometry Rect=”0, 0, 40, 80” />>
</Path.Data>
</Path>
在这一点,你可能想知道什么时候你要使用RectangleGeometry、EllipseGeometry或LineGeometry在Path中,来取代更简单的Rectangle、Ellipse和Line。原因是Path使你可以使用一种特殊类型的几何对象,称为GeometryGroup,来创建一个带有多个几何体的形状。
这里有一个显著的不同在使用多个明显的形状和有一个单独的带有多个几何题的形状之间。让我们看一下示例7-16。
[b]示例7-16
<Canvas>
<Ellipse Fill="Blue" Stroke="Black" Width="40" Height="80" />
<Ellipse Canvas.Left="10" Canvas.Top="10" Fill="Blue" Stroke="Black"
Width="20" Height="60" />
</Canvas>
这绘制了两个椭圆,一个在另一个的上面。他们都有一个黑色的轮廓,因此你可以看到更简单的一个在更大的另一个中,如图7-19所示。
图7-19
<Canvas>
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<EllipseGeometry Center="20, 40" RadiusX="20" RadiusY="40" />
</Path.Data>
</Path>
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<EllipseGeometry Center="20, 40" RadiusX="10" RadiusY="30" />
</Path.Data>
</Path>
</Canvas>
[/b]因为示例7-17中的代码与示例7-16中的代码是等价的,它导致了准确的相同输出,正如图7-19所示。到目前为止,使用几何体取代形状,并没有在生成的结果中制造出不同,这是因为我们仍然使用多个形状。因此,我们现在将显示如何把所有的椭圆放进一个单独的Path以及看到这是如何影响到这些结果。示例7-18显示了改动后的标记。
示例7-18
<Canvas Canvas.Left="100">
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<GeometryGroup>
<EllipseGeometry Center="20, 40" RadiusX="20" RadiusY="40" />
<EllipseGeometry Center="20, 40" RadiusX="10" RadiusY="30" />
</GeometryGroup>
</Path.Data>
</Path>
</Canvas>
这个版本正好有一个单独的路径。它的Data属性包含了一个GeometryGroup。这就允许任何数量的几何体对象被添加到同样的路径。这里我们已经添加了两个EllipseGeometry元素——先前在两个独立的路径中。结果如图7-20所示,明显不同于图7-19。在形状中间有一个洞。因为默认的奇偶填充规则在使用中,较小的椭圆在较大的椭圆中生成一个洞。
图7-20
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure>
<PathFigure.Segments>
<StartSegment Point="0,0" />
<LineSegment Point="50,0" />
<LineSegment Point="50,50" />
<LineSegment Point="0,50" />
<CloseSegment />
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
结果显示在图7-22,这看起来是做了巨大的努力为获取一个简单的结果。我们使用了17个线段标记来达到我们看到的效果——一个单独的矩形。这是为什么WPF提供一些类为这些较简单的形状和几何体。你不用严格地需要任何东西——因为你可以取代的使用Path和PathGeometry,但是它们要求更少的努力。正规的,你会为比较复杂的形状而使用Path。
图7-22
<Canvas>
<Ellipse Fill="Cyan" Stroke="Black" Width="140" Height="60" />
<Path Fill="Cyan" Stroke="Black" Canvas.Left="180">
<Path.Data>
<PathGeometry>
<PathFigure>
<StartSegment Point="0,11" />
<ArcSegment Point="50,61" Size="70,30"
SweepFlag="False" LargeArc="False" />
<CloseSegment />
</PathFigure>
<PathFigure>
<StartSegment Point="30,11" />
<ArcSegment Point="80,61" Size="70,30"
SweepFlag="True" LargeArc="True" />
<CloseSegment />
</PathFigure>
<PathFigure>
<StartSegment Point="240,1" />
<ArcSegment Point="290,51" Size="70,30"
SweepFlag="False" LargeArc="True" />
<CloseSegment />
</PathFigure>
<PathFigure>
<StartSegment Point="280,1" />
<ArcSegment Point="330,51" Size="70,30"
SweepFlag="True" LargeArc="False" />
<CloseSegment />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
</Canvas>
你可能想知道为什么Ellipse的宽度为140和高度为60,这是每个ArcSegment的Size属性值的两倍。这是因为ArcSegment解释了Size为椭圆的两个半径,而椭圆上的Width和Height属性指出了全部的大小。
图7-24显示了结果,以及,正如你能看到的,每一个形状都有一条径直的对角线以及一条椭圆的弧形。在所有的四种情形中,直线的边缘具有相同的长度和方向,圆弧的边缘来自同一个椭圆的不同部分。
在图7-24中,椭圆的轴是垂直和水平的。有时你可能想要使用一个椭圆,而轴并没有对齐到你的主轴。ArcSegment提供了Xrotation属性,允许你详细指出需要旋转的度数。
图7-24
<StartSegment Point="0,50" />
<BezierSegment Point1="60,50" Point2="100,0" Point3="100,50" />
虽然贝赛尔曲线是弹性的,你很少使用到一个如此简单的曲线。当定义带有弯曲边缘的形状时,对于一个形状而言这是普通的——使用贝赛尔曲线定义它的边缘。WPF因此提供了一个PolyBezierSegment属性,这是一个Point结构的数组。每个贝赛尔曲线三个实体在这个数组中:两个控制点和一个终结点。(通常的,每条曲线开始于前一条曲线终结的位置。)示例7-22显示了一个带有两个曲线的片断。图7-29显示了结果。
示例7-22
<StartSegment Point="0,0" />
<PolyBezierSegment>
<PolyBezierSegment.Points>
<Point X="0" Y="10"/>
<Point X="20" Y="10"/>
<Point X="40" Y="10"/>
<Point X="60" Y="10"/>
<Point X="120" Y="15"/>
<Point X="100" Y="50"/>
</PolyBezierSegment.Points>
</PolyBezierSegment>
图7-29
<PolyBezierSegment Points="0,10 20,10 40,10 60,10 120,15 100,50" />
同样,如果你从代码中生成坐标,处理一个单独的PolyBezierSegment和传递给它一个Point数据的数组,这比处理一些独立的片断要容易的多。
贝赛尔曲线在曲线的形状上提供了许多控件。然而,你可能不总是想要弹性的等级。QuadraticBezierSegment使用了一个较简单的等式——仅使用了一个指向定义了曲线形状的控件。这没有提供同样范围的曲线形状,作为一个立方体的贝赛尔曲线,但是如果所有你想要的只是一个简单的形状,这就减少了三分之一你需要提供的坐标对数量。
QuadraticBezierSegment使用上与正常的BezierSegment类似。唯一的不同是他每有一个Point3属性,只有Point1和Point2。Point1是共享控制点,Point2是终结点。QuadraticBezierSegment是一个多曲线的装备。你以与PolyBezierSegment相同的方式使用它,除了你只需要为每个片断提供两个点。
7.2.7.2结合形状
Path自有它的玄机——我们至今没有检查到的。它有联合若干几何体以形成一个新几何体的能力。这是不同于添加到两个几何体到一个Path中,它联合了成对的几何体在某种程度上形成了一个单独的具有完整形状的新几何体。
示例7-23和示例7-24各自定义了路径,它们全都使用了同样的RectangleGeometry和EllipseGeometry。区别是示例7-23把它们都放入了一个GeometryGroup中,而示例7-24把它们都放入了一个CombinedGeometry中
示例7-23
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<GeometryGroup>
<RectangleGeometry Rect="0,0,50,50" />
<EllipseGeometry Center="50,25" RadiusX="30" RadiusY="10" />
</GeometryGroup>
</Path.Data>
</Path>
示例7-24
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<CombinedGeometry CombineMode="Exclude">
<CombinedGeometry.Geometry1>
<RectangleGeometry Rect="0,0,50,50" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center="50,25" RadiusX="30" RadiusY="10" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
图7-30显示了示例7-23和示例7-24的结果。CombinedGeometry导致了带有多个图形的一个形状,CombinedGeometry生成了一个单独的图形。椭圆几何体在矩形几何体咬进去一口。这只是联合若干几何体方式中的一种。CombineMode属性决定了使用哪一个,图7-31显示了所有的五种可利用的类型。
图7-30
<!-- Longhand -->
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure>
<PathFigure.Segments>
<StartSegment Point="0,0" />
<LineSegment Point="50,0" />
<LineSegment Point="50,50" />
<LineSegment Point="0,50" />
<CloseSegment />
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
<!-- Shorthand -->
<Path Fill="Cyan" Stroke="Black" Data="M 0,0 L 50,0 50,50 0,50 Z" />
文本形式的Path.Data属性的语法是简单的。这个字符串包含一个命令序列。命令是一个依照某些数字化参数的字母。需要的参数数量由被选择的命令决定。线条仅需要一对坐标。曲线则需要更多的数据。
如果你忽略了这个字母,同样的命令将会被作为上一次使用。例如,示例7-25使用了L命令,这是“Line”的简写,代表了LineSegment。这只要求2个数字,线段终结点的坐标。以及在我们的示例中,这里有6个数字。这简单的指明了每行有3条线。表7-3列出了这些命令,它们的等价片断类型,以及它们的用法。
[b]Table 7-3. Path.Data commands
M命令有特殊的待遇。在任何地方使用StartSegment而不是第一个PathFigure片断是合理的。如果你使用M命令在区域中间,这就意味着你将要开始一个新的PathFigure。这支持多个图形在这个复杂文本格式中表示。
注意到有两种方式详细指明一个BezierSegment。C命令让你提供所有的控制点。S命令为你的外观在第一个片断生成第一个控制点,以及生成指向前一个片断镜像的第一个控制点。这就确保了片断的切线与前一个片断对齐,导致了以一种平滑的方式连接线段。
所有这些命令都可以两种方式使用。你可以详细指出命令以它的大写或小写形式。以大写的形式,坐标是相于Path元素的位置。如果命令是小写的,坐标是相对于路径中前一个片断的终结点。
现在我们已经检查到当前提供的形状,但是到目前位置,我们已经相当没有危险的在我们的为这些形状填充和轮廓的选择中。我们已经使用命名的颜色和简单的轮廓样式。WPF允许我们通过它的笔刷和钢笔类,使用更多不同种类的绘制样式。
7.2.1基本图形类
在这一节列出的所有元素,派生于一个共同的抽象基类Shape。虽然你不能直接使用这个类,知道它还是有帮助的,因为它定义了一组共同的特性——你可以在任何形状上使用。这些共同的属性都被连接到形状的内部和外部被绘制的地方。
Fill属性详细指出了Brush要用于填充内部。Line和Polyline这些类没有内部,所以它们没有Fill属性。(这比通过有独立的Shape和FilledShape基类使继承层次复杂化的发式要简单的多)Stroke属性详细指出了用来画形状轮廓的Brush。
如果为你的形状没有详细指出它的Fill或Stroke属性,这将是不可见的,因为这两种属性默认都是透明的。
这看起来特殊——Stroke属性是Brush类型。正如我们早时看到的,WPF定义了Pen类来详细指明一个线条的厚度、dsah样式以及样子,因此如果Stroke属性是Brush类型的,这将是更有意义的。WPF实际上确实在内部使用了Pen来绘制形状的边框。Stroke属性为Brush类型主要是因为它的便利。所有的Pen样式通过独立的Shape属性对外暴露,正如表7-1所示。这详细指明了该场景的标记——在你乐于使用默认的钢笔设置的地方,你不需要提供一个完整的Pen定义仅仅是设置边框颜色。
Shape property | Pen equivalent |
---|---|
Stroke | Brush |
StrokeThickness | Thickness |
StrokeLineJoin | LineJoin |
StrokeMiterLimit | MiterLimit |
StrokeDashArray | DashArray |
StrokeDashCap | DashCap |
StrokeDashOffset | DashOffset |
StrokeStartLineCap | StartLineCap |
StrokeEndLineCap | EndLineCap |
7.2.2矩形
Rectangle实现了它的名称所示的。无论任何形状,它可以被填充来绘制,作为一个边框。或者全部。不但绘制一个正常的矩形,它还能画一个圆角矩形。
Rectangle不提供任何属性用于设置它的大小和位置。它依赖于同样的外观机制如其它UI元素。位置由面板容器决定。长和宽都有它的父一级自动设置,或者使用标准外观属性来显示地设置Width和Height。
示例7-6显示了Canvas面板上的一个Rectangle。这里,Width和Height都被显示地设置,以及外观被详细的指定——通过使用附属的Canvas.Left和Canvas.Top属性。
示例7-6
<Canvas>
<Rectangle Fill="Yellow" Stroke="Black"
Canvas.Left="30" Canvas.Top="10"
Width="100" Height="20" />
</Canvas>
示例7-7显示了另一种方法。没有一个矩形有其显示设置的外观和大小。取代的,它们依赖于Grid容器。图7-10显示了结果。
图7-10
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Rectangle Grid.Column="0" Grid.Row="0" Fill="LightGray" />
<Rectangle Grid.Column="1" Grid.Row="0" Fill="Black" />
<Rectangle Grid.Column="0" Grid.Row="1" Fill="DarkGray" />
<Rectangle Grid.Column="1" Grid.Row="1" Fill="White" />
</Grid>
Rectangle通常使用其父面板的坐标系统来排列。这意味着它的边缘将通常是垂直的或水平的,即使父一级面板被旋转过了,Rectangle也当然会随着它一起旋转。如果你想要旋转一个Rectangle相对于它的容器面板,你可以使用有效的RenderTransform属性在所有的用户界面元素上,正如示例7-8所证明的,这个示例说明了RenderTransform的使用来旋转一系列矩形。结果如图7-11所示。
[b]示例7-8
<Canvas>
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Indigo" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Violet" RenderTransform="rotate 45" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Blue" RenderTransform="rotate 90" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Cyan" RenderTransform="rotate 135" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Green" RenderTransform="rotate 180" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Yellow" RenderTransform="rotate 225" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Orange" RenderTransform="rotate 270" />
<Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
Fill="Red" RenderTransform="rotate 315" />
</Canvas>
图7-11
<Rectangle Width="100" Height="50" Fill="Black" RadiusX="30" RadiusY="40" />
图7-12[/b]
<Ellipse Width="100" Height="50" Fill="Yellow" Stroke="Black" />
图7-13[/b]
<StackPanel Orientation="Vertical">
<TextBlock Background="LightGray">Foo</TextBlock>
<Line Stroke="Green" X1="20" Y1="10" X2="100" Y2="40" />
<TextBlock Background="LightGray">Bar</TextBlock>
<Line Stroke="Green" X1="0" Y1="10" X2="100" Y2="0" />
</StackPanel>
示例7-11使用了垂直的StackPanel来排列TextBlock和Line元素的交错序列。TextBlock元素有灰色的背景使之易于看到每个元素垂直的区域。结果如图7-14所示。
图7-14
<Polyline Stroke="Blue"
Points="0,30 10,30 15,0 18,60 23,30 35,30 40,0 43,60 48,30 160,30" />
图7-15
<StackPanel Orientation="Horizontal">
<Polyline Fill="Yellow" Stroke="Blue" Points="40,10 70,50 10,50" />
<Polygon Fill="Yellow" Stroke="Blue" Points="40,10 70,50 10,50" />
</StackPanel>
正如你在图7-16中看到的,Polyline忽略了Fill属性。这个形状并没有它的内部,它被置为敞开的。Polygon,另一方面,通过在首尾两条线段之间绘制一条额外的线段而关闭了形状,同时,它绘制了形状的内部。
图7-16
<StackPanel Orientation="Horizontal">
<Polygon Fill="Yellow" Stroke="Blue" FillRule="EvenOdd"
Points="50,30 13,41 36,11 36,49 14,18" />
<Polygon Fill="Yellow" Stroke="Blue" FillRule="Nonzero"
Points="50,30 13,41 36,11 36,49 14,18" />
</StackPanel>
默认规则是EvenOdd,这用于图7-17中的左边的Polygon。这是能理解的最简单规则。为了决定一个特定的围绕区域是在形状的内部和外部,EvenOdd规则对线段的数量进行计数——你不得不跨越从一个端点到另一个完全在形状外的端点。如果这个数量是奇数的,这个点就是在形状中;反之,就是在外面。
第二个填充规则,Nonzero,是非常精细的。从图7-17中,你可以已经想到任何密闭的区域被认为是在图形中的,但是这并不是那样十分简单的。Nonzero规则表现了一个和Nonzero类似的过程,而不是简单的对线段的数量计数。考虑到线段运行位置的方向——它增长或减少的每条线段跨越的数量,依赖于这个方向。如果在末端总计为nonzero,这个点被认为是在形状内部。
在图7-17中右边的Polygon,Nonzero规则导致了所有的密闭区域作为内部的一部分。然而,如果形状的轮廓沿着一条较轻微盘旋的路径,结果可能有一点更混合,如示例7-15所示。
示例7-15
<Polygon Fill="Yellow" Stroke="Blue" FillRule="Nonzero"
Points="10,10 60,10 60,25 20,25 20,40 40,40 40,18 50,18 50,50 10,50" />
示例7-15的结果显示在图7-18中。这说明了Nonzero规则并不是像第一次看到的那样直接。
图7-18
<Rectangle Fill=”Blue” Width=”40” Height=”80”>
是有效的Path速记:
<Path Fill=”Blue”>
<Path.Data>
<RectangleGeometry Rect=”0, 0, 40, 80” />>
</Path.Data>
</Path>
在这一点,你可能想知道什么时候你要使用RectangleGeometry、EllipseGeometry或LineGeometry在Path中,来取代更简单的Rectangle、Ellipse和Line。原因是Path使你可以使用一种特殊类型的几何对象,称为GeometryGroup,来创建一个带有多个几何体的形状。
这里有一个显著的不同在使用多个明显的形状和有一个单独的带有多个几何题的形状之间。让我们看一下示例7-16。
[b]示例7-16
<Canvas>
<Ellipse Fill="Blue" Stroke="Black" Width="40" Height="80" />
<Ellipse Canvas.Left="10" Canvas.Top="10" Fill="Blue" Stroke="Black"
Width="20" Height="60" />
</Canvas>
这绘制了两个椭圆,一个在另一个的上面。他们都有一个黑色的轮廓,因此你可以看到更简单的一个在更大的另一个中,如图7-19所示。
图7-19
<Canvas>
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<EllipseGeometry Center="20, 40" RadiusX="20" RadiusY="40" />
</Path.Data>
</Path>
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<EllipseGeometry Center="20, 40" RadiusX="10" RadiusY="30" />
</Path.Data>
</Path>
</Canvas>
[/b]因为示例7-17中的代码与示例7-16中的代码是等价的,它导致了准确的相同输出,正如图7-19所示。到目前为止,使用几何体取代形状,并没有在生成的结果中制造出不同,这是因为我们仍然使用多个形状。因此,我们现在将显示如何把所有的椭圆放进一个单独的Path以及看到这是如何影响到这些结果。示例7-18显示了改动后的标记。
示例7-18
<Canvas Canvas.Left="100">
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<GeometryGroup>
<EllipseGeometry Center="20, 40" RadiusX="20" RadiusY="40" />
<EllipseGeometry Center="20, 40" RadiusX="10" RadiusY="30" />
</GeometryGroup>
</Path.Data>
</Path>
</Canvas>
这个版本正好有一个单独的路径。它的Data属性包含了一个GeometryGroup。这就允许任何数量的几何体对象被添加到同样的路径。这里我们已经添加了两个EllipseGeometry元素——先前在两个独立的路径中。结果如图7-20所示,明显不同于图7-19。在形状中间有一个洞。因为默认的奇偶填充规则在使用中,较小的椭圆在较大的椭圆中生成一个洞。
图7-20
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure>
<PathFigure.Segments>
<StartSegment Point="0,0" />
<LineSegment Point="50,0" />
<LineSegment Point="50,50" />
<LineSegment Point="0,50" />
<CloseSegment />
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
结果显示在图7-22,这看起来是做了巨大的努力为获取一个简单的结果。我们使用了17个线段标记来达到我们看到的效果——一个单独的矩形。这是为什么WPF提供一些类为这些较简单的形状和几何体。你不用严格地需要任何东西——因为你可以取代的使用Path和PathGeometry,但是它们要求更少的努力。正规的,你会为比较复杂的形状而使用Path。
图7-22
<Canvas>
<Ellipse Fill="Cyan" Stroke="Black" Width="140" Height="60" />
<Path Fill="Cyan" Stroke="Black" Canvas.Left="180">
<Path.Data>
<PathGeometry>
<PathFigure>
<StartSegment Point="0,11" />
<ArcSegment Point="50,61" Size="70,30"
SweepFlag="False" LargeArc="False" />
<CloseSegment />
</PathFigure>
<PathFigure>
<StartSegment Point="30,11" />
<ArcSegment Point="80,61" Size="70,30"
SweepFlag="True" LargeArc="True" />
<CloseSegment />
</PathFigure>
<PathFigure>
<StartSegment Point="240,1" />
<ArcSegment Point="290,51" Size="70,30"
SweepFlag="False" LargeArc="True" />
<CloseSegment />
</PathFigure>
<PathFigure>
<StartSegment Point="280,1" />
<ArcSegment Point="330,51" Size="70,30"
SweepFlag="True" LargeArc="False" />
<CloseSegment />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
</Canvas>
你可能想知道为什么Ellipse的宽度为140和高度为60,这是每个ArcSegment的Size属性值的两倍。这是因为ArcSegment解释了Size为椭圆的两个半径,而椭圆上的Width和Height属性指出了全部的大小。
图7-24显示了结果,以及,正如你能看到的,每一个形状都有一条径直的对角线以及一条椭圆的弧形。在所有的四种情形中,直线的边缘具有相同的长度和方向,圆弧的边缘来自同一个椭圆的不同部分。
在图7-24中,椭圆的轴是垂直和水平的。有时你可能想要使用一个椭圆,而轴并没有对齐到你的主轴。ArcSegment提供了Xrotation属性,允许你详细指出需要旋转的度数。
图7-24
<StartSegment Point="0,50" />
<BezierSegment Point1="60,50" Point2="100,0" Point3="100,50" />
虽然贝赛尔曲线是弹性的,你很少使用到一个如此简单的曲线。当定义带有弯曲边缘的形状时,对于一个形状而言这是普通的——使用贝赛尔曲线定义它的边缘。WPF因此提供了一个PolyBezierSegment属性,这是一个Point结构的数组。每个贝赛尔曲线三个实体在这个数组中:两个控制点和一个终结点。(通常的,每条曲线开始于前一条曲线终结的位置。)示例7-22显示了一个带有两个曲线的片断。图7-29显示了结果。
示例7-22
<StartSegment Point="0,0" />
<PolyBezierSegment>
<PolyBezierSegment.Points>
<Point X="0" Y="10"/>
<Point X="20" Y="10"/>
<Point X="40" Y="10"/>
<Point X="60" Y="10"/>
<Point X="120" Y="15"/>
<Point X="100" Y="50"/>
</PolyBezierSegment.Points>
</PolyBezierSegment>
图7-29
<PolyBezierSegment Points="0,10 20,10 40,10 60,10 120,15 100,50" />
同样,如果你从代码中生成坐标,处理一个单独的PolyBezierSegment和传递给它一个Point数据的数组,这比处理一些独立的片断要容易的多。
贝赛尔曲线在曲线的形状上提供了许多控件。然而,你可能不总是想要弹性的等级。QuadraticBezierSegment使用了一个较简单的等式——仅使用了一个指向定义了曲线形状的控件。这没有提供同样范围的曲线形状,作为一个立方体的贝赛尔曲线,但是如果所有你想要的只是一个简单的形状,这就减少了三分之一你需要提供的坐标对数量。
QuadraticBezierSegment使用上与正常的BezierSegment类似。唯一的不同是他每有一个Point3属性,只有Point1和Point2。Point1是共享控制点,Point2是终结点。QuadraticBezierSegment是一个多曲线的装备。你以与PolyBezierSegment相同的方式使用它,除了你只需要为每个片断提供两个点。
7.2.7.2结合形状
Path自有它的玄机——我们至今没有检查到的。它有联合若干几何体以形成一个新几何体的能力。这是不同于添加到两个几何体到一个Path中,它联合了成对的几何体在某种程度上形成了一个单独的具有完整形状的新几何体。
示例7-23和示例7-24各自定义了路径,它们全都使用了同样的RectangleGeometry和EllipseGeometry。区别是示例7-23把它们都放入了一个GeometryGroup中,而示例7-24把它们都放入了一个CombinedGeometry中
示例7-23
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<GeometryGroup>
<RectangleGeometry Rect="0,0,50,50" />
<EllipseGeometry Center="50,25" RadiusX="30" RadiusY="10" />
</GeometryGroup>
</Path.Data>
</Path>
示例7-24
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<CombinedGeometry CombineMode="Exclude">
<CombinedGeometry.Geometry1>
<RectangleGeometry Rect="0,0,50,50" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center="50,25" RadiusX="30" RadiusY="10" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
图7-30显示了示例7-23和示例7-24的结果。CombinedGeometry导致了带有多个图形的一个形状,CombinedGeometry生成了一个单独的图形。椭圆几何体在矩形几何体咬进去一口。这只是联合若干几何体方式中的一种。CombineMode属性决定了使用哪一个,图7-31显示了所有的五种可利用的类型。
图7-30
<!-- Longhand -->
<Path Fill="Cyan" Stroke="Black">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure>
<PathFigure.Segments>
<StartSegment Point="0,0" />
<LineSegment Point="50,0" />
<LineSegment Point="50,50" />
<LineSegment Point="0,50" />
<CloseSegment />
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
<!-- Shorthand -->
<Path Fill="Cyan" Stroke="Black" Data="M 0,0 L 50,0 50,50 0,50 Z" />
文本形式的Path.Data属性的语法是简单的。这个字符串包含一个命令序列。命令是一个依照某些数字化参数的字母。需要的参数数量由被选择的命令决定。线条仅需要一对坐标。曲线则需要更多的数据。
如果你忽略了这个字母,同样的命令将会被作为上一次使用。例如,示例7-25使用了L命令,这是“Line”的简写,代表了LineSegment。这只要求2个数字,线段终结点的坐标。以及在我们的示例中,这里有6个数字。这简单的指明了每行有3条线。表7-3列出了这些命令,它们的等价片断类型,以及它们的用法。
Command | Command name | Segment type | Parameters |
---|---|---|---|
M (or m) | Move | StartSegment | Coordinate pair: start point |
L (or l) | Line | LineSegment | Coordinate pair: end point |
H (or h) | Horizontal Line | LineSegment | Single coordinate: end x-coordinate (y-coordinate will be the same as before) |
V (or v) | Vertical Line | LineSegment | Single coordinate: end y-oordinate (x-coordinate will be the same as before) |
C (or c) | Cubic Bézier Curve | BezierSegment | Three coordinate pairs: two control points and end point |
Q (or q) | Quadratic Bézier Curve | QuadraticBezierSegment | Two coordinate pairs: control point and end point |
S (or s) | Smooth Bézier Curve | BezierSegment | Two coordinates: second control point and end point (first control point generated automatically) |
A (or a) | Elliptical Arc | ArcSegment | Seven numbers: Size pair, Rotation, LargeArc, SweepFlag, end point coordinate pair |
Z (or z) | Close path | CloseSegment | None |
注意到有两种方式详细指明一个BezierSegment。C命令让你提供所有的控制点。S命令为你的外观在第一个片断生成第一个控制点,以及生成指向前一个片断镜像的第一个控制点。这就确保了片断的切线与前一个片断对齐,导致了以一种平滑的方式连接线段。
所有这些命令都可以两种方式使用。你可以详细指出命令以它的大写或小写形式。以大写的形式,坐标是相于Path元素的位置。如果命令是小写的,坐标是相对于路径中前一个片断的终结点。
现在我们已经检查到当前提供的形状,但是到目前位置,我们已经相当没有危险的在我们的为这些形状填充和轮廓的选择中。我们已经使用命名的颜色和简单的轮廓样式。WPF允许我们通过它的笔刷和钢笔类,使用更多不同种类的绘制样式。
相关文章推荐
- 《Programming WPF》翻译 第7章 1.图形基础
- java.lang.UnsupportedClassVersionError: Bad version number in .class file异常的处理方法
- PLSQL流程控制
- Redis内存使用优化与存储
- Linear Filtering and Prediction
- 《Programming WPF》翻译 第6章 5.我们进行到哪里了?
- php 处理上百万条的数据库如何提高处理查询速度
- 如何使用SAS Switch 技术 (第一部分 基本部署)
- centos中部署java项目
- 卡尔曼滤波 -- 从推导到应用(一) 到 (二)
- shuxue
- jQuery select显示部分选项
- 《Programming WPF》翻译 第6章 4.应用程序全球化
- angular.js学习笔记--概念总结
- 结构体,枚举类型
- LightOJ 1038 - Race to 1 Again (期望dp)
- iOS开发语言之OC 初级内存管理
- Eclipse安装SVN插件
- NSURLSession
- DHCP