1
votes

I am learning animations in WPF and so, I decided to create an analogue clock.

Please take a look at my code so that I can explain my problem clearly :

<Window x:Class="Animation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:PresentationOptions="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:self="clr-namespace:Animation"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <self:MainWindowViewModel />
    </Window.DataContext>

    <Window.Resources>
        <self:SecondsToAngleConverter x:Key="secondsToAngleConverter" />
        <self:HoursToAngleConverter x:Key="hoursToAngleConverter" />
    </Window.Resources>

    <Canvas>
        <Ellipse Height="150" Width="150" Fill="Orange" />
        <TextBlock Visibility="Collapsed" Text="{Binding DataContext.Second, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Converter={StaticResource secondsToAngleConverter}}" Width="160" Canvas.Left="10" Canvas.Top="190" />
        <Line x:Name="secondHand" X1="75" Y1="75" X2="75" Y2="10" Stroke="Red" RenderTransformOrigin="1,1">
            <Line.RenderTransform>
                <RotateTransform  x:Name="secondHandRotateTransform"/>
            </Line.RenderTransform>
            <Line.Triggers>
                <EventTrigger RoutedEvent="Line.Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="secondHandRotateTransform" Storyboard.TargetProperty="Angle"
                                             To="{Binding DataContext.Second, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Converter={StaticResource secondsToAngleConverter}}" 
                                             Duration="0:0:1"/>
                            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="secondHandRotateTransform" Storyboard.TargetProperty="Angle" Duration="0:0:1" RepeatBehavior="Forever" IsCumulative="True">
                                <DiscreteDoubleKeyFrame Value="6" />
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Line.Triggers>
        </Line>
        <Line X1="75" Y1="75" X2="75" Y2="10" Stroke="Black" StrokeThickness="3" RenderTransformOrigin="1,1">
            <Line.RenderTransform>
                <RotateTransform  x:Name="minuteHandRotateTransform"/>
            </Line.RenderTransform>
            <Line.Triggers>
                <EventTrigger RoutedEvent="Line.Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="minuteHandRotateTransform" Storyboard.TargetProperty="Angle"
                                             To="{Binding DataContext.Minute, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Converter={StaticResource secondsToAngleConverter}}" 
                                             Duration="0:0:1"/>
                            <DoubleAnimation Storyboard.TargetName="minuteHandRotateTransform" Storyboard.TargetProperty="Angle"
                                              IsCumulative="True" By="6" RepeatBehavior="Forever" 
                                             Duration="0:1:0"/>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Line.Triggers>
        </Line>
        <Line X1="75" Y1="75" X2="75" Y2="25" Stroke="Black" StrokeThickness="3" RenderTransformOrigin="1,1">
            <Line.RenderTransform>
                <RotateTransform  x:Name="hourHandRotateTransform"/>
            </Line.RenderTransform>
            <Line.Triggers>
                <EventTrigger RoutedEvent="Line.Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="hourHandRotateTransform" Storyboard.TargetProperty="Angle"
                                             To="{Binding DataContext.Hour, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Converter={StaticResource hoursToAngleConverter}}" 
                                             Duration="0:0:1"/>
                            <DoubleAnimation Storyboard.TargetName="hourHandRotateTransform" Storyboard.TargetProperty="Angle"
                                              IsCumulative="True" By="30" RepeatBehavior="Forever" 
                                             Duration="1:0:0"/>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Line.Triggers>
        </Line>
    </Canvas>
</Window>

The ViewModel code :

public class MainWindowViewModel
{
    public MainWindowViewModel()
    {
        Hour = DateTime.Now.Hour;
        Minute = DateTime.Now.Minute;
        Second = DateTime.Now.Second;
    }

    public static int Hour { get; set; }
    public static int Minute { get; set; }
    public static int Second { get; set; }

}

Code for converters :

SecondsToAngleConverter.cs:

public class SecondsToAngleConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null || value == DependencyProperty.UnsetValue)
        {
            return (6 * DateTime.Now.Second);
        }
        else
        {
            return (6 * (int)value);
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

HoursToAngleConverter.cs:

public class HoursToAngleConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null || value == DependencyProperty.UnsetValue)
        {
            if (DateTime.Now.Hour > 12)
            {
                return (30 * (DateTime.Now.Hour - 12));
            }
            else
            {
                return (30 * DateTime.Now.Hour);
            }
        }
        else
        {
            if ((int)value > 12)
            {
                return (30 * ((int)value - 12));
            }
            else
            {
                return (30 * (int)value);
            }
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Scenario :

  1. Seconds, minutes & hours hands are animated for 1 second to set them to the correct DateTime.Now and then these animations stops.
  2. The animations for animating second hand, minute hand and hour hand begins now.
  3. These animations do not depend on each other.

Problems :

When program starts

  1. Animation for minute hand starts which is given a duration of 1 minute to rotate 6 degrees on its right
  2. If DateTime.Now.Second returns a value greater than 0, then second hand will need less than 60 seconds to reach 12 on the clock. But at the same time the animation clock of minute hand will not have reached the 1 minute, so there will be some difference in the position of the minute hand than a physical clock.

Similarly

  1. Animation for hour hand starts which is given a duration of 1 hour to rotate 30 degrees on its right
  2. If DateTime.Now.Minute returns a value greater than 0, then minute hand will need less than 60 minutes to reach 12 on the clock. But at the same time the animation clock of hour hand will not have reached the 1 hour, so There will be some difference in the position of hour hand than a physical clock.
4
is it possible for you to share a working sample?pushpraj
So if I understand it properly, your minute and hour hands don't update appropriately if a minute or hour is already partially underway?Nzall
@NateKerkhofs yes that's the exact problemVishal

4 Answers

1
votes

Sample 1 :Basic simple approach to design an analogue clock.

 <Grid>
    <Rectangle Fill="Black"   Height="5" Width="200" HorizontalAlignment="Center" VerticalAlignment="Center" >
        <Rectangle.RenderTransform>
            <RotateTransform x:Name="hrHand" />
        </Rectangle.RenderTransform>
        <Rectangle.Triggers>
            <EventTrigger RoutedEvent="Loaded">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation  Duration="12:0:0" To="360" RepeatBehavior="Forever" Storyboard.TargetName="hrHand" Storyboard.TargetProperty="Angle"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Rectangle.Triggers>
    </Rectangle>
    <Rectangle Fill="Black" Height="2" Width="200" HorizontalAlignment="Center" VerticalAlignment="Center" >
        <Rectangle.RenderTransform>
            <RotateTransform x:Name="minHand" />
        </Rectangle.RenderTransform>
        <Rectangle.Triggers>
            <EventTrigger RoutedEvent="Loaded">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation  Duration="1:0:0" To="360" RepeatBehavior="Forever"  Storyboard.TargetName="minHand" Storyboard.TargetProperty="Angle"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Rectangle.Triggers>
    </Rectangle>
    <Rectangle Fill="Red"    Height="1" Width="200"   HorizontalAlignment="Center" VerticalAlignment="Center" >
        <Rectangle.RenderTransform>
            <RotateTransform x:Name="sechand"/>
        </Rectangle.RenderTransform>
        <Rectangle.Triggers>
            <EventTrigger RoutedEvent="Loaded">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation  Duration="0:1:0" RepeatBehavior="Forever" To="360"  Storyboard.TargetName="sechand" Storyboard.TargetProperty="Angle"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Rectangle.Triggers>
    </Rectangle>
</Grid>

enter image description here


Sample 2 :Approach using current time

xaml code:ContentControl and code in window resource used for design purpose only.

<Window x:Class="WpfApplication8.Clock"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Clock" Height="900" Width="1600">
<Window.Resources>
    <Grid x:Key="DesignGrid" Height="500" Width="500" >
        <Grid.Clip>
            <EllipseGeometry RadiusX="500" Center="250,250" RadiusY="500"/>              
        </Grid.Clip>
        <Ellipse Height="900" Width="900" Fill="White"/>
        <Rectangle Height="2" Fill="Black"/>
        <Rectangle Height="2" Fill="Black" RenderTransformOrigin="0.5,0.5">
            <Rectangle.RenderTransform>
                <RotateTransform Angle="30"/>
            </Rectangle.RenderTransform>
        </Rectangle>
        <Rectangle Height="2" Fill="Black" RenderTransformOrigin="0.5,0.5">
            <Rectangle.RenderTransform>
                <RotateTransform Angle="60"/>
            </Rectangle.RenderTransform>
        </Rectangle>
        <Rectangle Height="2" Fill="Black" RenderTransformOrigin="0.5,0.5">
            <Rectangle.RenderTransform>
                <RotateTransform Angle="90"/>
            </Rectangle.RenderTransform>
        </Rectangle>
        <Rectangle Height="2" Fill="Black" RenderTransformOrigin="0.5,0.5">
            <Rectangle.RenderTransform>
                <RotateTransform Angle="120"/>
            </Rectangle.RenderTransform>
        </Rectangle>
        <Rectangle Height="2" Fill="Black" RenderTransformOrigin="0.5,0.5">
            <Rectangle.RenderTransform>
                <RotateTransform Angle="150"/>
            </Rectangle.RenderTransform>
        </Rectangle>
        <Ellipse Height="400" Width="400" Fill="White"/>
    </Grid>       
</Window.Resources>
<Grid Name="gd">
    <Grid.Resources>
        <Storyboard x:Key="animation">
            <DoubleAnimation  Duration="0:1:0" RepeatBehavior="Forever"  Storyboard.TargetName="hand" Storyboard.TargetProperty="Angle"/>
            <DoubleAnimation  Duration="1:0:0" RepeatBehavior="Forever"  Storyboard.TargetName="minHand" Storyboard.TargetProperty="Angle"/>
            <DoubleAnimation  Duration="12:0:0" RepeatBehavior="Forever" Storyboard.TargetName="hrHand" Storyboard.TargetProperty="Angle"/>
        </Storyboard>
    </Grid.Resources>
    <ContentControl Content="{StaticResource DesignGrid}"></ContentControl>
    <Grid Margin="200,0,0,0" Height="500" Width="500" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Rectangle Fill="Black"   Height="5" Width="200" HorizontalAlignment="Center" VerticalAlignment="Center" >
            <Rectangle.RenderTransform>
                <RotateTransform x:Name="hrHand" />
            </Rectangle.RenderTransform>
        </Rectangle>
        <Rectangle Fill="Black" Height="2" Width="200" HorizontalAlignment="Center" VerticalAlignment="Center" >
            <Rectangle.RenderTransform>
                <RotateTransform x:Name="minHand" />
            </Rectangle.RenderTransform>
        </Rectangle>
        <Rectangle Fill="Red"    Height="1" Width="200" HorizontalAlignment="Center" VerticalAlignment="Center" >
            <Rectangle.RenderTransform>
                <RotateTransform x:Name="hand"/>
            </Rectangle.RenderTransform>

        </Rectangle>
    </Grid>
</Grid>

c# code

InitializeComponent();
        this.Loaded += Clock_Loaded;
    }

    void Clock_Loaded(object sender, RoutedEventArgs e)
    {
        var sb = gd.Resources["animation"] as Storyboard;

        var sec = sb.Children[0] as DoubleAnimation;
        var min = sb.Children[1] as DoubleAnimation;
        var hr = sb.Children[2] as DoubleAnimation;

        sec.From = DateTime.Now.Second * 6 - 90;
        min.From = (DateTime.Now.Minute + ((double)DateTime.Now.Second / 60.0)) * 6 - 90;
        hr.From = (DateTime.Now.Hour + ((double)DateTime.Now.Minute / 60.0)) * 30 - 90;

        sec.To = sec.From + 360;
        min.To = min.From + 360;
        hr.To = hr.From + 360;
        sb.Begin();
    }

Explanation enter image description hereenter image description here


Result

enter image description here

0
votes

You should do what a regular analog clock does, and work with a timer on a complete circle. So instead of making your minute hand go 6 degrees per minute, you should let it go 360 degrees per hour. And instead of your hour hand going 30 degrees per hour, it should go 360 degrees per 12 hours.

  1. Calculate how many degrees your minute hand is past the start of the hour, up to .1 degree (1 second) and move it there. the math is: ((minutes*60)+seconds)/10.
  2. once there, start a 1 hour timer that makes the minute hand go 360 degrees, with .1 degree every second.
  3. If you want your timer to first go to 1 hour, you need to first start a separate timer based on the unturned degrees and the part of the hour that's yet to come.

If you want your clock to go in a classic method where the minute hand only updates every minute (but you still want it to jump on the right time, instead of after each minute passed), you need to check on every convert if DateTime.Now.Second == 0. if it is, return the value, else return Binding.DoNothing.

0
votes

I have to use two more doubleAnimations with two more Converters. The code is as below:

From the above code I need to add a double animation to 2nd storyboard as follows:

<DoubleAnimation Storyboard.TargetName="minuteHandRotateTransform" Storyboard.TargetProperty="Angle"
                 By="{Binding DataContext.Second, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Converter={StaticResource synchronizeMinuteHandConverter}}"
                 Duration="0:0:1" />

Similarly for 3rd storyboard :

<DoubleAnimation Storyboard.TargetName="hourHandRotateTransform" Storyboard.TargetProperty="Angle"
                 By="{Binding DataContext.Minute, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Converter={StaticResource synchronizeHourHandConverter}}" 
                 Duration="0:0:1"/>

SynchronizeMinuteHandConverter.cs :

public class SynchronizeMinuteHandConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null || value == DependencyProperty.UnsetValue)
        {
            return (DateTime.Now.Second / 10);
        }
        else
        {
            if ((int)value > 59)
            {
                value = (int)value - 60;
            }

            return ((int)value / 10);
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

SynchronizeHourHandConverter.cs

public class SynchronizeHourHandConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null || value == DependencyProperty.UnsetValue)
        {
            return (DateTime.Now.Minute / 2);
        }
        else
        {
            if ((int)value > 59)
            {
                value = (int)value - 60;
            }

            return ((int)value / 2);
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
0
votes

Here is an easy way to generate a smooth analogue sweep second hand. The rectangle's angle is being animated with a doubleanimation applied to it's angle of rotation property. Overlaying similar code for minutes and seconds would yield a basic clock.

<Page.Resources>
        <Storyboard x:Name="myStoryboard">
            <DoubleAnimationUsingKeyFrames
                RepeatBehavior="Forever"
                Storyboard.TargetName="ellipse1"
                Storyboard.TargetProperty="(Ellipse.RenderTransform).(CompositeTransform.Rotation)">
                <LinearDoubleKeyFrame Value="-90" KeyTime="0:0:0"/>
                <LinearDoubleKeyFrame Value="270" KeyTime="0:0:59"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
    </Page.Resources>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Rectangle x:Name="ellipse1"
                Width="320" Height="10" Fill="Red" RenderTransformOrigin="0.5,0.5">
            <Rectangle.Clip>
                <RectangleGeometry Rect="110,0,320,320"/>
            </Rectangle.Clip>
            <Rectangle.RenderTransform>
                <CompositeTransform Rotation="-90" />
            </Rectangle.RenderTransform>
        </Rectangle>

        <Rectangle VerticalAlignment="Center" HorizontalAlignment="Center" Stroke="Black"
                   Width="640" Height="1"></Rectangle>
        <Rectangle VerticalAlignment="Center" HorizontalAlignment="Center" Stroke="Black"
                   Width="1" Height="640"></Rectangle>
    </Grid>