Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Binary Clock 3D Screensaver in WPF

0.00/5 (No votes)
20 Jan 2009 1  
Screensaver of a 3D binary clock - demonstrating WPF's layout possibilities, styling/templating and data binding
BinaryClock_Overview.png

Table of Contents

Introduction

Although you may find several binary clock projects throughout the web, this article will show you not only how to create a binary clock, but also how to host it in space and use it as a 3D rotating screensaver – all of this utilizing Windows Presentation Foundation and its powerful concepts such as data binding, data templates, styling, automatic layout, animation and 3D support.

This binary clock project originated as a mere experiment to help me understand the above WPF concepts. What you see here is therefore part of my WPF learning process, which I thought might be useful also for other enthusiasts of the technology.

Background

Maybe you already know the concept of a Binary Clock: it's a clock where you read the 6 digits of current time as 6 columns, each consisting of some lit and unlit LEDs. Actually, we can better call it a pseudo-binary clock, since it shows the digits in BCD (binary coded decimal) notion. If you like the concept, but don't have it on your desk yet, then you can have it at least as your screensaver by downloading the attached binaries.

The Core - Binary Clock

First, the binary clock has a simple task: to show its six digits, one by one. Secondly, the digits themselves don't even need to know anything about their visual representation, thus to represent a BCD digit, it’s fully enough to have a purely data class for them. The idea behind displaying a data class is to apply a DataTemplate to it. This way we could simply put six instances of the Digit class into some items container of our window and leave the rest of the look-and-feel-work to be done by our DataTemplate.

This is what the basic structure of that template looks like:

<DataTemplate DataType="{x:Type local:Digit}">

    <DataTemplate.Resources>
        ...
    </DataTemplate.Resources>

    <!-- BCD Digits consist of 4 bits -->
    <StackPanel Orientation="Vertical">

        <Rectangle Style="{StaticResource fourthBit}" />
        <Rectangle Style="{StaticResource thirdBit}" />
        <Rectangle Style="{StaticResource secondBit}" />
        <Rectangle Style="{StaticResource firstBit}" />

    </StackPanel>
</DataTemplate> 

Each BCD digit consists of four binary digits/bits. They're stacked one above the other, the least significant bit being placed at the bottom and the most significant bit at the top.

It’s the style applied to any specific bit which tells whether that bit needs to be lit or unlit at a certain moment of time. For example, the least significant bit needs to be lit if the BCD digit’s value modulo 2 gives nonzero (that is, every other second of time). Similarly, the bit above the least significant bit needs to be lit if the BCD digit’s value modulo 4 gives a result of 2 or greater number, etc. This logic can be generalized by using a WPF value converter, having some simple modulo computations inside. The DigitModuloConverter class will tell us if the specified modulo of any integer value is greater than the supplied parameter or not.

class DigitModuloConverter : DependencyObject, IValueConverter
{
    public static DependencyProperty ModuloModeProperty = ...
    public int ModuloMode { ... }
    public object Convert(object value, Type targetType, object parameter,
                          System.Globalization.CultureInfo culture)
    {
        int source = (int)value;
        int param = 0;
        Int32.TryParse(parameter as string, out param);
        return (source % this.ModuloMode > param);
    }
    ...
}

Having that converter in our toolbox, we can directly convert a BCD digit’s value to the on/off state of its bits. This is achieved by Data Triggers of WPF: each bit will use a highlighted fill and highlighted stroke color whenever its assigned DigitModuloConverter tells so. Otherwise, it remains or switches back to a basic, dark color.

<DataTemplate.Resources>

    <!-- Digit converters -->    
    <local:DigitModuloConverter x:Key="moduloTwoConverter" ModuloMode="2" />
    <local:DigitModuloConverter x:Key="moduloFourConverter" ModuloMode="4" />
    <local:DigitModuloConverter x:Key="moduloEightConverter" ModuloMode="8" />
    <local:DigitModuloConverter x:Key="moduloSixteenConverter" ModuloMode="16" />

        ...
    <!-- Appearance of 1st bit in Digit (the least significant one) -->
    <Style TargetType="Rectangle" x:Key="firstBit"
                                  BasedOn="{StaticResource bitsCommonStyle}">
        <Style.Triggers>

            <DataTrigger Binding="{Binding Path=Value,
                                   Converter={StaticResource moduloTwoConverter}}"
                                   Value="True">
                <Setter Property="Stroke" 
			Value="{DynamicResource fullRectBorderBrush}" />

                <Setter Property="Fill" Value="{DynamicResource fullRectBrush}" />
            </DataTrigger>
        </Style.Triggers>
    </Style>

    ...
</DataTemplate.Resources>

When all of this is set up, it’s enough to have a timer which updates our BCD digit values each second. The BCD digits – i.e. instances of the Digit class – are declared in XAML and their update is done in the setTimeToDigits() function.

private void setTimeToDigits()
{
    System.DateTime now = System.DateTime.Now;

    this.hourUpper.Value = (int)now.Hour / 10;
    this.hourLower.Value = now.Hour % 10;

    this.minUpper.Value = (int)now.Minute / 10;
    this.minLower.Value = now.Minute % 10;

    this.secUpper.Value = (int)now.Second / 10;
    this.secLower.Value = now.Second % 10;
}

The Digit instances need to have their IsUpperDigit and IsHourDigit properties properly set. The styles of the bits read these attributes and hide some unnecessary bits based on them. E.g. the BCD digit of “tens” of hours will have the upper 2 bits hidden, because we have only 24 hours in a day. Also, the BCD digit of “tens” of minutes will have the most significant bit hidden, since even to display 59 minutes we need only 3 bits for the “tens” part.

<StackPanel ...>
    <StackPanel ...>
        <ContentControl>
            <local:Digit x:Name="hourUpper" IsUpperDigit="True" IsHourDigit="True"/>

        </ContentControl>

        <ContentControl>
            <local:Digit x:Name="hourLower" IsHourDigit="True" />
        </ContentControl>

    </StackPanel>
    <StackPanel ...>
        <ContentControl>

            <local:Digit x:Name="minUpper" IsUpperDigit="True" />

        </ContentControl>
        <ContentControl>
            <local:Digit x:Name="minLower" />
        </ContentControl>
    </StackPanel>

    <StackPanel ...>
        <ContentControl>
            <local:Digit x:Name="secUpper" IsUpperDigit="True" />
        </ContentControl>

        <ContentControl>

            <local:Digit x:Name="secLower" />
        </ContentControl>
    </StackPanel>
</StackPanel>

Screensaver in 3D and Configuration with Live Preview

What a Windows screensaver needs to be is basically an *.exe module accepting some well-defined command line arguments. A screensaver primarily needs to display a full screen window, but it should also support a preview mode and provide some optional settings widow for customization.

This project goes even further: it supports not only a simple preview mode, but inside the settings window it offers also a live preview to interactively show how each change will affect the output. 

BinaryClock_Settings_Video.png 

Click to see the video demonstrating the Live Preview.

This could be achieved by reusing the BinaryClockPage class, which provides the core functionality for the binary clock (see the previous section).

The 3D effect of the clock is achieved by adding the Digit class instances into a ViewPort3D object. This way we project 2D Rectangles to a 3D surface in space:

<Viewport3D>

    ...
    <!-- 2D elements on 3D surface -->

    <Viewport2DVisual3D>
        <!-- Give the plane a slight rotation -->
        <Viewport2DVisual3D.Transform>
            <RotateTransform3D>
                <RotateTransform3D.Rotation>

                    <AxisAngleRotation3D x:Name="planeRotation"
                                         Angle="0"
                                         Axis="0, -1, -0.17" />

                </RotateTransform3D.Rotation>

            </RotateTransform3D>
        </Viewport2DVisual3D.Transform>

        <!-- The Geometry, Material, and Visual for the Viewport2DVisual3D -->
        <Viewport2DVisual3D.Geometry>

            <MeshGeometry3D ... />
        </Viewport2DVisual3D.Geometry>

        <Viewport2DVisual3D.Material>
            <DiffuseMaterial Viewport2DVisual3D.IsVisualHostMaterial="True"
                             Brush="White"/>
        </Viewport2DVisual3D.Material>
        ...
    </Viewport2DVisual3D>

    ...
</Viewport3D>

The binary clock has a rotating animation around the vertical axis. The speed of this rotation can be adjusted through the settings window with the help of a simple slider. Another slider serves for setting the camera distance, thus making the binary clock appear closer or further from the screen.  

Every time the slider for rotation speed is moved, the rotation animation needs to be restarted to apply the new animation duration.

Multi-monitor Support

The screensaver will handle multiple monitors present in your system in a very straightforward way: it will replicate the same content on each monitor. Should you notice that the clocks in these different instances are not always synchronized then yes, you're simply right. This visual side effect is caused by the fact that each clock instance uses its own timer, started only after the previous instance was created. The issue can possibly be addressed in some future update of the article.

Skinning Support

Skinning of BinaryClock screensaver is achieved utilizing WPF's ResourceDictionary concept. This allows us to change skins in runtime. Each "skin" is placed into a separate XAML file containing a ResourceDictionary, so that we can switch to use any of these XAML files in runtime. Each ResourceDictionary defines the same brush objects, but with different color settings. Thus, simply by loading a different ResourceDictionary in runtime, our binary clock colors will be automatically switched to the new skin (this is fully transparent to us, since we reference these brushes as DynamicResource objects).

Code snippet from the XAML file defining the "Blue" skin:

<ResourceDictionary ...>
    <!-- Unlit LED border color -->
    <SolidColorBrush x:Key="emptyRectBorderBrush" Color="#FF00087F" />
	...
</ResourceDictionary>

This is how the skin's elements are used by BinaryClock:

<!-- Basic appearance of all Digits -->
<Style TargetType="Rectangle" x:Key="bitsCommonStyle">
	<Setter Property="Stroke" Value="{DynamicResource emptyRectBorderBrush}" />
	...
</Style>

Future Enhancements

If you like this screensaver and have some proposals for improvements, please feel free to let me know, as I still plan to add some new features in the future. These may include new settings to adjust 3D effects, possibility to have a readable, decimal hint, but also bugfixes, etc. 

History 

  • 18 Jan 2009: Skinning support
  • 23 Dec 2008: Small bugfix for multi-monitor systems, based on a reader comment
  • 22 Dec 2008: Initial revision

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here