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