Tải bản đầy đủ (.pdf) (102 trang)

programming windows phone 7 phần 5 ppt

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (1.69 MB, 102 trang )






























Chapter 13
Vector Graphics


The world of two-dimensional computer graphics is generally divided between vector
graphics and raster graphics—a graphics of lines and a graphics of pixels—a graphics of draw
programs and a graphics of paint programs—a graphics of cartoons and a graphics of
photographs.
Vector graphics is the visual realization of analytic geometry. Two-dimensional coordinate
points in the form (x, y) define straight lines and curves. In Silverlight, these curves can be arcs
on the circumference of an ellipse or Bezier curves, either in the customary cubic form or in a
simplified quadratic form. You can “stroke” these lines with a pen of a desired brush, width,
and style. A series of connected lines and curves can also define an enclosed area that can be
filled with a brush.
Raster graphics (which I’ll discuss in the next chapter) involves bitmaps. In Silverlight it is very
easy to display a PNG or JPEG file using an Image element as I demonstrated as early as
Chapter 4. But as I’ll show you in the next chapter, it’s also possible to generate bitmaps
algorithmically in code using the WriteableBitmap class. The worlds of raster graphics and
vector graphics intersect when an ImageBrush is used to fill an area, or when vector graphics
are used to generate an image on a WriteableBitmap.
The Shapes Library
A Silverlight program that needs to draw vector graphics uses classes defined in the
System.Windows.Shapes namespace, commonly referred to as the Shapes library. This
namespace consists of an abstract class named Shape and six sealed classes that derive from
Shape:
Object
DependencyObject (abstract)
FrameworkElement (abstract)
Shape (abstract)
Rectangle (sealed)
Ellipse (sealed)
Line (sealed)
Polyline (sealed)
Polygon (sealed)

Path (sealed)
393































The Shape class derives from FrameworkElement, which means that these objects get touch
input, participate in layout, and can have transforms. In Silverlight there is insufficient
information to allow you to derive a class from Shape itself.
You’ve already seen Rectangle and Ellipse, but these are really two oddball classes in the realm
of vector graphics because they don’t contain any coordinate points. You can just stick an
Ellipse in a UserControl and it fills the whole control. You can size the element, but positioning
it at an arbitrary point requires a Margin or Padding property, or a RenderTransform, or
putting it on a Canvas and using the Left and Top attached properties.
The other four classes of Shape are different; these allow you to position the elements with
actual coordinate points. Although I’ll discuss the Path class last, it is so versatile that it is
pretty much the only class you need for all your vector graphics jobs. If you need to draw an
arc or a Bezier spline, you’ll be using the Path class.
Shape defines 11 settable properties that are inherited by all its descendants:
• Fill of type Brush
• Stroke of type Brush
• StrokeThickness of type double
• StrokeStartLineCap and StrokeEndLineCap of type PenLineCap
• StrokeLineJoin of type PenLineJoin
• StrokeMiterLimit of type double
• StrokeDashArray of type DoubleCollection
• StrokeDashCap of type PenLineCap
• StrokeDashOffset of type double
• Stretch property of type Stretch
You’ve already seen the first three properties in connection with Rectangle and Ellipse. The Fill
property specifies the Brush used to fill the interior of the figure; the Stroke property is the
Brush used to color the outline of the figure, and StrokeThickness is the width of that outline.
All the other properties can be used with Rectangle and Ellipse as well. Although the two
enumerations (PenLineCap and PenLineJoin) allude to a Pen, there is no Pen class in

Silverlight. Conceptually, the properties beginning with the word Stroke together comprise an
object traditionally regarded as a pen.
394




























Canvas and Grid
The Line class defines four properties of type double named X1, Y1, X2, and Y2. The line is
drawn from the point (X1, Y1) to the point (X2, Y2) relative to its parent:
<Canvas Background="LightCyan">
<Line X1="50" Y1="100"
X2="200" Y2="150"
Stroke="Blue" />
</Canvas>
Many of the examples in this program will be shown as a snippet of XAML and the
corresponding image in a 480-square pixel area. At the end of the chapter I’ll describe the
program that created these images. For the printed page I’ve made the resolution of these
images about 240 dots per inch so they are approximately the same size as what you would
see on the actual phone.
The line begins at the coordinate point (50, 100) and ends at the point (200, 150). All
coordinates are relative to an upper-left origin; increasing values of X go from left to right;
increasing values of Y go from top to bottom.
The X1, Y1, X2, and Y2 properties are all backed by dependency properties so they can be the
targets of styles, data bindings, and animations.
Although the Canvas panel seems like a natural for vector graphics, you’ll get the same image
if you use a single-cell Grid:
<Grid Background="LightCyan">
<Line X1="50" Y1="100"
X2="200" Y2="150"
Stroke="Blue" />
</Grid>
395
































Normally when you use a Canvas you use the Canvas.Left and Canvas.Top attached properties
to position elements within the Canvas. Those properties are not required with the Line
because it has its own coordinates. You could use the attached properties with the Line but

the values are compounded with the coordinates:
<Canvas Background="LightCyan">
<Line X1="50" Y1="100"
X2="200" Y2="150"
Canvas.Left="150"
Canvas.Top="100"
Stroke="Blue" />
</Canvas>
Usually when you’re working with elements that indicate actual coordinate positions, you’ll
use the Canvas.Left and Canvas.Top attached properties only for special purposes, such as
moving an object relative to the Canvas.
Moreover, you’ll recall that a Canvas always reports to the layout system that it has a size of
zero. If you subject the Canvas to anything other than Stretch alignment, it will shrink into
nothingness regardless of its contents.
For these reasons, I tend to put my vector graphics in a single-cell Grid rather than a Canvas.
If a Grid contains one or more Line elements (or any other coordinate-based elements), it will
report a size that comprises the maximum non-negative X coordinate and the maximum non-
negative Y coordinate of all its children. This can sometimes seem a little weird. If a Grid
contains a Line from (200, 300) to (210, 310), the Line will report an ActualWidth of 210 and
an ActualHeight of 310, and the Grid will be 210 pixels wide and 310 pixels tall, even though
the rendered Line needs only a tiny corner of that space. (Actually, the Line and the Grid will
be at least an extra pixel larger to accommodate the StrokeThickness of the rendered Line.)
Coordinates can be negative, but the Grid does not take account of negative coordinates. A
negative coordinate will actually be displayed to the left of or above the Grid. I have spent
much time thinking about this behavior, and I am convinced it is correct.
Overlapping and ZIndex
Here are two lines:
396









































<Grid Background="LightCyan">
<Line X1="100" Y1="300"
X2="200" Y2="50"
Stroke="Blue" />
<Line X1="50" Y1="100"
X2="300" Y2="200"
Stroke="Red" />
</Grid>
The second one overlaps the first one. You can see that more clearly if you go beyond the
default 1-pixel thickness of the line using StrokeThickness:
<Grid Background="LightCyan">
<Line X1="100" Y1="300"
X2="200" Y2="50"
Stroke="Blue"
StrokeThickness="5" />
<Line X1="50" Y1="100"
X2="300" Y2="200"
Stroke="Red"
StrokeThickness="30" />
</Grid>
If you would prefer that the blue line be on top of the red line, there are two ways you can do
it. You could simply swap the order of the two lines in the Grid:
<Grid Background=”LightCyan”>

<Line X1="50" Y1="100"
X2="300" Y2="200"
Stroke="Red"
StrokeThickness="30" />
<Line X1="100" Y1="300"
X2="200" Y2="50"
Stroke="Blue"
StrokeThickness="5" />
</Grid>
Or, you could set the Canvas.ZIndex property. Although this property is defined by Canvas it
works with any type of panel:
397



































<Grid Background="LightCyan">
<Line Canvas.ZIndex="1"
X1="100" Y1="300"
X2="200" Y2="50"
Stroke="Blue"
StrokeThickness="5" />
<Line Canvas.ZIndex="0"
X1="50" Y1="100"
X2="300" Y2="200"
Stroke="Red"
StrokeThickness="30" />
</Grid>
Polylines and Custom Curves
The Line element looks simple but the markup is a little bloated. You can actually reduce the

markup for drawing a single line by switching from the Line to the Polyline:
<Grid Background="LightCyan">
<Polyline Points="100 300 200 50"
Stroke="Blue"
StrokeThickness="5" />
<Polyline Points="50 100 300 200"
Stroke="Red"
StrokeThickness="30" />
</Grid>
The Points property of the Polyline class is of type PointCollection, a collection of Point objects.
In XAML you indicate multiple points by just alternating the X and Y coordinates. You can
string out the numbers with spaces between them as I’ve done, or you can clarify the markup
a little with commas. Some people prefer commas between the X and Y coordinates:
<Polyline Points="100,300 200,50" …
Others (including me) prefer to separate the individual points with commas:
<Polyline Points="100 300, 200 50"
The advantage of Polyline is that you can have as many points as you want:
398






































<Grid Background="LightCyan">
<Polyline Points="100 300, 200 50,
350 100, 200 250"
Stroke="Blue"
StrokeThickness="5" />

<Polyline Points=" 50 100, 300 200,
300 400"
Stroke="Red"
StrokeThickness="30" />
</Grid>
Each additional point increases the total polyline by another line segment.
The Polyline does have one significant disadvantage that Line doesn’t have: Because you’re
now dealing with a collection of Point objects, the individual points can’t be targets of a style,
or a data binding, or an animation. This is not to say that you can’t change the PointCollection
at runtime and have that change reflected in the rendered Polyline. You surely can, as I’ll
demonstrate in the GrowingPolygons program later in this chapter.
Although the Polyline can draw some simple connected lines, it tends to feel underutilized if
it’s not fulfilling its true destiny of drawing complex curves, usually generated algorithmically
in code. The Polyline is always a collection of straight lines, but if you make those lines short
enough and numerous enough, the result will be indistinguishable from a curve.
For example, let’s suppose you want to use Polyline to draw a circle. Commonly, a circle
centered at the point (0, 0) with a radius R is defined as all points (x, y) that satisfy the
equation:
This is also, of course, the Pythagorean Formula.
But when generating points to draw a graphical circle, this formula tends to be a little clumsy:
You need to pick values of x between –R and R, and then solve for y (keeping in mind that
most values of x correspond to two values of y) and even if you do this in a systematic
manner, you’re going to get a higher density of points in the region where x is close to 0 than
the region where y is close to 0.
A much better approach for computer graphics involves parametric equations, where both x
and y are functions of a third variable, sometimes called t to suggest time. In this case that
third variable is simply an angle ranging from 0 to 360°.
Suppose the circle is centered on the point (0, 0) and has a radius of R. The circle will be
enclosed within a box where values of x go from –R on the left to +R on the right. In keeping
399
































with the Silverlight convention that increasing values of y go down, values of y range from –R
on the top to +R on the bottom.
Let’s begin with an angle of 0° at the rightmost edge of the circle, which is the point (R, 0),
and let’s go clockwise around the circle. As the angle goes from 0° to 90°, x goes from R to 0,
and then x goes to –R at 180° and then goes back down to zero at 270° and back to R at
360°. This is a familiar pattern:
At the same time, the values of y go from 0 to R to 0 to –R and back to 0, or
Depending where the circle begins, and in what direction you go, you could have slightly
different formulas where the sine and cosine functions are switched, or one or both or
negative.
If you use different values of R for the two formulas, you’ll draw an ellipse. If you want the
circle centered at the point (C
x
, C
y
), you can add these values to the previous results:
In a program, you put those two formulas in a for loop that increments an angle value
ranging from 0 to 360 to generate a collection of points.
How much granularity is required to make the resultant circle look smooth? In this particular
example, it depends on the radius. The circumference of a circle is 2πR, so if the radius is 240
pixels (for example), the circumference is approximately 1,500 pixels. Divide by 360° and you
get about 4, which means that if you increment the angle in the for loop by 0.25°, the
resultant points will be about a pixel apart. (You’ll see later in this chapter that you can get by
with a lot fewer points.)
Let’s create a new projecvt. Bring up the MainPage.cs file and install a handler for the Loaded
event to allow accessing the dimensions of the ContentPanel grid. Here are calculations for
center and radius for a circle to occupy the center of a content panel and reach to its edges:
Point center = new Point(ContentPanel.ActualWidth / 2,
ContentPanel.ActualHeight / 2 - 1);
double radius = Math.Min(center.X - 1, center.Y - 1);

Notice the pixel subtracted from the calculation of the radius. This is to prevent the circle
from being geometrically the same as the content area size. The stroke thickness straddles the
geometric line so it would otherwise get cropped off at the edges.
400



















Now create a Polyline and set the Stroke and StrokeThickness properties:
Polyline polyline = new Polyline();
polyline.Stroke = this.Resources["PhoneForegroundBrush"] as Brush;
polyline.StrokeThickness = (double)this.Resources["PhoneStrokeThickness"];
Calculate the Point objects in a for loop based on the formulas I’ve just showed you and add
them to the Points collection of the polyline:
for (double angle = 0; angle < 360; angle += 0.25)

{
double radians = Math.PI * angle / 180;
double x = center.X + radius * Math.Cos(radians);
double y = center.Y + radius * Math.Sin(radians);
polyline.Points.Add(new Point(x, y));
}
Now add the Polyline to the Grid:
ContentPanel.Children.Add(polyline);
And here’s the result:
So big deal. We created a circle a hard way rather than an easy way. And it’s not even a
complete circle: Because the angle in the for loop didn’t go all the way to 360, there’s actually
a little gap at the right side.
But instead of fixing that problem, let’s do something a little different. Let’s make the angle
go all the way to 3600:
401





















for (double angle = 0; angle < 3600; angle += 0.25)
Now the loop will go around the circle 10 times. Let’s use that angle and the original radius
value to calculate a scaledRadius:
double scaledRadius = radius * angle / 3600;
And use that scaledRadius value for multiplying by the sine and cosine values. Now the result
is an Archimedian spiral:
Here’s the complete class:
Silverlight Project: Spiral File: MainPage.xaml.cs (excerpt)
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
Loaded += OnLoaded;
}
void OnLoaded(object sender, RoutedEventArgs args)
{
Point center = new Point(ContentPanel.ActualWidth / 2,
ContentPanel.ActualHeight / 2 - 1);
double radius = Math.Min(center.X - 1, center.Y - 1);
402






































Polyline polyline = new Polyline();
polyline.Stroke = this.Resources["PhoneForegroundBrush"] as Brush;
polyline.StrokeThickness = (double)this.Resources["PhoneStrokeThickness"];
for (double angle = 0; angle < 3600; angle += 0.25)
{
double scaledRadius = radius * angle / 3600;
double radians = Math.PI * angle / 180;
double x = center.X + scaledRadius * Math.Cos(radians);
double y = center.Y + scaledRadius * Math.Sin(radians);
polyline.Points.Add(new Point(x, y));
}
ContentPanel.Children.Add(polyline);
}
}
It’s not necessary to create the Polyline object in code: You could define it in XAML and then
just access it to put the points in the Points collection. In Chapter 15 I’ll show you how to
apply a rotation animation to the spiral so that you can hypnotize yourself.
Caps, Joins, and Dashes
When you’re displaying thick lines, you might want a little different appearance on the ends
of the lines. These are known as line caps—“caps” like a hat. The available caps are members
of the PenLineCap enumeration: Flat (the default), Square, Round, and Triangle. Set the
StrokeStartLineCap property to one of these values for the cap at the beginning of the line,
and set StrokeEndLineCap for the cap at the end. Here are Round and Triangle capping off a
30-pixel line:
<Grid Background="LightCyan">
<Polyline Points=" 50 100, 300 200,
300 400"
Stroke="HotPink"

StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Triangle" />
</Grid>
The difference between Flat and Square might not be obvious at first. To better clarify the
difference, the following markup displays a thinner line over the thick line with the same
coordinates to indicate the geometric start and end of the line:
403














































<Grid Background="LightCyan">
<Polyline Points=" 50 100, 300 200,
300 400"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Flat"
StrokeEndLineCap="Square" />

<Polyline Points=" 50 100, 300 200,
300 400"
Stroke="Black" />
</Grid>
The Flat cap (at the upper left) cuts off the line at the geometric point. The Square extends the
line for half the line thickness. My favorite caps are the rounded ones:
<Grid Background="LightCyan">
<Polyline Points=" 50 100, 300 200,
300 400"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round" />
<Polyline Points=" 50 100, 300 200,
300 400"
Stroke="Black" />
</Grid>
As you can see, they also extend the rendered size of the line by half the stroke thickness.
You can also specify what happens at the corners. Set the StrokeLineJoin property to a
member of the PenLineJoin enumeration. Here’s Round:
<Grid Background="LightCyan">
<Polyline Points=" 50 100, 300 200,
100 300"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round" />
<Polyline Points=" 50 100, 300 200,
100 300"

Stroke="Black" />
</Grid>
404















































Or Bevel:
<Grid Background="LightCyan">
<Polyline Points=" 50 100, 300 200,
100 300"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Bevel" />
<Polyline Points=" 50 100, 300 200,
100 300"

Stroke="Black" />
</Grid>
Or Miter, which is the default:
<Grid Background="LightCyan">
<Polyline Points=" 50 100, 300 200,
100 300"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Miter" />
<Polyline Points=" 50 100, 300 200,
100 300"
Stroke="Black" />
</Grid>
The Miter join has a little built-in problem. If the lines meet at a very sharp angle, the miter
can be very long. For example, a 10-pixel wide line that makes an angle of 1° will have a miter
point over 500 pixels long! To avoid this type of weirdness a StrokeMiterLimit property kicks in
for extreme cases:
<Grid Background="LightCyan">
<Polyline Points="50 230, 240 240,
50 250"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Miter" />
<Polyline Points="50 230, 240 240,
50 250"
Stroke="Black" />

</Grid>
The default value is 10 (relative to half the StrokeThickness) but you can make it longer if you
want:
405

















































<Grid Background="LightCyan">
<Polyline Points="50 230, 240 240,
50 250"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Miter"

StrokeMiterLimit="50" />
<Polyline Points="50 230, 240 240,
50 250"
Stroke="Black" />
</Grid>
Here are two lines, one thick, one thin overlaying the thick line, with the same geometric
points, going from the upper-left to the lower-left:
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round" />
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Black" />
</Grid>
You can make the line dashed by setting the StrokeDashArray, which is generally just two
numbers, for example 1 and 1:
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="1 1" />

<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Black" />
</Grid>
What this means is that a dash will be drawn for one-line thickness (30 pixels in this case),
followed by a one-line thickness gap, and repeated until the end. As you can see, the caps are
406








































really handled a little differently; they are drawn or not drawn depending on whether they
occur when a dash or a gap is in progress.
You can make the dashes longer by increasing the first number,
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="2 1" />
<Polyline Points="100 100, 380 100,
380 380, 100 380"

Stroke="Black" />
</Grid>
However, you’ll probably also want to give the dashes their own caps. Set StrokeDashCap to a
member of the PenLineCap enumeration, either Flat (the default), Triangle, Square, or Round,
which is my preference:
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="2 1"
StrokeDashCap="Round" />
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Black" />
</Grid>
A little problem has arisen. Each of the dashes has acquired a rounded cap, so they’ve each
increased in length on both ends by half the line thickness, and now the dashes actually
touch. You need to fix that by increasing the gap:
407





















































<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="2 2"
StrokeDashCap="Round" />
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Black" />
</Grid>
If you want to draw a dotted line with actual round dots, obviously you want to use the Round
dash cap, and you want each dot to be separated by its neighbor by the dot width. The

StrokeDashArray required for this job is somewhat non-intuitive. It’s a dash length of 0 and a
gap length of 2:
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="0 2"
StrokeDashCap="Round" />
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Black" />
</Grid>
You can have more than two numbers. Here’s a dot and dash configuration:
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="0 2 2 2"
StrokeDashCap="Round" />
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Black" />

408





































</Grid>
You don’t even need an even number of numbers:
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="1 2 3"
StrokeDashCap="Round" />
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Black" />
</Grid>
The other dash-related property is StrokeDashOffset, and it is also relative to the thickness of
the line. This property lets you start the dashes in the middle of a dash, which makes the first
dash (at the upper-left corner) smaller than the rest:
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="HotPink"

StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="2 2"
StrokeDashCap="Round"
StrokeDashOffset="1" />
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Black" />
</Grid>
409































Or you can start with a gap:
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="HotPink"
StrokeThickness="30"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="2 2"
StrokeDashCap="Round"
StrokeDashOffset="3" />
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Black" />
</Grid>
You can use a dotted line around an ellipse if you want:
<Grid Background="LightCyan">

<Ellipse Width="400" Height="400"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stroke="Red"
StrokeThickness="23.22"
StrokeDashArray="0 1.5"
StrokeDashCap="Round" />
</Grid>
It’s an unusual look, but you really have to experiment or do some calculations so you don’t
get half a dot in there.
410










































Polygon and Fill
The Polyline that I’ve been using to demonstrate dotted lines is only three sides of a square:
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Red"
StrokeThickness="20"
StrokeStartLineCap="Round"

StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="0 2"
StrokeDashCap="Round" />
</Grid>
But if you set the Fill brush, the interior is filled as if the polyline describes a closed area:
<Grid Background="LightCyan">
<Polyline Points="100 100, 380 100,
380 380, 100 380"
Stroke="Red"
StrokeThickness="20"
Fill="Blue"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="0 2"
StrokeDashCap="Round" />
</Grid>
If you want the figure to be really closed, you can add another point to the Points collection
that is the same as the first point, or you can use a Polygon rather than a Polyline:
<Grid Background="LightCyan">
<Polygon Points="100 100, 380 100,
380 380, 100 380"
Stroke="Red"
StrokeThickness="20"
Fill="Blue"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round"
StrokeDashArray="0 2"

StrokeDashCap="Round" />
</Grid>
Both elements have the same Points collection, but the Polygon is closed automatically if
necessary.
411




































Once you start filling enclosed area with Polygon, a question comes up about how the interior
should be handled when boundary lines overlap. The Polygon class defines a property named
FillRule that gives you a choice. The classic example is the five-pointed star. Here’s the default
FillRule, called EvenOdd:
<Grid Background="LightCyan">
<Polygon Points="240 48, 352 396,
58 180, 422 180,
128 396"
Stroke="Red"
StrokeThickness="10"
Fill="Blue"
FillRule="EvenOdd" />
</Grid>
The EvenOdd algorithm determines if an enclosed area should be filled or not by conceptually
taking a point in that area, for example, somewhere in the center, and drawing an imaginary
line out to infinity. That imaginary line will cross some boundary lines. If it crosses an odd
number of boundary lines, such as happens in the five points, then the area is filled. For an
even number, like the center, the area is not filled.
The alternative is a FillRule called NonZero:
<Grid Background="LightCyan">

<Polygon Points="240 48, 352 396,
58 180, 422 180,
128 396"
Stroke="Red"
StrokeThickness="10"
Fill="Blue"
FillRule="NonZero" />
</Grid>
The NonZero fill rule is a bit more complex because it takes account of the directions that
boundary lines are drawn. If the boundary lines drawn in one direction balance out the
boundary lines drawn in the opposite direction, then the area is not filled. In any interior area
of this star, however, all the boundary lines go in the same direction.
Neither of these two FillRule options guarantees that all interior areas get filled. Here’s a
rather artificial figure that has an enclosed but unfilled area even with NonZero:
412





























<Grid Background="LightCyan">
<Polygon Points=" 80 160, 80 320,
240 320, 240 80,
400 80, 400 240,
160 240, 160 400,
320 400, 320 160"
Stroke="Red"
StrokeThickness="10"
Fill="Blue"
FillRule="NonZero" />
</Grid>
The Stretch Property
The only settable property defined by Shape that I haven’t discussed yet is Stretch. This is
similar to the same property in the Image element; you set it to a member of the Stretch
enumeration, either None (the default), Fill, Uniform, or UniformToFill. Here’s an innocent little
Polygon:
<Grid Background="LightCyan">

<Polygon Points="250 200, 250 210,
230 270, 230 260"
Stroke="Red"
StrokeThickness="4" />
</Grid>
Now here’s the same Polygon with its Stretch property set to Fill.
<Grid Background="LightCyan">
<Polygon Points="250 200, 250 210,
230 270, 230 260"
Stroke="Red"
StrokeThickness="4"
Stretch="Fill" />
</Grid>
413































Regardless of the coordinates, it stretches to fill the container with a change in aspect ratio as
well. To retain the aspect ratio, use Uniform or UniformToFill just as with the Image element.
You can probably see why the Stretch property of Shape isn’t used very often in connection
with vector graphics, but if you need a particular vector image to fill an area of arbitrary size,
it’s a welcome option.
Dynamic Polygons
As you’ve seen, when a property backed by a dependency property is changed at runtime, the
element with that property changes to reflect that change. This is a result of the support for a
property-changed handler built into dependency properties.
Certain collections will also respond to changes. Collection classes that derive from
PresentationFrameworkCollection respond to changes when an object is added to or removed
from a collection. A notification is funneled up to the element containing the collection. In
some cases, changes to dependency properties in the members of the collection also trigger
notifications. (Unfortunately, the exact nature of this notification process is hidden from the
application programmer.) The UIElementCollection that the Panel classes uses for its Children
property derives from this class, as does the PointCollection in Polyline and Polygon.

At runtime, you can dynamically add Point objects to the PointCollection, or remove them
from the PointCollection, and a Polyline or Polygon will change.
The GrowingPolygons project has a MainPage.xaml file that instantiates a Polygon element
and gives it a couple properties:
Silverlight Project: GrowingPolygons File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<Polygon Name="polygon"
Stroke="{StaticResource PhoneForegroundBrush}"
StrokeThickness="{StaticResource PhoneStrokeThickness}" />
</Grid>
The code-behind file waits until the Loaded event is fired before determining the size of the
content panel (just as in the Spiral program) and it begins by obtaining similar information.
But the OnLoaded handler just adds two points to the Points collection of the Polygon to
define a vertical line; everything else happens during Tick events of a DispatcherTimer (which
of course requires a using directive for System.Windows.Threading):
414


















































Silverlight Project: GrowingPolygons File: MainPage.xaml.cs (excerpt)
public partial class MainPage : PhoneApplicationPage
{
Point center;
double radius;
int numSides = 2;
public MainPage()
{
InitializeComponent();
Loaded += OnLoaded;
}
void OnLoaded(object sender, RoutedEventArgs args)
{
center = new Point(ContentPanel.ActualWidth / 2 - 1,
ContentPanel.ActualHeight / 2 - 1);
radius = Math.Min(center.X, center.Y);
polygon.Points.Add(new Point(center.X, center.Y - radius));
polygon.Points.Add(new Point(center.X, center.Y + radius));
DispatcherTimer tmr = new DispatcherTimer();
tmr.Interval = TimeSpan.FromSeconds(1);
tmr.Tick += OnTimerTick;
tmr.Start();
}
void OnTimerTick(object sender, EventArgs args)
{
numSides += 1;

for (int vertex = 1; vertex < numSides; vertex++)
{
double radians = vertex * 2 * Math.PI / numSides;
double x = center.X + radius * Math.Sin(radians);
double y = center.Y - radius * Math.Cos(radians);
Point point = new Point(x, y);
if (vertex < numSides - 1)
polygon.Points[vertex] = point;
else
polygon.Points.Add(point);
}
PageTitle.Text = "" + numSides + " sides";
}
}
Every second, the program replaces all but one of the Point objects in the Points collection of
the Polygon. The first Point in the collection—which is the Point at the top center of the
content area—is the only one that remains the same. In addition, the Tick handler adds a new
415



















Point object at the end of the collection. The result is a polygon that gains one new side every
second:
You can see for yourself how many points are needed before this polygon visually turns into a
circle!
Notice that the program entirely replaces the Point object in the collection rather than
attempting to modify the X and Y properties of the existing object in the collection. Point is a
structure, and it implements no notification mechanism. There is no way for the
PointCollection to know if a property of a particular Point in the collection has been changed.
Only when the entire Point object is replaced does the PointCollection know about it.
If you’re doing something like this is in a real application, you might want to detach the
PointCollection from the Polygon when you’re making a lot of changes to it. This prevents a
long series of notifications firing that inform the Polygon that the PointCollection has changed.
The code would look something like this:
PointCollection points = polygon.Points;
polygon.Points = null;
// make changes to points collection
polygon.Points = points;
416
































The PointCollection is detached by saving a reference to it and setting the Points property to
null. When all changes have been made, the PointCollection is reattached to the Polygon, and
the Polygon responds to the new collection of points.
The Path Element
Although Line, Polyline, and Polygon are all convenient and easy to use, their functionality is

pretty much subsumed in the last of the Shape descendents, Path.
The Path class defines just one property of its own named Data of type Geometry, but
geometries are a very important concept in Silverlight vector graphics. In general, a geometry
is a collection of straight lines and curves, some of which might be connected to each other
(or not) and might define enclosed areas (or not). In other graphics programming
environments, the geometry might be called a graphics path. In Silverlight, Path is an element
that uses a Geometry object for its Data property.
It’s important to recognize that a Geometry object is nothing but naked coordinate points.
There is no concept of brushes or line thickness or styles with a geometry. That’s why you
need to combine a Geometry with a Path element to actually render something on the screen.
The Geometry defines the coordinate points; the Path defines the stroke brush and fill brush.
Geometry fits into the Silverlight class hierarchy like so:
Object
DependencyObject (abstract)
Geometry (abstract)
LineGeometry (sealed)
RectangleGeometry (sealed)
EllipseGeometry (sealed)
GeometryGroup (sealed)
PathGeometry (sealed)
Just as the Path element is pretty much the only Shape derivative you really need, the
PathGeometry class is the only Geometry derivative you really need. But of course I’m going to
discuss the others as well because they’re often quite convenient. You can’t derive from
Geometry yourself.
Geometry defines four public properties:
• get-only static Empty of type Geometry
• get-only static StandardFlatteningTolerance of type double
• get-only Bounds of type Rect
417

×