Programmatic Drawing with Silverlight 2.0/3.0 – An Analogue Clock

Download the source project

Image

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.

image  

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

Published Wednesday, August 12, 2009 9:57 PM by REA_ANDREW
Filed under: , ,

Comments

# Links (8/13/2009) &laquo; Steve Pietrek &#8211; Everything SharePoint

Pingback from  Links (8/13/2009) &laquo; Steve Pietrek &#8211; Everything SharePoint

# Daily tech links for .net and related technologies - August 12-17, 2009

Sunday, August 16, 2009 6:34 AM by Sanjeev Agarwal

Daily tech links for .net and related technologies - August 12-17, 2009 Web Development How to use Ninject

# re: Programmatic Drawing with Silverlight 2.0/3.0 – An Analogue Clock

Monday, September 07, 2009 4:34 PM by Todd Henderson

I think this example is great!  However, when I run the code (it runs perfectly) I made some changes and when I re-run, the changes don't apply??  Any ideas?  Changes such as color, break-points and text to be displayed. Thanks in advance, -Todd

# Animated Clocks for SVG and Silverlight

Wednesday, December 30, 2009 3:22 AM by Community Blogs

At one point in time I found a cool clock graphic and I was looking for it again because it gave a fairly

Leave a Comment

(required) 
(required) 
(optional)
(required)