Dynamic Polygons, Geometries and ArcSegment in Windows Phone 7

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.

This chapter is taken from book "Programming Windows Phone 7" by Charles Petzold published by Microsoft press. http://www.charlespetzold.com/phone/index.html

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

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

namespace
GrowingPolygons
{
    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 Point object at the end of the collection. The result is a polygon that gains one new side every second:

th1.gif

The Path Element

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.

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; thePath 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)

LineGeometry defines two properties of type Point named StartPoint and EndPoint:

th2.gif

RectangleGeometry defines a property named Rect of type Rect, a structure that defines a rectangle with four numbers: two numbers indicate the coordinate point of the upper-left corner and two more numbers for the rectangle's size. In XAML you specify these four numbers sequentially: the x and y coordinates of the upper-left corner, followed by the width and then the height:

th3.gif

The EllipseGeometry also defines RadiusX and RadiusY properties, but these are for defining the lengths of the two axes. The center of the EllipseGeometry is provided by a property named Center of type Point:

th4.gif

Geometries and Transforms

If you're using EllipseGeometry and you don't want the axes of the ellipse to be aligned on the horizontal and vertical, you can apply a RotateTransform to it. And you have a choice. Because Path derives from UIElement, you can set this RotateTransform to the RenderTransform property of the Path:

th5.gif

Notice that the CenterX and CenterY properties of RotateTransform are set to the same values as the Center point of the EllipseGeometry itself so that the ellipse is rotated around its center. When working with Path and Geometry objects, it's usually easier to specify actual transform centers rather than to use RenderTransformOrigin.

The RenderTransform property has no effect on how the element is perceived in the layout system, but the Transform property of the Geometry affects the perceived dimensions. To see this difference, enclose a Path with an EllipseGeometry in a centered Border:

th6.gif

Grouping Geometries

One of the descendent classes of Geometry is GeometryGroup. This allows you to combine one or more Geometryobjects in a composite. Notice how theFillRule applies to this combination.

<Grid Background="LightCyan">
    <
Path Stroke="Maroon"
          StrokeThickness
="4"
          Fill="Green">
        <
Path.Data>
            <
GeometryGroup>
                <
RectangleGeometryRect=" 40 40 200 200" />
                <
RectangleGeometryRect=" 90 90 200 200" />
                <
RectangleGeometryRect="140 140 200 200" />
                <
RectangleGeometryRect="190 190 200 200" />
                <
RectangleGeometryRect="240 240 200 200" />
            </
GeometryGroup>
        </
Path.Data>
    </
Path>
</
Grid>

th7.gif

The ArcSegment

Things start getting tricky with ArcSegment. An arc is just a partial circumference of an ellipse, but because theArcSegment must fit in with the paradigm of start points and end points, the arc must be specified with two points on the circumference of some ellipse. But if you define an ellipse with a particular center and radii, how do you specify a point on that ellipse circumference exactly without doing some trigonometry?

The solution is to define only the size of this ellipse and not where the ellipse is positioned. The actual location of the ellipse is defined by the two points.

th8.gif

In code, you can determine algorithmically the points on a circle where you want the arc to begin and end. That's the idea behind the SunnyDay program, which combines LineSegment and ArcSegment objects into a composite image. The MainPage.xaml file instantiates the Path element: and assigns all the properties except the actual segments:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <
Path Fill="Yellow">
        <
Path.Data>
            <
PathGeometry>
                <
PathFigure x:Name="pathFigure"
                            IsClosed="True" />
            </
PathGeometry>
        </
Path.Data>
    </
Path>
</
Grid>

The Loaded event handler is then responsible for obtaining the size of the content area and calculating all the coordinates for the various path segments:

namespace SunnyDay
{
    public partial class MainPage : PhoneApplicationPage
    {
        const int BEAMCOUNT = 24;
        const double INCREMENT = Math.PI / BEAMCOUNT;       
        public MainPage()
        {
            InitializeComponent();
            Loaded += OnLoaded;
        } 
        void OnLoaded(object sender, RoutedEventArgs args)
        {
            Point center = new Point(ContentPanel.ActualWidth / 2,
                                     ContentPanel.ActualHeight / 2); 
            double radius = 0.45 * Math.Min(ContentPanel.ActualWidth,
                                            ContentPanel.ActualHeight);
            double innerRadius = 0.8 * radius;           
            for (int i = 0; i < BEAMCOUNT; i++)
            {
                double radians = 2 * Math.PI * i / BEAMCOUNT; 
                if (i == 0)
                {
                    pathFigure.StartPoint = new Point(center.X, center.Y - radius);
                } 
                LineSegment lineSeg = new LineSegment();
                lineSeg.Point = new Point(
                    center.X + innerRadius * Math.Sin(radians + INCREMENT / 2),
                    center.Y - innerRadius * Math.Cos(radians + INCREMENT / 2));
                pathFigure.Segments.Add(lineSeg); 
                ArcSegment arcSeg = new ArcSegment();
                arcSeg.Point = new Point(
                    center.X + innerRadius * Math.Sin(radians + 3 * INCREMENT / 2),
                    center.Y - innerRadius * Math.Cos(radians + 3 * INCREMENT / 2));
                pathFigure.Segments.Add(arcSeg); 
                lineSeg = new LineSegment();
                lineSeg.Point = new Point(
                    center.X + radius * Math.Sin(radians + 2 * INCREMENT),
                    center.Y - radius * Math.Cos(radians + 2 * INCREMENT));
                pathFigure.Segments.Add(lineSeg);
            }
        }
    }
}

The result is a rather cartoonish sun:

th9.gif