How to Draw the Bézier curve in Windows Phone 7

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

The Bézier curve is a spline, which is a type of curve used to approximate discrete data with a smooth continuous function. Silverlight supports the standard two-dimensional form of the cubic Bézier curve but also a quadratic Bézier curve that is somewhat simpler and faster, so I'll discuss that one first.

Perhaps the best way to become familiar with Bézier curves is to experiment with them. The QuadraticBezier program draws a single Bézier curve but lets you manipulate the three points to see what happens.

The XAML file assembles four Path elements and a Polyline in the single-cell Grid. The first Path is the quadratic Bézier itself. Notice that p0 is provided by the StartPoint property of PathFigure, while p1, and p2 correspond to the Point1 and Point2 properties of QuadraticBezierSegment:

<
Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <
Path Stroke="{StaticResource PhoneForegroundBrush}"
          StrokeThickness="2">
        <
Path.Data>
            <
PathGeometry>
                <
PathFigure x:Name="pathFig"
                            StartPoint="100 100">
                    <
QuadraticBezierSegment x:Name="pathSeg"
                                            Point1="300 250"
                                            Point2="100 400" />
                </
PathFigure>
            </
PathGeometry>
        </
Path.Data>
    </
Path>
    <
Polyline Name="ctrlLine"
              Stroke="{StaticResource PhoneForegroundBrush}"
              StrokeDashArray="2 2"
              Points="100 100, 300 250, 100 400" />
    <
Path Name="pt0Dragger"
          Fill="{StaticResource PhoneAccentBrush}"
          Opacity="0.5">
        <
Path.Data>
            <
EllipseGeometry x:Name="pt0Ellipse"
                             Center="100 100"
                             RadiusX="48"
                             RadiusY="48" />
        </
Path.Data>
    </
Path>
    <
Path Name="pt1Dragger"
          Fill="{StaticResource PhoneAccentBrush}"
          Opacity="0.5">
        <
Path.Data>
            <
EllipseGeometry x:Name="pt1Ellipse"
                             Center="300 250"
                             RadiusX="48"
                             RadiusY="48" />
        </
Path.Data>
    </
Path>
    <
Path Name="pt2Dragger"
          Fill="{StaticResource PhoneAccentBrush}"
          Opacity="0.5">
        <
Path.Data>
            <
EllipseGeometry x:Name="pt2Ellipse"
                             Center="100 400"
                             RadiusX="48"
                             RadiusY="48" />
        </
Path.Data>
    </
Path>
</
Grid>

The Polyline element draws a dotted line from the two end points to the control point. The remaining three Path elements are "draggers," that is, they let you drag any of the three points. The initial screen looks like this:

thh1.gif

The code-behind file provides all the dragging logic. Because Silverlight for Windows Phone does not support bindings for properties not defined by FrameworkElement derivatives, I wasn't able to hook all the corresponding points together in the XAML file. Instead, they have to be set individually in the Manipulation overrides:


namespace QuadraticBezier
{
    public partial class MainPage : PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent();
        } 
        protected override void OnManipulationStarted(ManipulationStartedEventArgs args)
        {
            if (args.OriginalSource == pt0Dragger ||
                args.OriginalSource == pt1Dragger ||
                args.OriginalSource == pt2Dragger)
            {
                args.ManipulationContainer = ContentPanel;
                args.Handled = true;
            }
            base.OnManipulationStarted(args);
        } 
        protected override void OnManipulationDelta(ManipulationDeltaEventArgs args)
        {
            Point translate = args.DeltaManipulation.Translation; 
            if (args.OriginalSource == pt0Dragger)
            {
                pathFig.StartPoint = Move(pathFig.StartPoint, translate);
                ctrlLine.Points[0] = Move(ctrlLine.Points[0], translate);
                pt0Ellipse.Center = Move(pt0Ellipse.Center, translate);
                args.Handled = true;
            }
            else if (args.OriginalSource == pt1Dragger)
            {
                pathSeg.Point1 = Move(pathSeg.Point1, translate);
                ctrlLine.Points[1] = Move(ctrlLine.Points[1], translate);
                pt1Ellipse.Center = Move(pt1Ellipse.Center, translate);
                args.Handled = true;
            }
            else if (args.OriginalSource == pt2Dragger)
            {
                pathSeg.Point2 = Move(pathSeg.Point2, translate);
                ctrlLine.Points[2] = Move(ctrlLine.Points[2], translate);
                pt2Ellipse.Center = Move(pt2Ellipse.Center, translate);
                args.Handled = true;
            }
            base.OnManipulationDelta(args);
        } 
        Point Move(Point point, Point translate)
        {
            return new Point(point.X + translate.X, point.Y + translate.Y);
        }
    }
}

Being a quadratic, this version of the Bézier curve makes only a single turn, and it is extremely well behaved:

thh2.gif

The Path Markup Syntax

Silverlight supports a type of "mini-language" that allows you to encode an entirePathGeometry in a string. The language consists of letters (such as M for Move, L for Line, A for Arc, and C for Cubic Bézier) that take the place of PathFigure and PathSegment objects. Each new PathFigure begins with a Move command. The syntax is described in the Graphics section of the online Silverlight documentation.

Here's an example:

thh3.gif

The Arc is probably the most complex syntax. It begins with the size of the ellipse, followed by a rotation angle, and then two flags, 1 for IsLargeArc and 1 for Clockwise, and concluding with the point. When drawing complete circles, you'll want to separate the circle into two halves and use two Arc commands (or two ArcSegment objects).

Besides using geometries for drawing you can use geometries for clipping. Here's the famous KeyholeOnTheMoon image:

thh4.gif

This program makes use of the Clip property of type Geometry. Clip is defined by FrameworkElement so you can use the property to make any element or control visually non-rectangular in shape, and the Path Markup Syntax makes it look trivial:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
      Background="{StaticResource PhoneAccentBrush}">
    <
Image Source="Images/BuzzAldrinOnTheMoon.png"
           Stretch="None"
           Clip="M 120 95 L 90 265 L 220 265 L 190 95 A 50 50 0 1 0 120 95" />
</
Grid>

I've also used Path Markup Syntax in the Analog Clock program. Here's what it looks like:

thh5.gif

The visuals consist of five Path elements. The curves on the hour and minute hand are Bézier splines. The tick marks are dotted arc segments.

The XAML file defines a Style that's used for all five Path elements:

<phone:PhoneApplicationPage.Resources>
    <
Style x:Key="pathStyle"
           TargetType="Path">
        <
Setter Property="Fill" Value="{StaticResource PhoneAccentColor}" />
        <
Setter Property="Stroke" Value="{StaticResource PhoneForegroundColor}" />
        <
Setter Property="StrokeThickness" Value="2" />
        <
Setter Property="StrokeStartLineCap" Value="Round" />
        <
Setter Property="StrokeEndLineCap" Value="Round" />
        <
Setter Property="StrokeLineJoin" Value="Round" />
        <
Setter Property="StrokeDashCap" Value="Round" />
    </
Style>
</
phone:PhoneApplicationPage.Resources>

In an attempt to keep the graphics simple, I devised an arbitrary coordinate system. The clock graphics are drawn as if the width and height of the clock were 200 pixels, and the center were the point (0, 0). The clock graphics are thus bounded by X coordinates of –100 on the left and 100 on the right, and Y coordinates of –100 on the top and 100 on the bottom.

These arbitrary coordinates of the clock are in part defined by the explicit Width and Height settings of this nested Grid:

<
Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"
      SizeChanged="OnContentPanelSizeChanged">
    <!-- Draw clock on Grid with center at (0, 0) -->
    <Grid Width="200" Height="200">
        <Grid.RenderTransform>
            <TransformGroup>
                <ScaleTransform x:Name="scaleClock" />
                <TranslateTransform X="100" Y="100" />
            </TransformGroup>
        </Grid.RenderTransform>
       ...
   
</Grid>
</
Grid>

The TranslateTransform shifts the whole Grid to the right and down. The upper-left coordinate of (–100, –100), for example, becomes (0, 0), and a coordinate of (100, 100) becomes (200, 200).

Notice the SizeChanged event handler set on the normal content grid. The code portion uses the actual size of the content area to set the ScaleTransform applied to the nested Grid. That scales the 200-pixel dimension to the actual size:

void OnContentPanelSizeChanged(object sender, SizeChangedEventArgs args)
{
    double scale = Math.Min(args.NewSize.Width, args.NewSize.Height) / 200;
    scaleClock.ScaleX = scale;
    scaleClock.ScaleY = scale;
}

Here are the five paths:

<!-- Tick marks (small and large). -->
<Path Data="M 0 -90 A 90 90 0 1 1 0 90
            A 90 90 0 1 1 0 -90"
      Style="{StaticResource pathStyle}"
      Fill="{x:Null}"
      StrokeDashArray="0 3.14159"
      StrokeThickness="3" />
<
Path Data="M 0 -90 A 90 90 0 1 1 0 90
            A 90 90 0 1 1 0 -90"
      Style="{StaticResource pathStyle}"
      Fill="{x:Null}"
      StrokeDashArray="0 7.854"
      StrokeThickness="6" />

<!-- Hour hand pointing up. -->
<Path Data="M 0 -60 C 0 -30, 20 -30, 5 -20 L 5 0
            C 5 7.5, -5 7.5, -5 0 L -5 -20
            C -20 -30, 0 -30 0 -60"
      Style="{StaticResource pathStyle}">
    <
Path.RenderTransform>
        <
RotateTransform x:Name="rotateHour" />
    </
Path.RenderTransform>
</
Path>

<!-- Minute hand pointing up. -->
<Path Data="M 0 -80 C 0 -75, 0 -70, 2.5 -60 L 2.5 0
            C 2.5 5, -2.5 5, -2.5 0 L -2.5 -60
            C 0 -70, 0 -75, 0 -80"

      Style="{StaticResource pathStyle}">
    <
Path.RenderTransform>
        <
RotateTransform x:Name="rotateMinute" />
    </
Path.RenderTransform>
</
Path>

<!-- Second hand pointing up. -->
<Path Data="M 0 10 L 0 -80"
      Style="{StaticResource pathStyle}">
    <
Path.RenderTransform>
        <
RotateTransform x:Name="rotateSecond" />
    </
Path.RenderTransform>
</
Path>

The StrokeDashArray settings on the first two Path elements were carefully calculated to produce the pattern of 1-second and 5-second tick marks around the face of the clock. The other three Path elements have RotateTransform objects set to their RenderTransform properties. These RotateTransforms are reset every second from the code-behind file:

namespace AnalogClock
{
    public partial class MainPage : PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent(); 
            DispatcherTimer tmr = new DispatcherTimer();
            tmr.Interval = TimeSpan.FromSeconds(1);
            tmr.Tick += new EventHandler(OnTimerTick);
            tmr.Start();
        } 
        void OnTimerTick(object sender, EventArgs args)
        {
            DateTime dt = DateTime.Now; 
            rotateSecond.Angle = 6 * dt.Second;
            rotateMinute.Angle = 6 * dt.Minute + rotateSecond.Angle / 60;
            rotateHour.Angle = 30 * (dt.Hour % 12) + rotateMinute.Angle / 12;
        } 
       .....
    }
}


Similar Articles