Sudoku II

Sudoku II

Hace tiempo escribí un artículo sobre un método de generación de sudokus en wakicode, ahora se me ha ocurrido obtener un método sencillo de resolución de sudoku. En esta primera parte mostraré como intentar resolverlos por pura lógica, con operaciones lógicas en pura esencia, sin necesidad de efectuar simulaciones por fuerza bruta o prueba error. Eso será más adelante cuando la lógica no tenga cabida.

El método que se me ha ocurrido lo hago del siguiente modo.

1.- Definiciones.

  • Ln. Capa de posición de un elemento n, se corresponde con un 1 si existe n (o es visible) y 0 si no es visible. En el ejemplo, podemos visualizar para un n=2, la matriz resultado de L2.
  • Bnc. Byte de columna para n. En el ejemplo B2c=110101111 que corresponde a un 1 si la columna tiene un 1 y 0 si la columna es ausente de 1.
  • Bnr. Byte de fila para n. En el ejemplo B2r=110110111 que corresponde (de arriba hacia abajo) a un 1 si la fila tiene un 1 y 0 si esta es ausente de 1.
  • Bns. Byte de bloque 3×3 para n. En el ejemplo B2s=101011111 que corresponde (de izquierda a derecha y de arriba abajo) a un 1 si el bloque 3×3 tiene un 1 y 0 si el bloque 3×3 es ausente de 1.
  • Mo. Matriz de elementos ocupados. Matriz 9×9 identificada por un 1 si la celda tiene un valor visible y un 0 si no lo tiene.
  • Mnc. Matriz de columnas. Operación OR de cada uno de los elementos de Mo con el bit correspondiente de Bnc. Mo OR Bnc
  • Mnr. Matriz de filas. Operación OR de cada uno de los elementos de Mnc con el bit correspondiente de Bnr. Mnc OR Bnr
  • Mns. Matriz de bloque. Operación OR de cada uno de los elementos de Mnr con Bns. Mnr OR Bns
Mnc. Mo OR Bnc. Los elementos de la derecha corresponden al resultado de la operación OR de los elementos de la izquierda del mismo color.

Una vez que conocemos los elementos participantes en el método pasamos a describir su desarrollo:

  1. Generar Mnc. Buscar en la matriz elementos únicos con valor 0. Esto significa que al realizar la operación OR del byte de columna con los elementos ocupados, queremos discriminar los elementos en los que únicamente pueden estar en esa posición. En el caso del ejemplo para n=2 no existen elementos únicos puesto que las columnas que tienen 0, tienen más de un 0. La columna 3ª tiene tres «0» y 5ª columna tiene dos «0».
  2. Generar Mnr. Buscar en la matriz elementos únicos con valor 0. En el caso del ejemplo para n=2 la matriz resultante si tiene elementos únicos puesto que las columnas que tienen 0, tienen más de un 0. La columna 3ª tiene tres «0» y la 5ª columna tiene dos «0».
    El caso de la figura indica que el lugar ocupado por los ceros únicos, es una celda ocupada en este caso por un 2
  3. Generar Mns. Buscar en la matriz elementos únicos con valor 0. En este caso se buscan elementos únicos en el bloque 3×3 y que para el caso concreto, ha coincidido con los elementos obtenidos de en Mnr.
    La matriz de la derecha corresponde con la operación OR del Byte y la matriz origen
  4. En cada generación de una nueva Matriz se realiza una búsqueda de estos elementos únicos. Solo en el caso de ubicaciones ambiguas en las que es posible ocupar dos celdas o tres o más celdas con 3 o más números posibles, habría que usar otros métodos de fuerza bruta o prueba y error hasta que no se me ocurra otro método lógico para este caso. Esto lo dejaremos para otro post.

Para terminar os dejo el diagrama de clases y cuando se me ocurra un método lógico de resolución del sudoku pondré el código en GitHub.

He aquí una muestra con C# y WPF resolviendo Sudokus generados aleatoriamente con 50 celdas ocultas. El algoritmo en la mayoría de los casos, para unas 50 celdas ocultas, no ha necesitado más de 7 iteraciones para resolver todos los elementos por lógica. Le he indicado que dependiendo del número de iteraciones cambie de color el número encontrado, del azul, verde, amarillo al rojo como el color de mayor iteración.

Genetic Algorithm & Binding

Genetic Algorithm & Binding
Ya que sabemos como implementar un algoritmo genético básico con la Interface de Waki, vamos a usar el algoritmo genético de ordenamiento del tablero de ajedrez del post Interface. Algoritmo Genético II y crearemos una interfaz gráfica con WPF y como no, aplicaremos lo que sabemos con Binding.
Lo primero que vamos a hacer es que la clase ChessBoard implemente la interfaz INotifyPropertyChanged del namespace System.ComponentModel. Una vez hecho esto, añadimos el evento public event PropertyChangedEventHandler PropertyChanged y creamos un método que realice la llamada. Ahora que ya podemos notificar cambios en las propiedades, le incluimos una llamada a este método desde la propiedad BestParent, es decir, cada vez que se encuentre una solución parcial en el algoritmo queremos que actualice la pantalla, para ello cambiamos también la propiedad Bestparent por su equivalente de propiedad completa.

public event PropertyChangedEventHandler PropertyChanged;

public void OnPropertyChanged(string property)
{
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
private List bestparent;

public List BestParent
{
        get { return bestparent; }
        set
        { 
            bestparent = value;
            OnPropertyChanged(nameof(BestParent));
        }
}

A la clase ChessPiece le he añadido una propiedad nueva llamada Picture para incluirle la forma de la pieza de ajedrez.
Acto seguido, preparamos nuesto código XAMl, donde yo lo que he hecho, como nuestra clase ChessBoard era una lista de objetos ChessPiece, no he cambiado a coordenadas y simplemente he incluido 64 Textblock, uno por casilla y el DataContext de cada Textblock es el que corresponde a cada objeto de la lista de objetos ChessPiece. Posteriormente cada Textblock tendrá un style asociado donde se le crea el Binding con el texto o la figura de ajedrez. (Las piezas de ajedrez son caracteres Unicode. ♜♞♝♛♚♟♖♘♗♕♔♙).
El estilo aplicado a cada Textblock es el siguiente:

<Style x:Key="piece" TargetType="TextBlock">
            <Setter Property="Text" Value="{Binding Picture}"/>

Código XAML

<Window x:Class="ChessBoardProject.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ChessBoardProject"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="20"/>
            <ColumnDefinition Width="20*"/>
            <ColumnDefinition Width="20"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"/>
            <RowDefinition Height="20*"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" x:Name="board">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="55"/>
                <ColumnDefinition Width="50"/>
                <ColumnDefinition Width="50"/>
                <ColumnDefinition Width="50"/>
                <ColumnDefinition Width="50"/>
                <ColumnDefinition Width="50"/>
                <ColumnDefinition Width="50"/>
                <ColumnDefinition Width="55"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="55"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="55"/>
            </Grid.RowDefinitions>
            

            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="0" Grid.Column="0"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="0" Grid.Column="1"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="0" Grid.Column="2"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="0" Grid.Column="3"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="0" Grid.Column="4"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="0" Grid.Column="5"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="0" Grid.Column="6"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="0" Grid.Column="7"/>

            <Rectangle Style="{StaticResource blackCell}" Grid.Row="1" Grid.Column="0"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="1" Grid.Column="1"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="1" Grid.Column="2"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="1" Grid.Column="3"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="1" Grid.Column="4"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="1" Grid.Column="5"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="1" Grid.Column="6"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="1" Grid.Column="7"/>

            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="2" Grid.Column="0"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="2" Grid.Column="1"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="2" Grid.Column="2"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="2" Grid.Column="3"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="2" Grid.Column="4"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="2" Grid.Column="5"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="2" Grid.Column="6"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="2" Grid.Column="7"/>

            <Rectangle Style="{StaticResource blackCell}" Grid.Row="3" Grid.Column="0"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="3" Grid.Column="1"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="3" Grid.Column="2"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="3" Grid.Column="3"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="3" Grid.Column="4"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="3" Grid.Column="5"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="3" Grid.Column="6"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="3" Grid.Column="7"/>

            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="4" Grid.Column="0"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="4" Grid.Column="1"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="4" Grid.Column="2"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="4" Grid.Column="3"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="4" Grid.Column="4"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="4" Grid.Column="5"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="4" Grid.Column="6"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="4" Grid.Column="7"/>

            <Rectangle Style="{StaticResource blackCell}" Grid.Row="5" Grid.Column="0"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="5" Grid.Column="1"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="5" Grid.Column="2"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="5" Grid.Column="3"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="5" Grid.Column="4"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="5" Grid.Column="5"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="5" Grid.Column="6"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="5" Grid.Column="7"/>

            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="6" Grid.Column="0"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="6" Grid.Column="1"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="6" Grid.Column="2"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="6" Grid.Column="3"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="6" Grid.Column="4"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="6" Grid.Column="5"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="6" Grid.Column="6"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="6" Grid.Column="7"/>

            <Rectangle Style="{StaticResource blackCell}" Grid.Row="7" Grid.Column="0"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="7" Grid.Column="1"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="7" Grid.Column="2"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="7" Grid.Column="3"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="7" Grid.Column="4"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="7" Grid.Column="5"/>
            <Rectangle Style="{StaticResource blackCell}" Grid.Row="7" Grid.Column="6"/>
            <Rectangle Style="{StaticResource whiteCell}" Grid.Row="7" Grid.Column="7"/>

            <Rectangle Fill="Transparent" Stroke="Black" StrokeThickness="3" Grid.RowSpan="8" Grid.ColumnSpan="8"/>

            <TextBlock DataContext="{Binding BestParent[0]}" Style="{StaticResource piece}"  Grid.Row="0" Grid.Column="0"/>
            <TextBlock DataContext="{Binding BestParent[1]}" Style="{StaticResource piece}"  Grid.Row="0" Grid.Column="1"/>
            <TextBlock DataContext="{Binding BestParent[2]}" Style="{StaticResource piece}"  Grid.Row="0" Grid.Column="2"/>
            <TextBlock DataContext="{Binding BestParent[3]}" Style="{StaticResource piece}"  Grid.Row="0" Grid.Column="3"/>
            <TextBlock DataContext="{Binding BestParent[4]}" Style="{StaticResource piece}"  Grid.Row="0" Grid.Column="4"/>
            <TextBlock DataContext="{Binding BestParent[5]}" Style="{StaticResource piece}"  Grid.Row="0" Grid.Column="5"/>
            <TextBlock DataContext="{Binding BestParent[6]}" Style="{StaticResource piece}"  Grid.Row="0" Grid.Column="6"/>
            <TextBlock DataContext="{Binding BestParent[7]}" Style="{StaticResource piece}"  Grid.Row="0" Grid.Column="7"/>

            <TextBlock DataContext="{Binding BestParent[8]}" Style="{StaticResource piece}"   Grid.Row="1" Grid.Column="0"/>
            <TextBlock DataContext="{Binding BestParent[9]}" Style="{StaticResource piece}"   Grid.Row="1" Grid.Column="1"/>
            <TextBlock DataContext="{Binding BestParent[10]}" Style="{StaticResource piece}"  Grid.Row="1" Grid.Column="2"/>
            <TextBlock DataContext="{Binding BestParent[11]}" Style="{StaticResource piece}"  Grid.Row="1" Grid.Column="3"/>
            <TextBlock DataContext="{Binding BestParent[12]}" Style="{StaticResource piece}"  Grid.Row="1" Grid.Column="4"/>
            <TextBlock DataContext="{Binding BestParent[13]}" Style="{StaticResource piece}"  Grid.Row="1" Grid.Column="5"/>
            <TextBlock DataContext="{Binding BestParent[14]}" Style="{StaticResource piece}"  Grid.Row="1" Grid.Column="6"/>
            <TextBlock DataContext="{Binding BestParent[15]}" Style="{StaticResource piece}"  Grid.Row="1" Grid.Column="7"/>

            <TextBlock DataContext="{Binding BestParent[16]}" Style="{StaticResource piece}"  Grid.Row="2" Grid.Column="0"/>
            <TextBlock DataContext="{Binding BestParent[17]}" Style="{StaticResource piece}"  Grid.Row="2" Grid.Column="1"/>
            <TextBlock DataContext="{Binding BestParent[18]}" Style="{StaticResource piece}"  Grid.Row="2" Grid.Column="2"/>
            <TextBlock DataContext="{Binding BestParent[19]}" Style="{StaticResource piece}"  Grid.Row="2" Grid.Column="3"/>
            <TextBlock DataContext="{Binding BestParent[20]}" Style="{StaticResource piece}"  Grid.Row="2" Grid.Column="4"/>
            <TextBlock DataContext="{Binding BestParent[21]}" Style="{StaticResource piece}"  Grid.Row="2" Grid.Column="5"/>
            <TextBlock DataContext="{Binding BestParent[22]}" Style="{StaticResource piece}"  Grid.Row="2" Grid.Column="6"/>
            <TextBlock DataContext="{Binding BestParent[23]}" Style="{StaticResource piece}"  Grid.Row="2" Grid.Column="7"/>

            <TextBlock DataContext="{Binding BestParent[24]}" Style="{StaticResource piece}"  Grid.Row="3" Grid.Column="0"/>
            <TextBlock DataContext="{Binding BestParent[25]}" Style="{StaticResource piece}"  Grid.Row="3" Grid.Column="1"/>
            <TextBlock DataContext="{Binding BestParent[26]}" Style="{StaticResource piece}"  Grid.Row="3" Grid.Column="2"/>
            <TextBlock DataContext="{Binding BestParent[27]}" Style="{StaticResource piece}"  Grid.Row="3" Grid.Column="3"/>
            <TextBlock DataContext="{Binding BestParent[28]}" Style="{StaticResource piece}"  Grid.Row="3" Grid.Column="4"/>
            <TextBlock DataContext="{Binding BestParent[29]}" Style="{StaticResource piece}"  Grid.Row="3" Grid.Column="5"/>
            <TextBlock DataContext="{Binding BestParent[30]}" Style="{StaticResource piece}"  Grid.Row="3" Grid.Column="6"/>
            <TextBlock DataContext="{Binding BestParent[31]}" Style="{StaticResource piece}"  Grid.Row="3" Grid.Column="7"/>

            <TextBlock DataContext="{Binding BestParent[32]}" Style="{StaticResource piece}"  Grid.Row="4" Grid.Column="0"/>
            <TextBlock DataContext="{Binding BestParent[33]}" Style="{StaticResource piece}"  Grid.Row="4" Grid.Column="1"/>
            <TextBlock DataContext="{Binding BestParent[34]}" Style="{StaticResource piece}"  Grid.Row="4" Grid.Column="2"/>
            <TextBlock DataContext="{Binding BestParent[35]}" Style="{StaticResource piece}"  Grid.Row="4" Grid.Column="3"/>
            <TextBlock DataContext="{Binding BestParent[36]}" Style="{StaticResource piece}"  Grid.Row="4" Grid.Column="4"/>
            <TextBlock DataContext="{Binding BestParent[37]}" Style="{StaticResource piece}"  Grid.Row="4" Grid.Column="5"/>
            <TextBlock DataContext="{Binding BestParent[38]}" Style="{StaticResource piece}"  Grid.Row="4" Grid.Column="6"/>
            <TextBlock DataContext="{Binding BestParent[39]}" Style="{StaticResource piece}"  Grid.Row="4" Grid.Column="7"/>

            <TextBlock DataContext="{Binding BestParent[40]}" Style="{StaticResource piece}"  Grid.Row="5" Grid.Column="0"/>
            <TextBlock DataContext="{Binding BestParent[41]}" Style="{StaticResource piece}"  Grid.Row="5" Grid.Column="1"/>
            <TextBlock DataContext="{Binding BestParent[42]}" Style="{StaticResource piece}"  Grid.Row="5" Grid.Column="2"/>
            <TextBlock DataContext="{Binding BestParent[43]}" Style="{StaticResource piece}"  Grid.Row="5" Grid.Column="3"/>
            <TextBlock DataContext="{Binding BestParent[44]}" Style="{StaticResource piece}"  Grid.Row="5" Grid.Column="4"/>
            <TextBlock DataContext="{Binding BestParent[45]}" Style="{StaticResource piece}"  Grid.Row="5" Grid.Column="5"/>
            <TextBlock DataContext="{Binding BestParent[46]}" Style="{StaticResource piece}"  Grid.Row="5" Grid.Column="6"/>
            <TextBlock DataContext="{Binding BestParent[47]}" Style="{StaticResource piece}"  Grid.Row="5" Grid.Column="7"/>

            <TextBlock DataContext="{Binding BestParent[48]}" Style="{StaticResource piece}"  Grid.Row="6" Grid.Column="0"/>
            <TextBlock DataContext="{Binding BestParent[49]}" Style="{StaticResource piece}"  Grid.Row="6" Grid.Column="1"/>
            <TextBlock DataContext="{Binding BestParent[50]}" Style="{StaticResource piece}"  Grid.Row="6" Grid.Column="2"/>
            <TextBlock DataContext="{Binding BestParent[51]}" Style="{StaticResource piece}"  Grid.Row="6" Grid.Column="3"/>
            <TextBlock DataContext="{Binding BestParent[52]}" Style="{StaticResource piece}"  Grid.Row="6" Grid.Column="4"/>
            <TextBlock DataContext="{Binding BestParent[53]}" Style="{StaticResource piece}"  Grid.Row="6" Grid.Column="5"/>
            <TextBlock DataContext="{Binding BestParent[54]}" Style="{StaticResource piece}"  Grid.Row="6" Grid.Column="6"/>
            <TextBlock DataContext="{Binding BestParent[55]}" Style="{StaticResource piece}"  Grid.Row="6" Grid.Column="7"/>

            <TextBlock DataContext="{Binding BestParent[56]}" Style="{StaticResource piece}"  Grid.Row="7" Grid.Column="0"/>
            <TextBlock DataContext="{Binding BestParent[57]}" Style="{StaticResource piece}"  Grid.Row="7" Grid.Column="1"/>
            <TextBlock DataContext="{Binding BestParent[58]}" Style="{StaticResource piece}"  Grid.Row="7" Grid.Column="2"/>
            <TextBlock DataContext="{Binding BestParent[59]}" Style="{StaticResource piece}"  Grid.Row="7" Grid.Column="3"/>
            <TextBlock DataContext="{Binding BestParent[60]}" Style="{StaticResource piece}"  Grid.Row="7" Grid.Column="4"/>
            <TextBlock DataContext="{Binding BestParent[61]}" Style="{StaticResource piece}"  Grid.Row="7" Grid.Column="5"/>
            <TextBlock DataContext="{Binding BestParent[62]}" Style="{StaticResource piece}"  Grid.Row="7" Grid.Column="6"/>
            <TextBlock DataContext="{Binding BestParent[63]}" Style="{StaticResource piece}"  Grid.Row="7" Grid.Column="7"/>
        </Grid>
        <Button Content="Start" Grid.Column="1" HorizontalAlignment="Right" Grid.Row="1" VerticalAlignment="Bottom" Width="75" Margin="20" Click="Button_Click"/>
    </Grid>
   
</Window>

Para no bloquear la interfaz, creamos una tarea y ejecutamos. Le he puesto un retardo de unos cuantos milisegundos en cada solución parcial para ver como actua el algoritmo y el resultado.
ScreenCapture_05-09-2020 12.44.10.gif
Licencia Creative Commons
Esta obra está bajo una Licencia Creative Commons Atribución-CompartirIgual 4.0 Internacional.

Dead XOR Alive

Dead XOR Alive

En uno de mis blogs, en Arithmos, publiqué un post sobre una historia de carceleros y presos; minutos después de acabar el artículo, dije, ¿por qué no hago una app que lo muestre?, dicho y hecho, me puse y en una tarde desarrollada estaba.

Y ¿por qué digo esto? para demostrar lo fácil que es desarrollar una pequeña aplicación con C# y WPF.

Si leen el artículo, podrán observar que la aplicación solo muestra un tablero de ajedrez, con monedas en cada casilla con una cara o una cruz aleatoriamente, un cuadro mágico que es el elegido por el carcelero y un cuadro del tablero que debe calcular uno de los presos para que el siguiente convicto les salve de la muerte, este último cuadro no es otro que una operación XOR.

El modelo podría ser una clase Chessboard que alberga objetos del tipo CoinBox, las monedas que ya de camino le diremos que color tiene el cuadro donde se apoyan en el tablero.

    public class Chessboard
    {
        public List CoinsBox { get; set; }

        public Chessboard() : this(-1) { }

        public Chessboard(int headsCount)
        {
            CoinsBox = new List();
            FillChessboard(headsCount);
        }

        ///
        /// Rellena el tablero con cuadros y monedas aleaotorias con cara y cruz
        /// 

        /// Número de caras. si es -1, se genera aletoriamente
        void FillChessboard(int headsCount)
        {
            var random = new Random();

            headsCount = headsCount == -1 ? random.Next(64) : headsCount;

            // Llenar Lista con monedas con cruz
            Enumerable.Range(0, 64).ToList()
            .ForEach(n => CoinsBox.Add(new CoinBox(n, false, (n / 8) % 2 == 0 ? n % 2 == 0 : n % 2 != 0)));

            // Cambiar n monedas a cara
            Enumerable.Range(0, headsCount).ToList().ForEach(n => CoinsBox[random.Next(64)].IsHeads = true);

            // Cuadro mágico
            CoinsBox[random.Next(64)].IsMagicBox = true;

            // Cuadro que debe cambiar
            var changed= CoinsBox
                .Where(n => n.IsHeads).Aggregate(MagicBox.Index, (acum, n) => acum ^ n.Index, op => op);
            CoinsBox[changed].IsChangedBox = true;

        }

        public CoinBox MagicBox => CoinsBox.Where(n => n.IsMagicBox).FirstOrDefault();
        public CoinBox ChangedBox => CoinsBox.Where(n => n.IsChangedBox).FirstOrDefault();
        public int ParityChessboard => CoinsBox
                .Where(n => n.IsHeads).Aggregate(0, (acum, n) => acum ^ n.Index, op => op);

        public string ParityChessboardToString => $"La paridad del tablero es {ParityChessboard}";
        public string ResultToString => $"Si cambiamos la moneda {ChangedBox.Index}, la paridad del tablero será {MagicBox.Index} que es el cuadrado mágico!!!";
        public string MagicBoxToString => $"El cuadro mágico seleccionado por el carcelero es el número {MagicBox.Index}";
        public string ChangeBoxToString => $"La moneda que debe cambiar el convicto es la número {ChangedBox.Index}";
        public string ListaXORToString => $"La operación XOR de las monedas que tienen cara {string.Join(", ", CoinsBox.Where(k => k.IsHeads).Select(i => i.Index))} y el cuadro mágico {MagicBox.Index} da como resultado {ChangedBox.Index} o la operación XOR entre el cuadro mágico {MagicBox.Index} y la paridad del tablero {ParityChessboard} es {ChangedBox.Index}";
    }

    public class CoinBox
    {

        public CoinBox(int index, bool isHeads, bool isBlack)
        {
            Index = index;
            IsHeads = isHeads;
            IsBlack = isBlack;
        }

        public int Index { get; set; }

        public bool IsHeads { get; set; }

        public bool IsTails { get { return !IsHeads; } }

        public bool IsChangedBox { get; set; }

        public bool IsMagicBox { get; set; }

        public bool IsBlack { get; set; }
    }

Como algo particular, la asignación del color del cuadro mediante la operación (n / 8) % 2 == 0 ? n % 2 == 0 : n % 2 != 0) con la que con el índice del cuadro le decimos si el cuadro es negro o blanco o las expresiones lambda que calculan la operación XOR sobre una lista de elementos mediante la función lambda Aggregate, la cual le pasamos un parámetro acumulador inicial y posteriormente es usado elemento a elemento con la operación XOR.Para la vista, he creado un control definido por el usuario llamado Box, el cual contiene el Binding cambiando su aspecto según las propiedades del objeto CoinBox asociado a su Datacontext. Como podéis ver a continuación, creo converter para transformar valores booleanos en Visibility, Brush o Bitmap. (Si alguien los quiere los converter, los cuelgo)

<UserControl x:Class="DxorAlive.Box"
             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:DxorAlive"
             mc:Ignorable="d" 
             d:DesignHeight="43" d:DesignWidth="43" Margin="1">
    <UserControl.Resources>
        <local:BoolToVisibleConverter x:Key="b2tvc" Reverse="False"/>
        <local:ImageConverter x:Key="itc"/>
        <local:BoolToColorConverter x:Key="btcc"/>
    </UserControl.Resources>
    <Grid>

        <Rectangle Width="64" Height="64" Stroke="Gray" StrokeThickness="1" Fill="{Binding IsBlack, Converter={StaticResource btcc}}"/>
        <Rectangle Width="62" Height="62" Stroke="Red" StrokeThickness="2" Visibility="{Binding IsMagicBox, Converter={StaticResource b2tvc}}"/>
        <Rectangle Width="60" Height="60" Stroke="Orange" StrokeThickness="2" Visibility="{Binding IsChangedBox, Converter={StaticResource b2tvc}}"/>
        <Image Source="{Binding IsHeads, Converter={StaticResource itc}}" Height="48"/>
    </Grid>
</UserControl>

Ahora solo nos queda crear o con xml o desde la clase de la vista (runtime) una matriz de objetos cada uno con su datacontext. Yo lo he he desarrollado con código en vez de crear 64 cuadros del tipo Box. Llamamos al método FillChessboard y se genera un nuevo tablero.

        private void FillChessboard(int headsCount)
        {
            grid.Children.Clear();
            // Crear tablero
            var cb = new Chessboard(headsCount);

            //crear cuadros y monedas
            cb.CoinsBox.ForEach(n =>
            {
                var c = new Box();
                c.DataContext = n;
                var row = n.Index / 8;
                var col = n.Index % 8;
                Grid.SetRow(c, row);
                Grid.SetColumn(c, col+1);
                grid.Children.Add(c);
            });

            this.DataContext = cb;

        }

he aquí el resultado. Un saludo a tod@s

deadxoralive1.png
Os dejo el enlace del ejecutable

PD: Y como llevo una temporada enamorado de Kotlin, os paso código en este lenguaje para una app de consola. I ♥ kt

 
import kotlin.random.Random

fun main(args: Array) {
    val c= Chessboard(5)
    println(c)

}


class Chessboard(var headsCount: Int) {
    val coinsBox = mutableListOf()

    init {
        headsCount = if (headsCount == -1) Random.nextInt(64) else headsCount
        fillChessboard()
    }

    private fun fillChessboard() {
        // Rellenar de monedas
        (0..63).toList().forEach {
            coinsBox.add(CoinBox(it, false, if ((it / 8) % 2 == 0) (it % 2 == 0) else (it % 2 != 0)))
        }
        // Caras
        (0..headsCount!!).toList().forEach { coinsBox[Random.nextInt(64)].isHeads = true }
        // cuadro mágico
        val vMB = Random.nextInt(64)
        coinsBox[vMB].isMagicBox = true
        // moneda cambiante
        coinsBox[coinsBox.filter { i-> i.isHeads }.fold(vMB) { acc, opXOR -> acc.xor(opXOR.index) }].isChangedBox=true

    }

    override fun toString(): String {
        return "$magic\n$parityToString\n$changed\n$result\n$listaXOR\n$chessboard"
    }

    val magicBox = coinsBox.filter { n -> n.isMagicBox }.firstOrNull()
    val changedBox = coinsBox.filter { n -> n.isChangedBox }.firstOrNull()
    val parityChessBoard = coinsBox[coinsBox.filter { i-> i.isHeads }.fold(0) { acc, opXOR -> acc.xor(opXOR.index) }]

    val parityToString= "La paridad del tablero es ${parityChessBoard.index}"
    val result = "Si cambiamos la moneda ${changedBox?.index}, la paridad del tablero será ${magicBox?.index} que es el cuadrado mágico"
    val magic = "El cuadro seleccionado por el carcelero es el número ${magicBox?.index}"
    val changed = "La moneda que debe cambiar el convicto es la número ${changedBox?.index}"
    val listaXOR = "La operación XOR de las monedas que tienen cara ${coinsBox.filter { k-> k.isHeads }.joinToString(", "){it.index.toString()}} " +
            "y el cuadro mágico ${magicBox?.index} da como resultado ${changedBox?.index} " +
            "\no la operación XOR entre el cuadro mágico ${magicBox?.index} y la paridad del tablero ${parityChessBoard.index} es ${changedBox?.index}"
    val chessboard = coinsBox.joinToString("") {k-> "\t${k.index} ${if (k.isHeads) "H" else "T"} ${if ((k.index+1) % 8==0) "\n" else ""}"  }

}


data class CoinBox(
    val index: Int,
    var isHeads: Boolean,
    var isBlack: Boolean,
    var isChangedBox: Boolean = false,
    var isMagicBox: Boolean = false

Binding WPF vs Binding Android – I

Binding WPF vs Binding Android – I
Este post, es el inicio de una tetralogía con el que pretendo mostrar los aspectos de desarrollo de ambas plataformas, Microsoft y Android en cuanto al uso de la arquitectura MVVM y Databinding.
Antes de nada veamos cuales son las diferencias esenciales del modelo vista controlador (MVC) y el modelo vista vista-modelo (MVVM).

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
    }
}

Semáforo I

Semáforo I

Vamos a crear un sistema de control de semáforos. Un sistema de control de tráfico, debe coordinar semáforos conforme a la situación geográfica, prioridad de paso, alta circulación y sinfín de factores, en este caso vamos a crear uno sencillo que nos permita cruzar una avenida de una sola vez. Para ello debemos tener un sistema de control y semáforos lógicamente. La mayoría de los semáforos, como de todos es bien sabido, tienen 3 luces, una roja, otra ámbar y otra verde, algunos además tienen control de peatones e indicaciones auxiliares, pero nosotros vamos a crear uno sencillo, con sus tres luces y más adelante ya veremos si ampliamos sus funcionalidades.

Su funcionamiento básico es verde, pasa a amarillo esperando durante unos segundos pasando a rojo finalizando el bucle, vuelve al verde y comienza de nuevo. También puede estar parpadeando.

Pues empezando por lo básico, vamos a crear las luces y como estamos en 2017, vamos a crearlos de LED,s, ya que cambiar el color y añadir un degradado radial no es muy complicado, lo vamos a hacer un poco más difícil o más fácil según se vea.

He usado WPF para crear un control de usuario y dentro de este, he añadido un grid donde voy a distribuir los LED,s dándole un aspecto como el siguiente (el código lo adjunto al final del post)
semaforo01

Además, le he agregado algunas propiedades de dependencia en las que podamos cambiar el color de los LED cuando está encendida la luz, (en este caso yo las he puesto blancas), el color del LED cuando está apagada y el color de fondo cuando están de igual modo apagadas o encendidas. A continuación el código

        #region DependecyProperties

        public bool LightIsEnabled
        {
            get { return (bool)GetValue(LightIsEnabledProperty); }
            set { SetValue(LightIsEnabledProperty, value); }
        }

        public Brush LEDColorON
        {
            get { return (Brush)GetValue(LEDColorONProperty); }
            set { SetValue(LEDColorONProperty, value); }
        }

        public Brush LEDColorOFF
        {
            get { return (Brush)GetValue(LEDColorOFFProperty); }
            set { SetValue(LEDColorOFFProperty, value); }
        }

        public Brush LEDLightBackgroundOFF
        {
            get { return (Brush)GetValue(LEDLightBackgroundProperty); }
            set { SetValue(LEDLightBackgroundProperty, value); }
        }

        public Brush LEDLightBackgroundON
        {
            get { return (Brush)GetValue(LEDLightBackgroundONProperty); }
            set { SetValue(LEDLightBackgroundONProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LEDLightBackgroundONProperty =
            DependencyProperty.Register("LEDLightBackgroundON", typeof(Brush), typeof(LEDLight), new PropertyMetadata(Brushes.Lime));

        // Using a DependencyProperty as the backing store for LEDLightBackground.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LEDLightBackgroundProperty =
            DependencyProperty.Register("LEDLightBackgroundOFF", typeof(Brush), typeof(LEDLight), new PropertyMetadata(Brushes.Silver));

        // Using a DependencyProperty as the backing store for LEDColorOFF.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LEDColorOFFProperty =
            DependencyProperty.Register("LEDColorOFF", typeof(Brush), typeof(LEDLight), new PropertyMetadata(Brushes.LightGray));

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LEDColorONProperty =
            DependencyProperty.Register("LEDColorON", typeof(Brush), typeof(LEDLight), new PropertyMetadata(Brushes.White));

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

        #endregion

Y ¿como se enciende y se apaga? He incluido un par de DataTrigger que comprueban si el valor de la propiedad LightIsEnabled es verdadero y si es así asigna el valor de la propiedad LEDColorON a cada LED y el valor de la propiedad LEDLightBackgroundON al relleno de la ellipse interna. Si al crear una instancia de este, cambiamos esta propiedad, la luz se encenderá o se apagará.
Ahora solo nos queda crear una instancia desde un formulario y ver si funciona

Video5.gif
Fondo de color y LED,s blancos

Video6.gif

Fondo y LED,s de color

y las instancias desde XAML, quedarían así:


y el código del control de usuario. En la próxima entrega el semáforo.

<UserControl x:Class="Controls.LEDLight"
             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="200" d:DesignWidth="200">
    <Grid x:Name="grid">
        <Grid.Resources>
            <!--ESTILO DE LED-->
            <Style x:Key="ellipseLED" TargetType="Ellipse">
                <Setter Property="Margin" Value="1"/>
                <Setter Property="HorizontalAlignment" Value="Stretch"/>
                <Setter Property="VerticalAlignment" Value="Stretch"/>
                <Setter Property="Fill" Value="{Binding LEDColorOFF}"/>
                <Setter Property="Grid.RowSpan" Value="2"/>
                <Setter Property="Grid.ColumnSpan" Value="2"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding LightIsEnabled}" Value="True">
                        <Setter Property="Fill" Value="{Binding LEDColorON}"/>
                    </DataTrigger>
                    <MultiDataTrigger>
                        <MultiDataTrigger.Conditions>
                            <Condition Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=controls:LEDLight},Path=LEDLightInsideShape}" Value="{x:Static controls:LEDLight+LEDLightInsideShapeEnum.LeftArrow}"/>
                            <Condition Binding="{Binding LightIsEnabled}" Value="True"/>
                        </MultiDataTrigger.Conditions>
                        <Setter Property="Fill" Value="{Binding LEDColorON}"/>
                    </MultiDataTrigger>
                </Style.Triggers>
            </Style>
            <!--ESTILO DEL CIRCULO CENTRAL EXTERIOR-->
            <Style x:Key="centralEllipse" TargetType="Ellipse">
                <Setter Property="Fill" Value="Transparent"/>
                <Setter Property="Stroke" Value="DarkGray"/>
                <Setter Property="StrokeThickness" Value="5"/>
            </Style>
            <!--ESTILO DEL FONDO DEL CIRCULO INTERIOR-->
            <Style x:Key="centralEllipse2" TargetType="Ellipse">
                <Setter Property="Fill" Value="{Binding LEDLightBackgroundOFF}"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding LightIsEnabled}" Value="True">
                        <Setter Property="Fill" Value="{Binding LEDLightBackgroundON}"/>
                    </DataTrigger>
                </Style.Triggers>

            </Style>
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="10*"/>
        </Grid.ColumnDefinitions>
        <!--Ellipse central. INTERIOR-->
        <Ellipse Grid.Row="0" Grid.ColumnSpan="27" Grid.RowSpan="28" Style="{StaticResource  centralEllipse2}"/>

        <!--Linea 0-->
        <Ellipse Grid.Row="1" Grid.Column="8" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="1" Grid.Column="10" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="1" Grid.Column="12" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="1" Grid.Column="14" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="1" Grid.Column="16" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 1-->
        <Ellipse Grid.Row="3" Grid.Column="5" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="3" Grid.Column="7" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="3" Grid.Column="9" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="3" Grid.Column="11" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="3" Grid.Column="13" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="3" Grid.Column="15" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="3" Grid.Column="17" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="3" Grid.Column="19" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 2-->
        <Ellipse Grid.Row="5" Grid.Column="4" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="5" Grid.Column="6" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="5" Grid.Column="8" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="5" Grid.Column="10" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="5" Grid.Column="12" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="5" Grid.Column="14" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="5" Grid.Column="16" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="5" Grid.Column="18" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="5" Grid.Column="20" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 3-->
        <Ellipse Grid.Row="7" Grid.Column="3" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="7" Grid.Column="5" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="7" Grid.Column="7" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="7" Grid.Column="9" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="7" Grid.Column="11" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="7" Grid.Column="13" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="7" Grid.Column="15" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="7" Grid.Column="17" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="7" Grid.Column="19" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="7" Grid.Column="21" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 4-->
        <Ellipse Grid.Row="9" Grid.Column="2" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="9" Grid.Column="4" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="9" Grid.Column="6" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="9" Grid.Column="8" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="9" Grid.Column="10" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="9" Grid.Column="12" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="9" Grid.Column="14" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="9" Grid.Column="16" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="9" Grid.Column="18" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="9" Grid.Column="20" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="9" Grid.Column="22" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 5-->
        <Ellipse Grid.Row="11" Grid.Column="1" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="3" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="5" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="7" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="9" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="11" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="13" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="15" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="17" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="19" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="21" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="11" Grid.Column="23" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 6-->
        <Ellipse Grid.Row="13" Grid.Column="2" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="13" Grid.Column="4" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="13" Grid.Column="6" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="13" Grid.Column="8" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="13" Grid.Column="10" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="13" Grid.Column="12" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="13" Grid.Column="14" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="13" Grid.Column="16" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="13" Grid.Column="18" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="13" Grid.Column="20" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="13" Grid.Column="22" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 7-->
        <Ellipse Grid.Row="15" Grid.Column="1" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="3" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="5" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="7" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="9" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="11" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="13" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="15" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="17" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="19" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="21" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="15" Grid.Column="23" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 8-->
        <Ellipse Grid.Row="17" Grid.Column="2" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="17" Grid.Column="4" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="17" Grid.Column="6" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="17" Grid.Column="8" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="17" Grid.Column="10" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="17" Grid.Column="12" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="17" Grid.Column="14" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="17" Grid.Column="16" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="17" Grid.Column="18" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="17" Grid.Column="20" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="17" Grid.Column="22" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 9-->
        <Ellipse Grid.Row="19" Grid.Column="3" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="19" Grid.Column="5" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="19" Grid.Column="7" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="19" Grid.Column="9" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="19" Grid.Column="11" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="19" Grid.Column="13" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="19" Grid.Column="15" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="19" Grid.Column="17" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="19" Grid.Column="19" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="19" Grid.Column="21" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 10-->
        <Ellipse Grid.Row="21" Grid.Column="4" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="21" Grid.Column="6" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="21" Grid.Column="8" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="21" Grid.Column="10" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="21" Grid.Column="12" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="21" Grid.Column="14" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="21" Grid.Column="16" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="21" Grid.Column="18" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="21" Grid.Column="20" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 11-->
        <Ellipse Grid.Row="23" Grid.Column="5" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="23" Grid.Column="7" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="23" Grid.Column="9" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="23" Grid.Column="11" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="23" Grid.Column="13" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="23" Grid.Column="15" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="23" Grid.Column="17" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="23" Grid.Column="19" Style="{StaticResource ellipseLED}"/>
        
        <!--Linea 12-->
        <Ellipse Grid.Row="25" Grid.Column="8" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="25" Grid.Column="10" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="25" Grid.Column="12" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="25" Grid.Column="14" Style="{StaticResource ellipseLED}"/>
        <Ellipse Grid.Row="25" Grid.Column="16" Style="{StaticResource ellipseLED}"/>
        
        <!--Ellipse central. EXTERIOR-->
        <Ellipse Grid.Row="0" Grid.ColumnSpan="27" Grid.RowSpan="28" Style="{StaticResource centralEllipse}"/>
    </Grid>
</UserControl>

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

Tabla periódica 2

Tabla periódica 2
En el post que escribí sobre la tabla periódica, pudimos aprender como usar MVVM (Model View ViewModel) y hacer aplicaciones robustas y consistentes con muy poco código; por una parte teníamos las clases ViewModel y por otra la interfaz gráfica que se encarga de realizar los binding necesarios para visualizar su contenido. En aquel caso, cargábamos los datos de los elementos de la tabla periódica desde un archivo xml y se mostraban en pantalla ordenados tal y como conocemos la tabla periódica actualmente; pulsando sobre cualquier elemento, se visualizaba un elemento con algunos datos ampliados, bueno pues en esta ocasión vamos a imaginar que nuestro cliente nos solicita incluir en nuestra tabla un método que visualice la temperatura y según vaya esta variando, el estado de cada elemento lo haga también, ya sea inferior al punto de fusión, estado sólido, sea superior al punto de fusión e inferior al punto de ebullición, estado líquido, o sea superior a este último en que el estado cambiaría a estado gaseoso. Aquí os muestro un diagrama de cambio de estado y un ejemplo del elemento Galio y los puntos de fusión y ebullición.TP201.png

TP202.png

En el archivo xml ya teníamos incluidos los datos correspondientes a los puntos de fusión y ebullición, por tanto solo nos queda implementar nuestra modificación.

En el anterior post sobre la tabla periódica, el estado del elemento se trataba mediante un converter(clase que implementaba la interfaz IValueConverter en el binding al que le pasábamos el estado y devolvía un color, de modo que según el estado, se le asignaba un color al símbolo. En este caso además del estado, tenemos más parámetros y son la temperatura actual y los puntos de fusión y ebullición. Y ¿cómo hacemos para mostrar los estados de los elementos con variación de temperatura? Se me ocurre incluir un control Sliderque varíe desde los -2500ºC hasta los 6000ºC (valor inferior y superior del menor y mayor valor de fusión y ebullición respectivamente) y el valor de este pasarlo junto a los valores de los puntos de fusión y ebullición de cada elemento, pero debéis recordar que la clase que implementa IValueConverter solo podíamos pasar un valor y un parámetro, por lo que para nuestra modificación no nos serviría. Entonces es cuando tenemos que echar mano de otra interfaz muy similar pero que se adapta mucho más a nuestras necesidades y es IMultiValueConverter. Esta interfaz contiene dos métodos Convert y ConvertBack como la interfaz IValueConverter pero con la diferencia de que los valores que se pasan se trata de un array de objetos, por lo cual ahora podemos pasar todos nuestros valores al conversor; a continuación os muestro la clase.

    public class StateToColorConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            SolidColorBrush scb = new SolidColorBrush();
            try
            {
                double temp= double.Parse(values[0].ToString());
                double meltingPoint = (double)values[1];
                double boilingPoint = (double)values[2];
                string state = values[3].ToString();

                //parameter
                if (temp <= boilingPoint)
                {
                    // Sólido
                    if (state == "Artificial") scb.Color = Brushes.DarkRed.Color;
                    else scb.Color = Brushes.Black.Color;
                }
                else if (temp > boilingPoint && temp < meltingPoint)
                {
                    // Líquido
                    scb.Color = Brushes.Blue.Color;
                }
                else
                {
                    // Gas
                    scb.Color = Brushes.LightBlue.Color;
                }
                return scb;

            }
            catch (InvalidCastException)
            {
                return scb;
            }
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            return null;
        }

    }

El código XAML para enlazar los datos con esta clase, debemos hacerlo con un multibinding. Lo primero, instanciar la clase y asignarle un key , segundo desde la propiedad que queremos obtener el color que es el control que visualiza el símbolo con TextBlock.Foreground, le añadimos el tag y posteriormente añadimos tantos binding como valores queremos pasar

<UserControl x:Class="DimitriMendeleyev.ElementControl"
             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:DimitriMendeleyev"
             mc:Ignorable="d"
             xmlns:Controls="http://metro.mahapps.com/winfx/xaml/controls"
             d:DesignHeight="60" d:DesignWidth="60" x:Name="_element">
    <UserControl.Resources>
        <local:SerialChemicalToColorConverter x:Key="sctcc"/>
        <local:StateToColorConverter x:Key="stcc"/>
    </UserControl.Resources>
    <Grid>
        <Button Width="auto" Height="auto"
                Background="{Binding Path=SerialChemical, Converter={StaticResource sctcc}, Mode=OneTime}"
                x:Name="buttonElement">
            <StackPanel>
                <TextBlock x:Name="AtomicNumberTextBlock" Text="{Binding AtomicNumber, Mode=OneTime}" FontSize="12" TextAlignment="Center"/>
                <TextBlock x:Name="SymbolTextBlock" Text="{Binding Symbol, Mode=TwoWay}" FontSize="15" TextAlignment="Center" FontWeight="Bold">
                    <TextBlock.Foreground>
                        <MultiBinding Converter="{StaticResource stcc}">
                            <Binding Path="Temperature"/>
                            <Binding Path="MeltingPoint"/>
                            <Binding Path="BoilingPoint"/>
                            <Binding Path="StandarState"/>
                        </MultiBinding>
                    </TextBlock.Foreground>
                </TextBlock>
                <TextBlock x:Name="NameTextBlock" Text="{Binding Name, Mode=OneTime}" FontSize="10" TextAlignment="Center" />
            </StackPanel>
        </Button>
    </Grid>
</UserControl>

Queda un detalle y es que la temperatura actual hay que pasarla a cada elemento para que este pueda hacer el binding y esto lo he conseguido añadiendo una nueva propiedad a la clase Element y PeriodicTable, entonces enlazamos la propiedad Value del slider que hemos incluido con la propiedad de la PeriodicTable y cada vez que cambie esta, se actualizan las propiedades de cada objeto Element.

/* ****************************************************************
* © JOAQUIN MARTINEZ RUS 2016
* PROYECTO:        Tabla periódica
* Archivo:         Element.cs
* Descripción:     Clase element.
*
* Historial:
*                  1. 12 sep 2016. Creación
*                  2. 03 oct 2016. Modificación. Se añade la propiedad
*                  Temperature para implementación de estados variables
*                  en función de la temperatura
*
* Comentarios:      Elemento de la tabla periódica
*
*
*******************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;

namespace DimitriMendeleyev
{
    public class Element: INotifyPropertyChanged
    {
        public string Symbol { get; set; }
        public string Name { get; set; }
        public int AtomicNumber { get; set; }
        public double AtomicMass { get; set; }
        public double Density { get; set; }
        public string Valencia { get; set; }
        public double MeltingPoint { get; set; }
        public double BoilingPoint { get; set; }
        public string StandarState { get; set; }
        public string SerialChemical { get; set; }
        public int Period { get; set; }
        public int Group { get; set; }
        private double temperature;

        public double Temperature
        {
            get { return temperature; }
            set
            {
                temperature = value;
                OnPropertyChanged("Temperature");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        ///
<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));
            }
        }

    }
}

/* ****************************************************************
* © JOAQUIN MARTINEZ RUS 2016
* PROYECTO:        Tabla periódica
* Archivo:         PeriodicTable.cs
* Descripción:     Clase PeriodicTable.
*
* Historial:
*                  1. 12 sep 2016. Creación
*                  2. 03 oct 2016. Modificación. Se añade la propiedad
*                  Temperature para implementación de estados variables
*                  en función de la temperatura
*
* Comentarios:     Tabla periódica
*
*
*******************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;

namespace DimitriMendeleyev
{
    public class PeriodicTable: INotifyPropertyChanged
    {
        public PeriodicTable()
        {
            Elements = new List();
            SerialChemical = new List();
            SerialChemical.AddRange(new string[] { "Metales", "Metaloides" , "Otros no metales",
                "Halógenos", "Alcalinotérreos", "Alcalinos", "Metales de Transición",
                "Gases nobles", "Lantánidos", "Actínidos"});
            States = new List();
            States.AddRange(new string[] { "Gas", "Líquido", "Sólido", "Artificial" });
        }

        public List Elements { get; set; }
        public List SerialChemical { get; set; }
        public List States { get; set; }
        private double temperature;

        public double Temperature
        {
            get { return temperature; }
            set
            {
                temperature = value;
                setTemperatures();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        ///
<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));
            }
        }

        void setTemperatures()
        {
            foreach (var element in Elements)
            {
                element.Temperature = this.Temperature;
            }
        }
    }
}

el código XAML del control es

        <TextBlock x:Name="Temp" Grid.Column="4" Grid.Row="8" Grid.RowSpan="2" Text="{Binding ElementName=slider, Path=Value, StringFormat='{}{0:0}°C'}" FontSize="40" TextAlignment="Center" VerticalAlignment="Top" Grid.ColumnSpan="3"/>
        <Slider x:Name="slider" Grid.ColumnSpan="12" Grid.Column="7" Grid.RowSpan="2"
                HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="30" Margin="10"
                Grid.Row="8" Minimum="-2500" Maximum="6000" Value="{Binding Temperature, Mode=TwoWay}"
                Ticks="1" TickFrequency="15" SmallChange="1"/>

Y el resultado es el siguiente

TablaPeriodica.gif

Con esto debemos aprender varias cosas, la primera es que un código bien definido inicialmente, nos va a permitir realizar modificaciones sobre nuestra aplicación con facilidad y por otra parte, aprovecharnos de las condiciones que WPF nos brinda, nos permite seguir manteniendo aplicaciones robustas con los menos BUG,s posibles aunque en este caso de la tabla periódica, la sencillez de la aplicación no creo que sea un buen ejemplo, no obstante nunca hay que fiarse.

Una vez más, saludos

Sucesión de Fibonacci

Sucesión de Fibonacci
Publiqué en mi otro blog sobre mates, física y otros temas un post acerca de la sucesión y la espiral de Fibonacci y gustándome como me gustan las funciones recursivas, no podía quedar atrás una implementación en C# de esta sucesión.
Para ellos, en este artículo expondré diversos métodos para calcular la sucesión de Fibonacci a los cuales se les pasará como argumento el valor de n de la función y que a continuación os muestro:
El primero de ellos será mediante la función recursiva. Este es el método natural de cálculo de la sucesión
espiral01.png

        public double GetRecursiveFunction(int n)
        {
            if ((n == 0) || (n == 1)) return 1;
            else return GetRecursiveFunction(n -1) + GetRecursiveFunction(n - 2);
        }

Este método tiene el inconveniente de la complejidad recurrente que se aplica sobre su cálculo, es decir que si solicitamos el valor del elemento 39, haríamos una llamada a GetRecursiveFunction(39) , la cual una vez dentro de esta, llamará a los elementos GetRecursiveFunction(38) y GetRecursiveFunction(37) de la misma función para sumarlos, y estos a su vez a sus dos anteriores hasta llegar a GetRecursiveFunction(0) o GetRecursiveFunction(1) devolviendo el valor de 1 a partir de entonces comenzará a sumar. Esto significa que para cada cálculo se realiza la suma de las iteraciones de los dos elementos anteriores más 1. Si para n igual a 6 se llama a la función 15 veces y para n igual a 7, 25 veces, para n igual a 8 se llamaría a la función 15 más 25 y más 1 de la primera llamada. Esto supone un gasto de recursos enorme ya que para n igual a 40 el número de iteraciones se elevaría a la suma de 78.176.337, 126.491.971 y 1, un total de 204.668.309 iteraciones para obtener el elemento 40 de la sucesión; en tiempo, aproximadamente unos 8 segundos en un dual core con SSD. A todos los efectos, no es eficiente.El siguiente método de cálculo de la sucesión de Fibonacci, será el de la función general extraída mediante la ecuación de recurrencia y sus raíces.espiral14.png

Esta función tiene como inconveniente la precisión y muestro el por qué. En primero lugar creo una propiedad de solo lectura para calcular ϕ (Phi) o el número áureo, el cual es igual a
Espiral04.png
provocando una falta de precisión; por otra parte el uso de la exponenciación a n, lo que a su vez provoca que un número excesivamente grande, sea tratado exponencialmente. Todo esto unido para un n grande, nos hace obtener un número extremadamente aproximado a cualquier elemento de la sucesión, pero sin llegar a serlo, por lo que debemos auxiliarnos de la función de redondeo para obtener unos resultados satisfactorios. A pesar de los complejos cálculos, es muy rápida.

        public double Phi
        {
            get
            {
                return (1 + Math.Sqrt(5)) / 2;
            }
        }

        public double GetGeneralFuncion(int n)
        {
            double result = (double)((Math.Pow(Phi, n) - Math.Pow((1 - Phi), n)) / Math.Sqrt(5));
            return Math.Round(result);
        }

La siguiente función es la iterativa. Esta parte de los dos primeros valores 0 y 1 y va obteniendo el siguiente en cada iteración, sencilla y muy rápida. No tiene problemas de redondeo. El inconveniente que tiene es que no almacena los valores anteriores, sino que simplemente los calcula.

        public double GetIterativeFunction(int n)
        {
            double f0 = 0;
            double f1 = 1;
            for (int i = 1; i < n; i++)
            {
                f1 = f0 + f1;
                f0 = f1 - f0;
            }

            return f1;
        }

Por último, una función parecida a la iterativa pero en la cual quedan almacenados los elementos de la sucesión mediante un objeto List, de este modo se asignan los dos primeros valores y se van generando los siguientes añadiéndolos a la lista según se van calculando.

        public List numbers = new List();
        public void GetArrayListFunction(int n)
        {
            numbers.Clear();
            numbers.Add(0);
            numbers.Add(1);
            for (int i = 2; i < n; i++)
            {
                numbers.Add(numbers[i - 1] + numbers[i - 2]);
            }
        }

Una vez que hemos visto las funciones para obtener los elementos de la sucesión de Fibonacci, vamos a calcular la espiral de Fibonacci mediante dos métodos, XAML y código puro y duro.
Si lo hacemos con XAML, creamos un objeto Path y dentro de Path.Data con el siguiente código:

        <Path Stroke="Red" StrokeThickness="2" >
        <Path.Data>
            <PathGeometry>
                <PathGeometry.Figures>
                    <PathFigureCollection>
                            <PathFigure StartPoint="610,610">
                                <PathFigure.Segments>
                                <PathSegmentCollection>
                                        <ArcSegment Point="600, 600" Size="10 10" />
                                        <ArcSegment Point="590, 610" Size="10 10" />
                                        <ArcSegment Point="610, 630" Size="20 20" />
                                        <ArcSegment Point="640, 600" Size="30 30" />
                                        <ArcSegment Point="590, 550" Size="50 50" />
                                        <ArcSegment Point="510, 630" Size="80 80" />
                                        <ArcSegment Point="640, 760" Size="130 130" />
                                        <ArcSegment Point="850, 550" Size="210 210" />
                                        <ArcSegment Point="510, 210" Size="340 340" />
                                        <ArcSegment Point="-40, 760" Size="550 550" />
                                        <ArcSegment Point="850, 1650" Size="890 890" />
                                        <ArcSegment Point="2290, 210" Size="1440 1440" />
                                    </PathSegmentCollection>
                            </PathFigure.Segments>
                        </PathFigure>
                    </PathFigureCollection>
                </PathGeometry.Figures>
            </PathGeometry>
        </Path.Data>
        </Path>

Lo que hacemos es crear objetos ArcSegment como tantos elementos de la sucesión queramos incluir, pero su límite es evidente, por lo que podríamos crear un método genérico que creara una espiral en base al argumento n, de modo que llamamos por ejemplo a la función iterativa basada en List y con los datos almacenados en esta lista, usarlos para crear tanto objetos ArcSegment como elementos tenga la sucesión. El problema es que la sucesión de Fibonacci tiene un crecimiento alto y la espiral desaparecerá rápidamente de nuestra ventana, pero calcular, calcula correctamente la espiral.
He creado dos propiedades para el centro de la espiral, x e y un ángulo como variable angular. (También podríamos haber usado la función polar de la espiral, pero he escogido esta para mostrar cada arco de segmento).
Para seleccionar el centro de cada arco, he ingeniado un método que mediante el seno y coseno del ángulo de la función, sume o reste los valores de la sucesión y asigne correctamente el centro.
Una vez creados los arcos, los añadimos al objeto Figure y como en XAML vamos añadiendo a la colección.

        public double CenterX { get; set; } = 512;
        public double CenterY { get; set; } = 384;
        public double Angle { get; set; } = Math.PI;

        void create(int n)
        {

            Fibonacci fib = new Fibonacci();
            fib.GetArrayListFunction(n);

            PathGeometry pathGeometry = new PathGeometry();

            PathFigure figure = new PathFigure();
            figure.StartPoint = new Point(CenterX, CenterY);

            for (int i =0; i < fib.numbers.Count; i++)
            {
                double sign = (i % 2 == 0 ? 1 : -1);
                double x = sign==1? Math.Round(Math.Cos(Angle)) : Math.Round(Math.Sin(Angle));
                double y = sign == 1 ? Math.Round(Math.Sin(Angle - Math.PI / 2)) : Math.Round(Math.Cos(Angle-Math.PI/2));
                double a = Math.Round(fib.numbers[i] * sign * x);
                double b = Math.Round(fib.numbers[i] * sign * y);

                CenterX = CenterX + a;
                CenterY = CenterY + b;

                ArcSegment arcSegment = new ArcSegment(new Point(CenterX, CenterY),
                    new Size(fib.numbers[i], fib.numbers[i]),
                    0,false,SweepDirection.Counterclockwise,true);

                figure.Segments.Add(arcSegment);
                Angle += Math.PI/2;

            }

            pathGeometry.Figures.Add(figure);
            path.Data = pathGeometry;
            path.Stroke = Brushes.Green;
        }

el resultado en ambos casos

fib02.png
y esto es todo,
saludos!