Thursday, 12 June 2014

Create a StopWatch Counter control in WPF(XAML)

In this article we are going to see how to create a Stop Watcher counter control in WPF, it is also use as Time Wait Control showing the user as how much time they have to wait.

For this we have to remember some mathematical formulas, Like getting the point from the given angle. and getting radius from the angle.




To get the Point.

/* Find the point with radius and angle */
       X =  centerX + Raidus * Sin(rad) 
       Y = centerY + Radius * Cos(rad) 

            
       rad = angle * pi/180 

From the above two formulas you can derive the point and the radius for a angle. Then we have to convert the number of ticks to the circle angle , so a converter is needed, in which we can split the data to angle.It is Multi Value Converter. i.e it takes the multiple input parameters.

public class TickToAngleConverter:IMultiValueConverter
{

        public object Convert(object[] values, Type targetType, object parameter,                          CultureInfo culture)
        {
            double tick = (double)values[0];
            ProgressBar bar = values[1] as ProgressBar;
            return 359.98 * (tick / (bar.Maximum - bar.Minimum))+180;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter,                  CultureInfo culture)
        {
            throw new NotImplementedException();
        }

}

in the above you can see that angle is mention as 359.98, i.e 360 is also mention 0, to avoid this it mentioned in nearest value.

In the above converter you can see that two values are bind for this converter one is angle another one is a control, which is used to get the maximum and minimum value from that, based on this values we are getting the angle from the Tick.

Now write a class which is derived from the Shape class to draw the arc, In shape class we have in build things to calculate and draw, we enough to give the geometry , remaining things it will take care.
So now we are going to define a geometry, then how we are getting the tick value to draw the arc. So for this two property is declared one is start another is end.

public class TimerArc:Shape
    {

        public double Start
        {
            get { return (double)GetValue(StartProperty); }
            set { SetValue(StartProperty, value); }
        }
       
        public static readonly DependencyProperty StartProperty =
                      DependencyProperty.Register("Start", typeof(double), typeof(TimerArc),
                            new FrameworkPropertyMetadata(0D,
                            FrameworkPropertyMetadataOptions.AffectsRender));



        public double End
        {
            get { return (double)GetValue(EndProperty); }
            set { SetValue(EndProperty, value); }
        }
       
        public static readonly DependencyProperty EndProperty =
                        DependencyProperty.Register("End", typeof(double), typeof(TimerArc),
                            new FrameworkPropertyMetadata(0D,
                            FrameworkPropertyMetadataOptions.AffectsRender));



       

        protected override System.Windows.Media.Geometry DefiningGeometry
        {
            get { return GetGeometry(); }
        }

        protected override System.Windows.Size MeasureOverride(Size constraint)
        {
            return base.MeasureOverride(constraint);
        }

        protected override System.Windows.Size ArrangeOverride(Size finalSize)
        {
            return base.ArrangeOverride(finalSize);
        }


        private Geometry GetGeometry()
        {
            Point startpoint =GetPoint(Math.Min(Start,End));
            Point endpoint = GetPoint(Math.Max(Start,End));    
      
            /* To draw the arc in perfect way instead of seeing it as Big arc */
            Size arc = new Size(Math.Max(0,(this.RenderSize.Width-this.StrokeThickness)/2),
                                Math.Max(0,(this.RenderSize.Height-this.StrokeThickness)/2));
           
            bool isgreaterthan180 = Math.Abs(End - Start) > 180;

            StreamGeometry strmgeometry = new StreamGeometry();
            using(StreamGeometryContext drawcontext = strmgeometry.Open())
            {
                drawcontext.BeginFigure(startpoint,false,false);
                drawcontext.ArcTo(endpoint, arc, 0, isgreaterthan180, SweepDirection.Counterclockwise, true, false);
            }

            double translate = this.StrokeThickness/2;
            strmgeometry.Transform = new TranslateTransform(translate, translate);

            return strmgeometry;
        }

        private Point GetPoint(double angle)
        {
            /* rad = angle * pi/180 */
            double rad = angle * (Math.PI / 180);

            double xrad = (this.RenderSize.Width - this.StrokeThickness) / 2;
            double yrad = (this.RenderSize.Height - this.StrokeThickness) / 2;

            /* Find the point with radius and angle */
            /* X=  centerX + Raidus * Sin(rad) */
            /* Y = centerY + Radius * Cos(rad) */
           
            double x = xrad + xrad * Math.Sin(rad);
            double y = yrad + yrad * Math.Cos(rad);

            return new Point(x,y);
        }


    }

How we are going to bind the data , first for this we have to create the progress bar from which we have to bind the value property to the End Dependency property.

Create a User Control:

<UserControl x:Class="CustomTimerControl.TimerControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
           
             xmlns:local="clr-namespace:CustomTimerControl"
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <local:TickToAngleConverter x:Key="tickconv" />
       
        <Style TargetType="ProgressBar">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ProgressBar">
                        <Grid>
                       
                            <Ellipse Fill="{TemplateBinding Background}" Stroke="Black"/>
                            <Ellipse Fill="White" Stroke="Black" Margin="40" />
                           
                            <local:TimerArc Stroke="{TemplateBinding BorderBrush}"                                            StrokeThickness="30" Margin="5">
                                <local:TimerArc.Start>
                                    <MultiBinding Converter="{StaticResource tickconv}">
                                        <Binding Path="Minimum" 
                                     RelativeSource="{RelativeSource TemplatedParent}"/>
                                        <Binding Path="." 
                                         RelativeSource="{RelativeSource TemplatedParent}"/>
                                    </MultiBinding>
                                </local:TimerArc.Start>
                                <local:TimerArc.End>
                                    <MultiBinding Converter="{StaticResource tickconv}">
                                        <Binding Path="Value" 
                                          RelativeSource="{RelativeSource TemplatedParent}"/>
                                        <Binding Path="." 
                                          RelativeSource="{RelativeSource TemplatedParent}"/>
                                    </MultiBinding>
                                </local:TimerArc.End>
                            </local:TimerArc>
                           
                            <TextBlock x:Name="counter"
                                       Text="{Binding Value,
                                       RelativeSource={RelativeSource TemplatedParent},
                                              StringFormat=\{0:0\}}"
                                       Foreground="{Binding CountForeColor,
                                   RelativeSource={RelativeSource AncestorType=UserControl}}"
                                       VerticalAlignment="Center"
                                       HorizontalAlignment="Center"
                                       FontSize="70"
                                       FontWeight="Bold"
                                       FontStyle="Italic"/>
                           
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
       
    </UserControl.Resources>
    <Viewbox>
        <ProgressBar Height="300" Width="300" x:Name="timer"
                     Minimum="0" Maximum="60"                                                                    Value="0"                                                                                     BorderBrush="Orange">
            <ProgressBar.Background>
                <LinearGradientBrush>
                    <GradientStop Color="#FFF9F9F9" Offset="0"/>
                    <GradientStop Color="#FFd9d9d9" Offset="1"/>
                </LinearGradientBrush>
            </ProgressBar.Background>
        </ProgressBar>
    </Viewbox>
    <UserControl.Triggers>
        <EventTrigger RoutedEvent="Window.Loaded">
            <BeginStoryboard>
                <Storyboard RepeatBehavior="1">
                    <DoubleAnimation From="{Binding Maximum,ElementName=timer}"
                                     To="{Binding Minimum,ElementName=timer}"
                                     Storyboard.TargetName="timer"
                                     Storyboard.TargetProperty="Value"
                                     Duration="0:1">
                       
                    </DoubleAnimation>                                    
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </UserControl.Triggers>
</UserControl>




From the Above user control, we are setting the on loaded event to fire for timer to start.

Main.Xaml:

<Window x:Class="CustomTimerControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:CustomTimerControl"
        Title="Timer Window" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
            <RowDefinition Height="20" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <local:TimerControl CountForeColor="Orange" Grid.Row="1" Grid.Column="1"
                            Margin="0,38,0,0" Grid.RowSpan="2" />
    </Grid>
</Window>



OUTPUT:








From this article you can learn how to create a control which is looks like time to wait or stop watcher


4 comments:

  1. GOOD more plz , how can i set the time from text box ?

    ReplyDelete
  2. It is easy, now the value is bind from the progressbar, now you bind the value of textbox with the progress bar or create a another dependency property called value and binds a value to that property from any control

    ReplyDelete
  3. I think you have missed the CS file of the User coontrol

    ReplyDelete
  4. Hi , great post!
    How can I make it start empty with no bar, and finish with complete Orange bar ?

    ReplyDelete