Stu Smith: Making It Up As I Go Along“ My life working for BinaryComponents, coding, design, and other stuff. ”
I've been trying to get myself up to speed with .NET 3 and 3.5 using Orcas recently, and rather than just "play" I've set myself a little application to write: a "world time" application that sits in the system tray and displays clocks for various timezones around the world. (It's a sufficiently simple application that I should be able to complete it in my spare time, such as I have any, but one that will also be useful to me and hopefully other MicroISVs). I'm writing this as hopefully a little taster for any MFC or WinForms developers who haven't had much of a look at WPF yet. (I'll do some LINQ articles shortly too). I'm going to write this in a kind of "literate programming" style - not complete code, but snippets that could be connected together.
Here's what I've produced so far, so you can see where the articles lead:
Not great, but a starting point.
Each clock is a graphical object (as opposed to say a flow-layout dialog), so we use a canvas:
<!-- Clock.xaml -->
<Canvas Width="100" Height="100" x:Name="_canvas">
<!-- + Background -->
<!-- + Markers -->
<!-- + Hands -->
<!-- + Highlights -->
</Canvas>
The width and height I've set don't really matter since we can scale the clock to whatever size we like, but it means that my measurements inside the clock can be in percentages.
Starting with the background, I want a circle with a graded fill from top to bottom, surrounded by a white "glow". (Eventually this is going to pop-up as a desktop widget, so I want the clocks to have a border to distinguish them from the user's desktop).
<!-- Clock.xaml, * Background -->
<Ellipse Canvas.Left="0" Canvas.Top="0" Width="100" Height="100">
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Offset="0.0" Color="White" />
<GradientStop Offset="0.95" Color="White" />
<GradientStop Offset="1.0" Color="Transparent" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Canvas.Left="3" Canvas.Top="3" Width="94" Height="94">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0.4,0.1" EndPoint="0.6,0.9">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.0" Color="#888888" />
<GradientStop Offset="1.0" Color="#111111" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
I originally used an "outer glow" bitmap effect, but under animation is wobbled a bit. So we now have:
That's the easy bit done. Markers next. Although the idea of WPF is to include the graphical elements in the XAML, for the little markers around the edge that would be silly - dozens of nearly identical elements says "loop" to me and for that we need code. The XAML is just a placeholder:
<!-- Clock.xaml, * Markers -->
<Canvas x:Name="_markersCanvas" />
And the actual elements are added in code:
// Clock.xaml.cs
protected override void OnInitialized( EventArgs e )
{
base.OnInitialized( e );
for( int i = 0; i < 60; ++i )
{
Rectangle marker = new Rectangle();
if( ( i % 5 ) == 0 )
{
marker.Width = 3;
marker.Height = 8;
marker.Fill = new SolidColorBrush( Color.FromArgb( 0xe0, 0xff, 0xff, 0xff ) );
marker.Stroke = new SolidColorBrush( Color.FromArgb( 0x80, 0x33, 0x33, 0x33 ) );
marker.StrokeThickness = 0.5;
}
else
{
marker.Width = 0.5;
marker.Height = 3;
marker.Fill = new SolidColorBrush( Color.FromArgb( 0x80, 0xff, 0xff, 0xff ) );
marker.Stroke = null;
marker.StrokeThickness = 0;
}
TransformGroup transforms = new TransformGroup();
transforms.Children.Add( new TranslateTransform( -( marker.Width / 2 ), marker.Width / 2 - 40 - marker.Height ) );
transforms.Children.Add( new RotateTransform( i * 6 ) );
transforms.Children.Add( new TranslateTransform( 50, 50 ) );
marker.RenderTransform = transforms;
_markersCanvas.Children.Add( marker );
}
for( int i = 1; i <= 12; ++i )
{
TextBlock tb = new TextBlock();
tb.Text = i.ToString();
tb.TextAlignment = TextAlignment.Center;
tb.RenderTransformOrigin = new Point( 1, 1 );
tb.Foreground = Brushes.White;
tb.FontSize = 4;
tb.RenderTransform = new ScaleTransform( 2, 2 );
double r = 34;
double angle = Math.PI * i * 30.0 / 180.0;
double x = Math.Sin( angle ) * r + 50, y = -Math.Cos( angle ) * r + 50;
Canvas.SetLeft( tb, x );
Canvas.SetTop( tb, y );
_markersCanvas.Children.Add( tb );
}
}
That's a fair bit of code, but it goes to show that there's nothing magical about XAML - it's just a convenient way of creating elements, and we can do the same in code, albeit in a slightly long-winded way. The markers are just rectangles; to position them I just position them at the top-center of the canvas and rotate around the center. I couldn't find a way to exactly position centered text on a canvas, so in the end I used the following technique:
We have the basic background now.
I'll cover the hands and the "highlights" in a separate article since this is getting to be a bit long, but hopefully you can see how things are starting to fit together. For me the most important thing in WPF compared to WinForms or MFC is something that isn't in this article, and indeed won't be because we just don't need it: there's no WM_PAINT or OnPaint handler. Everything I've done so far is done once -- the XAML just "sits there", the OnInitialized method is called once -- and thereafter WPF takes over.