jueves, 21 de julio de 2016

Redireccionar a wakicode

Hola a tod@s
Si queréis visitar mi blog, me he mudado a wakicode.com donde podréis consultar los artículos de este blog y los que vaya añadiendo.
Saludos y bienvenidos a mi nuevo sitio
Nota: Este blog se redireccionará automáticamente a https://wakicode.com.

martes, 19 de julio de 2016

Calculadora Números Complejos (C# y WPF)

Java y C# son lenguajes muy parecidos, de hecho C# deriva de C++, Java y otros lenguajes alimentándose de lo bueno de cada uno; particularmente para mi es el mejor por su potencia y sus increíbles mejoras que nos hacen la programación mucho más fácil.
En el anterior post, describí una calculadora de números complejos en Java y en este, lo implemento con C# y WPF. Con esto intento tres cosas, en primer lugar demostrar que la clase (muy pero que muy similar a la de Java) es invariable porque me daría igual usar una aplicación de consola o gráfica, otra es que podáis ver la diferencia entre lenguajes pero la similitud estructural y por último implementarla mediante WPF con el cual nos vamos a ahorrar mucho código quedando la aplicación muy limpia y robusta y de camino os enseño como se aplican estilos a los controles.

Lo primero vayamos a la clase Complex. Las principales diferencias con la de Java son las que voy a enumerar.
  1. Implemento la interfaz INotifyPropertyChanged de modo que la clase debe contener el evento public event PropertyChangedEventHandler PropertyChanged; el cual invocamos mediante el método OnPropertyChanged(string p_PropertyName) cada vez que cambia la propiedad de la parte real o imaginaria de un número complejo.
  2. En esta clase, he sobrecargado las operaciones básicas de suma, resta, multiplicación, división y negación (a este operador le he asignado el opuesto). Para ver como lo he hecho, os paso el código de la operación suma con el que con un método
    public static Complex operator + (Complex _complex1, Complex _complex2)
            {
                return new Complex(_complex1.Real + _complex2.Real, _complex1.Imaginary + _complex2.Imaginary);
            }
    
    permitiéndome efectuar sumas sobre dos complejos.

A continuación paso el código completo de la clase.
/* ************************************************************************************************************************************************
* © JOAQUIN MARTINEZ RUS 2015
* PROYECTO:        ComplexCalc. Calculadora de números complejos
* Archivo:         Complex.cs
* Descripción:     Clase de números complejos
* Historial:
*                  1. Joaquin Martínez Rus - 18 jul 2016. Creación
*
* Comentarios:
*
*
**************************************************************************************************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ComplexCal
{
    public class Complex: INotifyPropertyChanged
    {
        #region Constructor

        public Complex(double _real=0, double _imaginary=0)
        {
            this.Real = _real;
            this.Imaginary = _imaginary;
        }

        public Complex(Complex _complex)
        {
            this.Real = _complex.Real;
            this.Imaginary = _complex.Imaginary;
        }

        #endregion

        #region Propiedades

        private double real;
        private double imaginary;

        public event PropertyChangedEventHandler PropertyChanged;

        public double Module { get; set; }
        public double DegreesAngle { get; set; }
        public double RadiansAngle { get; set; }

        public double Imaginary
        {
            get { return imaginary; }
            set
            {
                imaginary = value;
                SetPolarComplex();
                OnPropertyChanged("Imaginary");
                OnPropertyChanged("ToString");
            }
        }

        public double Real
        {
            get { return real; }
            set
            {
                real = value;
                SetPolarComplex();
                OnPropertyChanged("Real");
                OnPropertyChanged("ToString");
            }
        }

        public string ToBinomialString
        {
            get
            {
                return "z = " + this.Real.ToString("F2") + (this.Imaginary < 0 ? " - " : " + ") + Math.Abs(this.Imaginary).ToString("F2") + " i";
            }
        }

        public string ToPolarString
        {
            get
            {
                return "z = " + this.Module.ToString("F2") + ") " + this.DegreesAngle.ToString("F1") + "°";
            }
        }

        public string ToString
        {
            get { return this.ToBinomialString + new string(' ',10) + this.ToPolarString; }
        }

        #endregion

        #region Private Methods

        /// 
        /// Asigna el módulo y el argumento de un número complejo
        /// 
        void SetPolarComplex()
        {
            this.Module= Math.Sqrt(Math.Pow(this.Real, 2) + Math.Pow(this.Imaginary, 2));
            this.RadiansAngle= Math.Atan2(this.Imaginary, this.Real);
            this.DegreesAngle = this.RadiansAngle * 180 / Math.PI;
        }

        #endregion

        #region Static Public Methods

        // SOBRECARGA DE LOS OPERADORES BÁSICOS

        public static Complex operator + (Complex _complex1, Complex _complex2)
        {
            return new Complex(_complex1.Real + _complex2.Real, _complex1.Imaginary + _complex2.Imaginary);
        }

        public static Complex operator - (Complex _complex1, Complex _complex2)
        {
            return new Complex(_complex1.Real - _complex2.Real, _complex1.Imaginary - _complex2.Imaginary);
        }

        public static Complex operator * (Complex _complex1, Complex _complex2)
        {
            return new Complex((_complex1.Real * _complex2.Real - _complex1.Imaginary * _complex2.Imaginary),
                (_complex1.Real * _complex2.Imaginary + _complex1.Imaginary * _complex2.Real));
        }

        public static Complex operator / (Complex _complex1, Complex _complex2)
        {
            double xx = (_complex1.Real * _complex2.Real + _complex1.Imaginary * _complex2.Imaginary) / (Math.Pow(_complex2.Real, 2) + Math.Pow(_complex2.Imaginary, 2));
            double yy = (_complex1.Imaginary * _complex2.Real - _complex1.Real * _complex2.Imaginary) / (Math.Pow(_complex2.Real, 2) + Math.Pow(_complex2.Imaginary, 2));
            return new Complex(xx, yy);
        }

        public static Complex operator ! (Complex _complex)
        {
            return new Complex(-_complex.Real, -_complex.Imaginary);
        }

        #endregion

        #region Public Methods

        /// 
        /// Inicia el evento de cambio de propiedad
        /// 
        /// Nombre de la propiedad
        public void OnPropertyChanged(string p_PropertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(p_PropertyName));
            }
        }

        /// 
        /// Efectúa una copia del objeto
        /// 
        /// Objeto desde el cual se efectúa la copia
        public void Clone(Complex _complex)
        {
            this.Real = _complex.Real;
            this.Imaginary = _complex.Imaginary;
        }

        /// 
        /// Devuelve el conjugado de un número complejo
        /// 
        /// Número complejo al que se le calcula
        /// Objeto Complex
        public Complex Conjugate(Complex complex)
        {
            return new Complex(complex.Real, -complex.Imaginary);
        }

        /// 
        /// Devuelve el conjugado de un número complejo
        /// 
        /// Objeto Complex
        public Complex Conjugate()
        {
            return Conjugate(this);
        }

        /// 
        /// Determina cuando una instancia y un objeto tienen los mismos valores
        /// 
        /// El objeto a comparar con la instancia
        /// Valor booleano
        public bool Equals(Complex _complex)
        {
            if (_complex == null)
            {
                return false;
            }
            if (this.GetType() != _complex.GetType())
            {
                return false;
            }

            return this.Real == _complex.Real && this.Imaginary == _complex.Imaginary;
        }

        /// 
        /// Obtiene el inverso del objeto Complex actual
        /// 
        /// Objeto Complex
        public Complex Reverse()
        {
            return Reverse(this);
        }

        /// 
        /// Obtiene el inverso de un objeto Complex
        /// 
        /// Objeto Complex del que se obtiene el inverso
        /// Objeto Complex
        public Complex Reverse(Complex _complex)
        {
            double denominador = Math.Pow(_complex.Real, 2) + Math.Pow(_complex.Imaginary, 2);
            return new Complex(_complex.Real / denominador, -_complex.Imaginary / denominador);
        }

        /// 
        /// Retorna el objeto Complex en formato Polar
        /// 
        /// La salida se muestra en grados sexagesimales
        /// String
        public string PolarToString(bool isDegrees)
        {
            return this.Module.ToString("D1") + ")" + (isDegrees?this.DegreesAngle.ToString("D1"):this.RadiansAngle.ToString("D2"));
        }

        /// 
        /// Retorna el objeto Complex en formato Polar
        /// 
        /// String
        public string PolarToString()
        {
            return PolarToString(true);
        }

        #endregion
    }
}

Ahora damos paso al código XAML del formulario. Lo primero declaramos los recursos de la ventana con Window.Resources y en este lugar, incluimos los estilos. He creado tres estilos, uno llamado normal aplicado a los controles TextBlock, otro llamado header que hereda de normal y por tanto es aplicado a los mismos controles y un tercer estilo llamado textBox aplicado a controles TextBox (muy original, eh?), por tanto a cada control que le asigne este estilo se le aplicarán los valores de las propiedades incluidas en el estilo a no ser que se sobreescriban al crear el control.( En el control TextBox he incluido un validador para que no se incluya texto que no sea numérico, pero eso lo veremos otro día)
Una vez creados los estilos vamos a centrarnos en el control TextBlock del siguiente código:

<StackPanel x:Name="stackResult" Orientation="Horizontal" Grid.Column="2" Grid.Row="5" Grid.ColumnSpan="5">
    <TextBox x:Name="textBoxReal1"  Style="{StaticResource textBox}" Text="{Binding Real}"/>
    <TextBlock x:Name="labelComplexNumberResult" Style="{StaticResource normal}" Text="{Binding ToString}" Margin="123,0,0,0"/>
</StackPanel>
  1. Le asignamos un nombre
  2. Le asignamos el estilo con Style="{StaticResource textBox}"
  3. Le asignamos el valor enlazado de la propiedad Text con  la propiedad Real de la clase Complex. Style="{StaticResource textBox}" Text="{Binding Real}"¿Y de donde extrae los datos la propiedad Text?
Para que los controles sepan de donde tienen que extraer los datos de la clase Complex, al crear la ventana principal, asigno a cada StackPanel donde se encuentran contenidos los controles de cada número, la clase desde donde se alimentará y cada vez que se alteren los datos de cada clase, se visualizarán automáticamente en los controles sin necesidad de código. Por tanto en el code-behind de la ventana principal solo y exclusivamente incluyo la declaración de los tres objetos Complex, los dos de cálculo y el resultado, la asignación del DataContext de los StackPanel, los métodos de los eventos de cambio de propiedad y cambio de operación y el cálculo  de la operación.
/* ************************************************************************************************************************************************
* © JOAQUIN MARTINEZ RUS 2015
* PROYECTO:        ComplexCalc. Calculadora de números complejos
* Archivo:         MainWindow.cs
* Descripción:     Clase de la ventana principal
* Historial:
*                  1. Joaquin Martínez Rus - 18 jul 2016. Creación
*
* Comentarios:
*
*
**************************************************************************************************************************************************/
using System.Windows;
using System.Windows.Controls;

namespace ComplexCal
{
    /// 
    /// Interaction logic for MainWindow.xaml
    /// 
    public partial class MainWindow : Window
    {
        Complex c1 = new Complex(2,4);
        Complex c2 = new Complex(3,5);
        Complex ComplexResult=new Complex();

        public MainWindow()
        {
            InitializeComponent();

            // asignar a los Stackpanel el Datacontext de cada objeto Complex

            stackC1.DataContext = c1;
            stackC2.DataContext = c2;
            stackResult.DataContext = ComplexResult;

            // Crear evento para que cada vez que se cambie una propiedad, se calcule la operación
            c1.PropertyChanged += Complex_PropertyChanged;
            c2.PropertyChanged += Complex_PropertyChanged;

        }

        private void Complex_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            Calculate(comboBox.SelectionBoxItem.ToString());
        }

        private void comboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            ComboBoxItem op = e.AddedItems[0] as ComboBoxItem;
            Calculate(op.Content.ToString());
        }

        /// 
        /// Efectúa el cálculo entre dos números complejos
        /// 
        /// Operador
        void Calculate(string operatorToString)
        {
            switch (operatorToString)
            {
                case "+":
                    ComplexResult.Clone(c1 + c2);
                    break;
                case "-":
                    ComplexResult.Clone(c1 - c2);
                    break;
                case "x":
                    ComplexResult.Clone(c1 * c2);
                    break;
                case "÷":
                    ComplexResult.Clone(c1 / c2);
                    break;
                case "Inverso":
                    ComplexResult.Clone(c1.Reverse());
                    break;
                case "Conjugado":
                    ComplexResult.Clone(c1.Conjugate());
                    break;
                case "Opuesto":
                    ComplexResult.Clone(!c1);
                    break;
            }

        }

    }
}

y el código XAML full:
<Window x:Class="ComplexCal.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:ComplexCal"
        mc:Ignorable="d"
        Title="Complex Calc" Height="480" Width="640">
    <Window.Resources>

        <Style x:Key="normal" TargetType="TextBlock">
            <Setter Property="VerticalAlignment" Value="Center"/>            
            <Setter Property="FontSize" Value="14"/>
            <Setter Property="FontFamily" Value="Segoe UI"/>
            <Setter Property="Height" Value="25"/>
            <Setter Property="Width" Value="Auto"/>
        </Style>
        <Style x:Key="header" TargetType="TextBlock" BasedOn="{StaticResource normal}">
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="VerticalAlignment" Value="Bottom"/>
            <Setter Property="HorizontalAlignment" Value="Center"/>            
        </Style>
        <Style x:Key="textBox" TargetType="TextBox">
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="Height" Value="25"/>
            <Setter Property="Width" Value="50"/>
            <Setter Property="HorizontalAlignment" Value="Center"/>
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <DockPanel LastChildFill="true">
                            <Border Background="OrangeRed" DockPanel.Dock="right" Margin="5,0,0,0" 
                                Width="20" Height="20" CornerRadius="5"
                                ToolTip="{Binding ElementName=customAdorner, 
                                          Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
                                <TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" 
                                   FontWeight="Bold" Foreground="white" />
                            </Border>
                            <AdornedElementPlaceholder Name="customAdorner" VerticalAlignment="Center" >
                                <Border BorderBrush="red" BorderThickness="1" />
                            </AdornedElementPlaceholder>
                        </DockPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Margin" Value="20,0,0,0"/>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="28*"/>
            <ColumnDefinition Width="27*"/>
            <ColumnDefinition Width="82*"/>
            <ColumnDefinition Width="132*"/>
            <ColumnDefinition Width="154*"/>
            <ColumnDefinition Width="60*"/>
            <ColumnDefinition Width="149*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="49*"/>
            <RowDefinition Height="155*"/>
        </Grid.RowDefinitions>
        <TextBlock x:Name="labelReal" Grid.Column="2" Grid.Row="1" Style="{StaticResource header}" Text="Real"/>
        <TextBlock x:Name="labelImaginaria" Grid.Column="3" Grid.Row="1" Style="{StaticResource header}" Text="Imaginaria"/>
        <TextBlock x:Name="labelNumberHeader" Grid.Column="4" Grid.Row="1" Style="{StaticResource header}" Text="Número complejo"/>
        <TextBlock x:Name="z1" Grid.Column="1" Grid.Row="2" Style="{StaticResource normal}" Text="z1" Margin="0,12"/>
        <TextBlock x:Name="z2" Grid.Column="1" Grid.Row="4" Style="{StaticResource normal}" Text="z2" Margin="0,12" />
        <TextBlock x:Name="op" Grid.Column="2" Grid.Row="3" Style="{StaticResource normal}" Text="Operación" Margin="0,12"/>
        <ComboBox x:Name="comboBox" Grid.Column="3" HorizontalAlignment="Left" Grid.Row="3" VerticalAlignment="Center" Width="80" Height="25" SelectionChanged="comboBox_SelectionChanged">
            <ComboBoxItem Content="+" IsSelected="True"/>
            <ComboBoxItem Content="-"/>
            <ComboBoxItem Content="x"/>
            <ComboBoxItem Content="÷"/>
            <ComboBoxItem Content="Inverso"/>
            <ComboBoxItem Content="Conjugado"/>
            <ComboBoxItem Content="Opuesto"/>
        </ComboBox>
        <StackPanel x:Name="stackC1" Orientation="Horizontal" Grid.Column="2" Grid.Row="2" Grid.ColumnSpan="5">
            <TextBox x:Name="textBoxReal1"  Style="{StaticResource textBox}" Text="{Binding Real}"/>
            <TextBox x:Name="textBoxImagin1" Style="{StaticResource textBox}" Text="{Binding Imaginary}"/>
            <TextBlock x:Name="labelComplexNumber1" Style="{StaticResource normal}" Text="{Binding ToString}" Margin="50,0,0,0" />
        </StackPanel>
        <StackPanel x:Name="stackC2" Orientation="Horizontal" Grid.Column="2" Grid.Row="4" Grid.ColumnSpan="5">
            <TextBox x:Name="textBoxReal2" Style="{StaticResource textBox}" Text="{Binding Real}"/>
            <TextBox x:Name="textBoxImagin2" Style="{StaticResource textBox}" Text="{Binding Imaginary}"/>
            <TextBlock x:Name="labelComplexNumber2" Style="{StaticResource normal}" Text="{Binding ToString}" Margin="50,0,0,0" />
        </StackPanel>
        <StackPanel x:Name="stackResult" Orientation="Horizontal" Grid.Column="2" Grid.Row="5" Grid.ColumnSpan="5">
            <TextBlock x:Name="result"  Style="{StaticResource header}" Text="Resultado" VerticalAlignment="Center"/>
            <TextBlock x:Name="labelComplexNumberResult" Style="{StaticResource normal}" Text="{Binding ToString}" Margin="123,0,0,0"/>
        </StackPanel>
    </Grid>
</Window>

Pues esto es todo, la clase Complex en C# y su ejecución bajo WPF con los controles enlazados. 
Nos vemos. Saludos

lunes, 18 de julio de 2016

Calculadora de números complejos (Java)

En este caso, voy a detallar una clase sencilla que implementa una calculadora de números complejos en Java.
Para los que no lo recuerden, un número complejo consta de dos partes, una parte real y una parte imaginaria; la parte imaginaria se compone del número √-1 o lo que es lo mismo i. Un número complejo lo podemos representar vectorialmente como z = (a,b), binomial z = a + bi, polar mediante un módulo y un ángulo z = r α, trigonométrica z = r (cos α + i sin α) y por último la forma exponencial y que no voy a incluir en la clase aunque a mi parecer es la más elegante e = cos α + i sin α.
Cada forma tiene su fin, por ejemplo la suma y las resta son más fáciles con la forma binomial mientras que la multiplicación, división y exponenciación lo son con la forma polar; en el caso de la clase todos los cálculos los he realizado con la forma binomial.
Si pasamos a la clase, como ya he comentado está implementada en Java y consta de cuatro propiedades, parte real mediante x, parte imaginaria mediante y, el módulo que es calculado en base de x e y, y por último el argumento o ángulo.

A continuación paso el diagrama de la clase donde se exponen los métodos con la mayoría de las operaciones:
Pasando un ejemplo
        // Pasamos dos números complejos
        // Efectuamos los cálculos 
        Complex c1=new Complex(4,5);
        Complex c2=new Complex(7,6);
        
        System.out.println("(" + c1.toString() + ") + (" + c2.toString() + ") = " + c1.addComplex(c2));
        System.out.println("(" +c1.toString() + ") - (" + c2.toString() + ") = " + c1.substractComplex(c2));
        System.out.println("(" +c1.toString() + ") x (" + c2.toString() + ") = " + c1.multiplyComplex(c2));
        System.out.println("(" +c1.toString() + ") / (" + c2.toString() + ") = " + c1.divideComplex(c2));
        System.out.println("Opuesto de " + c1.toString() + " = " + c1.opposite().toString());
        System.out.println("Inverso  de " + c1.toString() + " = " + c1.reverse().toString());
        System.out.println(c1.toString() + " = " + c1.polarToString());
        System.out.println(c1.toString() + " = " + c1.polarToString(true));
        System.out.println(c2.toString() + " = " + c2.polarToString());
        System.out.println(c2.toString() + " = " + c2.polarToString(true));
        c2.convertToBinomial(5, 36.86*Math.PI/180);
        System.out.println("Convertir 5) 36.86º a binomial = " + c2.toString());
Resultado


(4 + 5i) + (7 + 6i) = 11 + 11i
(4 + 5i) - (7 + 6i) = -3 -1i
(4 + 5i) x (7 + 6i) = -2 + 59i
(4 + 5i) / (7 + 6i) = 0,68 + 0,13i
Opuesto de 4 + 5i = -4 -5i
Inverso  de 4 + 5i = 0,1 -0,12i
4 + 5i = 5,7)0,896 radians
4 + 5i = 5,7)51,3º
7 + 6i = 9,9)0,709 radians
7 + 6i = 9,9)40,6º
Convertir 5) 36.86º a binomial = 4 -3i

A continuación paso el código:

/******************************************************
 *  @author:    Joaquín Martínez Rus (c) 2016
 *  @version:   1.0 
 *  File:       Complex.java
 *  Created:    17/07/2016
 *  Project:    Calculadora de números complejos
 *  Comments:   Clase Complex.
 *******************************************************/
public class Complex {
    
    /**
     * Inicia una nueva instancia de la clase Complex con valor z = 0 + 0i;
     */
    public Complex(){
        this(0,0);
    }
    
    /**
     * Inicia una nueva instancia de la clase Complex
     * @param _x Parte real
     * @param _y Parte imaginaria
     */
    public Complex(double _x, double _y){
        this.x=_x;
        this.y=_y;
        this.setModule();
        this.setGrades();
    }
    
    double x;
    double y;
    double module;
    double grades;

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    public double getModule() {
        
        return module;
    }

    /**
     * Asigna y calcula el valor del módulo
     */
    public void setModule(){
        this.module=Math.sqrt(Math.pow(x, 2)+ Math.pow(x, 2));
    }
    
    /**
     * Asigna el valor del módulo
     */
    public void setModule(double module) {
        this.module = module;
    }

    public double getGrades() {
        return grades;
    }

    public void setGrades() {
        this.grades = Math.atan2(y, x);
    }
    
    public void setGrades(double grades) {
        this.grades = grades;
    }
    
    /**
     * Suma al número complejo otro número complejo
     * @param _complex Número complejo de la suma
     * @return Objeto Complex
     */
    public Complex addComplex(Complex _complex){
        return new Complex(this.x + _complex.x, this.y + _complex.y);
    }
    
    /**
     * Resta al número complejo otro número complejo
     * @param _complex Número complejo de la resta
     * @return Objeto Complex
     */
    public Complex substractComplex(Complex _complex){
        return new Complex(this.x - _complex.x, this.y - _complex.y);
    }
    
    /**
     * Multiplica al número complejo otro número complejo
     * @param _complex Número complejo de la multiplicación
     * @return Objeto Complex
     */
    public Complex multiplyComplex(Complex _complex){
        return new Complex((this.x * _complex.x - this.y * _complex.y), 
                (this.x * _complex.y + this.y * _complex.x));
    }
    
    /**
     * Divide el número complejo actual entre el valor del parámetro
     * @param _complex Denominador
     * @return Objeto Complex
     */
    public Complex divideComplex(Complex _complex){
        return divideComplex(this,_complex);
    }
    
    /**
     * Divide dos números complejos
     * @param _complex1 Numerador
     * @param _complex2 Denominado
     * @return Objeto Complex
     */
    public Complex divideComplex(Complex _complex1, Complex _complex2){
        double xx = (_complex1.x * _complex2.x + _complex1.y * _complex2.y)/(Math.pow(_complex2.x,2)+Math.pow(_complex2.y,2));
        double yy = (_complex1.y * _complex2.x - _complex1.x * _complex2.y)/(Math.pow(_complex2.x,2)+Math.pow(_complex2.y,2));
        return new Complex(xx, yy);
    }
    
    /**
     * Obtiene un número complejo en forma vectorial
     * @return Cadena de texto
     */
    public String vectorialtoString(){
        return "(" + ConsoleUtilities.toNumber(this.x,2) + ", " + ConsoleUtilities.toNumber(this.y,2) + ")";
    }
    
    /**
     * Obtiene un número complejo en forma binomial
     * @return Cadena de texto
     */
    @Override
    public String toString(){
        return ConsoleUtilities.toNumber(this.x,2) + (this.y < 0? " - ": " + ") + ConsoleUtilities.toNumber(this.y,2) + "i";
    }
    
    /**
     * Obtiene un número complejo en forma polar
     * @return Cadena de texto
     */
    public String polarToString(){
        return  polarToString(false);
    }
    
    /**
     * Obtiene un número complejo en forma polar
     * @param isDegrees Mostrar como grados centigrados o radianes
     * @return Cadena de texto
     */
    public String polarToString(boolean isDegrees){
        double angle=isDegrees?this.getGrades()*180/Math.PI:this.getGrades();
        String angleToString = (isDegrees?ConsoleUtilities.toNumber(angle,1):ConsoleUtilities.toNumber(angle,3)) + (isDegrees? "º": " radians");
        return  ConsoleUtilities.toNumber(this.module,1) + ")" + angleToString;
    }
    
    /**
     * Obtiene el conjugado de un número complejo
     * @param complex Número complejo
     * @return Objeto Complex
     */
    public Complex conjugate(Complex complex){
        return new Complex(complex.x, - complex.y);
    }
    
    /**
     * Calcula el opuesto de un número complejo
     * @return Objeto Complex
     */
    public Complex opposite(){
        return opposite(this);
    }
    
    /**
     * Calcula el opuesto de un número complejo
     * @param _complex Número complejo
     * @return Objeto Complex
     */
    public Complex opposite(Complex _complex){
        return new Complex(-_complex.x, - _complex.y);
    }
    /**
     * Obtiene el conjugado de un número complejo
     * @return Objeto Complex
     */
    public Complex conjugate(){
        return conjugate(this);
    }
    
    /**
     * Obtiene el inverso de un número complejo
     * @return Objeto Complex
     */
    public Complex reverse(){
        return reverse(this);
    }
    
    /**
     * Obtiene el inverso de un número complejo
     * @param complex Número complejo del cálculo
     * @return Objeto Complex
     */
    public Complex reverse(Complex complex){
        double denominador=Math.pow(complex.x, 2) + Math.pow(complex.y, 2);  
        return new Complex( complex.x/denominador, - complex.y/denominador);
    }
    
    /**
     * Convierte un número complejo de forma polar a binomial
     * @param module Modulo del número complejo
     * @param argument Argumento en radianes del número complejo
     * @return Objeto Complex
     */
    public Complex convertPolarToBinomial(double module, double argument){
        double _x = Math.abs(module) * Math.cos(argument);
        double _y = Math.abs(module) * Math.sin(argument);
        return new Complex(_x,-_y);
    } 
    
    /**
     * Asigna los valores real e imaginario en base al módulo y el argumento
     * @param module Modulo del número complejo
     * @param argument Argumento en radianes del número complejo
     */
    public void convertToBinomial(double module, double argument){
        Complex _complex=convertPolarToBinomial(module, argument);
        this.x=_complex.x;
        this.y=_complex.y;
    }
    
    /**
     * Comprueba dos números complejos
     * @param _complex Número complejo a comparar
     * @return Objeto Complex
     */
    public boolean equals(Complex _complex){
        if (_complex==null) {
            return false;
        }
        if (this.getClass()!=_complex.getClass()) {
            return false;
        }
        
        return this.x ==_complex.x && this.y == _complex.y;
    }
}


Consejos.
  • La clase debe estar bien definida. Constructores, propiedades y métodos
  • Antes de escribir código, genera un diagrama de clases como mínimo (esto implica pensar que vas a hacer), además una buena estructura puede hacer nuestro código más robusto.
  • Piensa en las cuatro características de la Programación Orientada a Objetos, Abstracción, Encapsulamiento, Herencia y Polimorfismo (hay alguna más, pero estas son las principales y debes tenerlas muy claras)
  • Los métodos deben contener el código justo. Un método con mucho código no hace la clase legible y lo vuelve débil.
  • Las clases deben funcionar en cualquier medio. Si usara esta clase en modo gráfico con ventanas en vez de modo consola, debería de funcionar del igual modo, solo debo llamar al método de operación y obtener su resultado mediante los métodos apropiados.
  • Usa los comentarios, tanto dentro de los métodos como en su documentación. Por ejemplo, si llamo al método divideComplex de la clase Complex, cuando estoy escribiendo el método, aparecerá el método y el texto que nosotros escribimos, en este caso yo escribí "Divide el número complejo actual entre el valor del parámetro" junto con los parámetros y el valor retornado. Cuando las clases se hacen muy grandes y complejas, es necesario documentarlas todo lo que se pueda.

  • Además de los comentarios, a mi me gusta agrupar el código por regiones con Java uso
// <editor-fold defaultstate="collapsed" desc="REQUEST FROM KEYBOARD">
    
    /**
     * Devuelve un valor desde el teclado
     * @param textIn Texto que se visualiza en la consola
     * @return Texto procedente del teclado
     * @throws IOException 
     */
    public static String readText(String textIn, boolean allowEmpty) throws IOException{
        
        // Declaración de variables
        BufferedReader keyboardIn=new BufferedReader(new InputStreamReader(System.in));
        String textOut=null;
        
        // Imprimir texto de consola
        System.out.println(textIn);
        
        // Solicitar datos desde teclado
        
        do {
            textOut=keyboardIn.readLine();
        } while (ConsoleUtilities.isNullOrEmpty(textOut) && !allowEmpty);
        
        // Retornar texto
        return textOut;
    }

    // </editor-fold>
Esto me va permitir expandir o contraer todo el código contenido entre las etiquetas o para el caso de C#


#region Private Methods
      // Aquí iría el código
#endregion

o Visual Basic

#Región MiRegion

#End Region

  • Cíñete a la nomenclatura estándar con cada lenguaje de programación, por ejemplo Java o C# o Visual Basic.
  • Créate un clase con herramientas, por ejemplo, para aplicaciones del tipo consola en Java, tengo una clase con métodos estáticos donde implemento herramientas que puedo usar en este medio como un método que genera un menú automáticamente, un lector desde teclado de texto o números, un formateador de texto, un serializador, etc. Para C# tengo otro tipo de clases donde extiendo funcionalidades a las clases creadas, en fin, código que reutilizaré más a menudo de lo que me pienso.
  • Si hacemos todo esto, en un futuro nos será más fácil, modificar, entender que hicimos o ampliar nuestras clases.
Y esto es todo por hoy. Saludos!

miércoles, 13 de julio de 2016

Mensajería única (2)

Una vez que podemos pasar datos entre formularios como en la entrada Mensajería única, WPF nos lo pone mucho más fácil y con mucho menos código.
Lo primero de todo debemos enlazar los controles con los datos de la clase y para ello debemos tener algún conocimiento de XAML y WPF (no es el fin de este artículo enseñar WPF)

Para llevar a cabo el paso de mensajes entre formularios mediante XAML, he creado una nueva ventana con un control Grid con tres columnas y tres filas, un control TextBlock y un control ProgressBar.
Las propiedades de los controles se enlazan con datos, estos datos proceden de una clase de negocio y esta clase es Message.
En WPF, para instanciar una clase, debemos hacerlo desde los recursos de la página o de un control, dependiendo del ámbito que se pretenda abarcar. En este caso, yo declaro la clase Message en los recursos del objeto Grid.

Una vez declarada, se creará una instancia de Message en cada nueva ventana y los controles se enlazan a la clase con Binding en la propiedad del control que pretendemos enlazar y haciendo referencia a la propiedad de la clase Message. He incluido un conversor en la propiedad Visibility del control ProgressBar para convertir valores booleanos en valores de la enumeración Visibility como Visible, Hidden o Collapse; si es true, Visible y si es False, Collapse. En este caso hacemos los mismo, que es declarar la clase ConvertBoolToVisible (la cual implementa la interfaz IValueConverter) con una key llamada btv.
He hecho un binding sobre la propiedad Text del TextBlock a la propiedad infoText de la clase Message, la propiedad Value del control ProgressBar con la propiedad progress de Message y por último la propiedad Visibility con progressIsVisible ya su vez con el conversor btv.

<Window x:Class="WpfApplication1.Window2"
        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:WpfApplication1"
        mc:Ignorable="d"
        Title="Window2" Height="300" Width="300">
    
    <Grid x:Name="grid" DataContext="message">
        <Grid.Resources>
            <local:ConvertBoolToVisible x:Key="btv"/>
            <local:Message x:Key="message"/>

        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="100"/>
            <RowDefinition Height="100*"/>
            <RowDefinition Height="100*"/>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100*"/>
            <ColumnDefinition Width="100*"/>
            <ColumnDefinition Width="100*"/>
        </Grid.ColumnDefinitions>
        <TextBlock x:Name="tb" Text="{Binding infoText}" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <ProgressBar x:Name="pb" Minimum="0" Maximum="100" Value="{Binding progress}" 
                     Visibility="{Binding Path=progressIsVisible, Converter={StaticResource btv}}"
                     Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="3" HorizontalAlignment="Center" Width="150" Margin="71,0,71,65"/>
    </Grid>
</Window>

Y el código (code-behind)? Muchísmo más reducido, ya que en este caso solo existe un objeto Message de clase, asignamos dinámicamente el DataContext del control Grid (que será desde donde se alimenten los controles) y añadimos el evento CollectionChanged.
Para este caso, he efectuado dos cambios en la clase Message:
  • Creación de un nuevo método público llamado Clone en la clase Message que copia las propiedades de un objeto Message al mismo objeto.
  • Implementación de la interfaz INotifyPropertyChanged por la clase Message de modo que debe implementar el método OnPropertyChanged para que la ventana detecte los cambios en las propiedades y pueda asignar los valores.
  • Creación del método OnPropertyChanged("propertyName") el cual llamará al evento this.PropertyChanged(this, new PropertyChangedEventArgs(p_PropertyName));
  • Modificación en el set de las propiedades, incluyendo la llamada al método OnPropertyChanged("propertyName").
Cada vez que se añada un nuevo mensaje, se clonará el objeto mensaje con los nuevos datos y se visualizarán en pantalla efectuando el mismo resultado que conseguimos mediante código, pero esta vez con menos código.

Clase Message modificada
using System;
using System.ComponentModel;

namespace WpfApplication1
{
    public class Message: INotifyPropertyChanged
    {
        #region Constructor

        public Message()
        {
            this.infoText = "";
            this.progress = 0;
            this.progressIsVisible = false;
        }

        public Message(string _text, bool _isVisible, int _progress)
        {
            setMessage(_text, _isVisible, _progress);
        }

        #endregion

        #region Properties

        private string _infotext;
        private int _progress;

        public int progress
        {
            get { return _progress; }
            set
            {
                //if (_progress!=value)
                //{

                //}

                _progress = value;

                OnPropertyChanged("progress");
            }
        }

        public string infoText
        {
            get { return _infotext; }
            set
            {
                //if (_infotext!=value)
                //{
                //    EventArgsMessage eventArgsMessage = new EventArgsMessage(_infotext,value);
                //}

                _infotext = value;
                OnPropertyChanged("infoText");
            }
        }

        private bool _progressIsVisible;

        public bool progressIsVisible
        {
            get { return _progressIsVisible; }
            set
            {
                _progressIsVisible = value;
                OnPropertyChanged("progressIsVisible");
            }
        }

        public string infoText2 { get; set; }

        #endregion

        #region Events

        public event EventHandler<EventArgsMessage> ChangedTextEventHandler;
        public event EventHandler<EventArgsProgress> ChangedProgressEventHandler;
        public event PropertyChangedEventHandler PropertyChanged;

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

        #endregion

        #region Private Methods

        void OnChangedText(EventArgsMessage e)
        {
            if (ChangedTextEventHandler!=null)
            {
                ChangedTextEventHandler(this, e);
            }
        }

        void OnChangedProgress(EventArgsProgress e)
        {
            if (ChangedProgressEventHandler!=null)
            {
                ChangedProgressEventHandler(this, e);
            }
        }

        #endregion

        #region Public Methods

        /// 
        /// Asigna los valores al mensaje
        /// 
        /// Texto del mensaje
        /// Visibilidad del mensaje
        /// Progreso
        public void setMessage(string _text, bool _isVisible, int _progress)
        {
            this.infoText = _text;
            this.progressIsVisible = _isVisible;
            this.progress = _progress;
        }

        /// 
        /// Borra los valores del mensaje
        /// 
        public void Clear()
        {
            setMessage("", false, 0);
        }

        /// 
        /// Efectua una copia de las propiedades de un mensaje
        /// 
        /// Objeto a clonar
        public void Clone(Message message)
        {
            this.infoText = message.infoText;
            this.infoText2 = message.infoText2;
            this.progressIsVisible = message.progressIsVisible;
            this.progress = message.progress;
        }

        #endregion
    }

    public class EventArgsMessage:EventArgs
    {
        public EventArgsMessage() { }
        public EventArgsMessage (string _oldText, string _newText)
        {
            this.oldText = oldText;
            this.newText = _newText;
        }

        public string oldText { get; set; }
        public string newText { get; set; }
    }

    public class EventArgsProgress : EventArgs
    {
        public EventArgsProgress() { }
        public EventArgsProgress(int _newValue)
        {
            this.newValue = _newValue;
        }

        public int newValue { get; set; }
    }

}

Code-Behind de la ventana

public partial class Window2 : Window
    {
        public Window2()
        {
            InitializeComponent();
            App.messages.CollectionChanged += Messages_CollectionChanged;
            this.grid.DataContext = m;
        }

        Message m = new Message();

        private void Messages_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
            {
                foreach (var item in e.NewItems)
                {
                    m.Clone((Message)item);
                }
            }
            
        }
    }

He aquí una muestra


Pues esto es todo. Estoy a vuestra disposición para cualquier duda.

Un saludo

miércoles, 6 de julio de 2016

Mensajería única

Estaba mejorando mi sistema de mensajería para mis aplicaciones y he pensado, ¿por qué no lo publicas?, pues ahí va. (Este en concreto es con WPF)
Cuando desarrollo mis aplicaciones, intento que todas tengan un sistema de mensajería con el que pueda comunicarse cualquier ventana de la aplicación con esta, de modo que generando un mensaje por ejemplo, "cargando caché de datos..." y una barra de progreso indique su estado, aparezca en la barra de estado o mediante un cuadro de dialogo, el mensaje "cargando caché de datos..." y la barra aumente su progreso, de modo que si tengo varias ventanas abiertas, podría visualizar los mismos mensajes en todas y esto mediante un ejemplo os lo voy a mostrar. Para comenzar creo una clase llamada Message que contiene cuatro propiedades, infoText que almacena el texto del mensaje principal, progress que almacena el valor del progreso, progressIsVisible que almacena si la barra de progreso es visible y por último infoText2 que almacena un segundo texto por si acaso lo necesito.
A esta clase le añado varios eventos, por si acaso los necesito, que son cuando cambia el texto del mensaje o cuando cambia el progreso. Extiendo varios EventArgs con EventArgsMessage para obtener los valores nuevo y antiguos y el valor del progreso EventArgsProgress. Contiene dos métodos, uno para asignar los valores de la propiedades y otro para borrarlos.
Por otra parte creo una colección del tipo ObservableCollection en la clase App de la aplicación de modo que cualquier ventana puede acceder a esta colección si la hacemos pública, la cual contiene un evento CollectionChanged el cual usaremos en cada ventana para capturar los mensajes


public static ObservableCollection<Message> messages = new ObservableCollection<Message>();

Y ahora solo nos queda capturar los eventos en cada ventana. Para ver esto, voy a crear una ventana principal que iniciará un hilo desde un botón con un proceso que suma desde 1 hasta 100 con un retardo de 50 ms y que en cada incremento, actualizará un control Textblock con el texto del mensaje y una barra de progreso con un valor. Al mismo tiempo, abriré otra ventana en la aplicación con un TextBlock y otra barra de progreso que deberían actualizarse de igual modo que lo hace la ventana principal y sin albergar ningún código en ella que le permita incrementar nada ni mostrar nada. Para mostrar el mensaje y actualizar las barras uso un delegado en cada ventana que se encargará de hacer este trabajo y para acceder a los controles sin errores, Dispatcher.Invoke(new MessageAddedHandler(setMessage), message) invocando al delegado y como argumento el nuevo mensaje. ¿Qué resultado obtendremos? Dos ventanas, la que contiene el botón y la que no. Al pulsar se inicia la carga y en ambas ventanas se actualizan tanto el texto como el valor de la barra de progreso al mismo tiempo.

Pasamos al código.
Clase Message

using System;
namespace WpfApplication1
{
    public class Message
    {
        #region Constructor
        public Message()
        {
            this.infoText = "";
            this.progress = 0;
            this.progressIsVisible = false;
        }
        public Message(string _text, bool _isVisible, int _progress)
        {
            setMessage(_text, _isVisible, _progress);
        }
        #endregion
        #region Properties
        private string _infotext;
        private int _progress;
        public int progress
        {
            get { return _progress; }
            set
            {
                if (_progress!=value)
                {
                }
                _progress = value;
            }
        }
        public string infoText
        {
            get { return _infotext; }
            set
            {
                if (_infotext!=value)
                {
                    EventArgsMessage eventArgsMessage = new EventArgsMessage(_infotext,value);
                }
                _infotext = value;
            }
        }
        public bool progressIsVisible { get; set; }
        public string infoText2 { get; set; }
        #endregion
        #region Events
        public event EventHandler ChangedTextEventHandler;
        public event EventHandler ChangedProgressEventHandler;
        #endregion
        #region Private Methods
        void OnChangedText(EventArgsMessage e)
        {
            if (ChangedTextEventHandler!=null)
            {
                ChangedTextEventHandler(this, e);
            }
        }
        void OnChangedProgress(EventArgsProgress e)
        {
            if (ChangedProgressEventHandler!=null)
            {
                ChangedProgressEventHandler(this, e);
            }
        }
        #endregion
        #region Public Methods
        /// 
        /// Asigna los valores al mensaje
        /// 
        /// Texto del mensaje
        /// Visibilidad del mensaje
        /// Progreso
        public void setMessage(string _text, bool _isVisible, int _progress)
        {
            this.infoText = _text;
            this.progressIsVisible = _isVisible;
            this.progress = _progress;
        }
        /// 
        /// Borra los valores del mensaje
        /// 
        public void Clear()
        {
            setMessage("", false, 0);
        }
        #endregion
    }
    public class EventArgsMessage:EventArgs
    {
        public EventArgsMessage() { }
        public EventArgsMessage (string _oldText, string _newText)
        {
            this.oldText = oldText;
            this.newText = _newText;
        }
        public string oldText { get; set; }
        public string newText { get; set; }
    }
    public class EventArgsProgress : EventArgs
    {
        public EventArgsProgress() { }
        public EventArgsProgress(int _newValue)
        {
            this.newValue = _newValue;
        }
        public int newValue { get; set; }
    }
}

Clase App

public partial class App : Application
    {
        public static ObservableCollection messages = new ObservableCollection();

        public App()
        {
            
        }
    }
Ventana principal con método de cálculo
Constructor

public MainWindow()
        {
            InitializeComponent();
            App.messages.CollectionChanged += Messages_CollectionChanged;
            Window1 w1 = new Window1();
            w1.Show();
        }

Propiedades. El delegado, un objeto CancellationTokenSource para cancelar el proceso y un flag para saber si la app está corriendo o no.

        delegate void MessageAddedHandler(Message _message);
        CancellationTokenSource cs;
        public bool isRunning { get; set; } = false; 

Captura del evento. Ocurre cuando la colección cambia

void Messages_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action==System.Collections.Specialized.NotifyCollectionChangedAction.Add)
            {
                foreach (var item in e.NewItems)
                {
                    Dispatcher.Invoke(new MessageAddedHandler(setMessage), (Message)item);
                }
            }
            else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
            {
                Dispatcher.Invoke(new MessageAddedHandler(setMessage), new Message());
            }
            
        }
 
Métodos. El primer método asigna un mensaje a los controles, el segundo inicia el proceso o lo cancela y el tercero suma 1 después de 50 ms: una vez que acaba, finaliza y borra los mensajes.

void setMessage(Message message)
        {
            this.textBlock.Text = message.infoText;
            this.pb.Value = message.progress;
            this.pb.Visibility = message.progressIsVisible ? Visibility.Visible : Visibility.Collapsed;
        }
 
         private void button_Click(object sender, RoutedEventArgs e)
        {
            if (isRunning)
            {
                this.button.Content = "Iniciar";
                cs.Cancel();
            }
            else
            {
                isRunning = true;
                cs = new CancellationTokenSource();
                this.button.Content = "Cancelar";
                var t = Task.Factory.StartNew(() => doSomeThing(cs.Token),cs.Token);
                
            }
        }
        void doSomeThing(CancellationToken ct)
        {
            try
            {
                for (int i = 0; i <= 100; i++)
                {
                    ct.ThrowIfCancellationRequested();
                    Thread.Sleep(50);
                    App.messages.Add(new Message(String.Format("Cargando {0}", i), true, i));                    
                }
                App.messages.Clear();
            }
            catch (OperationCanceledException ex)
            {
                isRunning = false;
                App.messages.Clear();
                return;
            }
        }
 
Ventana que captura mensajería. En esta ventana solo se captura el evento cuando la colección cambia y la asignación de los valores al mensaje.

public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            App.messages.CollectionChanged += Messages_CollectionChanged; ;
        }
        delegate void MessageAddedHandler(Message _message);
        private void Messages_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
            {
                foreach (var item in e.NewItems)
                {
                    Dispatcher.Invoke(new MessageAddedHandler(setMessage), (Message)item);
                }
            }
            else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
            {
                Dispatcher.Invoke(new MessageAddedHandler(setMessage), new Message());
            }
        }
        void setMessage(Message message)
        {
            this.textBlock.Text = message.infoText;
            this.pb.Value = message.progress;
            this.pb.Visibility = message.progressIsVisible ? Visibility.Visible : Visibility.Collapsed;
        }
    }
}
y con todo funcionando, cada vez que iniciemos desde la ventana principal el proceso, las dos ventanas deben mostrar el mismo texto y el mismo progreso.
Os dejo el proyecto completo en el enlace.

Saludos