7.2. Shapes
Shapes are drawing primitives, represented as elements in the
user interface tree. WPF supports a variety of different shapes and provides
element types for each of them.
7.2.1. Base Shape Class
All of the elements listed in this section derive from a common
abstract base class, Shape. Although you cannot use this type
directly, it is useful to know about it, because it defines a common set of
features that you can use on all shapes. These common properties are all
concerned with the way in which the interior and outline of the shape are
painted.
The Fill property specifies the Brush that
will be used to paint the interior. The Line and Polyline classes
don't have interiors, so they will ignore this property. (This was simpler than
complicating the inheritance hierarchy by having separate Shape and FilledShape
base classes.) The Stroke property specifies the Brush that
will be used to paint the outline of the shape.
 |
If you do not specify either a Fill or a Stroke
for your shape, it will be invisible, because both of these properties are
transparent by default.
|
|
It may seem peculiar that the Stroke property is of
type Brush. As we saw earlier, WPF defines a Pen class for
specifying a line's thickness, dash patterns, and the like, so it would make
more sense if the Stroke property were of type Pen. WPF does
in fact use a Pen internally to draw the outline of a shape. The Stroke
property is of type Brush mainly for convenienceall of the Pen
features are exposed through separate properties on Shape, as shown in
Table 7-1. This simplifies the markup in scenarios where you're happy
to use the default pen settingsyou don't need to provide a full Pen definition
just to set the outline color.
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 are described in detail in the "Brushes
and Pens" section, later in this chapter.
7.2.2. Rectangle
Rectangle does what its name suggests. As with any
shape, it can be drawn either filled in, as an outline, or both. As well as
drawing a normal rectangle, it can also draw one with rounded corners.
Rectangle doesn't provide any properties for setting
its size and location. It relies on the same layout mechanism as any other UI
element. The location is determined by the containing panel. The width and
height can either be set automatically by the parent, or they can be set
explicitly using the standard layout properties, Width and Height.
Example 7-6 shows
a Rectangle on a Canvas panel. Here, the Width and Height
have been set explicitly, and the location has been specified using the
attached Canvas.Left and Canvas.Top properties.
Example 7-6. Rectangle with explicit size and position
<Canvas>
<Rectangle Fill="Yellow" Stroke="Black"
Canvas.Left="30" Canvas.Top="10"
Width="100" Height="20" />
</Canvas>
Example 7-7 shows
the other approach. None of the rectangles have had their location or size set
explicitly. They rely on the containing Grid, instead.
Figure 7-10 shows the result.
Example 7-7. Rectangles with size and position
controlled by parent
<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>
A Rectangle will usually be aligned with the coordinate
system of its parent panel. This means that its edges will usually be
horizontal and vertical, although if the parent panel has been rotated, the Rectangle
will of course be rotated along with it. If you want to rotate a Rectangle
relative to its containing panel, you can use the RenderTransform property
available on all user-interface elements, as demonstrated in
Example 7-8, which illustrates the use of RenderTransform to
rotate a series of rectangles. The result is shown in
Figure 7-11.
Example 7-8. Rotating rectangles
<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>
To draw a rectangle with rounded corners, use the RadiusX
and RadiusY properties, as shown in
Example 7-9. Figure 7-12
shows the result.
Example 7-9. Rounded rectangle
<Rectangle Width="100" Height="50" Fill="Black" RadiusX="30" RadiusY="40" />
7.2.3. Ellipse
Ellipse is similar to Rectangle.
Obviously, it draws an ellipse rather than a rectangle, but the size, location,
rotation, fill, and stroke of an Ellipse are controlled in exactly the
same way as for a Rectangle, as
Example 7-10 shows. The result is shown in
Figure 7-13.
Example 7-10. Ellipse
<Ellipse Width="100" Height="50" Fill="Yellow" Stroke="Black" />
7.2.4. Line
The Line element draws a straight line from one point
to another. It has four properties controlling the location of the start and
end points: X1, Y1, X2, and Y2. These
coordinates are relative to wherever the parent panel chooses to locate the Line.
Consider Example 7-11.
Example 7-11. Two Lines in a StackPanel
<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>
Example 7-11 uses
a vertical StackPanel to arrange an alternating sequence of TextBlock
and Line elements. The TextBlock elements have gray
backgrounds to make it easier to see the vertical extent of each element. The
results are shown in Figure 7-14.
As you can see from Figure
7-14, the Line elements have been placed in the stack just
like any other element. The StackPanel has allocated enough height to
hold the line. The first of the lines is interesting in that there is some
space between the bottom of the TextBlock above it and the start of
the line. This is because the line's Y1 property has been set to 10,
indicating that the line should start slightly below the top of the location
allocated for Line element. The second Line element goes all
the way to the top because its Y2 property is set to 0, again
illustrating that the coordinate system of the line endpoints is relative to
the area allocated to the Line by the containing panel.
There is no way to set the line endpoints automatically as part
of your layout. (The only reason you can rely on the layout system to position Ellipse
and Rectangle elements is because their basic dimensions are
determined by rectangles and the layout system essentially deals with
rectangular arrangement.) For example, you cannot tell the Line to be
exactly as wide as the space allocated by the parent panel. If you want to do
this, just use the Rectangle insteadyou can draw a line by creating a
thin rectangle, rotating it if necessary. Or you could use a DrawingBrush
or VisualBrush, as these can automatically scale drawings to fill the
space available.
7.2.5. Polyline
A Polyline lets you draw a connected series of line
segments. Instead of having properties for start and end points, Polyline
has a Points property, containing a list of coordinate pairs, as shown
in Example 7-12. WPF simply
draws a line that goes through each of the points in turn, as shown in
Figure 7-15.
Example 7-12. Polyline
<Polyline Stroke="Blue"
Points="0,30 10,30 15,0 18,60 23,30 35,30 40,0 43,60 48,30 160,30" />
As with the Line class, the point coordinates in a Polyline
are relative to wherever the containing panel chooses to locate the Polyline.
7.2.6. Polygon
Polygon is very similar to Polyline. It has a Points
property that works in exactly the same way as Polyline's. The only
difference is that while Polyline always draws an open shape, Polygon
always draws a closed shape. To illustrate the difference,
Example 7-13 contains a Polyline and a Polygon. All
the same properties are set for both.
Example 7-13. A Polyline and a Polygon
<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>
As you can see in Figure
7-16, the Polyline has ignored the Fill property. The
shape does not have an interiorit is left open. The Polygon, on the
other hand, has closed the shape by drawing an extra line segment between the
last and first points, and it has painted the interior of the shape.
Because we are free to add points wherever we like to a Polygon,
it is easy to end up with a self-intersecting shape, one whose edge crosses
itself. With such shapes, there can be ambiguity about what counts as the
interior of the shape. Figure 7-17
shows such a shape and two possible ways of filling it.
The Polygon class provides a FillRule property
to choose the way in which such potentially ambiguous regions are dealt with.
(In some graphics systems, this is described as the winding rule.) WPF
supports two fill rules. Example 7-14
is the markup for Figure 7-17
and shows both fill rules in use.
Example 7-14. Fill rules
<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>
The default rule is EvenOdd, and this is used for the Polygon
on the left of Figure 7-17.
This is the simplest rule to understand. To determine whether a particular
enclosed region is inside or outside the shape, the EvenOdd rule
counts the number of lines you have to cross to get from that point to one
completely outside of the shape. If this number is odd, the point is inside the
shape. If it is even, the point is outside the shape.
The second fill rule, Nonzero, is more subtle. From
Figure 7-17, you might have thought that any enclosed area was deemed
to be inside the shape, but it's not quite that simple. The Nonzero rule
performs a similar process to EvenOdd, but rather than simply counting
the number of lines, it takes into account the direction in which the line is
running. It either increments or decrements the count for each line it crosses,
depending on the direction. If the sum total at the end is nonzero, the point
is considered to be inside the shape.
In the Polygon on the right in
Figure 7-17, the Nonzero rule has resulted in all enclosed
regions being part of the interior. However, if the outline of the shape
follows a slightly more convoluted path, the results can be a little more
mixed, as Example 7-15 shows.
Example 7-15. Nonzero fill rule with more complex shape
<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" />
The results of Example
7-15 are shown in Figure
7-18. This illustrates that the Nonzero rule is not quite as
straightforward as it may have first seemed.
The Nonzero rule is a bit of an oddity. It was
popularized by PostScript, so most drawing systems support it, but it's not
always easy to get useful results from a Polygon with this fill rule.
It makes more sense in the context of the Path element, which supports
multiple figures in a single shape.
7.2.7. Path
Path is by far the most powerful shape. All of the
shapes we have looked at up to now have been supplied for convenience, because
it is possible to draw all of them with a Path. Path also
makes it possible to draw considerably more complex shapes than is possible
with the previous shapes we have seen.
Like Polygon, Path has a FillRule property
to control the fill rule. In place of the Points property, Path
has a Data property. Its type is Geometry, which is an
abstract base class. A Geometry object represents a particular shape.
There are various concrete classes for representing different kinds of shapes.
Three of these will sound rather familiar: RectangleGeometry, EllipseGeometry,
and LineGeometry. These represent the same shapes that we saw earlier.
So this Rectangle:
<Rectangle Fill="Blue" Width="40" Height="80" />
is effectively shorthand for this Path:
<Path Fill="Blue">
<Path.Data>
<RectangleGeometry Rect="0, 0, 40, 80" />
</Path.Data>
</Path>
At this point, you might be wondering when you would ever use RectangleGeometry,
EllipseGeometry, or LineGeometry in a Path instead
of the simpler Rectangle, Ellipse
, and Line. The reason is that Path lets you use a special
kind of geometry object called a GeometryGroup to create a shape with
multiple geometries.
There is a significant difference between using multiple
distinct shapes and having a single shape with
multiple geometries. Let's look at an example (Example
7-16).
Example 7-16. Two Ellipses
<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>
This draws two ellipses, one on top of the other. They both have
a black outline, so you can see the smaller one inside the larger one, as
Figure 7-19 shows.
Since the Ellipse shape is just a simple way of
creating an EllipseGeometry, the code in
Example 7-16 is equivalent to the code in
Example 7-17. (As you can see, using a Path is considerably
more verbose. This is why the Ellipse and other simple shapes are
provided.)
Example 7-17. Two Paths with EllipseGeometries
<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>
Because the code in Example
7-17 is equivalent to that in
Example 7-16, it results in exactly the same output, as shown in
Figure 7-19. So far, using geometries instead of shapes hasn't made a
difference in the rendered results. This is because we are still using multiple
shapes. So, we will now show how both ellipses can be put into a single Path
and see how this affects the results.
Example 7-18 shows the modified markup.
Example 7-18. One Path with two EllipseGeometries
<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>
This version has just a single path. Its Data property
contains a GeometryGroup. This allows any number of geometry objects
to be added to the same path. Here we have added the two EllipseGeometry
elements that were previously in two separate paths. The result, shown in
Figure 7-20, is clearly different from the one in
Figure 7-19there is now a hole in the middle of the shape. Because the
default even-odd fill rule was in play, the smaller ellipse makes a hole in the
larger one.
Shapes with holes can be created
only by combining multiple figures into a single shape. You could try to get a
similar effect to that shown in Figure
7-20 by drawing the inner Ellipse with a Fill color
of White, but that trick fails to work as soon as you draw the shape
on top of something else, as Figure
7-21 shows.
 |
You might be wondering if you could just draw the
inner ellipse using the transparent color, but that doesn't work
either. Drawing something as totally transparent has the same effect as drawing
nothing at allthat's what transparency means. Only by knocking a hole in the
shape can we see through it.
To understand why, think about the drawing process. When it
renders our elements to the screen, WPF draws the items one after the other. It
starts with whatever's at the backthe text in this case. Then it draws the
shape on top of the textwhich will effectively obliterate the text that was
underneath the shape. (It's still there in the element tree of course, so WPF
can always redraw it later if you change or remove the shape.) Since you just
drew over the text, you can't draw another shape on top to "undraw" a hole into
the first shape. So if you want a hole in a shape, you'd better make sure that
the hole is there before you draw the shape!
|
|
We have not yet looked at the most flexible geometry: PathGeometry
. This can draw any shape that the Polyline or Polygon can
represent, but it can draw many more besides.
A PathGeometry contains one or more PathFigure
objects, and each PathFigure represents a single open or closed shape
in the path. To define the shape of each figure's outline, you use a sequence
of PathSegment objects.
 |
PathGeometry's ability to contain multiple
figures overlaps slightly with Path's ability to contain multiple
geometries. This is just for convenienceif you need to make a shape where every
piece will be a PathGeometry object, it is more compact to have a
single PathGeometry with multiple PathFigures. If you just
want to use simpler geometries like LineGeometry or RectangleGeometry,
it is simpler to use a GeometryGroup and avoid PathGeometry altogether.
|
|
Example 7-19 shows
a simple path. This contains just a single figure and draws a square.
Example 7-19. A square Path
<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>
The result is shown in
Figure 7-22. This seems like a vast amount of effort for such a simple
resultwe've used 17 lines of markup to achieve what could have been done with a
single Rectangle element. This is why WPF supplies classes for the
simpler shapes and geometries. You don't strictly need any of them because you
can use Path and PathGeometry instead, but they require much
less effort. Normally, you would use Path only for more complex
shapes.
Even though Example
7-19 produces a very simple result, it illustrates most of the
important features of a Path with a PathGeometry. As with all
the previous examples, the geometry is in the path's Data property.
The PathGeometry is a collection of PathFigures, so all of
the interesting data is inside its Figures property. This example
contains just one PathFigure, but you can add as many as you like. The
shape of the PathFigure is determined by the items in its Segments
property.
The sequence of segments always begins with a StartSegment,
which determines the starting point of the path. This is then followed by one
or more segments that determine the shape of the figure. In
Example 7-19, these are all LineSegments because the shape has
only straight edges, but several types of curve are also on offer. Finally,
this example ends with a CloseSegment, indicating that this is a
closed shape. If we wanted to create an open shape (like a Polyline)
we would simply omit the final CloseSegment.
 |
You might be wondering why LineSegments don't
work like the Line shape. With a Line, we specify start and
end points, as in Example 7-11.
This seems simpler than LineSegment, which needs us to begin with StartSegment.
However, line segments in a PathFigure can't work like
that because there cannot be any gaps in the outline of a figure. With the Line
element, each Line is a distinct shape in its own right, but with a PathFigure,
each segment is a part of the shape's outline. To define a figure fully and
unambiguously, each segment must start off from where the previous one
finished. This is why the LineSegment specifies only an end point for
the line. All of the segment types work this way. Of course, the figure needs
to start somewhere, which is why we always begin with a StartSegment.
It is illegal to use StartSegment anywhere other than at the start of
a figure.
|
|
Example 7-19 isn't
very excitingit just uses straight line segments. We can create much more
interesting shapes by using one of the curved segment types
instead. Table 7-2 shows
all of the segment types.
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 lets you add elliptical curves to the edge
of a shape. ArcSegment is a little more complex to use than a simple LineSegment.
As well as specifying the end point of the segment, we must also specify two
radii for the ellipse with the Size property.
The ellipse size and the line start and end points don't provide
enough information to define the curve unambiguously, because there are several
ways to draw an elliptical arc given these constraints. Consider a segment with
a particular start and end point, and a given size and orientation of ellipse.
For this segment there will usually be two ways in which the ellipse can be
positioned so that both the start and end point lie on the boundary of the
ellipse, as Figure 7-23 shows.
In other words, there will be two ways of "slicing" an ellipse with a
particular line.
For each of these two ways of slicing the ellipse, there will be
two resulting segments, a small one and a large one. This means that there are
four ways in which the curve can be drawn between two points.
ArcSegment provides two flags enabling you to select
which of the curves you require. LargeArc determines whether you get
the larger or smaller slice size. SweepFlag chooses which side of the
line the slice is drawn on. Example
7-20 shows markup illustrating all four combinations of these flags. It
also shows the whole ellipse.
Example 7-20. ArcSegments
<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>
 |
You may be wondering why the Ellipse has a
width of 140 and a height of 60, which is double the Size of each ArcSegment.
This is because the ArcSegment interprets the Size as the two
radii of the ellipse, while the Width and Height properties
on the Ellipse indicate the total size.
|
|
Figure 7-24 shows
the results, and, as you can see, each of the shapes has one straight diagonal
line and one elliptical curve. The straight-line edge has the same length and
orientation in all four cases. The curved edge is from different parts of the
same ellipse.
In Figure 7-24,
the ellipse's axes are horizontal and vertical. Sometimes you will want to use
an ellipse where the axes are not aligned with your main drawing axes. ArcSegment
provides an XRotation property, allowing you to specify the amount of
rotation required in degrees.
Figure 7-25 shows
four elliptical arcs . These use the same start
and end points as Figure 7-24
and the same ellipse size. The only difference is that an XRotation of
45 degrees has been specified, rotating the ellipse before slicing it.
 |
There are two degenerate cases in which there will
not be two ways of slicing the ellipse. The first is when the slice cuts the
ellipse exactly in half. In this case, the LargeArc flag is
irrelevant, because both slices are exactly the same size.
The other case is when the ellipse is too smallif the widest
point at which the ellipse could be sliced is narrower than the segment is
long, there is no way in which the segment can be drawn correctly. You should
try to avoid this. (If you do make the ellipse too small, WPF seems to scale up
the ellipse so that it is large enough, preserving the aspect ratio between the
x- and y-axes.)
|
|
The remaining four curve types from
Table 7-2BezierSegment, PolyBezierSegment, QuadraticBezierSegment,
and PolyQuadraticBezierSegmentare all variations on the same theme.
They all draw Bézier curves .
7.2.7.1. Bézier curves
Bézier curves are curved line segments joining two points based
on a particular mathematical formula. It is not necessary to understand the
details of the formula in order to use Bézier curves. What makes Bézier curves
useful is that they offer a fair amount of flexibility in the shape of the
curve. This has made them very popularmost vector drawing programs offer them.
(If you'd like to understand the formula,
http://mathworld.wolfram.com/BezierCurve.html and
http://en.wikipedia.org/wiki/Bézier_curve both provide good
descriptions.) Figure 7-26 shows
a variety of Bézier curve segments.
Each of the five lines shown in
Figure 7-26 is a single BezierSegment
. Bézier curves have become very widely used in graphics systems because of the
wide variation in shapes that even a single segment can offer. They are also
fairly straightforward to use.
As with all of the segment types, a BezierSegment starts
from where the previous segment left off and defines a new end point. It also
requires two control points to be defined, and it is these that
determine the shape of the curve. Figure
7-27 shows the same curves again, but with the control points drawn on.
It also shows lines connecting the control points to the segment end points,
because this makes it easier to see how the control points affect the curve
shapes.
The most obvious way in which the control points are influencing
the shapes of the curves is that they determine the tangent. At the start and
end of each segment, the direction in which the curve runs at that point is
exactly the same as the direction of the line joining the start point to the
corresponding control point.
There is a second, less obvious way in which control points
work. The distance between the start or end point and its corresponding control
point (i.e., the length of the straight lines added on
Figure 7-27) also has an effect. This essentially determines how
extreme the curvature is.
Figure 7-28 shows
a set of Bézier curves similar to those in
Figure 7-27. The tangents of both ends of the lines remain the same,
but, in each case, the distance between the start point and the first control
point is reduced to one quarter of what it was before, while the other is the
same as before. As you can see, this reduces the influence of the first control
point. In all four cases, the shape of the curve is dominated by the control
point that is further from its endpoint.
Example 7-21 shows
the markup for the second curve segment in
Figure 7-27. The Point1 property determines the location of
the first control pointthe one associated with the start point. Point2
positions the second control point. Point3 is the end point.
Example 7-21. BezierSegment
<StartSegment Point="0,50" />
<BezierSegment Point1="60,50" Point2="100,0" Point3="100,50" />
Flexible though Bézier curves are, you will rarely use just a
single one. When defining shapes with curved edges, it is more common for a
shape to have many Bézier curves defining its edge. WPF therefore supplies a PolyBezierSegment
type, which allows multiple curves to be represented in a single segment. It
defines a single Points property, which is an array of Point structures.
Each Bézier curve requires three entries in this array: two control points and
an end point. (As always, each curve starts from where the previous one left
off.) Example 7-22 shows a
sample segment with two curves. Figure
7-29 shows the results.
Example 7-22. PolyBezierSegment
<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>
This markup is somewhat less convenient than simply using a
sequence of BezierSegment elements. Fortunately, you can provide all
of the point data in string form. This is equivalent to
Example 7-22:
<PolyBezierSegment Points="0,10 20,10 40,10 60,10 120,15 100,50" />
Also, if you are generating coordinates from code, dealing with
a single PolyBezierSegment and passing it an array of Point data
is easier than working with lots of individual segments.
Cubic Bézier curves provide a lot of control over the shape of
the curve. However, you might not always want that level of flexibility. The QuadraticBezierSegment
uses a simpler equation that uses just one control point to define the shape of
the curve. This does not offer the same range of curve shapes as a cubic
Bézier curve, but if all you want is a simple shape, this reduces the number of
coordinate pairs you need to provide by one third.
QuadraticBezierSegment is similar in use to the normal BezierSegment.
The only difference is that it has no Point3 propertyjust Point1
and Point2. Point1 is the shared control point, and Point2
is the end point. PolyQuadraticBezierSegment is the multi-curve
equivalent. You use this in exactly the same way as PolyBezierSegment,
except you only need to provide two points for each segment.
7.2.7.2. Combining shapes
Path has one more trick up its sleeve that we have not
yet examined. It is capable of combining geometries
to form new geometries. This is different from adding two geometries to a Pathit
combines pairs of geometries in a way that forms a single geometry with a whole
new shape.
Example 7-23 and
Example 7-24 each define
paths, both of which make use of the same RectangleGeometry and EllipseGeometry.
The difference is that Example 7-23
puts both into a GeometryGroup, while
Example 7-24 puts them into a CombinedGeometry
.
Example 7-23. Multiple geometries
<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>
Example 7-24. Combined geometries
<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>
Figure 7-30 shows
the results of Example 7-23 and
Example 7-24. While the GeometryGroup
has resulted in a shape with multiple figures, the CombinedGeometry has
produced a single figure. The ellipse geometry has taken a bite out of the
rectangle geometry. This is just one of the ways in which geometries can be
combined. The CombineMode property determines which is used, and
Figure 7-31 shows all five available modes.
The combine modes have the following effects:
-
Union
-
Builds a shape where any point that was inside either of the two
original shapes will also be inside the new shape.
-
Intersect
-
Creates a shape where only points that were inside both shapes
will be in the new shape.
-
Xor
-
Creates a shape where points that were in one shape or the other
but not both will be in the new shape.
-
Exclude
-
Creates a shape where points inside the first shape but not
inside the second will be included.
-
Complement
-
Builds a shape where points inside the second but not inside the
first shape will be included in the new shape.
7.2.7.3. Data property text format
We have now looked at all of the features that Path has
to offer. As you have seen, we can end up with some pretty verbose markup.
Fortunately, the Path element offers a shorthand mechanism that allows
most of the features we have seen to be exploited without having to type quite
so much.
So far, we have been setting the Data property using
XAML's property-element syntax (see
Appendix A for more details on this syntax). However, we can supply a
string instead. Example 7-25 shows
both techniques. As you can see, the string form is some 16 lines shorter.
Example 7-25. Path.Data as text
<!-- 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" />
The syntax for the text form of the Path.Data property
is simple. The string must contain a sequence of commands. A command is a
letter followed by some numeric parameters. The number of parameters required
is determined by the chosen command. Lines require just a coordinate pair.
Curves require more data.
If you omit the letter, the same command will be used as last
time. For example, Example 7-25
uses the L commandthis is short for "Line" and represents a LineSegment.
This requires only two numbers, the coordinates of the line end point. And yet
in our example, there are six numbers. This simply indicates that there are
three lines in a row. Table 7-3
lists the commands, their equivalent segment type, and their usage.
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
|
The M command gets special treatment. It is illegal to
use a StartSegment anywhere other than as the first segment of a PathFigure.
If you use an M command in the middle of the data, this is taken to
mean that you would like to start a new PathFigure. This enables
multiple figures to be represented in this compact text format.
Notice that there are two ways of specifying a BezierSegment.
The C command lets you provide all of the control points. The S
command generates the first control point for youit looks at the previous
segment, and makes the first control point a mirror image of the previous one.
This ensures that the segment's tangent aligns with the previous segment's
tangent, resulting in a smooth join between the lines.
All of these commands can be used in two ways. You can specify
the command in either uppercase or lowercase. In the uppercase form,
coordinates are relative to the position of the Path element. If the
command is lowercase, the coordinates are taken to be relative to the end point
of the previous segment in the path.
We have now examined all of the shapes on offer, but so far, we
have been rather unadventurous in our choice of fills and outlines for these
shapes. We have used nothing but standard named colors and simple outline
styles. WPF allows us a much greater variety of drawing styles through its
brush and pen classes.
 |