Semáforo II

Semáforo II
En esta entrega vamos a crear el semáforo. Aunque no lo parezca, pero un semáforo a pesar de su simplicidad visual, tiene aspectos bastante complejos y sobre todo cuando tienen que estar sincronizados o comunicados con otros, existen muchas matemáticas detrás de los modelos de tráfico de las grandes ciudades.

Sin darle más complejidad que la que necesitamos para nuestro proyecto final, que es un sistema de control de semáforos en una avenida para poder cruzarla de una sola vez sin pararnos, le asignaremos las siguientes características:

  • Tiempo en verde
  • Tiempo en amarillo
  • Tiempos en rojo
  • Tiempo de retardo para sincronziación de semáforos
  • Estado en el que se encuentra
  • Función de funcionamiento en manual o automático
  • Función de inicio automático
  • Función de parpadeo

En el diseño XAML, creamos unos objetos rectangle y le incluimos los controles de usuario creados en el post Semáforo I. El cambio de color lo podiamos haber hecho con converter, con propiedades, pero esta vez he optado por hacerlo con estilos y DataTrigger. A cada objeto LEDLight (luz del semáforo) le asigno un estilo genérico con algún efecto de fundido para que parezca más real; luego este estilo es heredado por otros tres estilos, uno por cada color donde los DataTrigger hacen su trabajo comprobando el estado del semáforo y activando la propiedad LightIsEnabled a Truesi cumple con la condición del DataTrigger. (Código al final del post)

Luego en el código de clase, le añado un DispatcherTime que nos hará las funciones de temporizador, creamos unas cuantas propiedades de dependencia para cumplir con nuestras características.
Tiene un método privado que será llamado desde el temporizador y este asignará el estado al semáforo y como consecuencia activando el color correspondiente.

        void changingState()
        {
            // Retardo inicial
            if (DelayTime > 0)
            {
                timer.Interval = new TimeSpan(0, 0, (int)DelayTime);
                DelayTime = 0;
                return;
            }

            // Parpadeando
            if (IsFlashingAmberLight)
            {
                if (State.Equals(LEDTrafficLightState.Yellow))
                {
                    State = LEDTrafficLightState.None;
                    timer.Interval = new TimeSpan(0, 0, 0, 0, 500);
                }
                else
                {
                    State = LEDTrafficLightState.Yellow;
                    timer.Interval = new TimeSpan(0, 0, 0, 1);
                }

                return;
            }

            // Cambio de color
            switch (State)
            {
                case LEDTrafficLightState.Red:
                    // SEMÁFORO EN VERDE
                    State = LEDTrafficLightState.Green;
                    
                    timer.Interval = new TimeSpan(0, 0, GreenTime);
                    break;
                case LEDTrafficLightState.Yellow:
                    // SEMÁFORO EN VERDE
                    State = LEDTrafficLightState.Red;
                    timer.Interval = new TimeSpan(0, 0, RedTime);
                    break;
                case LEDTrafficLightState.Green:
                    // SEMÁFORO EN AMBAR
                    State = LEDTrafficLightState.Yellow;
                    timer.Interval = new TimeSpan(0, 0, YellowTime);
                    break;
                default:
                    break;
            }
        }

y este el resultado instanciando varios semáforos en una ventana

            <controls:LEDTrafficLight x:Name="LEDTL1" Width="100" Height="300" GreenTime="4" RedTime="4" DelayTime="1" Margin="10"/>
            <controls:LEDTrafficLight x:Name="LEDTL2" Width="100" Height="300" GreenTime="5" RedTime="4" DelayTime="2" Margin="10"/>
            <controls:LEDTrafficLight x:Name="LEDTL3" Width="100" Height="300" GreenTime="5" RedTime="5" DelayTime="3" Margin="10"/>
            <controls:LEDTrafficLight x:Name="LEDTL4" Width="100" Height="300" GreenTime="5" RedTime="5" IsFlashingAmberLight="True" Margin="10"/>  

Video7
La próxima entrega, el sistema de control de semáforos

<UserControl x:Class="Controls.LEDTrafficLight"
             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" 
             xmlns:controls="clr-namespace:Controls"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="100">
    <Grid x:Name="grid">
        <Grid.Resources>
            <Style x:Key="LightStyle" TargetType="controls:LEDLight">
                <Setter Property="Margin" Value="5"/>
                <Style.Triggers>
                    <Trigger Property="LightIsEnabled" Value="True">
                        <Trigger.ExitActions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation
                  Storyboard.TargetProperty="Opacity"
                  From="0" To="1" Duration="0:0:0.2" />
                                </Storyboard>
                            </BeginStoryboard>
                        </Trigger.ExitActions>
                        <Trigger.EnterActions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation
                  Storyboard.TargetProperty="Opacity"
                  From="0" To="1" Duration="0:0:0.1" />
                                </Storyboard>
                            </BeginStoryboard>
                        </Trigger.EnterActions>
                    </Trigger>
                </Style.Triggers>
            </Style>
            <Style x:Key="RedLightStyle" TargetType="controls:LEDLight" BasedOn="{StaticResource LightStyle}">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=controls:LEDTrafficLight},Path=State }" Value="{x:Static controls:LEDTrafficLight+LEDTrafficLightState.Red}">
                        <Setter Property="LightIsEnabled" Value="True"/>
                    </DataTrigger>

                </Style.Triggers>
            </Style>
            <Style x:Key="YellowLightStyle" TargetType="controls:LEDLight" BasedOn="{StaticResource LightStyle}">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=controls:LEDTrafficLight},Path=State }" Value="{x:Static controls:LEDTrafficLight+LEDTrafficLightState.Yellow}">
                        <Setter Property="LightIsEnabled" Value="True"/>
                    </DataTrigger>

                </Style.Triggers>
            </Style>
            <Style x:Key="GreenLightStyle" TargetType="controls:LEDLight" BasedOn="{StaticResource LightStyle}">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=controls:LEDTrafficLight},Path=State }" Value="{x:Static controls:LEDTrafficLight+LEDTrafficLightState.Green}">
                        <Setter Property="LightIsEnabled" Value="True"/>
                    </DataTrigger>

                </Style.Triggers>
            </Style>
            <Style x:Key="rectangleTraficLight" TargetType="Border">
                <Setter Property="Width" Value="{Binding ElementName=redEllipse, Path=ActualWidth}"/>
                <Setter Property="Height" Value="5"/>
                <Setter Property="Background" Value="Gray"/>
                <Setter Property="Margin" Value="1"/>
                <Setter Property="VerticalAlignment" Value="Top"/>
                <Setter Property="CornerRadius" Value="3"/>
            </Style>
            <Style x:Key="borderColor" TargetType="Border">
                <Setter Property="BorderBrush" Value="Gray"/>
                <Setter Property="BorderThickness" Value="2"/>
                <Setter Property="Margin" Value="2,0,2,2"/>
                <Setter Property="CornerRadius" Value="3"/>
            </Style>
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="100*"/>
            <RowDefinition Height="100*"/>
            <RowDefinition Height="100*"/>
        </Grid.RowDefinitions>
        <Border Grid.RowSpan="3" BorderBrush="DarkGreen" BorderThickness="5" Background="DarkGreen" Opacity=".5" CornerRadius="5"/>
        <Border Style="{StaticResource borderColor}" Grid.Row="0">
            <controls:LEDLight x:Name="RedLight" LEDLightBackgroundON="Red" Style="{StaticResource RedLightStyle}"/>
        </Border>
        <Border Style="{StaticResource borderColor}" Grid.Row="1">
            <controls:LEDLight x:Name="YellowLight"  LEDLightBackgroundON="Gold" Style="{StaticResource YellowLightStyle}"/>
        </Border>
        <Border Style="{StaticResource borderColor}" Grid.Row="2">
            <controls:LEDLight x:Name="GreenLight" Style="{StaticResource GreenLightStyle}"/>
        </Border>
    </Grid>
</UserControl>
namespace Controls
{
    /// <summary>
    /// Lógica de interacción para LEDTrafficLight.xaml
    /// </summary>
    public partial class LEDTrafficLight : UserControl
    {
        #region Constructor

        public LEDTrafficLight()
        {
            InitializeComponent();
            this.DataContext = this;
            timer.Tick += Timer_Tick;
            timer.IsEnabled = StartAuto;
        }

        #endregion

        #region Enum

        public enum LEDTrafficLightState
        {
            Red,
            Yellow,
            Green,
            None
        }

        #endregion

        #region DependencyProperties

        public int GreenTime
        {
            get { return (int)GetValue(GreenTimeProperty); }
            set
            {
                value = value > YellowTime ? value - YellowTime : YellowTime + 1;
                SetValue(GreenTimeProperty, value);
            }
        }

        public int RedTime
        {
            get { return (int)GetValue(RedTimeProperty); }
            set
            {
                if (value <= 0) value = 1;
                SetValue(RedTimeProperty, value);
            }
        }

        public int YellowTime
        {
            get { return (int)GetValue(YellowTimeProperty); }
            set { SetValue(YellowTimeProperty, value); }
        }

        public int DelayTime
        {
            get { return (int)GetValue(TimeProperty); }
            set { SetValue(TimeProperty, value); }
        }

        public bool IsFlashingAmberLight
        {
            get { return (bool)GetValue(IsFlickingProperty); }
            set
            {
                SetValue(IsFlickingProperty, value);
                State = LEDTrafficLightState.Yellow;
                // en 100 ms cambia a Parpadeo
                timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
            }
        }

        public bool IsManual
        {
            get { return (bool)GetValue(IsManualProperty); }
            set
            {
                SetValue(IsManualProperty, value);
                timer.IsEnabled = !value;
            }
        }

        public bool StartAuto
        {
            get { return (bool)GetValue(StarAutoProperty); }
            set
            {
                SetValue(StarAutoProperty, value);
                timer.IsEnabled = value;
            }
        }

        public LEDTrafficLightState State
        {
            get { return (LEDTrafficLightState)GetValue(StateProperty); }
            set { SetValue(StateProperty, value); }
        }

        #region DependecyProperties Register

        // Using a DependencyProperty as the backing store for StarAuto.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty StarAutoProperty =
            DependencyProperty.Register("StartAuto", typeof(bool), typeof(LEDTrafficLight), new PropertyMetadata(true));

        // Using a DependencyProperty as the backing store for IsManual.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsManualProperty =
            DependencyProperty.Register("IsManual", typeof(bool), typeof(LEDTrafficLight), new PropertyMetadata(false));

        // Using a DependencyProperty as the backing store for IsFlicking.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsFlickingProperty =
            DependencyProperty.Register("IsFlashingAmberLight", typeof(bool), typeof(LEDTrafficLight), new PropertyMetadata(false));

        // Using a DependencyProperty as the backing store for State.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty StateProperty =
            DependencyProperty.Register("State", typeof(LEDTrafficLightState), typeof(LEDTrafficLight), new PropertyMetadata(LEDTrafficLightState.Red));

        // Using a DependencyProperty as the backing store for Time.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TimeProperty =
            DependencyProperty.Register("DelayTime", typeof(int), typeof(LEDTrafficLight), new PropertyMetadata(0));

        // Using a DependencyProperty as the backing store for TimeYellow.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty YellowTimeProperty =
            DependencyProperty.Register("YellowTime", typeof(int), typeof(LEDTrafficLight), new PropertyMetadata(2));

        // Using a DependencyProperty as the backing store for TimeRed.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty RedTimeProperty =
            DependencyProperty.Register("RedTime", typeof(int), typeof(LEDTrafficLight), new PropertyMetadata(7));

        // Using a DependencyProperty as the backing store for TimeGreen.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty GreenTimeProperty =
            DependencyProperty.Register("GreenTime", typeof(int), typeof(LEDTrafficLight), new PropertyMetadata(7));

        #endregion

        #endregion

        #region Fields & Const & Event

        DispatcherTimer timer = new DispatcherTimer();

        #endregion


        #region Publics Method

        /// <summary>
        /// Inicia el semáforo cuando se encuentra en modo automático
        /// </summary>
        public void Start()
        {
            if (IsManual) return;
            timer.Start();
        }

        /// <summary>
        /// Detiene el semáforo
        /// </summary>
        public void Stop()
        {
            timer.Stop();
        }

        /// <summary>
        /// Cambia el semáforo a rojo
        /// </summary>
        public void OnRed()
        {
            State = LEDTrafficLightState.Red;
        }

        /// <summary>
        /// Cambia el semáforo a verde
        /// </summary>
        public void OnGreen()
        {
            State = LEDTrafficLightState.Green;
        }

        /// <summary>
        /// Cambia el semáforo a amarillo
        /// </summary>
        public void OnYellow()
        {
            State = LEDTrafficLightState.Yellow;
        }

        public void Test()
        {
            throw new NotImplementedException();
        }

        #endregion

        #region Privates methods

        /// <summary>
        /// Cambia el estado del semáforo según el estado anterior
        /// </summary>
        void changingState()
        {
            // Retardo inicial
            if (DelayTime > 0)
            {
                timer.Interval = new TimeSpan(0, 0, (int)DelayTime);
                DelayTime = 0;
                return;
            }

            // Parpadeando
            if (IsFlashingAmberLight)
            {
                if (State.Equals(LEDTrafficLightState.Yellow))
                {
                    State = LEDTrafficLightState.None;
                    timer.Interval = new TimeSpan(0, 0, 0, 0, 500);
                }
                else
                {
                    State = LEDTrafficLightState.Yellow;
                    timer.Interval = new TimeSpan(0, 0, 0, 1);
                }

                return;
            }

            // Cambio de color
            switch (State)
            {
                case LEDTrafficLightState.Red:
                    // SEMÁFORO EN VERDE
                    State = LEDTrafficLightState.Green;
                    
                    timer.Interval = new TimeSpan(0, 0, GreenTime);
                    break;
                case LEDTrafficLightState.Yellow:
                    // SEMÁFORO EN VERDE
                    State = LEDTrafficLightState.Red;
                    timer.Interval = new TimeSpan(0, 0, RedTime);
                    break;
                case LEDTrafficLightState.Green:
                    // SEMÁFORO EN AMBAR
                    State = LEDTrafficLightState.Yellow;
                    timer.Interval = new TimeSpan(0, 0, YellowTime);
                    break;
                default:
                    break;
            }

        }

        private void Timer_Tick(object sender, EventArgs e)
        {
            changingState();
        }

        #endregion
    }
}

Controles de Usuario en WPF 2

Controles de Usuario en WPF 2
Continuando con los controles de usuario en WPF, voy a mostrarles un control un poco más complejo y en este caso se trata de un reloj un poco especial, se trata de un reloj binario.
Se trata de un reloj, pero los dígitos se muestran uno a uno en modo binario mediante una bola de indicador. Por ejemplo para indicar la hora 15, se muestra un 1 en binario 001 y un 5 en binario 101.He creado un control básico que hará las veces de indicador con un tono grisáceo cuando el indicador está apagado o es un cero y un tono verdoso para indicar un 1 o cuando está encendido.

indicadores

Y ahora muestro el código de la bolita

<UserControl x:Class="Controls.EllipseUserControl"
             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"
             xmlns:Controls="clr-namespace:Controls"
             mc:Ignorable="d"
             d:DesignHeight="48" d:DesignWidth="48" x:Name="ellipseUserControl">
    <Grid x:Name="gridEllipse" Height="{Binding ElementName=ellipseUserControl,Path=Height}"
          Width="{Binding ElementName=ellipseUserControl,Path=Width}">
        <Grid.Resources>
            <Style x:Key="ellipseStyle" TargetType="Ellipse">
                <Setter Property="VerticalAlignment" Value="Stretch"/>
                <Setter Property="HorizontalAlignment" Value="Stretch"/>
                <Setter Property="Margin" Value="2"/>
                <Setter Property="Fill">
                    <Setter.Value>
                        <RadialGradientBrush GradientOrigin="0.6,0.2"
                    Center="0.6,0.2"
                    RadiusX="0.4" RadiusY="0.4">
                            <RadialGradientBrush.GradientStops>
                                <GradientStop Color="White" Offset="0" />
                                <GradientStop Color="{Binding ElementName=gridEllipse, Path=Parent.ActiveColor}" Offset="1.5" />
                            </RadialGradientBrush.GradientStops>
                        </RadialGradientBrush>
                    </Setter.Value>
                </Setter>
            </Style>
        </Grid.Resources>
        <Ellipse x:Name="ellipse" Style="{StaticResource ellipseStyle}"/>
    </Grid>
</UserControl>

Podemos apreciar que el control básico solo tiene un Grid y dentro de este una ellipse, la ellipse tiene un estilo declarado en Grid.Resources y el cual tiene un degradado radial y desde una de sus propiedades GradientStop enlazamos con una propiedad DependecyProperty ActiveColor, de modo que podamos convertir valores booleanos en colores y se la asignemos a esta propiedad y por tanto esta propiedad cambiará el color de GradientStop. Esta propiedad la declaramos en la clase del control de usuario del siguiente modo:


public Color ActiveColor
{
get { return (Color)GetValue(ActiveColorProperty); }
set { SetValue(ActiveColorProperty, value); }
}

// Using a DependencyProperty as the backing store for IsActiveColor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ActiveColorProperty =
DependencyProperty.Register("ActiveColor", typeof(Color), typeof(EllipseUserControl), new PropertyMetadata(Brushes.Red.Color));

Ahora vamos al trabajo duro, que es crear un control de usuario más complejo, basado en unos cuantos controles básicos. Para las horas usaremos para:

  • Horas. Primer dígito 2, o vale 0, 1 o 2 (00, 01 o 11). Segundo dígito 4 indicadores, desde 0 a 9 y el 9 es 1001 en binario.
  • Minutos. Primer dígito. Valor máximo es 5, por tanto 3 indicadores (101), segundo dígito valor máximo 9, cuatro indicadores (1001)
  • Segundos. Igual que los minutos.

Ahora que tenemos el diseño del reloj, lo generamos en XAML

<UserControl x:Class="Controls.BinaryClock"
             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"
             xmlns:Controls="clr-namespace:Controls"
             mc:Ignorable="d"
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             d:DesignHeight="155" d:DesignWidth="180" >
    <UserControl.Resources>
        <Controls:BooleanToBrushes x:Key="btb"/>
    </UserControl.Resources>
    <Grid x:Name="grid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="30"/>
            <ColumnDefinition Width="30"/>
            <ColumnDefinition Width="30"/>
            <ColumnDefinition Width="30"/>
            <ColumnDefinition Width="30"/>
            <ColumnDefinition Width="30"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="25"/>
            <RowDefinition Height="25"/>
            <RowDefinition Height="25"/>
            <RowDefinition Height="25"/>
            <RowDefinition Height="25"/>
            <RowDefinition Height="25"/>
            <RowDefinition Height="25"/>
        </Grid.RowDefinitions>
        <!-- HORAS -->
        <TextBlock Text="H" Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" HorizontalAlignment="Center" FontSize="20"/>
        <Border BorderThickness="1" BorderBrush="LightCoral" Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="4" Margin="1"/>

        <Controls:EllipseUserControl x:Name="hour11" Grid.Column="0" Grid.Row="4" ActiveColor="{Binding ElementName=grid, Path=Parent.H10, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="hour12" Grid.Column="0" Grid.Row="3" ActiveColor="{Binding ElementName=grid, Path=Parent.H11, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="hour21" Grid.Column="1" Grid.Row="4" ActiveColor="{Binding ElementName=grid, Path=Parent.H20, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="hour22" Grid.Column="1" Grid.Row="3" ActiveColor="{Binding ElementName=grid, Path=Parent.H21, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="hour24" Grid.Column="1" Grid.Row="2" ActiveColor="{Binding ElementName=grid, Path=Parent.H22, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="hour28" Grid.Column="1" Grid.Row="1" ActiveColor="{Binding ElementName=grid, Path=Parent.H23, Converter={StaticResource btb}}"/>

        <!-- MINUTOS -->
        <TextBlock Text="M" Grid.Column="2" Grid.Row="0" Grid.ColumnSpan="2" HorizontalAlignment="Center" FontSize="20"/>
        <Border BorderThickness="1" BorderBrush="LightBlue" Grid.Column="2" Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="4" Margin="1"/>

        <Controls:EllipseUserControl x:Name="minute11" Grid.Column="2" Grid.Row="4" ActiveColor="{Binding ElementName=grid, Path=Parent.M10, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="minute12" Grid.Column="2" Grid.Row="3" ActiveColor="{Binding ElementName=grid, Path=Parent.M11, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="minute14" Grid.Column="2" Grid.Row="2" ActiveColor="{Binding ElementName=grid, Path=Parent.M12, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="minute21" Grid.Column="3" Grid.Row="4" ActiveColor="{Binding ElementName=grid, Path=Parent.M20, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="minute22" Grid.Column="3" Grid.Row="3" ActiveColor="{Binding ElementName=grid, Path=Parent.M21, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="minute24" Grid.Column="3" Grid.Row="2" ActiveColor="{Binding ElementName=grid, Path=Parent.M22, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="minute28" Grid.Column="3" Grid.Row="1" ActiveColor="{Binding ElementName=grid, Path=Parent.M23, Converter={StaticResource btb}}"/>

        <!-- SEGUNDOS -->
        <TextBlock Text="S" Grid.Column="4" Grid.Row="0" Grid.ColumnSpan="2" HorizontalAlignment="Center" FontSize="20"/>
        <Border BorderThickness="1" BorderBrush="LightGreen" Grid.Column="4" Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="4" Margin="1"/>

        <Controls:EllipseUserControl x:Name="second11" Grid.Column="4" Grid.Row="4" ActiveColor="{Binding ElementName=grid, Path=Parent.S10, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="second12" Grid.Column="4" Grid.Row="3" ActiveColor="{Binding ElementName=grid, Path=Parent.S11, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="second14" Grid.Column="4" Grid.Row="2" ActiveColor="{Binding ElementName=grid, Path=Parent.S12, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="second21" Grid.Column="5" Grid.Row="4" ActiveColor="{Binding ElementName=grid, Path=Parent.S20, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="second22" Grid.Column="5" Grid.Row="3" ActiveColor="{Binding ElementName=grid, Path=Parent.S21, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="second24" Grid.Column="5" Grid.Row="2" ActiveColor="{Binding ElementName=grid, Path=Parent.S22, Converter={StaticResource btb}}"/>
        <Controls:EllipseUserControl x:Name="second28" Grid.Column="5" Grid.Row="1" ActiveColor="{Binding ElementName=grid, Path=Parent.S23, Converter={StaticResource btb}}"/>

        <TextBlock Text="{Binding ElementName=grid, Path=Parent.Hour}" Grid.Column="0" Grid.Row="5" Grid.ColumnSpan="2" HorizontalAlignment="Center" FontSize="22"/>
        <TextBlock Text="{Binding ElementName=grid, Path=Parent.Minutes}" Grid.Column="2" Grid.Row="5" Grid.ColumnSpan="2" HorizontalAlignment="Center" FontSize="22"/>
        <TextBlock Text="{Binding ElementName=grid, Path=Parent.Seconds}" Grid.Column="4" Grid.Row="5" Grid.ColumnSpan="2" HorizontalAlignment="Center" FontSize="22"/>
    </Grid>
</UserControl>

como podéis apreciar, he distribuido los controles para dar forma al reloj quedando su diseño así
bin01
Cada indicador usa una clase converter para convertir el valor booleano en un color y con esto finaliza el código XAML.

public class BooleanToBrushes : IValueConverter
    {
        #region Constructor

        #endregion

        #region Propiedades

        #endregion

        #region IValueConverter

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            bool bValue = (bool)value;
            if (bValue)
            {
                return Brushes.Green.Color;
            }
            else
            {
                return Brushes.Gray.Color;
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return null;
        }

        #endregion
    }

Vamos al código. Hay una propiedad booleana por cada uno de los indicadores. Un DispatcherTime para calcular los valores en cada segundo y un par de métodos para convertir la hora a binario y pasarla a cada uno de los indicadores mediante las propiedades. Paso el código.

    public partial class BinaryClock : UserControl,INotifyPropertyChanged
    {
        public BinaryClock()
        {
            InitializeComponent();

            dispatcher.Tick += Dispatcher_Tick; ;
            dispatcher.Interval = TimeSpan.FromSeconds(1);
            dispatcher.Start();
        }

        #region Privates Method

        List setDecimalToBinary(int value, bool isHour)
        {
            List values = new List();
            string valueToString = value.ToString("00");
            string digit1 = valueToString.Substring(0, 1);
            string digit2 = valueToString.Substring(1, 1);
            string binaryDigit1 = convertToBinary(Convert.ToInt16(digit1)).PadLeft(2, '0');
            if (!isHour)
            {
                binaryDigit1 = binaryDigit1.PadLeft(3, '0');
            }
            string binaryDigit2 = convertToBinary(Convert.ToInt16(digit2)).PadLeft(4, '0');
            foreach (var item in binaryDigit1 + binaryDigit2)
            {
                values.Add(item.Equals('0') ? false : true);
            }
            return values;
        }

        string convertToBinary(int value)
        {
            return Convert.ToString(value, 2);
        }

        List setDecimalToBinary(int value)
        {
            return setDecimalToBinary(value, false);
        }

        private void Dispatcher_Tick(object sender, EventArgs e)
        {
            DateTime horaActual = DateTime.Now;

            Hour = horaActual.Hour.ToString();
            Minutes = horaActual.Minute.ToString();
            Seconds = horaActual.Second.ToString();

            var hora = setDecimalToBinary(horaActual.Hour, true);
            H10 = hora[1];
            H11 = hora[0];

            H20 = hora[5];
            H21 = hora[4];
            H22 = hora[3];
            H23 = hora[2];

            var minutos = setDecimalToBinary(horaActual.Minute);
            M10 = minutos[2];
            M11 = minutos[1];
            M12 = minutos[0];

            M20 = minutos[6];
            M21 = minutos[5];
            M22 = minutos[4];
            M23 = minutos[3];

            var segundos = setDecimalToBinary(horaActual.Second);
            S10 = segundos[2];
            S11 = segundos[1];
            S12 = segundos[0];

            S20 = segundos[6];
            S21 = segundos[5];
            S22 = segundos[4];
            S23 = segundos[3];

        }

        #endregion

        public event PropertyChangedEventHandler PropertyChanged;

        #region Public Methods

        ///
<summary>
        /// Inicia el evento de cambio de propiedad
        /// </summary>

        /// Nombre de la propiedad
        public void OnPropertyChanged(string p_PropertyName)
        {

            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(p_PropertyName));
            }
        }

        #endregion

        #region Properties &amp; fields

        private bool h10;
        private bool h11;
        private bool h20;
        private bool h21;
        private bool h22;
        private bool h23;
        private bool m10;
        private bool m11;
        private bool m12;
        private bool m20;
        private bool m21;
        private bool m22;
        private bool m23;
        private bool s10;
        private bool s11;
        private bool s12;
        private bool s20;
        private bool s21;
        private bool s22;
        private bool s23;
        DispatcherTimer dispatcher = new DispatcherTimer();

        private string hour;
        private string minute;
        private string seconds;

        public string Hour
        {
            get { return hour; }
            set
            {
                hour = value;
                OnPropertyChanged("Hour");
            }
        }

        public string Minutes
        {
            get { return minute; }
            set
            {
                minute = value;
                OnPropertyChanged("Minutes");
            }
        }

        public string Seconds
        {
            get { return seconds; }
            set
            {
                seconds = value;
                OnPropertyChanged("Seconds");
            }
        }

        // HORAS. 1ER DIGITO
        public bool H10
        {
            get { return h10; }
            set
            {
                h10 = value;
                OnPropertyChanged("H10");
            }
        }

        public bool H11
        {
            get { return h11; }
            set
            {
                h11 = value;
                OnPropertyChanged("H11");
            }
        }

        // HORAS. 2º DIGITO
        public bool H20
        {
            get { return h20; }
            set
            {
                h20 = value;
                OnPropertyChanged("H20");
            }
        }
        public bool H21
        {
            get { return h21; }
            set
            {
                h21 = value;
                OnPropertyChanged("H21");
            }
        }
        public bool H22
        {
            get { return h22; }
            set
            {
                h22 = value;
                OnPropertyChanged("H22");
            }
        }
        public bool H23
        {
            get { return h23; }
            set
            {
                h23 = value;
                OnPropertyChanged("H23");
            }
        }

        //MINUTOS. 1ER DIGITO
        public bool M10
        {
            get { return m10; }
            set
            {
                m10 = value;
                OnPropertyChanged("M10");
            }
        }
        public bool M11
        {
            get { return m11; }
            set
            {
                m11 = value;
                OnPropertyChanged("M11");
            }
        }
        public bool M12
        {
            get { return m12; }
            set
            {
                m12 = value;
                OnPropertyChanged("M12");
            }
        }
        // MINUTOS. 2º DIGITO
        public bool M20
        {
            get { return m20; }
            set
            {
                m20 = value;
                OnPropertyChanged("M20");
            }
        }
        public bool M21
        {
            get { return m21; }
            set
            {
                m21 = value;
                OnPropertyChanged("M21");
            }
        }
        public bool M22
        {
            get { return m22; }
            set
            {
                m22 = value;
                OnPropertyChanged("M22");
            }
        }
        public bool M23
        {
            get { return m23; }
            set
            {
                m23 = value;
                OnPropertyChanged("M23");
            }
        }

        // SEGUNDOS. 1ER DIGITO
        public bool S10
        {
            get { return s10; }
            set
            {
                s10 = value;
                OnPropertyChanged("S10");
            }
        }
        public bool S11
        {
            get { return s11; }
            set
            {
                s11 = value;
                OnPropertyChanged("S11");
            }
        }
        public bool S12
        {
            get { return s12; }
            set
            {
                s12 = value;
                OnPropertyChanged("S12");
            }
        }
        // SEGUNDOS. 2º DIGITO
        public bool S20
        {
            get { return s20; }
            set
            {
                s20 = value;
                OnPropertyChanged("S20");
            }
        }
        public bool S21
        {
            get { return s21; }
            set
            {
                s21 = value;
                OnPropertyChanged("S21");
            }
        }
        public bool S22
        {
            get { return s22; }
            set
            {
                s22 = value;
                OnPropertyChanged("S22");
            }
        }
        public bool S23
        {
            get { return s23; }
            set
            {
                s23 = value;
                OnPropertyChanged("S23");
            }
        }

        #endregion
    }

Pues con este código, cada segundo, se comprueba la hora y se pasa a cada uno de los indicadores un valor booleano correspondiente a su valor en binario, este se convierte en un color con el converter y et voilá, un reloj binario. Le he añadido la hora con números arábigos para que podáis contrastar. Y el resultado… en la cabecera.Espero que os haya gustado.

Controles de usuario en WPF

Controles de usuario en WPF

Entre todas las cosas que me gustan de WPF, una de las que más, es la versatilidad, los programadores podemos hacer practicamente lo que queramos. Hace unos “pocos” años, el programador debía servirse de controles de otras empresas porque a veces, los controles básicos de Microsoft se quedaban “cortos” para cuestiones más complejas; posteriormente, se podían crear controles de este tipo pero para que funcionaran (como deben funcionar), había que afinar mucho, pero la facilidad con la que se puede crear y la complejidad que se le puede dar a un control actualmente es bestial. Para que se hagan una idea, en mis proyectos tengo un control de usuario creado sobre una clase Persona estándar, de modo que cada vez que tengo que iniciar un proyecto en el que esa clase hace acto de presencia, el control con sus binding lo hace de igual modo y ya es cuestión de adaptar si queremos que aparezca tal propiedad o no, pero las validaciones de datos como DNI, cuenta bancaria, tarjetas,etc, los cálculos (por ejemplo la edad), las máscaras de entrada, editar y guardar, marcas de edición, etc., es decir un sinfín de procesos, se encuentran hechos. Solo nos quedaría, darle forma a la interfaz gráfica para adaptarla a las necesidades del cliente.

Un control de usuario podría ser, una control ListView con cada uno de sus items que fueran un Treeview por ejemplo, podríamos crear un  control ProgressBar con varios colores como un vumeter, un Progressbar circular, un TextBox inteligente que nos ayude a escribir, podríamos crear un control que realice funciones de filtrado sobre un contenido de registros, dándole una colección de entrada y entregando una colección de salida con los parámetros de filtrado, podríamos crear un reloj analógico como control, un reloj binario, un lo que se nos venga a la cabeza, esa es su función, crear objetos complejos en base a otros controles.

En este artículo, como ejempo vamos a mostrar un control básico que hará las siguientes funciones:

  • Control slider
  • Debe iluminarse la zona que hemos desplazado con el cursor
  • Debe aparecer un texto con el valor
  • Dependiendo del valor, debe cambiar su color

Vamos a ver el código XAML

<UserControl x:Class="Controls.SliderUserControl" 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" xmlns:local="clr-namespace:Controls" mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="300">
    <StackPanel>
        <Grid>
            <Grid.Resources>
                <local:ValueToBrush x:Key="vtb"/>
            </Grid.Resources>
            <ProgressBar Minimum="{Binding ElementName=mainSlider,Path=Minimum}" Maximum="{Binding ElementName=mainSlider,Path=Maximum}" Value="{Binding ElementName=mainSlider, Path=Value}" Height="10"/>
            <Slider x:Name="mainSlider" Maximum="100" Minimum="0" Value="30" BorderBrush="Gray" BorderThickness="1" Background="{Binding ElementName=mainSlider,Path=Value,Converter={StaticResource vtb}}" Opacity=".2"/>
        </Grid>
        <TextBlock Text="{Binding ElementName=mainSlider, Path=Value, StringFormat={}{0:N0}}" HorizontalAlignment="Center" FontSize="16"/>
    </StackPanel>
</UserControl>

Analizando el código, creamos un slider que veremos con más detenimiento, un control progressbar que nos indicará visualmente el valor del slider y un TextBlock.
El control ProgressBar le hacemos un binding a la propiedad Maximun, Minimun y Value a las mismas propiedades del control Slider.
El control TextBlock le hacemos un binding a la propiedad Text pero dándole formato para que no aparezcan decimales.
El control Slider tiene alguna cosa más y es que la propiedad Opacity la hemos bajado a 0.2 y la propiedad Background se alimenta de la propiedad Value la cual es convertida en un objeto Brush desde una clase que implementa IValueConverter llamada ValueToBrush. Esta clase es instanciada desde el código XAML y la llamamos en el binding de la propiedad Text.

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((double)value < 50)
{
return Brushes.Green;
}
else if ((double)value < 75)
{
return Brushes.Yellow;
}
else
{
return Brushes.Red;
}
}

de este modo cambia de color dependiendo del valor.
El resultado es el siguiente:Video

Lo mejor de todo, el control sin una sola línea de código (sin contar el converter 😉 ). En próximas ediciones, algún control más complejo.

Un saludo