Đorđe Herceg - Personal web site

Adding the clock control to Geometrijica

June 16, 2011.


Back to main page

This example shows how you can add arbitrary Silverlight GUI controls to Geometrijica. Currently, for each new control two additional classes must be implemented - one expression class deriving from GExpression or preferably Fn, and a wrapper class for the control , deriving from VisualBase.

The Clock Silverlight control

We shall base our clock control on the example from the MSDN:

However we will not use the provided C# code, as we want to control the clock hands by ourselves, by binding their values to expressions in Geometrijica. For that we will need to implement three public properties for hours, minutes and seconds. We will then implement the logic to rotate the clock hands when these properties are set.

In Visual Studio, create a new Silverlight user control, and then paste XAML from Listing 1 and C# code from Listing 2 into it.

Listing 1: XAML for the clock control

<!-- 
    Modified from the Microsoft Website:
    Walkthrough: Creating a Silverlight Clock by Using Expression Blend or Visual Studio
    http://msdn.microsoft.com/en-us/library/bb404709(v=vs.95).aspx
    The design of the clock was copied from the above example, but the animation storyboard was
    left out and replaced by logic for positioning of the clock hands. This was also changed
    from the original in that now the hour and minute hands also move slightly between hours and
    minutes as seconds pass.
-->
    <UserControl x:Class="Geometrijica.UI.Controls.Clock"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="400" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Viewbox>
            <Canvas Width="400" Height="400" >

                <!-- Clock Shadow -->
                <Ellipse Width="330" Height="330" Canvas.Left="40" Canvas.Top="40" Fill="Black" Opacity="0.5"/>

                <!-- Outer rim -->
                <Ellipse Stroke="#FF000000" x:Name="outerCircle" Width="330" Height="330" 
         Canvas.Left="32" Canvas.Top="32">
                    <Ellipse.Fill>
                        <!--This linear gradient creates a subtle shadow effect on the outer rim. -->
                        <LinearGradientBrush EndPoint="0.196,0.127" StartPoint="0.852,0.814">
                            <GradientStop Color="#FFC0C0C0" Offset="0.788"/>
                            <GradientStop Color="#FFE4E5F4" Offset="0.995"/>
                        </LinearGradientBrush>
                    </Ellipse.Fill>
                </Ellipse>

                <!-- Bevel -->
                <Ellipse Stroke="#FF000000" Width="290" Height="281" Canvas.Left="52" 
         Canvas.Top="57">
                    <Ellipse.Fill>
                        <!-- This linear gradient creates a subtle shadow effect on 
         the outer rim. -->
                        <LinearGradientBrush EndPoint="0.867,0.848" StartPoint="0.232,0.126">
                            <GradientStop Color="black" Offset="0.1"/>
                            <GradientStop Color="#FFE4E5F4" Offset="0.995"/>
                        </LinearGradientBrush>
                    </Ellipse.Fill>
                </Ellipse>

                <!-- Clock Face -->
                <Ellipse Stroke="#FF000000" Width="273" Height="265"  
         Canvas.Left="60" Canvas.Top="65" Fill="#FF000000"/>

                <!-- Central Clock Circle -->
                <Ellipse Fill="#FF000000" Stroke="#FF008000" StrokeThickness="7" 
         Width="32" Height="31" Canvas.Left="180" Canvas.Top="190"/>

                <!-- Minute Hand -->
                <Rectangle Fill="Green" Width="8" Height="80" Canvas.Left="192.5" Canvas.Top="226"
           RenderTransformOrigin="0.41,-0.26" x:Name="MinutedHand">
                    <Rectangle.RenderTransform>
                        <RotateTransform x:Name="minuteHandTransform"/>
                    </Rectangle.RenderTransform>
                </Rectangle>

                <!-- Hour Hand -->
                <Rectangle Fill="Green" Width="10" Height="60" Canvas.Left="192.5" Canvas.Top="226"
           RenderTransformOrigin="0.35,-0.35" x:Name="HourHand">
                    <Rectangle.RenderTransform>
                        <RotateTransform x:Name="hourHandTransform"/>
                    </Rectangle.RenderTransform>
                </Rectangle>

                <!-- Second Hand -->
                <Rectangle Fill="Red" Width="5" Height="80" Canvas.Left="192.5" Canvas.Top="226"
           RenderTransformOrigin="0.65,-0.26" x:Name="SecondHand" >
                    <Rectangle.RenderTransform>
                        <RotateTransform x:Name="secondHandTransform"/>
                    </Rectangle.RenderTransform>
                </Rectangle>

            </Canvas>
        </Viewbox>
    </Grid>
</UserControl>

Listing 2: C# code behind the XAML. Note that we have removed the original code and replaced it with three simple methods which calculate clock hands' angles depending on the values for hours, minutes and seconds.

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 Geometrijica.UI.Controls
{
    public partial class Clock : UserControl
    {
        public Clock()
        {
            InitializeComponent();
        }

        private int _hour;
        private int _minute;
        private int _second;

        public int Hour
        {
            get { return _hour; }
            set
            {
                _hour = value % 12;
                hourHandTransform.Angle = 180 + _hour * 30 + _minute / 2 + _second / 120;
            }
        }

        public int Minute
        {
            get { return _minute; }
            set
            {
                _minute = value % 60;
                minuteHandTransform.Angle = 180 + _minute * 6 + _second / 10;
            }
        }

        public int Second
        {
            get { return _second; }
            set
            {
                _second = value % 60;
                secondHandTransform.Angle = 180 + _second * 6;
            }
        }

    }
}

Now we are ready to implement the FClock expression class. This class will be imported into Geometrijica as a function with named arguments for location, hours, minutes and seconds.  Create an empty class and paste code from Listing 3 into it. Note that the class is named FClock, which stands for "clock function", but the function name specified in the Function attribute is "Clock". In the constructor we register named arguments, which will act as bindable properties. We also need to register at least one function signature, which will be matched with user input during parsing.

Now we implement properties which will act as wrappers for all four named arguments. We only need to implement the get accessor, as the properties can only be set in the Engine.SetProperty method. These properties will be needed by the VClock class in order to read the values for hours, minutes and seconds.

The Eval method first needs to evaluate the properties, and then to try to cast them to appropriate expression types. If any of the conversions fail, we throw an exception which will be caught by the evaluation engine and transformed into an Error expression. Otherwise, we return this as the result.

The VisualType method returns the VClock class type, which is a wrapper around the Clock user control. This is class will be called by the GeoCanvas to render the clock on screen.

Listing 3. The FClock class

using System;
using Geometrijica.Functions;
using Geometrijica.Expressions;
using Geometrijica.UI;

namespace Geometrijica.Shapes
{
    [Function("Clock")]
    public class FClock: Fn
    {
        static FClock()
        {
            Fn.RegisterSignatures(typeof(FClock),
                new SignatureCollection(
                        new NamedArgsSignature("Location", "Hour", "Minute", "Second")
                    )
                );

            Fn.RegisterNamedArguments(typeof(FClock),
                "Location", "Hour", "Minute", "Second"
                );
        }

        public GExpression Hour
        {
            get { return GetPropertyValue("Hour"); }
        }

        public GExpression Minute
        {
            get { return GetPropertyValue("Minute"); }
        }

        public GExpression Second
        {
            get { return GetPropertyValue("Second"); }
        }

        public GExpression Location
        {
            get { return GetPropertyValue("Location"); }
        }

        public override Expressions.GExpression Eval()
        {
            EvaluateProperties();
            Number h = Hour as Number;
            Number m = Minute as Number;
            Number s = Second as Number;
            SPoint pt = Location as SPoint;
            if ((pt != null) && (h != null) && (m != null) && (s != null))
            {
                return this;
            }
            else
            {
                throw new ArgumentException("Invalid point or time");
            }
        }

        public override Type VisualType
        {
            get
            {
                return typeof(VClock);
            }
        }

        public override string ToString()
        {
            return PrefixForm("Clock", "Location", "Hour", "Minute", "Second");
        }
    }
}

The VClock class inherits from VisualBase, which provides the following members:

  • public GeoCanvas GeoCanvas - used in derived classes to obtain information about the visible region of the Cartesian plane, and call the GeoCanvas.Geo2Scr method to convert point coordinates into screen pixel coordinates;
  • public Canvas DrawingCanvas - the actual Canvas control used for rendering;
  • public Var Variable - the named variable from the Engine this visual is bound to
  • public abstract void RegisterMyVisuals() - called by the GeoCanvas during initialization; the implementer should register all shapes and controls which are to be drawn;
  • public abstract void UpdateScrLocation() - called by the GeoCanvas when the bound Var changes; this can happen many times in rapid succession, so be careful and optimize your code.

Below is the VClock class. Create a new class in Visual Studio and paste this code into it.

Listing 4. The VClock class

using System;
using Geometrijica.Shapes;
using Geometrijica.Expressions;
using Geometrijica.UI.Controls;
using System.Windows;

namespace Geometrijica.UI
{
    public class VClock : VisualBase
    {
        private Clock textBox;

        public VClock()
        {
            textBox = new Clock();
            textBox.Width = 100;
            textBox.Height = 100;
        }

        public override void RegisterMyVisuals()
        {
            GeoCanvas.AddVisual(this, textBox);
        }

        public override void UpdateScrLocation()
        {
            FClock result = (FClock)Variable.Result;
            Point scr = GeoCanvas.Geo2Scr(((SPoint)result.Location).Value);
            textBox.Margin = new Thickness(scr.X, scr.Y, 0, 0);
            textBox.Hour = (int)((Number)result.Hour).Value;
            textBox.Minute = (int)((Number)result.Minute).Value;
            textBox.Second = (int)((Number)result.Second).Value;
        }
    }
}

Now try to compile the project. If you placed your visual control in one of the namespaces which are automatically registered with the evaluation engine, you are done. If not, then you need to call the Engine.RegisterFunctions method before you can use your new function / control in Geometrijica:

public void RegisterFunctions(Assembly assembly, string nameSpace)

This example is already included in the Visual Studio project which is available on SkyDrive.


Back to main page