lunes, 9 de enero de 2012

Patrones de diseño: Observer

Otro de los patrones de diseño más utilizados es el llamado Observer, que se enmarca dentro de los patrones de comportamiento. Este patrón representa la situación en la que tenemos uno o varios objetos (observers) pendientes del estado de otro (subject). Los observers se suscriben al objeto que quieren monitorear y este, cuando se producen cambios significativos en su estado, se lo notificará.
En .NET para implementar este patrón de diseño se me ocurren dos posibilidades. En primer lugar, podemos guardar en el objeto a monitorear una lista de los objetos observer y cuando haya que notificar algún cambio se recorre la lista de suscriptores y se ejecuta el método apropiado en cada elemento de dicha lista. Otro modo de realizar la tarea es utilizando delegados.
A continuación, muestro el código de un ejemplo muy intuitivo realizado de los dos modos comentados anteriormente. La idea es implementar una emisora de radio, y retransmitir para todos los oyentes que la tienen sintonizada.
El código que se muestra a continuación nos permite ver el resultado de implementar la primera versión.

using System;
using System.Collections.Generic;

namespace ObserverEmisoraRadio01
{
    class Program
    {
        static void Main(string[] args)
        {
            //Probando el patrón de diseño Observer
            IOyente oyente1 = new Oyente("Antonio");
            IOyente oyente2 = new Oyente("Marta");
            IOyente oyente3 = new Oyente("Raquel");
            //Creamos una emisora de radio y asignamos una canción inicial sonando
            eFM emisoraFM = new eFM("Ninguna");

            //Suscribimos los oyentes que sintonizan la emisora
            emisoraFM.Sintonizar(oyente1);
            emisoraFM.Sintonizar(oyente2);
            emisoraFM.Sintonizar(oyente3);

            //Cambiamos la canción que suena (debería notificarse a los oyentes)
            emisoraFM.CancionSonando = "On the floor";

            Console.WriteLine();

            //Cambiamos la canción que suena (debería notificarse a los oyentes)
            emisoraFM.CancionSonando = "Tonight";

            Console.WriteLine();

            //Desintonizamos uno de los oyentes
            emisoraFM.Desintonizar(oyente2);

            //Cambiamos la canción que suena (debería notificarse sólo a los oyentes todavía sintonizados)
            emisoraFM.CancionSonando = "Eye of the tiger";

            Console.ReadLine();
        }
    }


    //....
    //Clase abstracta que representa una emisora de radio genérica
    abstract class EmisoraRadio
    {
        private string _cancionSonando;
        private List<IOyente> _oyentes = new List<IOyente>();

        //Constructor
        public EmisoraRadio(string cancion)
        {
            _cancionSonando = cancion;
        }

        public string CancionSonando
        {
            get
            {
                return _cancionSonando;
            }
            set
            {
                if (_cancionSonando.CompareTo(value) != 0)
                {
                    _cancionSonando = value;
                    Notificar();
                }
            }

        }

        //
        //Método que envía una notificación a todos los oyentes que tienen la emisora
        //sintonizada
        public void Notificar()
        {
            foreach (IOyente oyente in _oyentes)
            {
                oyente.Update(this);
            }
        }

        //
        //Método que permite a un oyente suscribirse a una emisora
        public void Sintonizar(IOyente oyente)
        {
            _oyentes.Add(oyente);
        }

        //
        //Método que permite a un oyente desvincularse de una emisora
        public void Desintonizar(IOyente oyente)
        {
            _oyentes.Remove(oyente);
        }
    }

    //....
    //Clase que implementa una emisora de radio concreta
    class eFM : EmisoraRadio
    {
        public eFM(string cancion) : base(cancion) { }
    }

    //....
    //Interfaz que deben implementar los oyentes para poder escuchar alguna emisora de radio
    interface IOyente
    {
        void Update(EmisoraRadio emisora);
    }

    //....
    //Clase que se debe instanciar para crear los oyentes
    class Oyente:IOyente {
        private string _nombre;
        private EmisoraRadio _emisora;

        public Oyente(string nombre)
        {
            this._nombre = nombre;
        }

        public EmisoraRadio Emisora
        {
            get { return _emisora; }
            set { _emisora = value; }            
        }

        public void Update(EmisoraRadio emisora)
        {
            Console.WriteLine("Oyente " + _nombre + " escucha canción " + emisora.CancionSonando);
        }
    }
}
El resultado de ejecutar el código anterior se puede ver en la imagen siguiente. Dicho resultado coincide perfectamente con el esperado.

Por último, podemos ver el código correspondiente a la segunda versión planteada (utilizando delegados). La estructura básica del programa es semejante al caso anterior, aunque hay que hacer algunos cambios en algunos métodos. El código está comentado para facilitar la comprensión del mismo. El resultado de la ejecución será igual al caso anterior.

using System;
using System.Collections.Generic;

namespace ObserverEmisoraRadio01
{
    class Program
    {
        static void Main(string[] args)
        {
            //Probando el patrón de diseño Observer

            //Creamos los oyentes
            IOyente oyente1 = new Oyente("Antonio");
            IOyente oyente2 = new Oyente("Marta");
            IOyente oyente3 = new Oyente("Raquel");

            //Creamos una emisora de radio y asignamos una canción inicial sonando
            eFM emisoraFM = new eFM("Ninguna");

            //Suscribimos los oyentes que sintonizan la emisora
            oyente1.Sintonizar(emisoraFM);
            oyente2.Sintonizar(emisoraFM);
            oyente3.Sintonizar(emisoraFM);

            //Cambiamos la canción que suena (los oyentes que tengan la emisora sintonizada serán informados del cambio de canción)
            emisoraFM.CancionSonando = "On the floor";

            Console.WriteLine();

            //Cambiamos la canción que suena (los oyentes serán informados del cambio de canción)
            emisoraFM.CancionSonando = "Tonight";

            Console.WriteLine();

            //Desintonizamos uno de los oyentes (no será notificado de los cambios de canción a partir de aquí)
            oyente2.Desintonizar(emisoraFM);

            //Cambiamos la canción que suena (debería notificarse sólo a los oyentes todavía sintonizados)
            emisoraFM.CancionSonando = "Eye of the tiger";

            Console.ReadLine();
        }
    }


    //....
    //Clase abstracta que representa una emisora de radio genérica
    abstract class EmisoraRadio
    {
        private string _cancionSonando;
        //Definimos e instanciamos el delegado
        public delegate void SongChangeHandler(string tituloCancion);
        public SongChangeHandler OnSongChange;

        //Constructor
        public EmisoraRadio(string cancion)
        {
            _cancionSonando = cancion;
        }

        public string CancionSonando
        {
            get
            {
                return _cancionSonando;
            }
            set
            {
                if (_cancionSonando.CompareTo(value) != 0)
                {
                    _cancionSonando = value;
                    Notificar(value);
                }
            }

        }

        //
        //Método que envía una notificación a todos los oyentes que tienen la emisora
        //sintonizada
        public void Notificar(string cancion)
        {
            if (OnSongChange != null)
            {
                //Invoca el delegado: notifica todos los objetos agregados. La función que se ejecutará recibe como parámetro la cadena cancion.
                OnSongChange(cancion);
            }
        }

    }

    //....
    //Clase que implementa una emisora de radio concreta
    class eFM : EmisoraRadio
    {
        public eFM(string cancion) : base(cancion) { }
    }

    //....
    //Interfaz que deben implementar los oyentes para poder escuchar alguna emisora de radio
    interface IOyente
    {
        void Sintonizar(EmisoraRadio emisora);
        void Desintonizar(EmisoraRadio emisora);
    }

    //....
    //Clase que se debe instanciar para crear los oyentes
    class Oyente : IOyente
    {
        private string _nombre;
        private EmisoraRadio _emisora;
        private EmisoraRadio.SongChangeHandler songChanged;

        public Oyente(string nombre)
        {
            this._nombre = nombre;
            songChanged = null;
        }

        public EmisoraRadio Emisora
        {
            get { return _emisora; }
            set { _emisora = value; }
        }


        public void Sintonizar(EmisoraRadio emisora)
        {
            songChanged = new EmisoraRadio.SongChangeHandler(this.CambioCancion);
            emisora.OnSongChange += songChanged;
        }

        public void Desintonizar(EmisoraRadio emisora)
        {
            if (songChanged != null)
            {
                emisora.OnSongChange -= songChanged;
            }
        }

        public void CambioCancion(string tituloCancion)
        {
            Console.WriteLine("Oyente " + _nombre + " escucha canción " + tituloCancion);
        }
    }
}

lunes, 2 de enero de 2012

Patrones de diseño: Singleton

Estoy intentando ampliar mis conocimientos acerca de los patrones de diseño. Es un tema muy interesante, y además ayuda a profundizar y mejorar la comprensión sobre ciertos conceptos avanzados del diseño orientado a objetos.
He decidido empezar con el patrón llamado Singleton por su sencillez y porque es uno de los más conozco y he utilizado en ocasiones. Este patrón permite garantizar que no se pueda instanciar más de un objeto de una determinada clase. A continuación se puede ver el código de una clase que implementa el patrón Singleton de modo seguro en entornos multitarea mediante el uso de bloqueos.

    public sealed class Singleton
    {
        static Singleton instance = null;
        static readonly object padlock = new object(); //Objeto compartido para bloquear el acceso simultáneo

        public static String datos;
        Singleton()
        {
        }

        public static Singleton getInstance()
        {
            lock (padlock)
            {
                if (instance == null)
                {
                    instance = new Singleton();
                    datos = "Dato inicial";
                }
                return instance;
            }
        }
    }
Podemos ver con un ejemplo como se comporta la clase Singleton cuando se invoca varias veces. Se observa que en la variable datos de la clase se tiene el dato introducido inicialmente, y en ningún caso varía cuando se intenta obtener una nueva instancia de la clase porque la instancia obtenida es siempre la misma.

using System;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Singleton.getInstance();
            Console.WriteLine("Dato inicial: " + Singleton.datos);
            Singleton.datos = "Dato cualquiera";

            for (int i = 1; i <= 50; i++)
            {
                Thread t1 = new Thread(ThreadMain);
                t1.Name = "Hilo_" + i.ToString();
                t1.Start();
            }

            Console.ReadLine();
        }

        static void ThreadMain()
        {
            //
            Singleton.getInstance();
            Console.WriteLine(Thread.CurrentThread.Name + "  " + "datos: " + Singleton.datos);
        }
    }

A continuación se puede ver el resultado de la ejecución del código de prueba.


Versión 2:

public sealed class MySingleton
{
    private static MySingleTon _instance;

    private MySingleton()
    {
    }

    public static MySingleton Instance()
    {
        if(_instance == null)
        {
            _instance = new MySingleton();
        }
        return _instance;
    }
}

static void Main(string[] args)
{
    MySingleton single1 = MySingleton.Instance();
    MySingleton single2 = MySingleton.Instance();

    if(single1 == single2)
    {
        Console.WriteLine("These two objects are the same.");
    }
}

miércoles, 21 de diciembre de 2011

15-Puzzle

Hace unos días empecé a aprender a programar aplicaciones para Android. El primer ejercicio interesante que aparece es crear un juego N-puzzle. Mientras adquiero los conocimientos necesarios en Android para poder llevarlo a cabo y me deshago de la gripe que padezco, he decidido hacerlo en .NET. Es una versión bastante simple (hay que añadir la posiblidad de seleccionar el nivel de dificultad y alguna otra mejora) pero ayuda a practicar algunos conceptos interesantes. El resultado os lo muestro a continuación con el código y las imágenes.




using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace nPuzzle01
{
    public partial class Form1 : Form
    {
        private int indiceHueco = 16;

        public Form1()
        {
            InitializeComponent();
        }

        private void btn_Click(object sender, EventArgs e)
        {
            Button btn;
            String nombre = "";
            int indice = 0;
            int numero = 0;

            //Si el botón no es el hueco
            btn = (Button)sender;
            if ((btn.Tag!=null) && (btn.Tag.ToString() != ""))
            {
                //Comprobamos si el hueco es uno de los botones adyacentes
                nombre = btn.Name;
                indice = Convert.ToInt32(nombre.Substring(nombre.IndexOf('n') + 1));
                numero = Convert.ToInt32(btn.Tag);
           

                if (indice - 1 == indiceHueco)
                {
                    //Cambiamos los contenidos de los botones
                    nombre = "btn" + (indice - 1).ToString();
                    ((Button)pnl.Controls[nombre]).Tag = btn.Tag;
                    ((Button)pnl.Controls[nombre]).BackgroundImage = btn.BackgroundImage;
                    btn.BackgroundImage = null;
                    btn.BackColor = Color.LightGray;
                    indiceHueco = Convert.ToInt32(btn.Name.Substring(nombre.IndexOf('n') + 1));
                    btn.Tag = "";
                }

                if (indice + 1 == indiceHueco)
                {
                    //Cambiamos los contenidos de los botones
                    nombre = "btn" + (indice + 1).ToString();
                    ((Button)pnl.Controls[nombre]).Tag = btn.Tag;
                    ((Button)pnl.Controls[nombre]).BackgroundImage = btn.BackgroundImage;
                    btn.BackgroundImage = null;
                    btn.BackColor = Color.LightGray;
                    indiceHueco = Convert.ToInt32(btn.Name.Substring(nombre.IndexOf('n') + 1));
                    btn.Tag = "";
                }

                if (indice - 4 == indiceHueco)
                {
                    //Cambiamos los contenidos de los botones
                    nombre = "btn" + (indice - 4).ToString();
                    ((Button)pnl.Controls[nombre]).Tag = btn.Tag;
                    ((Button)pnl.Controls[nombre]).BackgroundImage = btn.BackgroundImage;
                    btn.BackgroundImage = null;
                    btn.BackColor = Color.LightGray;
                    indiceHueco = Convert.ToInt32(btn.Name.Substring(nombre.IndexOf('n') + 1));
                    btn.Tag = "";
                }

                if (indice + 4 == indiceHueco)
                {
                    //Cambiamos los contenidos de los botones
                    nombre = "btn" + (indice + 4).ToString();
                    ((Button)pnl.Controls[nombre]).Tag = btn.Tag;
                    ((Button)pnl.Controls[nombre]).BackgroundImage = btn.BackgroundImage;
                    btn.BackgroundImage = null;
                    btn.BackColor = Color.LightGray;
                    indiceHueco = Convert.ToInt32(btn.Name.Substring(nombre.IndexOf('n') + 1));
                    btn.Tag = "";
                }

            }
        }

        private void btnSeleccionar_Click(object sender, EventArgs e)
        {
            Image img;
            OpenFileDialog ofd;
            string ruta = "";
            string nombre = "";

            ofd = new OpenFileDialog();
            if (ofd.ShowDialog()== DialogResult.OK) {
                ruta = ofd.FileName;
                img = Bitmap.FromFile(ruta);

                Bitmap bmp = new Bitmap(300, 300);

                Graphics g = Graphics.FromImage(bmp);
                g.DrawImage(img, 0, 0, 300, 300);

                Bitmap bmp1, img1;

                for (int i = 1; i <= 4; i++)
                {
                    for (int j = 1; j <= 4; j++)
                    {
                        if (!(i == 1 && j == 1))
                        {
                            bmp1 = CropImage(bmp, new Rectangle((((j - 1) * bmp.Width) / 4),
                                (((i - 1) * bmp.Height) / 4), bmp.Width / 4, bmp.Height / 4));
                            img1 = new Bitmap(bmp.Width / 4, bmp.Height / 4);
                            Graphics g21 = Graphics.FromImage(img1);
                            g21.DrawImage(bmp1, 0, 0);
                            nombre = "btn" + (((4-i) * 4) + (5-j)).ToString();
                            ((Button)pnl.Controls[nombre]).BackgroundImage = img1;
                        }
                    }
                }

                //Cargamos el previo de la imagen
                pbPrevio.BackgroundImage = bmp;
                pbPrevio.BackgroundImageLayout = ImageLayout.Stretch;
            }
        }

        public Bitmap CropImage(Bitmap origen, Rectangle seccion)
        {
            //Un bitmap vacío para almacenar la imagen recortada
            Bitmap bmp = new Bitmap(seccion.Width, seccion.Height);
            Graphics g = Graphics.FromImage(bmp);

            //Dibuja el área dada (seccion) de la imagen original
            //en la posición (0,0) del bitmap vacío (bmp)
            g.DrawImage(origen, 0, 0, seccion, GraphicsUnit.Pixel);

            return bmp;
        }
    }
}



viernes, 16 de diciembre de 2011

Imprimir tickets

Un modo de generar un documento con un formato no muy complejo e imprimirlo es utilizando un formulario, añadiendole los controles deseados hasta obtener el aspecto requerido y después imprimirlo. Yo he puesto en práctica esta opción para la creación de tickets y facturas de venta.
A continuación, muestro como se realizaría esta tarea en el caso de los ticket. Para las facturas se hace de forma muy parecida, sólo hay que añadir los datos del cliente y cambiar un poco el formato.


Cuando se crea un objeto de la clase fTicket se le debe pasar una lista que incluye los artículos incluidos en la cuenta para la que se va a generar el ticket. La lista de la cuenta será una lista de objetos que contengan la información relevante de cada concepto a mostrar en el ticket.

Public Class fTicket
    Private _listaCuenta As ArrayList

    Public Sub New(ByRef cuenta As ArrayList)
        InitializeComponent()
        _listaCuenta = cuenta
    End Sub

Al mostrar el formulario se añadirán todos los controles (etiquetas, en este caso) necesariios para lograr el formato deseado. A continuación se muestra como se añadiría una línea por cada concepto en la cuenta, y posteriormente las lineas con los totales y el pie de página. La cabecera del ticket ya se ha creado en vista de diseño en el formulario ya que no cambia nunca.


    Private Sub fTicket_Shown(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Shown
        Dim itemCuenta As ItemCuenta
        Dim i As Integer = 0
        Dim yMax As Integer, yPie As Integer

        'Para cada elemento en la lista debemos crear una línea en
        'en el ticket (añadir las etiquetas correspondientes al formulario)
        For Each o As Object In _listaCuenta
            itemCuenta = CType(o, ItemCuenta)
            'Descartamos el primer elemento porque no contiene un artículo,
            'sólo la cantidad entregada por el cliente
            If itemCuenta.nombreArticulo <> "Entregado" Then

                Dim lblDescr As Label = New Label
                lblDescr.Location = New Point(6, 134 + (i * 15))
                lblDescr.Text = itemCuenta.nombreArticulo
                lblDescr.AutoSize = False
                lblDescr.Size = New System.Drawing.Size(117, 13)
                Dim lblCantidad As Label = New Label
                lblCantidad.Location = New Point(135, 134 + (i * 15))
                lblCantidad.Text = itemCuenta.cantidadArticulo.ToString()
                lblCantidad.AutoSize = False
                lblCantidad.Size = New System.Drawing.Size(19, 13)
                lblCantidad.TextAlign = System.Drawing.ContentAlignment.MiddleRight
                Dim lblPVP As Label = New Label
                lblPVP.Location = New Point(160, 134 + (i * 15))
                lblPVP.Text = itemCuenta.precioArticulo.ToString("0.00")
                lblPVP.AutoSize = False
                lblPVP.Size = New System.Drawing.Size(45, 13)
                lblPVP.TextAlign = System.Drawing.ContentAlignment.MiddleRight
                Dim lblImporte As Label = New Label
                lblImporte.Location = New Point(211, 134 + (i * 15))
                lblImporte.Text = itemCuenta.importeArticulo.ToString("0.00")
                lblImporte.AutoSize = False
                lblImporte.Size = New System.Drawing.Size(59, 13)
                lblImporte.TextAlign = System.Drawing.ContentAlignment.MiddleRight

                Me.Controls.Add(lblDescr)
                Me.Controls.Add(lblCantidad)
                Me.Controls.Add(lblPVP)
                Me.Controls.Add(lblImporte)
                i += 1
            End If

        Next

        'Obtenemos la posición de la última línea de la cuenta para desplazar
        yMax = 134 + ((i - 1) * 15)
        yPie = yMax + 30

        'Base Imponible
        Dim lblBaseImp As Label = New Label()
        lblBaseImp.AutoSize = True
        lblBaseImp.Location = New System.Drawing.Point(117, yPie)
        lblBaseImp.Size = New System.Drawing.Size(42, 13)
        lblBaseImp.Text = "BASE IMP."
        Me.Controls.Add(lblBaseImp)

        'Obtenemos el importe total que se corresponde con el IVA
        '
        Dim lblBaseImp2 As Label = New Label()
        lblBaseImp2.AutoSize = False
        lblBaseImp2.Location = New System.Drawing.Point(117, yPie + 17)
        lblBaseImp2.Size = New System.Drawing.Size(56, 13)
        lblBaseImp2.TextAlign = System.Drawing.ContentAlignment.MiddleRight
        lblBaseImp2.Text = Utilidades.CalcularImporteTotalSinIVA(_listaCuenta).ToString("0.00")

        'Importe IVA
        Dim lblImporteIVA As Label = New Label()
        lblImporteIVA.AutoSize = True
        lblImporteIVA.Location = New System.Drawing.Point(195, yPie)
        lblImporteIVA.Size = New System.Drawing.Size(36, 13)
        lblImporteIVA.Text = "IMPORTE IVA"
        Me.Controls.Add(lblImporteIVA)

        'Obtenemos la suma del importe total de los artículos de la cuenta sin IVA
        '
        Dim lblImporteIVA2 As Label = New Label()
        lblImporteIVA2.AutoSize = False
        lblImporteIVA2.Location = New System.Drawing.Point(198, yPie + 17)
        lblImporteIVA2.Size = New System.Drawing.Size(73, 13)
        lblImporteIVA2.TextAlign = System.Drawing.ContentAlignment.MiddleRight
        lblImporteIVA2.Text = Utilidades.CalcularImporteIVA(_listaCuenta).ToString("0.00")

        '
        Me.Controls.Add(lblBaseImp2)
        Me.Controls.Add(lblImporteIVA2)

        '
        Dim lblTotalPagar As Label = New Label()
        lblTotalPagar.AutoSize = True
        lblTotalPagar.Location = New System.Drawing.Point(98, yPie + 50)
        lblTotalPagar.Size = New Size(95, 13)
        lblTotalPagar.Text = "TOTAL A PAGAR:"

        '
        Dim lblTotalPagar2 As Label = New Label()
        lblTotalPagar2.AutoSize = False
        lblTotalPagar2.Location = New System.Drawing.Point(201, yPie + 50)
        lblTotalPagar2.TextAlign = System.Drawing.ContentAlignment.MiddleRight
        lblTotalPagar2.Size = New Size(70, 13)
        lblTotalPagar2.Text = Utilidades.CalcularImporteTotalCuenta(_listaCuenta).ToString("0.00")

        '
        Me.Controls.Add(lblTotalPagar)
        Me.Controls.Add(lblTotalPagar2)

        '
        Dim lblEntregado As Label = New Label()
        lblEntregado.AutoSize = True
        lblEntregado.Location = New System.Drawing.Point(98, yPie + 72)
        lblEntregado.Size = New Size(95, 13)
        lblEntregado.Text = "ENTREGADO:"

        '
        Dim lblEntregado2 As Label = New Label()
        lblEntregado2.AutoSize = False
        lblEntregado2.Location = New System.Drawing.Point(201, yPie + 72)
        lblEntregado2.TextAlign = System.Drawing.ContentAlignment.MiddleRight
        lblEntregado2.Size = New Size(70, 13)
        lblEntregado2.Text = CType(_listaCuenta(0), ItemCuenta).precioArticulo.ToString("0.00")

        '
        Me.Controls.Add(lblEntregado)
        Me.Controls.Add(lblEntregado2)

        '
        Dim lblCambio As Label = New Label()
        lblCambio.AutoSize = True
        lblCambio.Location = New System.Drawing.Point(98, yPie + 94)
        lblCambio.Text = "CAMBIO:"

        '
        Dim lblCambio2 As Label = New Label()
        lblCambio2.AutoSize = False
        lblCambio2.Location = New System.Drawing.Point(201, yPie + 94)
        lblCambio2.TextAlign = System.Drawing.ContentAlignment.MiddleRight
        lblCambio2.Size = New Size(70, 13)
        lblCambio2.Text = (CType(_listaCuenta(0), ItemCuenta).precioArticulo - Utilidades.CalcularImporteTotalCuenta(_listaCuenta)).ToString("0.00")

        '
        Me.Controls.Add(lblCambio)
        Me.Controls.Add(lblCambio2)

        'IVA Incluido
        Dim lblIVAIncluido As Label = New Label()
        lblIVAIncluido.Location = New System.Drawing.Point(193, yPie + 118)
        lblIVAIncluido.Text = "IVA INCLUIDO"

        '
        Me.Controls.Add(lblImporteIVA)
        Me.Controls.Add(lblImporteIVA2)

        'Gracias por su visita
        Dim lblGraciasVisita As Label = New Label()
        lblGraciasVisita.AutoSize = True
        lblGraciasVisita.Location = New System.Drawing.Point(13, yPie + 145)
        lblGraciasVisita.TextAlign = System.Drawing.ContentAlignment.MiddleRight
        'lblGraciasVisita.Size = New Size(78, 13)
        lblGraciasVisita.Text = "GRACIAS POR SU VISITA"

        '
        Me.Controls.Add(lblIVAIncluido)
        Me.Controls.Add(lblGraciasVisita)

        If yPie + 170 > Me.Height Then
            Me.MinimumSize = New Size(Me.Width, yPie + 190)
        End If

        PrintDocument1.Print()
    End Sub


En el código que se muestra a continuación se puede ver como se realiza la impresión del ticket. La idea consiste en dibujar el texto contenido en las etiquetas en el evento e en la misma posición en la que se situaron dichas etiquetas dentro del formulario.
Para llevar a cabo este proceso se recorren todos los controles del formulario y se descartan todos aquellos que no sean del tipo que queremos imprimir. Los que sean del tipo que deseamos imprimir se dibujarán en el objeto e en una posición acorde a la que tienen en el formulario.


    Private Sub PrintDocument1_PrintPage(ByVal sender As System.Object, ByVal e  As System.Drawing.Printing.PrintPageEventArgs) Handles PrintDocument1.PrintPage

        DibujarTicket(e.Graphics())
    End Sub





    Private Sub DibujarTicket(ByVal g As Graphics)
        ' Create the source rectangle from the BackgroundImage Bitmap Dimensions
        Dim srcRect As Rectangle = New Rectangle(0, 0, Me.Width, Me.Height)
        Dim nWidth As Integer = Me.Width       
        Dim nHeight As Integer = Me.Height
        Dim destRect As Rectangle = New Rectangle(0, 0, nWidth, CInt(Math.Ceiling(nHeight / 2)))
        Dim lbl As Label

        Dim scalex As Double = 1 * CSng(destRect.Width / srcRect.Width)
        Dim scaley As Double = 2 * CSng(destRect.Height / srcRect.Height)
        Dim aPen As New Pen(Brushes.Black, 1)
        ' Recorre todos los controles. Determina si es un label y dibuja el valor de la propiedad text
        ' en la posición correcta (en función de la propiedad location del control label en cuestión)

        For Each o As Object In Me.Controls
            If o.GetType() = Label1.GetType() Then
                lbl = CType(o, Label)
                If lbl.TextAlign = ContentAlignment.MiddleRight Then
                    g.DrawString(lbl.Text, lbl.Font, Brushes.Black, CSng((lbl.Bounds.Right - 6 * lbl.Text.Length) * scalex),
                                 CSng(lbl.Bounds.Top * scaley), New StringFormat())
                Else
                    g.DrawString(lbl.Text, lbl.Font, Brushes.Black, CSng((lbl.Bounds.Left) * scalex),
                                 CSng(lbl.Bounds.Top * scaley), New StringFormat())

                End If

            End If
        Next

        Me.Close()
    End Sub

Autentificación de usuarios

Últimamente, he estado trabajando en un pequeño proyecto y por eso no he añadido contenido al blog. Sin embargo, ahora que he terminado, he decidido crear algunas entradas aquí con cosas que creo que se utilizan muy a menudo en los proyectos de programación y que espero que sean útiles.
En esta entrada voy a mostrar el funcionamiento de un formulario de autentificación que yo he utilizado.


Public Class fAutentificacion

    Private Sub btnEntrar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEntrar.Click
        If txtUsuario.Text = "" And txtContrasena.Text = "" Then
            MessageBox.Show("Introduzca un usuario y una contraseña", "Autentificación", MessageBoxButtons.OK, MessageBoxIcon.Information)
        Else
            If txtUsuario.Text = "" Or Validacion.Usuario(txtUsuario.Text) = False Then
                MessageBox.Show("Introduzca un nombre de usuario con el formato correcto", "Autentificación", MessageBoxButtons.OK, MessageBoxIcon.Information)
            Else
                'Comprobamos si las credenciales son correctas
                If BD.CredencialesCorrectas(txtUsuario.Text, BD.PasswordHash(txtContrasena.Text)) Then
                    'Autentificación correcta
                    Dim fGestion As fAdmin
                    'Ocultamos el formulario del TPV
                    Me.Owner.Visible = False
                    'Ocultamos el formulario de autentificación
                    Me.Hide()
                    'Abrimos el formulario al que queremos acceder
                    fGestion = New fAdmin()
                    fGestion.ShowDialog(Me.Owner)
                Else
                    'Autentificación incorrecta
                    MessageBox.Show("Autentificación incorrecta", "Autentificación", MessageBoxButtons.OKCancel, MessageBoxIcon.Error)
                End If
            End If

        End If
    End Sub


    Private Sub fAutentificacion_FormClosing(ByVal sender As System.Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles MyBase.FormClosing
        Me.Owner.Visible = True
    End Sub

    Private Sub fAutentificacion_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        txtUsuario.Select()
    End Sub
End Class

En el código anterior se utilizan algunos métodos que yo he creado especificamente y que muestro a continuación para aclarar totalmente el funcionamiento del código.


    'Validación nombre usuario
    Public Shared Function Usuario(ByVal cadena As String) As Boolean

        If Regex.IsMatch(cadena, "^[a-zA-ZñÑ_][a-zA-ZñÑ0-9._]+$") Then
            Return True
        Else
            Return False
        End If
    End Function

El código anterior se ocupa de comprobar que el formato del nombre de usuario introducido es correcto. En este caso, sólo se admiten caracteres alfabéticos (mayúsculas y minúsculas y ñ y Ñ), numéricos y los símbolos de puntuación '.' y '_'. Además, el carácter '.' no podrá utilizarse como carácter inicial.
Para realizar la validación se utiliza una expresión regular.

    'Autentificación de las credenciales aportadas contra la base de datos
    Public Shared Function CredencialesCorrectas(ByRef usuario As String, ByRef contrasena As String) As Boolean
        Dim cadenaConexion As String
        Dim conexion As OleDbConnection
        Dim comando As OleDbCommand
        Dim resultado As Integer
        Dim consulta As String

        cadenaConexion = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & Environment.CurrentDirectory & "\ficheroBD.accdb;Persist Security Info=False;"
        conexion = New OleDbConnection(cadenaConexion)
        consulta = "SELECT COUNT(*) FROM PERSONAL WHERE StrComp(Nombre,'" & usuario & "',0)=0 AND Contrasena='" & contrasena & "' AND Acceso='Administrador'"

        conexion.Open()
        comando = New OleDbCommand(consulta, conexion)
        resultado = CType(comando.ExecuteScalar(), Integer)

        conexion.Close()

        If resultado > 0 Then
            Return True
        Else
            Return False
        End If

    End Function

El código arriba mostrado realiza la comparación entre los datos aportados por el usuario y los guardados en la base de datos para el nombre de usuario indicado.


    'Obtención de la función hash SHA1 de la contraseña (para no mantener
    'las contraseñas almacenadas en texto plano)
    Public Shared Function PasswordHash(ByVal password As String) As String
        Dim bytes() As Byte = Encoding.Unicode.GetBytes(password)
        Dim inArray() As Byte = HashAlgorithm.Create("SHA1").ComputeHash(bytes)

        Return Convert.ToBase64String(inArray)
    End Function

lunes, 24 de octubre de 2011

Multimedia

Estos últimos días he estado haciendo algunas pruebas con MCI (Multimedia Control Interface), algo nuevo para mi. A pesar de algunos contratiempos iniciales, que me hicieron incluso pensar en utilizar el control Windows Media Player, al final ha merecido la pena perder un poco de tiempo peleando con el MCI. Esta interfaz permite un amplísimo control sobre archivos multimedia (sonido y audio). A continuación, muestro el código y la vista de diseño de un simple reproductor de archivos de sonido o video realizado utilizando el MCI.

Imports System.Text

Public Class Form1
    'Declaramos la función que nos permitirá utilizar los comandos de MCI
    Private Declare Function mciSendString Lib "winmm.dll" Alias "mciSendStringA" _
       (ByVal lpstrCommand As String, ByVal lpstrReturnString As String, ByVal _
       uReturnLength As Integer, ByVal hwndCallback As Integer) As Integer


    Private Sub btnSelCarpeta_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSelFichero.Click
        Dim ofd As OpenFileDialog = New OpenFileDialog()
        Dim fichero As String, longitud As String = "                "
        Dim minutos As Integer, segundos As Integer

        'Obtenemos el fichero que queremos abrir (mostramos sólo archivos de video y audio)
        ofd.Filter = "Audio|*.wav;*.mp3|Video|*.avi;*.wmv;*.mpg"
        If ofd.ShowDialog() = DialogResult.OK Then
            'Establecemos el volumen por defecto
            tbVolumen.Value = 500
            fichero = Chr(34) & ofd.FileName & Chr(34)

            'Nos aseguramos de que el fichero está cerrado antes de abrirlo
            mciSendString("close miFichero", Nothing, 0, 0)
            mciSendString("open " & fichero & " type mpegvideo alias miFichero parent " & pnlPantalla.Handle.ToInt32 & " style child", Nothing, 0, 0)
            mciSendString("set miFichero time format ms", Nothing, 0, 0)

            'Obtenemos la duración de la reproducción en ms e inicializamos el control trackBar y la etiqueta con el tiempo total de reproducción
            mciSendString("status miFichero length", longitud, 128, 0)
            tPosicion.Enabled = True
            tbPosicion.Minimum = 0
            tbPosicion.Maximum = Val(longitud)
            minutos = Math.Floor(Val(longitud) / 60000)
            segundos = (Val(longitud) \ 1000) Mod 60
            lblTiempoTotal.Text = minutos.ToString & ":" & String.Format("{0:00}", segundos).Substring(0, 2)

            'Inicializamos el control de volumen y mostramos el video (si existe) en el panel correspondiente
            mciSendString("setaudio miFichero volume to " & tbVolumen.Value.ToString, Nothing, 0, 0)
            mciSendString("put miFichero window at 0 0 " & pnlPantalla.Width & " " & pnlPantalla.Height, Nothing, 0, 0)

        End If

    End Sub

    'Operaciones a realizar cuando se pulsa el botón reproducir
    Private Sub btnPlay_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnPlay.Click
        'Se ajusta el volumen al valor seleccionado en el control correspondiente
        mciSendString("setaudio miFichero volume to " & tbVolumen.Value.ToString, Nothing, 0, 0)
        'Habilitamos el temporizador por si la reproducción había sido pausada antes de pulsar el botón reproducir
        tPosicion.Enabled = True
        'Se reproduce el archivo desde el inicio
        mciSendString("play miFichero from 0", Nothing, 0, 0)

    End Sub

    'Operaciones a realizar cuando se pulsa el botón salir
    Private Sub btnSalir_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSalir.Click
        'Cerramos el fichero
        mciSendString("close miFichero", Nothing, 0, 0)
        'Cerramos la aplicación
        Application.Exit()
    End Sub

    'Operaciones a realizar cuando se pulsa el botón pausar
    Private Sub btnPause_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnPause.Click
        Dim longitud As String = "                ", formato As String = "                "
        Dim aCars() As Char = {"0"c}

        If tPosicion.Enabled = True Then
            'Si el fichero se está reproduciendo lo pausamos
            tPosicion.Enabled = False
            mciSendString("pause miFichero", Nothing, 0, 0)
        Else
            'Si el fichero no se está reproduciendo lo continuamos reproduciendo
            tPosicion.Enabled = True
            mciSendString("resume miFichero", Nothing, 0, 0)
        End If

    End Sub

    'Operación realizada al variar el control de volumen
    Private Sub tbVolumen_Scroll(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles tbVolumen.Scroll
        mciSendString("setaudio miFichero volume to " & tbVolumen.Value.ToString, Nothing, 0, 0)
    End Sub

    'Operaciones realizadas cuando se pulsa el botón parar
    Private Sub btnStop_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStop.Click
        Dim returnData As String = "                "

        mciSendString("stop miFichero", Nothing, 0, 0)
        lblTiempo.Text = "00:00"
    End Sub


    'Operaciones a realizar al arrancar la aplicación
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'Establecemos el volumen por defecto
        tbVolumen.Value = 500
        'Limpiamos el valor de los tiempos
        lblTiempo.Text = ""
        lblTiempoTotal.Text = ""
    End Sub

    'Operaciones realizadas cada vez que vence el temporizador que comprueba el estado de la reproducción
    Private Sub tPosicion_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles tPosicion.Tick
        Dim returnData As String = "                "
        Dim minutos As Integer, segundos As Integer

        'Obtenemos la posición actual de la reproducción y convertimos los milisegundos en minutos y segundos
        mciSendString("status miFichero position", returnData, 128, 0)
        tbPosicion.Value = Val(returnData)
        minutos = Math.Floor(Val(returnData) \ 60000)
        segundos = (Val(returnData) \ 1000) Mod 60
        'Reflejamos la posición actual (en el formato mm:ss) en la etiqueta correspondiente
        lblTiempo.Text = minutos.ToString & ":" & String.Format("{0:00}", segundos).Substring(0, 2)

    End Sub

    'Operaciones que se realizan cuando el usuario varía la posición del control trackBar que refleja la posición actual
    'de la reproducción en curso
    Private Sub tbPosicion_Scroll(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles tbPosicion.Scroll
        Dim posicion As Integer
        Dim returnData As String = "                "

        'Obtenemos la posición seleccionada por el usuario mediante el trackBar (en ms)
        posicion = tbPosicion.Value
        'Indicamos que se inicie la reproducción desde la posición indicada (en ms)
        mciSendString("play miFichero from " & posicion, returnData, 128, 0)
    End Sub

End Class

miércoles, 19 de octubre de 2011

Movimiento

En este ejemplo he decidido mostrar como se pueden utilizar algunos eventos del ratón y del teclado para mover objetos en el formulario. Es una aplicación muy sencilla, y espero que resulte igualmente instructiva. Utilizando únicamente estos eventos se pueden realizar cosas bastante llamativas. También utilizo el objeto Rectangle para controlar el área que ocupan los objetos y saber en todo momento si se solapan entre ellos.


Imports System.Collections

Public Class Form1
    Private dentro As Boolean
    Private rect1 As Rectangle
    Private rect2 As Rectangle
    Private rect3 As Rectangle
    Private rect4 As Rectangle
    Dim x0 As Integer, y0 As Integer
    Dim listaObstaculos As ArrayList

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'Definimos el área ocupada por los obstáculos mostrados en el formulario (se corresponde con el área de los
        'controles label fijos en el formulario, los de color gris)
        rect2 = New Rectangle(300, 300, 50, 50)
        rect3 = New Rectangle(380, 175, 15, 215)
        rect4 = New Rectangle(230, 415, 200, 20)

        'Cargamos los obstaculos en la lista
        listaObstaculos = New ArrayList()
        listaObstaculos.Add(rect2)
        listaObstaculos.Add(rect3)
        listaObstaculos.Add(rect4)

        dentro = False
    End Sub

    Private Sub lbl1_MouseDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles lbl1.MouseDown
        'Cuando pulsamos el ratón sobre la etiqueta móvil activamos la variable que controla cuando se va a desplazar la etiqueta
        'siguiendo los movimientos del ratón
        dentro = True
    End Sub

    Private Sub lbl1_MouseUp(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles lbl1.MouseUp
        'Desactivamos el seguimiento de los movimientos del ratón por parte de la etiqueta.
        dentro = False
    End Sub

    Private Sub lbl1_MouseMove(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles lbl1.MouseMove
        'Si la variable ha sido activada (el ratón fue pulsado sobre la etiqueta), la etiqueta seguirá los movimientos del ratón hasta
        'que se deje de pulsar este (y la variable vuelva a tener valor false).
        If dentro = True Then
            'Obtenemos las coordenadas actuales del ratón (a las que hay que mover la etiqueta)
            x0 = e.X + CType(sender, Label).Left
            y0 = e.Y + CType(sender, Label).Top

            'Obtenemos el área que cubrirá la etiqueta después de desplazarla
            rect1 = New Rectangle(x0, y0, lbl1.Width, lbl1.Height)

            'Si desplazando la etiqueta no se solapa con ninguno de los obstáculos, cambiamos la posición de la etiqueta a la del ratón
            'Si se solapase el área ocupada por la etiqueta con algún obstáculo no se actualizaría la posición de la etiqueta
            If (Rectangle.Intersect(rect1, rect2).IsEmpty = True) And (Rectangle.Intersect(rect1, rect3).IsEmpty = True) And (Rectangle.Intersect(rect1, rect4).IsEmpty = True) Then
                lbl1.Top = e.Y + CType(sender, Label).Top
                lbl1.Left = e.X + CType(sender, Label).Left
            End If
        End If

    End Sub

    'Este evento controla cuando se pulsa una tecla (sólo tenemos en cuenta las pulsaciones de las flechas)
    Private Sub Form1_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
        'Flecha arriba
        If e.KeyCode = Keys.Up Then
            'Obtenemos la posición que tendría la etiqueta después del desplazamiento indicado por la flecha pulsada
            x0 = lbl1.Left
            'Comprobamos el margen inferior

            y0 = lbl1.Top - 1
            If lbl1.Top > 0 Then
                y0 = lbl1.Top - 1
            Else
                y0 = lbl1.Top
            End If

            'Obtenemos el área que ocuparía la etiqueta
            rect1 = New Rectangle(x0, y0, lbl1.Width, lbl1.Height)

            'Si no hay solape se actualiza la posición
            If (lbl1.Top > 0) And Not chocaObstaculo(rect1) Then
                lbl1.Top = y0
            End If

        End If

        'Flecha abajo
        If e.KeyCode = Keys.Down Then
            'Obtenemos la posición que tendría la etiqueta después del desplazamiento indicado por la flecha pulsada
            x0 = lbl1.Left
            'Comprobamos el margen superior (la altura del formulario, con un pequeño ajuste)
            If lbl1.Top + lbl1.Height + 34 < Me.Height Then
                y0 = lbl1.Top + 1
            Else
                y0 = lbl1.Top
            End If

            'Obtenemos el área que ocuparía la etiqueta
            rect1 = New Rectangle(x0, y0, lbl1.Width, lbl1.Height)

            'Si no hay solape se actualiza la posición
            If (lbl1.Top + lbl1.Height + 34 < Me.Height) And Not chocaObstaculo(rect1) Then
                lbl1.Top = y0
            End If

        End If

        'Flecha derecha
        If e.KeyCode = Keys.Right Then
            'Obtenemos la posición que tendría la etiqueta después del desplazamiento indicado por la flecha pulsada
            y0 = lbl1.Top
            'Comprobamos el margen superior (el ancho del formulario, con un pequeño ajuste)
            If lbl1.Left + lbl1.Width + 8 < Me.Width Then
                x0 = lbl1.Left + 1
            Else
                x0 = lbl1.Left
            End If

            'Obtenemos el área que ocuparía la etiqueta
            rect1 = New Rectangle(x0, y0, lbl1.Width, lbl1.Height)

            'Si no hay solape se actualiza la posición
            If (lbl1.Left + lbl1.Width + 8 < Me.Width) And Not chocaObstaculo(rect1) Then
                lbl1.Left = x0
            End If

        End If

        'Flecha izquierda
        If e.KeyCode = Keys.Left Then
            'Obtenemos la posición que tendría la etiqueta después del desplazamiento indicado por la flecha pulsada
            y0 = lbl1.Top
            'Comprobamos el margen inferior
            If lbl1.Left > 0 Then
                x0 = lbl1.Left - 1
            Else
                x0 = lbl1.Left
            End If


            'Obtenemos el área que ocuparía la etiqueta
            rect1 = New Rectangle(x0, y0, lbl1.Width, lbl1.Height)

            'Si no hay solape se actualiza la posición
            If (lbl1.Left > 0) And Not chocaObstaculo(rect1) Then
                lbl1.Left = x0
            End If

        End If
    End Sub

    Private Function chocaObstaculo(ByVal rect As Rectangle) As Boolean
        Dim obstaculo As Rectangle

        'Recorremos todos los obstaculos de la lista
        For Each obj As Object In listaObstaculos
            obstaculo = CType(obj, Rectangle)
            'Si alguno de los obstáculos se solapa con el área ocupada por nuestra etiqueta móvil, este método devolverá
            'un valor
            If Rectangle.Intersect(obstaculo, rect).IsEmpty = False Then
                Return True
            End If
        Next
        Return False
    End Function
End Class