《Programming WPF》翻译 第7章 2.图形

图形时绘图的基础,代表用户界面树的元素。WPF支持多种不同的形状,并为它们每一个都提供了元素类型。

7.2.1基本图形类

在这一节列出的所有元素,派生于一个共同的抽象基类Shape。虽然你不能直接使用这个类,知道它还是有帮助的,因为它定义了一组共同的特性——你可以在任何形状上使用。这些共同的属性都被连接到形状的内部和外部被绘制的地方。

Fill属性详细指出了Brush要用于填充内部。LinePolyline这些类没有内部,所以它们没有Fill属性。(这比通过有独立的ShapeFilledShape基类使继承层次复杂化的发式要简单的多)Stroke属性详细指出了用来画形状轮廓的Brush

如果为你的形状没有详细指出它的FillStroke属性,这将是不可见的,因为这两种属性默认都是透明的。

这看起来特殊——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

Shape property

Pen equivalent

Stroke

Brush

StrokeThickness

Thickness

StrokeLineJoin

LineJoin

StrokeMiterLimit

MiterLimit

StrokeDashArray

DashArray

StrokeDashCap

DashCap

StrokeDashOffset

DashOffset

StrokeStartLineCap

StartLineCap

StrokeEndLineCap

EndLineCap


笔刷和钢笔都详细描述在“Brushes and Pens”一节,在本章的后面。

7.2.2矩形

Rectangle实现了它的名称所示的。无论任何形状,它可以被填充来绘制,作为一个边框。或者全部。不但绘制一个正常的矩形,它还能画一个圆角矩形。

Rectangle不提供任何属性用于设置它的大小和位置。它依赖于同样的外观机制如其它UI元素。位置由面板容器决定。长和宽都有它的父一级自动设置,或者使用标准外观属性来显示地设置WidthHeight

示例7-6显示了Canvas面板上的一个Rectangle。这里,WidthHeight都被显示地设置,以及外观被详细的指定——通过使用附属的Canvas.LeftCanvas.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

示例7-7

 

<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所示。

示例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


为了绘制一个圆角矩形,使用
RadiusXRadiusY属性,正如示例7-9所示。表7-12显示了结果。

示例7-9

<Rectangle Width="100" Height="50" Fill="Black" RadiusX="30" RadiusY="40" />

图7-12



7.2.3椭圆

Ellipse类似于Rectangle。明显地,它绘制了一个椭圆而不是一个矩形,但是Ellipse的大小、位置、填充和边框,都以与Rectangle同样的方式控制,正如示例7-10显示。结果显示在7-13

示例7-10

<Ellipse Width="100" Height="50" Fill="Yellow" Stroke="Black" />

图7-13



7.2.4 线条

Line元素绘制了一条直线,从一点到另一点。它有四个属性来控制起点和终点的位置:X1Y1X2Y2。这些坐标是相对于父面板选择放置Line的位置。考虑示例7-11

示例7-11

<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来排列TextBlockLine元素的交错序列。TextBlock元素有灰色的背景使之易于看到每个元素垂直的区域。结果如图7-14所示。

7-14



正如你在图
7-14看到的,Line元素像其它元素一样被放置在栈中。StackPanel被分配充分的高度以容纳Line。第一条Line是有趣的——在这条线上的TextBlock底部和该线段的起点间的一些空白。这是因为这条线段的Y1属性已经被设置为10,表明这条线段应该开始于放置Line元素位置的顶部的下面一点。第二个Line元素与上一条线段完全一致,因为它的Y2属性被设置为0,再次说明了这条线段终点的坐标系统是面板容器相对于放置Line的区域的。

这里没有办法自动设置线段的终点作为外观的一部分(你能依赖外观系统来定位EllipseRectangle元素,唯一的原因是它们的尺寸由矩形来决定,以及外观系统本质上处理了矩形的排列。)例如,你不能通知Line确切的宽度和父一级面板放置的位置一样。如果你想这么做,只需取代地使用Rectangle,你可以通过创建一个细矩形来绘制一条线段,如果需要就旋转它。或者你可以使用一个DrawingBrushVisualBrush,当这些可以自动伸缩图形来填充可利用的空间。

7.2.5 折线

Polyline使你绘制一系列连接的线段。取代现有的开始点和终结点的属性,Polyline有一个Points属性,包含了一个坐标对的清单,正如示例7-12所示。WPF简单地绘制了一条线段,按顺序通过每一个点,正如图7-15所示。

示例7-12

<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

由于使用Line类,Polyline中点的坐标是相对于面板容器选择来放置Polyline的位置。

7.2.6多边形

Polygon非常相似于Polyline。它有一个Points属性,工作方式与Polyline相同。唯一的区别是Polyline一直绘制一个敞开的形状,而Polygon则总是绘制一个封闭的形状。为了说明这个不同,示例7-13包含了一个Polyline和一个Polygon。所有同样的属性都设置为一样的。

示例7-13

<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



因为我们可以自由的添加点到
Polygon中的任何地方,这就易于以一个自交的形状而告终,它的边穿越了它自身。就是这样一个形状,这可能使含糊的关于有多少数量作为形状的内部。图7-17显示了这样一个形状以及两个可能的填充方式。

7-17



Polygon类提供了一个FillRule属性来选择一种潜在的处理任意区域的方式。(在一些绘图系统,这被描述为拓扑规则。)WPF支持两种填充规则。示例7-14是图7-17的标记,显示了使用中的填充规则。

示例7-14

<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中右边的PolygonNonzero规则导致了所有的密闭区域作为内部的一部分。然而,如果形状的轮廓沿着一条较轻微盘旋的路径,结果可能有一点更混合,如示例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

Nonzero规则有点怪。它是由PostScript普及的,因此大多数绘图系统都支持它,但是这并不总是容易的从一个带有填充规则的Polygon中获取有用的结果。在Path元素的上下文中更加有意义——支持多重配置在一个单独的形状中。

7.2.7 路径

Path是目前为止最强大的形状。所有的形状——到目前为止我们已经看到的——已经被便利地提供,因为使用Path绘制所有这些形状是可能的。Path还使绘制相当复杂的形状成为可能——比我们之前看到的形状更复杂。

就像PolygonPath有一个FillRule属性来控制填充规则。代替Points属性,Path有一个Data属性,它的类型是Geometry,这是一个抽象的基类。一个Geometry对象表示一个特定的形状。这里有大量的具体类来表示不同类型的形状。其中三种听起来相当熟悉:RectangleGeometryEllipseGeometryLineGeometry。这些表示了我们先前见到的相同形状。因此这个Rectangle

<Rectangle Fill=”Blue” Width=”40” Height=”80”>

是有效的Path速记:

<Path Fill=”Blue”>
         
<Path.Data>
             
<RectangleGeometry Rect=”0, 0, 40, 80” />>
         
</Path.Data>
</Path>

在这一点,你可能想知道什么时候你要使用RectangleGeometryEllipseGeometryLineGeometryPath中,来取代更简单的RectangleEllipseLine。原因是Path使你可以使用一种特殊类型的几何对象,称为GeometryGroup,来创建一个带有多个几何体的形状。

这里有一个显著的不同在使用多个明显的形状和有一个单独的带有多个几何题的形状之间。让我们看一下示例7-16

示例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


既然
Ellipse形状只是创建一个EllipseGeometry的简单方式,示例7-16中的代码与示例7-17中的代码是等价的。(正如你能看到的,使用Path是相当地更加冗长。这是为什么要提供Ellipse和其它简单的形状。)

示例7-17

<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>

因为示例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


带洞的形状可以仅通过结合多个图形到一个单独的形状中来创建。你可以试着通过绘制一个内部的填充为白色的
Ellipse,来得到类似的效果,如图7-20所示,但是这个“骗局”并不能工作——当你绘制一个形状在其他事物之上的时候,如图7-21所示。

7-21



你可能想知道如果使用可以用transparent透明色绘制内部的椭圆,但是这也不能运行。绘图时使用全部的透明色等效于,根本绘制不出任何东西。这就是透明的含义。只有在形状中钉进一个洞,我们才能识破它。
为了理解为什么,想一想绘图的过程。当它生成我们的元素到屏幕时,WPF绘制这些元素一个接着一个。在这种情形中,它以文字开始——而无论什么在后面,接着它绘制了文字之上的形状——这将有效的删除形状后面的文字(当然它仍然在元素树中存在,因此WPF可以总是在以后重绘它,一旦你改变或者移除了图形。)由于你仅在文字上绘制,你不能绘制另一个形状在其上来“恢复绘制”一个洞到第一个形状。因此如果你想要一个洞在形状上,在你绘制这个形状之前,你最好确定这个洞在那里。

我们还没有看到最有弹性的几何体:PathGeometry。它可以绘制任意PolygonPolyline能够表示的形状,除此之外,还可以绘制更多的形状。

PolyGeometry包含了一个或更多的PathFigure对象,以及每一个PathFigure代表了一个单独的开放或密闭的形状在路径中。为了定义每个图形的轮廓形状,你使用一个PathSegment对象的序列。 

PathGeometry包含多个图形的能力有点交叠于Path包含多个几何体的能力。这恰恰是为了便利,如果你需要生成一个每一部分都是一个PathGeometry对象的形状,这对一个单独的带有多个PathFiguresPathGeometry有更多的影响。如果你只是想使用更简单的几何体,如LineGeometryRectangleGeometry,比较简单的办法是使用GeometryGroup以及同时要避免使用PathGemeetry

示例7-19显示了一个简单的路径。它仅包含了一个单独的图形和绘制了一个矩形。

示例7-19

<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提供一些类为这些较简单的形状和几何体。你不用严格地需要任何东西——因为你可以取代的使用PathPathGeometry,但是它们要求更少的努力。正规的,你会为比较复杂的形状而使用Path

7-22



即使示例
7-19生成了一个非常简单的结果,它说明了带有PathGeometry属性的Path大多重要的样式。对比之前的所有示例,几何体在路径的Data属性中。PathGeometry是一个PathFigures的集合,因此所有有趣的数据都在他的Figures属性中。这个示例仅包含了PathFigure,但是你可以添加任意多你想要的。PathFigure的形状由它的Segments属性中的项决定。

这些片断的序列总是以StartSegment开始,这个标记决定了路径的起始点。接下来,它会沿着一个或多个决定图形的形状的分割而行。在示例7-19中,这些全都是LineSeqment,因为图形只有直边,但是也提供一些圆弧的类型。最后,这个示例以CloseSegment终止,指明这是一个密闭的形状。如果我们想创建一个开放的形状(如Polyline),我们可以简单的忽略最后的CloseSegment

你可能想知道为什么LineSegment不能像Line一样工作。对于Line,我们可以指出它的起点和终点,是示例7-11所示。这看起来比LineSegment简单,后者需要我们以StartSegment开始。
尽管如此,PathFigure中的线段不能工作,在图形的轮廓没有任何间隙。使用Line元素,每一个Line都是截然不同的独自形状,而使用PathFigure,每个片断都是形状轮廓的一部分。为了充分明确地定义一个图形,每个片断必须开始于先前结束的那个片断。这是为什么LineSegment只要为线段详细指明终止点。所有的片断类型以它们的方式工作。当然,图形需要开始于某处,这是为什么我们总是开始于一个StartSegment。这是不合理的使用StartSegment在其它任何地方而不是在图形的起点。

示例7-19并不是非常令人激动的。它只是使用了直线片断。替代的,我们可以创建更多有趣的形状,通过使用弧形片断类型中的一种。表7-2显示了所有的片断类型。

Table 7-2. Segment types

Segment type

Usage

StartSegment

Sets the starting point of the first segment of the figure.

CloseSegment

Indicates that this is a closed figure; where used, this must be the last segment.

LineSegment

Single straight line.

PolyLineSegment

Sequence of straight lines.

ArcSegment

Elliptical arc.

BezierSegment

Cubic Bézier curve.

QuadraticBezierSegment

Quadratic Bézier curve.

PolyBezierSegment

Sequence of cubic Bézier curves.

PolyQuadraticBezierSegment

Sequence of quadratic Bézier curves.



ArcSegment使你可以添加椭圆圆弧到形状的边缘。ArcSegment的使用比一个简单的LineSegment有一点更复杂。不仅要指出片段的终止点,我们必须还要通过Size属性指出椭圆的两个半径。

椭圆大小和线段的起始点终结点并未通过足够的信息来明确地定义圆弧,因为有很多种方式来在给定地约束下绘制一个椭圆形的圆弧。考虑一个带有特定起点和终点地片断,以及一个给定的大小和定位的椭圆。

为了这种片断,这里将通常有两种方式用来定位椭圆,从而它的起点和终点都在椭圆的边界上,正如图7-23所示。换句话说,有两种用一条特定的线段“切割”一个椭圆的方法。

7-23


作为两种分割椭圆式的任一种,就会有两种分割结果,一个小的和一个大的。这意味着有四种方式可以在两点间绘制一条弧线。

ArcSegment提供了两种标记,支持你选择你需要那种弧线。LargeArc决定你是选取较小的还是较大的那个分割片大小。SweepFlag选择在线段的哪一边绘制的分割。示例7-20显示了标记语法,说明所有的四种带有这些标记的组合。它同样显示了整个的椭圆。

示例7-20

 

<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,这是每个ArcSegmentSize属性值的两倍。这是因为ArcSegment解释了Size为椭圆的两个半径,而椭圆上的WidthHeight属性指出了全部的大小。

7-24显示了结果,以及,正如你能看到的,每一个形状都有一条径直的对角线以及一条椭圆的弧形。在所有的四种情形中,直线的边缘具有相同的长度和方向,圆弧的边缘来自同一个椭圆的不同部分。

在图7-24中,椭圆的轴是垂直和水平的。有时你可能想要使用一个椭圆,而轴并没有对齐到你的主轴。ArcSegment提供了Xrotation属性,允许你详细指出需要旋转的度数。

7-24


7-25显示了3中椭圆轴。它们使用了和图7-24相同的起点和终点以及相同的椭圆大小。唯一的不同是详细指出了XRocation45度,在分割前对其进行了旋转。

7-25


切割椭圆的方式不只有两种,这里有两种退化的情形。第一种是将椭圆从中间切为相同的两半。在这种情形种
LargeArc标记就是不相关的了,因为两个片断是相同的大小。

另一种情形是当椭圆非常的小,如果在椭圆可以被切割的最宽的点比片断的窄,这是没有办法正确绘制这个片段的。你应该试着避免这样。(如果你确实使椭圆非常小,WPF看起来按比例增加了椭圆使之足够大,在X轴和Y轴按比例保持它的样子。)

其余的四种圆弧类型来自表7-2BezierSegmentPolyBezierSegmentQuadraticBezierSegmentPolyQuadraticBezierSegment是同一个主题的四种变体。它们都绘制了贝赛尔曲线。

7.2.7.1贝赛尔曲线

贝赛尔曲线是基于一个特定的数学公式的连接两点的曲线片断。详细理解这个公式对于使用贝赛尔曲线是不必要的。它有用的地方在于为曲线图提供了相当程度的弹性。这使得它非常流行,大多数向量绘图程序都提供贝赛尔曲线。图7-26显示了各种贝赛尔曲线的片断。

7-26


7-26中显示的五条线中的每一条都是一个简单的BezierSegment。贝赛尔曲线在图形系统得到了非常广的使用——因为形状中广泛的变体,即使一个单独的片断也能够提供。它们还可以相当直接的使用。

作为所有片断类型,BezierSegment开始于上一个片断停止以及定义一个新的终结点的位置。它还需要两个控制点,来决定曲线的形状。图7-27再次显示了相同的曲线,但是曲线上带有控制点。它还显示了连接控制点到终止点片断的线段,因为这使得更加容易看到:控制点如何影响曲线形状。

7-27


控制点影响曲线形状的最明显方式是它们决定切线。在每个片断的起始点和终止点,曲线在那个点上运行的方向与连接起始点到相应的控制点的线段的方向是一样的。

这里还有一个次要的不太明显的控制点工作的方式。在开始点(或结束点)和它相应的控制点间的距离也会产生影响。这就从本质上决定了曲率的极端程度。

7-28显示了一组贝赛尔曲线类似于图7-27。这些结束与直线的切线是保持同样的,但是,在每种情形中,开始点和第一个控制点间的距离被减少到原先的四分之一,尔另一个和之前是一样的。正如你看道德,这减少了第一个控制点的影响,曲线的形状由远离终点的控制点支配。

7-28


示例
7-21显示了图7-27中第二个曲线片断的标记,Point1属性决定了第一个控制点的位置,这个控制点联合了起始点。Point2定位了第二个控制点。Point3是终结点。

示例7-21

<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


这个标记有点不太便利,相较于使用一个
BezierSegment元素序列。幸运的是,你可以以字符串的形式提供所有点数据。这等价于示例7-22:

<PolyBezierSegment Points="0,10 20,10 40,10 60,10 120,15 100,50" />

同样,如果你从代码中生成坐标,处理一个单独的PolyBezierSegment和传递给它一个Point数据的数组,这比处理一些独立的片断要容易的多。

贝赛尔曲线在曲线的形状上提供了许多控件。然而,你可能不总是想要弹性的等级。QuadraticBezierSegment使用了一个较简单的等式——仅使用了一个指向定义了曲线形状的控件。这没有提供同样范围的曲线形状,作为一个立方体的贝赛尔曲线,但是如果所有你想要的只是一个简单的形状,这就减少了三分之一你需要提供的坐标对数量。

QuadraticBezierSegment使用上与正常的BezierSegment类似。唯一的不同是他每有一个Point3属性,只有Point1Point2Point1是共享控制点,Point2是终结点。QuadraticBezierSegment是一个多曲线的装备。你以与PolyBezierSegment相同的方式使用它,除了你只需要为每个片断提供两个点。

7.2.7.2结合形状

Path自有它的玄机——我们至今没有检查到的。它有联合若干几何体以形成一个新几何体的能力。这是不同于添加到两个几何体到一个Path中,它联合了成对的几何体在某种程度上形成了一个单独的具有完整形状的新几何体。

示例7-23和示例7-24各自定义了路径,它们全都使用了同样的RectangleGeometryEllipseGeometry。区别是示例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


7-31


联合模式有以下的效果:

Union

            建立了一个形状,任意的在原始的两个形状中任一个的点,都在这个新的形状中。

Intersect

            建立了一个形状,任意的同时在原始的两个形状中的点,都在这个新的形状中。

Xor

            建立了一个形状,任意的只在原始的两个形状中其中一个的点,都在这个新的形状中。

Execlude

        建立了一个形状,任意的只在第一个而不在第二个形状中的点,都在这个新的形状中。

Complement

            建立了一个形状,任意的只在第二个而不在第一个形状中的点,都在这个新的形状中。

7.2.7.3数据属性文本格式

我们已经看到Path提供的所有的样式。正如你看到的,我们可以终结某些相当冗长的标记。幸运的是,Path元素提供了一个速记机制——允许大多数我们看到过的样式可以被开发而不用敲击太多代码。

到目前位置,我们已经设置了Data属性,使用了xaml的属性元素语法(见附录A获取更多语法的细节)。然而,我们可以替代的提供一个字符串。示例7-25显示了这两种技术。正如你看到的,使用字符串的形式只有16行这么短。

示例7-25

<!-- 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列出了这些命令,它们的等价片断类型,以及它们的用法。

Table 7-3. Path.Data commands

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


M命令有特殊的待遇。在任何地方使用StartSegment而不是第一个PathFigure片断是合理的。如果你使用M命令在区域中间,这就意味着你将要开始一个新的PathFigure。这支持多个图形在这个复杂文本格式中表示。

注意到有两种方式详细指明一个BezierSegmentC命令让你提供所有的控制点。S命令为你的外观在第一个片断生成第一个控制点,以及生成指向前一个片断镜像的第一个控制点。这就确保了片断的切线与前一个片断对齐,导致了以一种平滑的方式连接线段。

所有这些命令都可以两种方式使用。你可以详细指出命令以它的大写或小写形式。以大写的形式,坐标是相于Path元素的位置。如果命令是小写的,坐标是相对于路径中前一个片断的终结点。

现在我们已经检查到当前提供的形状,但是到目前位置,我们已经相当没有危险的在我们的为这些形状填充和轮廓的选择中。我们已经使用命名的颜色和简单的轮廓样式。WPF允许我们通过它的笔刷和钢笔类,使用更多不同种类的绘制样式。

posted @ 2008-04-04 00:53  包建强  Views(1528)  Comments(3Edit  收藏  举报