Mostrando las entradas con la etiqueta DrawItem. Mostrar todas las entradas
Mostrando las entradas con la etiqueta DrawItem. Mostrar todas las entradas

viernes, 27 de febrero de 2015

El Demo del Día: Graficar en el ListView de WinForms

Graficar en el ListView de WinForms

En este post veremos como crear graficos dentro de un ListView en Windows Forms, por ejemplo, para listar los empleados con su foto y una imagen con un número de estrellas que muestran su desempeño.

Requerimiento

Se necesita mostrar imágenes en los sub elementos de un elemento de un ListView. Por defecto, este muestra solo textos pero no imagenes.

Solución

- Configurar la Propiedad OwnerDraw en true del ListView
- Programar los eventos DrawColumnHeader, DrawItem y DrawSubItem para dibujar las cabeceras, las filas y pintar los sub elementos (textos e imágenes cargadas de archivos de disco).

Crear una Aplicación Windows Forms en C#

En Visual Studio crear un proyecto Windows Forms en C# con el nombre de "ListView_Grafico", luego cambiar el nombre del formulario a "frmEmpleado".

Crear una carpeta llamada "Imagenes" y agregar 9 archivos jpg con el nombre de 1.jpg, 2jpg, hasta 9.jpg con las fotos de los empleados. También conseguir una imagen "Rating.png" de 334 x 281 conteniendo el rating de desempeño, similar a la siguiente figura:



Nota: Vamos a usar un solo archivo (sprite) para mostrar los desempeños que van del 1 al 4.

Crear la Clase Entidad para el Empleado

Crear la clase "beEmpleado" escribiendo el siguiente código:

namespace ListView_Grafico
{
    public class beEmpleado
    {
        public int IdEmpleado { get; set; }
        public string Nombres { get; set; }
        public int Desempeño { get; set; }
    }
}

Programar el Formulario con el ListView

Regresar al formulario "frmEmpleado" y agregar un ListView llamado "lvwEmpleado" el cual debe estar acoplado a todo el formulario cuyo tamaño es de 600 de ancho x 550 de alto y esta centrado, similar a la siguiente figura:


Escribir el siguiente código en el formulario:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;

namespace ListView_Grafico
{
    public partial class frmEmpleado : Form
    {
        List<beEmpleado> lbeEmpleado;
        Bitmap bmp;
        Rectangle rect;        
        string ruta = @"C:\Data\NET\Practicas\WinForms\ListView_Grafico\Imagenes\";

        public frmEmpleado()
        {
            InitializeComponent();
        }

        private void crearImagenRating()
        {
            string archivo = String.Format("{0}Rating.png",ruta);
            bmp = new Bitmap(archivo);
        }

        private void crearColumnasListView()
        {
            lvwEmpleado.Columns.Add("Id", 50);
            lvwEmpleado.Columns.Add("Nombre del Empleado", 220);
            lvwEmpleado.Columns.Add("Foto", 100);
            lvwEmpleado.Columns.Add("Desempeño", 100);
        }

        private void configurarListView()
        {
            //Configurar el Alto de las Filas
            ImageList imgList = new ImageList ();
            imgList.ImageSize = new Size(1, 50);
            lvwEmpleado.SmallImageList = imgList;
            //Configurar la Apariencia
            lvwEmpleado.View = View.Details;
            lvwEmpleado.FullRowSelect = true;
            lvwEmpleado.HotTracking = true;
            //Configurar el Pintado
            lvwEmpleado.OwnerDraw = true;         
            lvwEmpleado.DrawColumnHeader += 
            new DrawListViewColumnHeaderEventHandler(dibujarCabecera);
            lvwEmpleado.DrawItem += 
            new DrawListViewItemEventHandler(dibujarFila);
            lvwEmpleado.DrawSubItem += 
            new DrawListViewSubItemEventHandler(dibujarCelda);
        }

        private void obtenerEmpleados()
        {
            lbeEmpleado = new List<beEmpleado>();
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 1, 
            Nombres = "Nadine Heredia", Desempeño = 2 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 2, 
            Nombres = "Ollanta Humala", Desempeño = 2 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 3, 
            Nombres = "Valeria Maza", Desempeño = 4 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 4, 
            Nombres = "Cristina Fernandez", Desempeño = 3 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 5, 
            Nombres = "Alan Garcia", Desempeño = 1 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 6, 
            Nombres = "Alberto Fujimori", Desempeño = 2 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 7, 
            Nombres = "Alejandro Toledo", Desempeño = 3 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 8, 
            Nombres = "Inocencio Perez", Desempeño = 1 });
            lbeEmpleado.Add(new beEmpleado { IdEmpleado = 9, 
            Nombres = "Luis Dueñas", Desempeño = 4 });
        }

        private void llenarListViewEmpleados()
        {
            ListViewItem fila;
            foreach (beEmpleado obeEmpleado in lbeEmpleado)
            {                
                fila = lvwEmpleado.Items.Add(obeEmpleado.IdEmpleado.ToString());
                fila.SubItems.Add(obeEmpleado.Nombres);
                fila.SubItems.Add(obeEmpleado.IdEmpleado.ToString());
                fila.SubItems.Add(obeEmpleado.Desempeño.ToString());
            }
        }

        private void crearListView(object sender, EventArgs e)
        {
            crearImagenRating();
            crearColumnasListView();
            configurarListView();
            obtenerEmpleados();
            llenarListViewEmpleados();            
        }

        private void dibujarCabecera(object sender, DrawListViewColumnHeaderEventArgs e)
        {
            e.Graphics.FillRectangle(Brushes.Black, e.Bounds);
            StringFormat sf= new StringFormat();
            sf.Alignment=StringAlignment.Center;
            sf.LineAlignment = StringAlignment.Center;
            e.Graphics.DrawString(e.Header.Text, new Font("Arial", 10, FontStyle.Bold),
            Brushes.White, e.Bounds, sf);
        }

        private void dibujarFila(object sender, DrawListViewItemEventArgs e)
        {
            rect = new Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.Width, 50);
            if ((e.State & ListViewItemStates.Selected) != 0)
            {
                e.Graphics.FillRectangle(Brushes.Black, rect);
                e.DrawFocusRectangle();
            }
        }

        private void dibujarCelda(object sender, DrawListViewSubItemEventArgs e)
        {
            if (e.ColumnIndex > -1)
            {
                rect = new Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.Width-20, 50);
                if (e.ColumnIndex < 2)
                {
                    Font fuente = new Font("Arial", 8);
                    Brush brocha = Brushes.Black;
                    StringFormat sf = new StringFormat();
                    sf.LineAlignment = StringAlignment.Center;
                    bool seleccionado = (e.ItemState & ListViewItemStates.Selected) != 0;
                    if (seleccionado)
                    {
                        brocha = Brushes.White;
                        fuente = new Font("Arial", 10, FontStyle.Bold);
                    }
                    e.Graphics.DrawString(e.SubItem.Text, fuente, brocha, rect,sf);
                    return;
                }
                else
                {
                    if (e.ColumnIndex == 2)
                    {
                        using (MemoryStream ms = new MemoryStream(obtenerFoto(e.SubItem.Text)))
                        {
                            Image img = Image.FromStream(ms);
                            e.Graphics.DrawImage(img, rect);
                        }
                    }
                    else
                    {
                        int n=int.Parse(e.SubItem.Text);
                        int h = 281/4;
                        int y=h*(n-1);
                        rect = new Rectangle(0, y, 334, h);
                        Image img = bmp.Clone(rect, bmp.PixelFormat);
                        e.Graphics.DrawImage(img, e.Bounds.X, e.Bounds.Y, 100, 40);
                    }
                }
            }
        }

        private byte[] obtenerFoto(string idEmpleado)
        {            
            string archivo = String.Format("{0}{1}.jpg", ruta, idEmpleado);
            byte[] foto = File.ReadAllBytes(archivo);
            return foto;
        }
    }
}

Nota: La función principal es "crearListView" y esta asociada al evento "Load" del formulario, la cual llama a casi todos los métodos usados.

Una de los problemas que se presento al momento de desarrollar fue aumentar el alto de cada fila, lo cual se logra asociando un ImageList a la propiedad SmallImageList del ListView, para lo cual se le da un tamaño al ImageList mediante su propiedad "ImageSize", por ejemplo, le damos el alto de 50.

Probar la Aplicación Windows Forms

Grabar la aplicación y ejecutarla con F5. Se mostrará la siguiente ventana:


Observar que la cabecera del ListView tiene fondo negro, texto de color blanco en negrita y centrado horizontal y alineado vertical al centro.

Los elementos del ListView cuando no están seleccionados tienen fondo blanco, texto de color negro y están alineados verticalmente al centro.

Al pasar el mouse por cada elemento este se selecciona (propiedad HotTracking=true) y el fondo es negro y el color del texto es blanco en negrita y con fuente mas grande.

En la tercera columna se muestran las fotos obtenidas de los archivos de acuerdo al código del empleado y finalmente, en la última columna se presenta una sección del archivo de Rating, el cual se dividió lógicamente en 5 filas y de acuerdo al desempeño se obtiene solo una fila.

Comentario Final

En este breve post he compartido como crear gráficos dentro de un ListView para mejorar su apariencia, hay controles de terceros muy buenos como el ObjectListView que permiten mostrar todo tipo de imágenes, y lo que quería era demostrar que también podemos hacer lo mismo configurando la propiedad OwnerDraw y programando en los eventos DrawColumnHeader, DrawItem y DrawSubItem.

Descarga

jueves, 15 de enero de 2015

El Demo del Día: ComboBox con Imágen y Múltiples Columnas en WinForms

ComboBox con Imágen y Múltiples Columnas en WinForms

Ayer en el seminario vimos como crear Librerías de Controles para WinForms y no pude completar el último Demo de un Control ComboBox enlazado a Datos que soporte Imagen de Base de Datos y permita visualizar Múltiples Columnas.

Requerimiento

Se desea mostrar la información de los registros en varias columnas en una lista desplegable (ComboBox) y ademas poder incluir al inicio una Imagen también desde un origen de datos.

Solución

Debemos sobre escribir la clase ComboBox de WinForms y crear un par de propiedades llamada "ImageMember" para el campo de imagen y "DataColumns" para la lista de columnas a visualizarse la cual contiene el nombre del campo, el ancho de la columna y la alineación.

El control ComboBox tiene 2 formas de presentación: la Normal y la Dibujada, lo cual se logra cambiar configurando la propiedad "DrawMode". En nuestro caso, modificaremos la propiedad "DrawMode" a "OwnerDrawFixed" y programaremos en el evento "DrawItem" el dibujo con las columnas a presentar y la imagen.

Crear el Procedimiento Almacenado en la Base de Datos Northwind de SQL Server

Para listar los datos del empleado incluyendo su foto escribir el siguiente código:

Create procedure [dbo].[uspEmployeesListar]
As
Select EmployeeId,LastName,FirstName,IsNull(BirthDate,'1/1/1900') as Birthdate,Photo
From Employees

Actualizar los registros del campo Photo reemplazandolo desde archivo de disco

El campo Foto (Photo) de la tabla Empleados (Employees) de Northwind tiene registros de tipo bmp que no se verán en la aplicación, hay que actualizarlos por archivos jpg desde el disco.

Para eso entrar como Administrador al SQL Managment Studio y ejecutar la siguiente instrucción:

Update Employees Set Photo=(Select img.* from Openrowset(Bulk 'C:\Data\NET\Practicas\WinForms\ComboBox_Imagen_MultiColumna\Empleados\1.jpg', Single_Blob) img) where EmployeeId = 1

Nota: Realizar lo mismo para modificar las imágenes tantos registros tengas en la tabla empleados, para lo cual cambiar el archivo y el Id del Empleado (EmployeeId).

Crear una Aplicación Windows Forms en C#

En Visual Studio crear un proyecto Windows Forms en C# con el nombre de "ComboBox_Imagen_MultiColumna", luego cambiar el nombre del formulario a "frmConsultaEmpleado".

Crear la Clase Entidad del Negocio

Crear la clase beEmpleado escribiendo el siguiente código:

using System;
namespace ComboBox_Imagen_MultiColumna
{
    public class beEmpleado
    {
        public int IdEmpleado { get; set; }
        public string Apellido { get; set; }
        public string Nombre { get; set; }
        public DateTime FechaNacimiento { get; set; }
        public byte[] Foto { get; set; }
    }
}

Crear la Clase de Acceso a Datos

Crear la clase daEmpleado escribiendo el siguiente código:

using System;
using System.Collections.Generic; //List
using System.Data; //CommandType, CommandBehavior
using System.Data.SqlClient; //SqlConnection, SqlCommand, SqlDataReader
namespace ComboBox_Imagen_MultiColumna
{
    public class daEmpleado
    {
        public List<beEmpleado> listar(SqlConnection con)
        {
            List<beEmpleado> lbeEmpleado = null;

            SqlCommand cmd = new SqlCommand("uspEmployeesListar", con);
            cmd.CommandType = CommandType.StoredProcedure;
            SqlDataReader drd = cmd.ExecuteReader(CommandBehavior.SingleResult);
            if (drd != null)
            {
                lbeEmpleado = new List<beEmpleado>();
                int posIdEmpleado = drd.GetOrdinal("EmployeeID");
                int posApellido = drd.GetOrdinal("LastName");
                int posNombre = drd.GetOrdinal("FirstName");
                int posFechaNacimiento = drd.GetOrdinal("BirthDate");
                int posFoto = drd.GetOrdinal("Photo");
                beEmpleado obeEmpleado;
                while (drd.Read())
                {
                    obeEmpleado = new beEmpleado();
                    obeEmpleado.IdEmpleado = drd.GetInt32(posIdEmpleado);
                    obeEmpleado.Apellido = drd.GetString(posApellido);
                    obeEmpleado.Nombre = drd.GetString(posNombre);
                    obeEmpleado.FechaNacimiento = drd.GetDateTime(posFechaNacimiento);
                    if (!drd.IsDBNull(posFoto)) obeEmpleado.Foto = (byte[])drd.GetValue(posFoto);
                    lbeEmpleado.Add(obeEmpleado);
                }
                drd.Close();
            }

            return (lbeEmpleado);
        }
    }
}

Nota: Tiene que haberse creado el Procedimiento Almacenado "uspEmployeesListar" en la BD Northwind.

Crear la Clase de Reglas del Negocio

Crear la clase brEmpleado escribiendo el siguiente código:

using System;
using System.Configuration; //ConfigurationManager
using System.Data.SqlClient; //SqlConnection
using System.Collections.Generic; //List
namespace ComboBox_Imagen_MultiColumna
{
    public class brEmpleado
    {
        public List<beEmpleado> listar()
        {
            List<beEmpleado> lbeEmpleado = null;
            string conexion = ConfigurationManager.ConnectionStrings["conNW"].ConnectionString;
            using (SqlConnection con = new SqlConnection(conexion))
            {
                try
                {
                    con.Open();
                    daEmpleado odaEmpleado = new daEmpleado();
                    lbeEmpleado = odaEmpleado.listar(con);
                }
                catch (SqlException ex)
                {
                    //grabarLog(ex);
                }
                catch (Exception ex)
                {
                    //grabarLog(ex);
                }
            } //con.Close(); con.Dispose();
            return (lbeEmpleado);
        }
    }
}

Nota: Hay que hacer referencia a la librería "System.Configuration.dll" para usar la clase ConfigurationManager para leer el archivo de configuración.

Crear una Clase Entidad para configurar las Columnas del Combo

Crear la clase "DataColumn" escribiendo el siguiente código:

using System.Windows.Forms;
namespace ComboBox_Imagen_MultiColumna
{
    public class DataColumn
    {
        public string ColumnName { get; set; }
        public string HeaderText { get; set; }
        public int ColumnWidth { get; set; }
        public HorizontalAlignment Alignment { get; set; }
    }
}

Crear una Clase para el ComboBox con Imagen y Múltiples Columnas

Crear la clase "ComboImage" que herede del ComboBox escribiendo el siguiente código:

using System;
using System.Text;
using System.IO;
using System.Drawing;
using System.Collections.Generic;
using System.Windows.Forms;

namespace ComboBox_Imagen_MultiColumna
{
    public class ComboImage:ComboBox
    {
        private dynamic dataSource;
        public string ImageMember { get; set; }
        public List<DataColumn> DataColumns { get; set; }

        public ComboImage()
        {
            this.ItemHeight = 25;
            this.DrawMode = DrawMode.OwnerDrawFixed;
            this.DrawItem += new DrawItemEventHandler(dibujarElemento);
            this.MeasureItem += new MeasureItemEventHandler(configurarTamaño);
        }

        private void dibujarElemento(object sender, DrawItemEventArgs e)
        {
            int x = e.Bounds.X;
            int y = e.Bounds.Y;
            int indice = e.Index;
            dataSource = DataSource;
            string item="";          
            if (DataColumns != null && DataColumns.Count > 0)
            {
                StringBuilder sb = new StringBuilder("");
                DataColumn columna;
                string valor = "";
                for (int i = 0; i < DataColumns.Count; i++)
                {
                    columna = DataColumns[i];
                    valor = dataSource[indice].GetType().GetProperty(columna.ColumnName).
                                GetValue(dataSource[indice], null).ToString();
                    if (columna.Alignment.Equals(HorizontalAlignment.Right))
                        sb.Append(String.Format("{0} │ ", valor.PadLeft(columna.ColumnWidth, ' ')));
                    else sb.Append(String.Format("{0} │ ", valor.PadRight(columna.ColumnWidth, ' ')));
                }
                item = sb.ToString();
            }
            else
            {
                if (!String.IsNullOrEmpty(DisplayMember))
                {
                    item = dataSource[indice].GetType().GetProperty(DisplayMember).
                               GetValue(dataSource[indice], null).ToString();
                }
            }
            if (!String.IsNullOrEmpty(ImageMember))
            {
                byte[] buffer = dataSource[indice].GetType().GetProperty(ImageMember).
                                         GetValue(dataSource[indice], null);
                if (buffer != null && buffer.Length > 0)
                {
                    MemoryStream ms = new MemoryStream(buffer);
                    Image img = null;
                    try{
                        img = Image.FromStream(ms);
                        e.Graphics.DrawImage(img, x, y, 25, 25);
                    }
                    catch(Exception ex)
                    {
                        //Grabar Log
                    }                                    
                }
                x = x + 20;
            }
            e.Graphics.DrawString(item, new Font("Courier New", 8), Brushes.Black, x, y+5);
            e.DrawFocusRectangle();
        }

        private void configurarTamaño(object sender, MeasureItemEventArgs e)
        {
            e.ItemWidth = this.Width;
            e.ItemHeight = 200;
        }
    }
}

Nota: Esta clase creada hereda las propiedades DataSource, DisplayMember y ValueMember del ComboBox pero adicionalmente se crean las propiedades DatColumns y ImageMember y se sobre escribe los eventos DrawItem y MeasureItem para dibujar las columnas y la imágen.

Diseñar y Programar el Formulario con el ComboImage creado

Abrir el formulario "frmConsultaEmpleado" y realizar el siguiente diseño:


Escribir el siguiente código en el formulario:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Drawing;
using System.Windows.Forms;

namespace ComboBox_Imagen_MultiColumna
{
    public partial class frmConsultaEmpleado : Form
    {
        private List<beEmpleado> lbeEmpleado;

        public frmConsultaEmpleado()
        {
            InitializeComponent();
        }

        private void listarEmpleados(object sender, EventArgs e)
        {
            brEmpleado obrEmpleado = new brEmpleado();
            lbeEmpleado = obrEmpleado.listar();
            cboEmpleado.DataSource = lbeEmpleado;
            cboEmpleado.DisplayMember = "Apellido";
            cboEmpleado.ValueMember = "IdEmpleado";
            cboEmpleado.ImageMember = "Foto";
            List<DataColumn> columnas = new List<DataColumn>();
            columnas.Add(new DataColumn { ColumnName="IdEmpleado",
                ColumnWidth=3, Alignment=HorizontalAlignment.Right});
            columnas.Add(new DataColumn { ColumnName = "Apellido",
                ColumnWidth = 20, Alignment = HorizontalAlignment.Left });
            columnas.Add(new DataColumn { ColumnName = "Nombre",
                ColumnWidth = 10, Alignment = HorizontalAlignment.Left });
            cboEmpleado.DataColumns = columnas;
            cboEmpleado.SelectedIndex = -1;
        }

        private void mostrarEmpleadoSeleccionado(object sender, EventArgs e)
        {
            int n = cboEmpleado.SelectedIndex;
            if (n > -1)
            {
                txtCodigo.Text = lbeEmpleado[n].IdEmpleado.ToString();
                txtApellido.Text = lbeEmpleado[n].Apellido;
                txtNombre.Text = lbeEmpleado[n].Nombre;
                txtFechaNacimiento.Text = lbeEmpleado[n].FechaNacimiento.ToShortDateString();
                byte[] foto = lbeEmpleado[n].Foto;
                MemoryStream ms = new MemoryStream(foto);
                try
                {
                    picFoto.Image = Image.FromStream(ms);
                }
                catch (Exception ex)
                {
                    picFoto.Image = null;
                }
            }
            else
            {
                txtCodigo.Clear();
                txtApellido.Clear();
                txtNombre.Clear();
                txtFechaNacimiento.Clear();
                picFoto.Image = null;
            }
        }
    }
}

Nota: Asociar al evento "load" del formulario la función "listarEmpleados" y al evento "SelectedIndexChanged" del ComboImage llamado "cboEmpleado" la función "mostrarEmpleadoSeleccionado".

Modificar el archivo App.Config para incluir la Cadena de Conexión

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="conNW" providerName="System.Data.SqlClient" connectionString="uid=UsuarioNW;pwd=123456;data source=DSOFT\Sqlexpress;initial catalog=NW"/>
  </connectionStrings>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
</configuration>

Probar la Aplicación Windows Forms

Grabar la aplicación y ejecutarla con F5. Desplegar el ComboBox y se mostrará la siguiente ventana:


Seleccionar un registro y se mostrará los datos del empleado, tal como se muestra a continuación:


Nota: La consulta es desconectada ya que los datos del empleado se obtienen de la Lista de Objetos.

Comentario Final

En este post vimos como mostrar en un ComboBox de WinForms múltiples columnas y también imágenes, para lo cual creamos un control personalizado configurando la propiedad "DrawMode" y agregamos propiedades "ImageMember" y "DataColumns", luego dibujamos en el evento "DrawItem".

Como ven no es tan díficil crear tus propios controles personalizados, como siempre nos ayudamos con un poco de Reflection para obtener propiedades (nombres y valores).

Espero les sirva.

Descarga
Demo_ComboBox_Imagen_MultiColumna