Download the source project
Image
Working in Silverlight
I have seen a lot of comparisons made between ActionScript and Silverlight with specific focus on the difference in lines of code, and more specifically when referring to the drawing api. What I thought I would do is do a little drawing and in this example I have developed a teeny tiny interface for a clock view, which has a method of simply SetTime(DateTime time) and I have made one implementation of this Clock view which is an analogue clock. I have used some simple ellipse equations to allow for dynamic resize and redraw and in hind sight I would refactor my code so as not to keep adding and removing the shapes which are UIElements. I will make some more clocks with this refactoring present. It does feel like a gauge control is on the way also, there are so many gauge controls out there including the, cool, free ones from Microsoft, well they are charting controls but never the less and absolutely amazing freebie.
So the whole purpose of this post is about programmatic drawing as opposed to using the XAML equivalent, which I might say would be worth looking at to replicate this example!
The Code
So to the code, first I have defined a short interface as follows:
namespace SilverlightClock
{
public interface IClockView
{
void setTime(DateTime time);
}
}
Next is the xaml mark-up, and the only thing I have amended is the Root UIElement which I have used a Canvas as opposed to the default Grid. This lets me set things out using the Canvas.LeftProperty and Canvas.TopProperty.
<UserControl x:Class="SilverlightClock.AnalogueClock"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Canvas x:Name="LayoutRoot" Background="White">
</Canvas>
</UserControl>
Next is the mark-up and class diagram for the actual UserControl - AnalogueClock.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace SilverlightClock
{
public partial class AnalogueClock : UserControl, IClockView
{
private DateTime _time = new DateTime(2000, 1, 1, 18, 15, 50);
private double hour;
private double minute;
private double second;
public AnalogueClock()
{
InitializeComponent();
SizeChanged += new SizeChangedEventHandler(AnalogueClock_SizeChanged);
Loaded += new RoutedEventHandler(AnalogueClock_Loaded);
}
void AnalogueClock_SizeChanged(object sender, SizeChangedEventArgs e)
{
Draw();
}
void AnalogueClock_Loaded(object sender, RoutedEventArgs e)
{
Draw();
}
void Draw()
{
LayoutRoot.Children.Clear();
var step = 360 / 60;
var innerRadiusX = (Width * 0.7) / 2;
var innerRadiusY = (Height * 0.7) / 2;
var outerRadiusX = (Width * 0.8) / 2;
var outerRadiusY = (Height * 0.8) / 2;
var textRadiusX = (Width * 0.9) / 2;
var textRadiusY = (Height * 0.9) / 2;
var outerCasing = new Ellipse();
outerCasing.Stroke = new SolidColorBrush(Colors.Black);
outerCasing.Width = (Width * 0.8);
outerCasing.Height = (Height * 0.8);
outerCasing.SetValue(Canvas.LeftProperty, Width * 0.1);
outerCasing.SetValue(Canvas.TopProperty, Height * 0.1);
LayoutRoot.Children.Add(outerCasing);
for (var i = 0; i < 60; i++)
{
var line = new Line
{
Stroke = new SolidColorBrush(Colors.Black),
X1 = (Width / 2) + Math.Sin((step * i) * (Math.PI / 180)) * innerRadiusX,
Y1 = (Height / 2) + Math.Cos((step * i) * (Math.PI / 180)) * innerRadiusY,
X2 = (Width / 2) + Math.Sin((step * i) * (Math.PI / 180)) * outerRadiusX,
Y2 = (Height / 2) + Math.Cos((step * i) * (Math.PI / 180)) * outerRadiusY
};
if (i % 5 == 0)
{
line.X1 = (Width / 2) + Math.Sin((step * i) * (Math.PI / 180)) * ((Width * 0.6) / 2);
line.Y1 = (Height / 2) + Math.Cos((step * i) * (Math.PI / 180)) * ((Height * 0.6) / 2);
var textblock = new TextBlock();
textblock.Text = i == 0 ? "12" : ((double)(i / 60D) * 12D).ToString();
var textX = (Width / 2) + Math.Sin(-((step * i + 180) % 360) * (Math.PI / 180)) * textRadiusX;
var textY = (Height / 2) + Math.Cos(-((step * i + 180) % 360) * (Math.PI / 180)) * textRadiusY;
textblock.SetValue(Canvas.LeftProperty, textX - textblock.ActualWidth / 2);
textblock.SetValue(Canvas.TopProperty, textY - textblock.ActualHeight / 2);
LayoutRoot.Children.Add(textblock);
}
LayoutRoot.Children.Add(line);
}
DrawHourHand();
DrawMinuteHand();
DrawSecondHand();
DrawMilliSeconds();
DrawLogo();
}
private void DrawLogo()
{
var textBlockLogo = new TextBlock();
textBlockLogo.Text = "andrewrea.co.uk";
textBlockLogo.FontFamily = new FontFamily("Arial");
textBlockLogo.FontSize = 9D;
textBlockLogo.SetValue(Canvas.LeftProperty, (Width - textBlockLogo.ActualWidth) / 2);
textBlockLogo.SetValue(Canvas.TopProperty, (Height - textBlockLogo.ActualHeight) / 3);
LayoutRoot.Children.Add(textBlockLogo);
}
private void DrawHourHand()
{
//Change hour value to percentage for use with 360
double hourPercentage = (hour + (minute / 60D)) / 12D;
//Get the Hour degree value
double hourDegree = 360 * hourPercentage;
DrawHand(Width / 5.5D, Height / 5.5D, -hourDegree, Colors.Black, 3);
}
private void DrawMinuteHand()
{
//Change minute value to percentage for use with 360
double minutePercentage = (minute + (second / 60D)) / 60;
//Get the minute percentage
double minuteDegree = 360 * minutePercentage;
DrawHand(Width / 4.5D, Height / 4.5D, -minuteDegree, Colors.Blue, 2);
}
private void DrawSecondHand()
{
double secondPercentage = second / 60D;
//Get the minute percentage
double secondDegree = 360 * secondPercentage;
DrawHand(Width / 3.5D, Height / 3.5D, -secondDegree, Colors.Red, 1);
}
private void DrawMilliSeconds()
{
//Figure out the second hand
double millisecond = _time.Millisecond;
//Change minute value to percentage for use with 360
double millisecondPercentage = millisecond / 1000;
//Get the minute percentage
double millisecondDegree = 360 * millisecondPercentage;
var milliContainer = new Ellipse();
milliContainer.Width = (Width * 0.1D) + 1;
milliContainer.Height = (Height * 0.1D) + 1;
milliContainer.Stroke = new SolidColorBrush(Colors.Black);
milliContainer.SetValue(Canvas.LeftProperty, (Width * 0.65) - (Width * 0.05D));
milliContainer.SetValue(Canvas.TopProperty, (Height * 0.65) - (Height * 0.05D));
var hand = new Line
{
Stroke = new SolidColorBrush(Colors.Green),
X1 = Width * 0.65,
Y1 = Height * 0.65,
X2 = (Width * 0.65) + -Math.Sin(-millisecondDegree * (Math.PI / 180)) * (Width * 0.05D),
Y2 = (Height * 0.65) + -Math.Cos(-millisecondDegree * (Math.PI / 180)) * (Height * 0.05D)
};
LayoutRoot.Children.Add(milliContainer);
LayoutRoot.Children.Add(hand);
}
private void DrawHand(double radiusX, double radiusY, double angle, Color color, double thickness)
{
var hand = new Line
{
Stroke = new SolidColorBrush(color),
X1 = Width / 2,
Y1 = Height / 2,
X2 = (Width / 2) + -Math.Sin(angle * (Math.PI / 180)) * radiusX,
Y2 = (Height / 2) + -Math.Cos(angle * (Math.PI / 180)) * radiusY
};
hand.StrokeThickness = thickness;
LayoutRoot.Children.Add(hand);
}
#region IClockView Members
public void SetTime(DateTime time)
{
_time = time;
hour = _time.Hour;
minute = _time.Minute;
second = _time.Second;
Draw();
}
#endregion
}
}
Next is the xaml mark-up for the actual page.xaml . Really I could have created a Presenter for this, but I haven’t. The setup is primed for one since I am declaring an interface for an actual clock view, and like many examples in many other programming languages, a good second view for this interface would be a DigitalClock user control. I think that it would be a nice second example to use to have a deeper look into how we can skin it up to such an extent for it to resemble to classic 80’s style red digit alarm clock.

So the xaml mark-up. This is simply the grid layout, labels and instances of the user control.
<UserControl x:Class="SilverlightClock.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:x2="clr-namespace:SilverlightClock"
Width="600" Height="460">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition />
<RowDefinition Height="30" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="London" Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center" FontWeight="ExtraBlack"></TextBlock>
<x2:AnalogueClock Height="200" Width="200" x:Name="ClockLondon"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Grid.Column="0" Grid.Row="1"></x2:AnalogueClock>
<TextBlock Text="New York" Grid.Column="1" Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center" FontWeight="ExtraBlack"></TextBlock>
<x2:AnalogueClock Height="200" Width="200" x:Name="ClockNewyork"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Grid.Column="1" Grid.Row="1"></x2:AnalogueClock>
<TextBlock Text="Sydney" Grid.Column="2" Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center" FontWeight="ExtraBlack"></TextBlock>
<x2:AnalogueClock Height="200" Width="200" x:Name="ClockSydney"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Grid.Column="2" Grid.Row="1"></x2:AnalogueClock>
<TextBlock Text="Paris" Grid.ColumnSpan="3" Grid.Row="2"
HorizontalAlignment="Center"
VerticalAlignment="Center" FontWeight="ExtraBlack"></TextBlock>
<x2:AnalogueClock Height="200" Width="600" x:Name="ClockParis"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Grid.ColumnSpan="3" Grid.Row="3"></x2:AnalogueClock>
</Grid>
</UserControl>
The last part is the code behind for this page.xaml, and it is something which I think I should have probably used a storyboard for, but I am not too sure. Either way I have used a timer and it is a bit too processor intensive, I think I would want to use the Silverlight equivalent of the flash Event.ENTER_FRAME and I say equivalent because Silverlight does not use frames 
namespace SilverlightClock
{
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
Loaded += new RoutedEventHandler(Page_Loaded);
}
void dt_Tick(object sender, EventArgs e)
{
ClockLondon.SetTime(DateTime.Now);
ClockNewyork.SetTime(DateTime.Now.AddHours(-5));
ClockSydney.SetTime(DateTime.Now.AddHours(10));
ClockParis.SetTime(DateTime.Now.AddHours(2));
}
void Page_Loaded(object sender, RoutedEventArgs e)
{
System.Windows.Threading.DispatcherTimer dt = new System.Windows.Threading.DispatcherTimer();
dt.Interval = new TimeSpan(0, 0, 0, 0, 100); // 500 Milliseconds
dt.Tick += new EventHandler(dt_Tick);
dt.Start();
}
}
}
I am setting the time zones using the DateTime methods and the rest is simply using the interface to set the time of the view.
I hope this is of some use and hopefully of interest to you.
Cheers for now,
Andrew