This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
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.
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.
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:
Although the Canvas
panel seems like a natural for vector graphics,
you'll get the same image if you use a single-cell
Grid:
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:
Overlapping and ZIndex
Here are two lines:
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:
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:
Or, you could set the
Canvas.ZIndex
property. Although this property is defined by
Canvas it works with
any type of panel:
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:
Now 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:
X2 + Y2 = R2
This is also, of course, the Pythagorean Formula.
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.
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:
let's do something a little different. Let's make the angle go all the way to
3600:
Here's the complete class:
namespace Spiral
{
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);
double radius =
Math.Min(center.X - 1, center.Y - 1);
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);
}
}
}
We use that scaledRadius value for multiplying by the sine and cosine values.
Now the result is an Archimedian spiral:
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:
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:
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:
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:
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 degree will have a miter point over 500 pixels long! To avoid this
type of weirdness a StrokeMiterLimit property kicks in for extreme cases:
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:
You can make the line dashed by setting the StrokeDashArray, which is
generally just two numbers, for example 1 and 1:
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:
You can use a dotted line around an ellipse if you want:
Polygon and Fill
The Polyline that I've been using to demonstrate dotted lines is only three
sides of a square:
But if you set the Fill brush, the interior is filled as if the polyline
describes a closed area:
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:
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 with its Stretch
property set to Fill.