This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
Basic Shapes:
The System.Windows.Shapes namespace includes
elements for displaying vector graphics-the use of straight lines and curves for
drawing and defining filled areas, to set the Width property of an Ellipse to
the Height to create a circle. The Fill can then be set to a RadialGradientBrush
that starts at White in the center and then goes to a gradient color at the
perimeter. Normally the gradient center is the point (0.5, 0.5) relative to the
ball's dimension, but you can offset that like so:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<Ellipse Width="300" Height="300">
<Ellipse.Fill>
<RadialGradientBrush Center="0.4 0.4"
GradientOrigin="0.4 0.4">
<GradientStop Offset="0" Color="White" />
<GradientStop Offset="1" Color="Red" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>
The offset white spot looks like reflection from a light
source, suggesting a three dimensional shape and the offset white spot looks
like reflection from a light source, suggesting a three dimensional shape:
Transforms: Transforms
apply a simple formula to all the coordinates of a visual object and cause that
object to be shifted to a different location, or change size, or be rotated, you
can apply transforms to any object that descends from
UIElement, and that
includes text, bitmaps, movies, panels, and all controls. The property defined
by UIElement
that makes transforms possible is
RenderTransform, which you set to an object of type
Transform.
The whole subject of transforms can be quite complex, particularly when
transforms are combined, so I'm really only going to show the basics here.
Although TransformGroup
is normally an advanced option, I have nevertheless used
TransformGroup in a
little project named TransformExperiment that allows you to play with the four
standard of transforms. It begins with all the properties set to their default
values:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBlock Text="Transform Experiment"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"
CenterX="0" CenterY="0" />
<SkewTransform AngleX="0" AngleY="0"
CenterX="0" CenterY="0" />
<RotateTransform Angle="0"
CenterX="0" CenterY="0" />
<TranslateTransform X="0" Y="0" />
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
You can experiment with this program right in Visual Studio. At first you'll
want to try out each type of transform independently of the others.
TranslateTransform is useful
for making drop shadows and effects where the text seems embossed or engraved.
Simply put two TextBlock
elements in the same location with
the same text, and all the same text properties, but different
Foreground properties. Without
any transforms, the second TextBlock
sits on top of the first
TextBlock. On one or the other,
apply a small ScaleTransform
and the result is magic. The
EmbossedText project demonstrates this technique. Here are two
TextBlock elements in the same
Grid:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBlock Text="EMBOSS"
Foreground="{StaticResource PhoneForegroundBrush}"
FontSize="96"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<TextBlock Text="EMBOSS"
Foreground="{StaticResource PhoneBackgroundBrush}"
FontSize="96"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock.RenderTransform>
<TranslateTransform X="2" Y="2" />
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
Notice I've used theme colors for the two
Foreground properties. With the default dark theme, the TextBlock
underneath is white, and the one on top is black like the
background but shifted a little to let the white one peak through a bit:
Generally this technique is applied to black text on a white background, but
it looks pretty good with this color scheme as well.
If you have a need to combine transforms in the original order that I had
them in TransformExperiment-the order scale, skew, rotate, translate-you can use
CompositeTransform to
set them all in one convenient class.
Let's make a clock. It won't be a digital clock, but it won't be entirely an
analog clock either. That's why I call it HybridClock. The hour, minute, and
second hands are actually
TextBlock objects that are rotated around the center of the content
grid. Here's the XAML:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0"
SizeChanged="OnContentPanelSizeChanged">
<TextBlock
Name="referenceText"
Text="THE
SECONDS ARE 99"
Foreground="Transparent"
/>
<TextBlock
Name="hourHand">
<TextBlock.RenderTransform>
<CompositeTransform
/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock
Name="minuteHand">
<TextBlock.RenderTransform>
<CompositeTransform
/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock
Name="secondHand">
<TextBlock.RenderTransform>
<CompositeTransform
/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
The code-behind file defines a few fields that will be used throughout the program, and the constructor sets up aDispatcherTimer, for which you'll need a usingdirective for System.Windows.Threading:
namespace HybridClock
{
public partialclass MainPage : PhoneApplicationPage
{
Point gridCenter;
Size textSize;
double scale;
public MainPage()
{
InitializeComponent();
DispatcherTimer tmr =new DispatcherTimer();
tmr.Interval = TimeSpan.FromSeconds(1);
tmr.Tick += OnTimerTick;
tmr.Start();
}
void OnContentPanelSizeChanged(object sender, SizeChangedEventArgs args)
{
gridCenter = newPoint(args.NewSize.Width / 2,
args.NewSize.Height / 2);
textSize = newSize(referenceText.ActualWidth,
referenceText.ActualHeight);
scale = Math.Min(gridCenter.X, gridCenter.Y) / textSize.Width;
UpdateClock();
}
void OnTimerTick(object sender, EventArgs e)
{
UpdateClock();
}
void UpdateClock()
{
DateTime dt =DateTime.Now;
double angle = 6 * dt.Second;
SetupHand(secondHand, "THE SECONDS ARE " + dt.Second, angle);
angle = 6 * dt.Minute + angle / 60;
SetupHand(minuteHand, "THE MINUTE IS " + dt.Minute, angle);
angle = 30 * (dt.Hour % 12) + angle / 12;
SetupHand(hourHand, "THE HOUR IS " + (((dt.Hour + 11) % 12) + 1), angle);
}
void SetupHand(TextBlock txtblk, string text,double angle)
{
txtblk.Text = text;
CompositeTransform xform = txtblk.RenderTransform asCompositeTransform;
xform.CenterX = textSize.Height / 2;
xform.CenterY = textSize.Height / 2;
xform.ScaleX = scale;
xform.ScaleY = scale;
xform.Rotation = angle - 90;
xform.TranslateX = gridCenter.X - textSize.Height / 2;
xform.TranslateY = gridCenter.Y - textSize.Height / 2;
}
}
}
Both ScaleX and ScaleY are set to the scaling factor calculated earlier. The angle parameter passed to the method is relative to the high-noon position, but the TextBlock elements are positioned at 3:00. That's why the Rotation angle offsets the angle parameter by -90 degrees. Both scaling and rotation are relative to CenterX and CenterY, which is a point at the left end of the text, but offset from the upper-left corner by half the text height. Here's the clock in action:
Animating at the Speed of Video
The use of the
DispatcherTimer with a one-second interval makes sense
for the HybridClock program because the positions of the clock hands need to be
updated only once per second.
A timer that is synchronous with the video refresh rate is ideal for animations, and Silverlight provides one in the very easy-to-use CompositionTarget.Rendering event. The event handler looks something like this:
void OnCompositionTargetRendering(object sender, EventArgs args)
{
TimeSpan renderingTime = (argsas RenderingEventArgs).RenderingTime;
...
}
CompositionTarget
is a static class with only one public member,
which is the Rendering event. Install the event handler like so:
CompositionTarget.Rendering
+= OnCompositionTargetRendering;
Unless you're coding a very animation-laden game, you probably don't want
this event handler installed for the duration of your program, so uninstall it
when you're done:
CompositionTarget.Rendering
-= OnCompositionTargetRendering;
The RotatingText project contains a TextBlock in the center of its content
grid:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<TextBlock
Text="ROTATE!"
FontSize="96"
HorizontalAlignment="Center"
VerticalAlignment="Center"
RenderTransformOrigin="0.5
0.5">
<TextBlock.RenderTransform>
<RotateTransform
x:Name="rotate"
/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
Notice the x:Nameattribute on the RotateTransform. You can't use Name here because that's defined by FrameworkElement. The code-behind file starts a CompositionTarget.Renderingevent going in its constructor:
namespace RotatingText
{
public partialclass MainPage : PhoneApplicationPage
{
TimeSpan startTime;
public MainPage()
{
InitializeComponent();
CompositionTarget.Rendering += OnCompositionTargetRendering;
}
void OnCompositionTargetRendering(object sender, EventArgs args)
{
TimeSpan renderingTime = (argsas RenderingEventArgs).RenderingTime;
if (startTime.Ticks == 0)
{
startTime = renderingTime;
}
else
{
TimeSpan elapsedTime = renderingTime - startTime;
rotate.Angle = 180 * elapsedTime.TotalSeconds % 360;
}
}
}
}
The event handler uses the renderingTime to pace the animation so there's one revolution every two seconds.