viernes, 28 de mayo de 2010

Integrar un gráfico de barras en un DataGridView

En ocasiones puede ser de utilidad mostrar una lista de datos (en un DataGridView) con un indicador gráfico de porcentaje en cada línea de la lista. Por ejemplo, imaginemos que tenemos una lista de expedientes y queremos mostrar de forma gráfica el porcentaje de ejecución de cada uno de ellos. Esta es una de las mejores formas.

En este artículo explicaré cómo conseguir colocar esas barras de progreso con efecto "glass" y del color que queramos en una columna de un DataGridView.

El código que comparto, también permite señalizar, por ejemplo un objetivo para poder comparar el valor real con dicho objetivo. Véase este ejemplo: en él se marca con el difusor verde el objetivo a alcanzar. Las líneas que lo superan aparecen en verde y las que no llegan en rojo.

Todo esto se consigue mediante un único método que, siendo informado adecuadamente de lo que queremos hacer, realiza todo el trabajo por nosotros.

Dicho método debe ser llamado desde el evento CellPainting del DataGridView. He aquí el código del método con todas sus formas de ser llamado (polimorfismo):
Public Class Barras

    ''' <summary>
    ''' Pinta el fondo de una celda con color degradado.
    ''' </summary>
    ''' <param name="oColor">Color a aplicar.</param>
    ''' <param name="e">Parámetros del evento CellPainting</param>
    ''' <remarks></remarks>
    Public Shared Sub PintaDegradado(ByVal oColor As Drawing.Color, ByVal e As DataGridViewCellPaintingEventArgs)
        Main.PintaDegradado(oColor, e, -1)
    End Sub

    ''' <summary>
    ''' Pinta una barra de progreso en una celda de un control DataGridView
    ''' </summary>
    ''' <param name="oColor">Color a usar para la barra.</param>
    ''' <param name="e">Parámetros del evento CellPainting.</param>
    ''' <param name="iPorcentaje">Porcentaje a representar.</param>
    ''' <remarks></remarks>
    Public Shared Sub PintaDegradado(ByVal oColor As Drawing.Color, ByVal e As DataGridViewCellPaintingEventArgs, ByVal iPorcentaje As Integer)
        Dim aCol As Drawing.Color() = {oColor}
        Dim aPor As Integer() = {iPorcentaje}
        If iPorcentaje = -1 Then
            Dim aPorN As Integer() = {}
            Main.PintaDegradado(aCol, e, aPorN)
        Else
            Main.PintaDegradado(aCol, e, aPor)
        End If
    End Sub

    ''' <summary>
    ''' Pinta una barra de progreso con señalización de objetivo en una celda de un control DataGridView.
    ''' </summary>
    ''' <param name="oColor">Color a usar para la barra.</param>
    ''' <param name="e">Parámetros del evento CellPainting.</param>
    ''' <param name="iPorcentaje">Porcentaje a representar.</param>
    ''' <param name="iObjetivo">Objetivo a marcar.</param>
    ''' <param name="oColorObjetivo">Color a usar para el objetivo.</param>
    ''' <remarks></remarks>
    Public Shared Sub PintaDegradado(ByVal oColor As Drawing.Color, ByVal e As DataGridViewCellPaintingEventArgs, ByVal iPorcentaje As Integer, ByVal iObjetivo As Integer, ByVal oColorObjetivo As Drawing.Color)
        Dim aCol As Drawing.Color() = {oColor, oColorObjetivo}
        Dim aPor As Integer() = {iPorcentaje, iObjetivo}
        Main.PintaDegradado(aCol, e, aPor)
    End Sub

    ''' <summary>
    ''' Pinta una barra en color degradado como fondo de una celda en un Grid.
    ''' </summary>
    ''' <param name="aColores">Matriz de colores a usar.</param>
    ''' <param name="e">Parámetros del evento CellPainting</param>
    ''' <param name="aPorcentajes">Matriz con los porcentajes a mostrar. Pueden ser uno o dos. El primero indica
    ''' el porcentaje de la barra de progreso a mostrar. El segundo un objetivo a marcar. Si sólo se indica uno y es cero,
    ''' se cubrirá todo el fondo de la celda con el primer color especificado.</param>
    ''' <remarks></remarks>
    Private Shared Sub PintaDegradado(ByVal aColores As Drawing.Color(), ByVal e As DataGridViewCellPaintingEventArgs, ByVal aPorcentajes As Integer())
        Dim oPin1 As Drawing2D.LinearGradientBrush = Nothing
        Dim oPin2 As Drawing2D.LinearGradientBrush = Nothing
        Dim oPinO As Drawing2D.LinearGradientBrush = Nothing
        Dim oColor As Drawing.Color = aColores(0)
        Try
            Dim oCelda As New Rectangle(e.CellBounds.X - 1, e.CellBounds.Y - 1, e.CellBounds.Width, e.CellBounds.Height)
            For iC As Integer = 0 To aPorcentajes.Length - 1
                If aPorcentajes(iC) > 100 Then aPorcentajes(iC) = 100
            Next
            Dim oRect1 As Rectangle
            Dim oRect2 As Rectangle
            Dim oObj As Rectangle
            Dim oFond As Rectangle
            Dim oCuad As Rectangle = Nothing
            Dim iPorcentaje As Integer = 0
            Dim bPor As Boolean = False
            If aPorcentajes.Length > 0 Then
                bPor = True
                iPorcentaje = aPorcentajes(0)
                If iPorcentaje > 0 Then
                    oRect1 = New Rectangle(oCelda.X + 4, oCelda.Y + 4, Math.Round(((oCelda.Width - 7) * iPorcentaje * 0.01) + 0.49), Math.Round((oCelda.Height - 8) / 2))
                    If oRect1.Width > oCelda.Width - 7 Then oRect1.Width = oCelda.Width - 7
                    oRect2 = New Rectangle(oCelda.X + 4, oRect1.Bottom - 1, oRect1.Width, (oCelda.Height - 6) - oRect1.Height)
                    oFond = New Rectangle(oCelda.X + 4, oCelda.Y + 4, oCelda.Width - 7, oCelda.Height - 7)
                    oPin1 = New Drawing2D.LinearGradientBrush(oRect1, Color.White, Color.FromArgb(180, oColor), Drawing2D.LinearGradientMode.Vertical)
                    oPin2 = New Drawing2D.LinearGradientBrush(oRect2, oColor, Color.FromArgb(70, oColor), Drawing2D.LinearGradientMode.Vertical)
                End If
                If aPorcentajes.Length > 1 Then
                    Dim iObj As Integer = aPorcentajes(1)
                    Dim iPos As Integer = oCelda.X + 4 + Math.Round(((oCelda.Width - 7) * iObj * 0.01) + 0.49)
                    Dim iIni As Integer = iPos - 20
                    If iIni < oCelda.X + 4 Then iIni = oCelda.X + 4
                    oObj = New Rectangle(iIni, oCelda.Y + 2, iPos - iIni, oCelda.Height - 4)
                    oPinO = New Drawing2D.LinearGradientBrush(oObj, Drawing.Color.FromArgb(0, aColores(1)), aColores(1), Drawing2D.LinearGradientMode.Horizontal)
                End If
                oCuad = New Rectangle(oCelda.X + 3, oCelda.Y + 3, oCelda.Width - 6, oCelda.Height - 6)
            Else
                oRect1 = New Rectangle(oCelda.X + 1, oCelda.Y + 1, oCelda.Width - 1, Math.Round(oCelda.Height / 2))
                oRect2 = New Rectangle(oCelda.X + 1, oRect1.Bottom - 1, oCelda.Width - 1, oCelda.Height - oRect1.Height)
                oFond = New Rectangle(oCelda.X + 1, oCelda.Y + 1, oCelda.Width - 1, oCelda.Height)
                oPin1 = New Drawing2D.LinearGradientBrush(oRect1, Color.White, Color.FromArgb(180, oColor), Drawing2D.LinearGradientMode.Vertical)
                oPin2 = New Drawing2D.LinearGradientBrush(oRect2, oColor, Color.FromArgb(70, oColor), Drawing2D.LinearGradientMode.Vertical)
            End If
            If bPor Then
                e.Graphics.DrawRectangle(Pens.DimGray, oCuad)
            End If
            If oPin1 IsNot Nothing Then
                e.Graphics.FillRectangle(Brushes.White, oFond)
                e.Graphics.FillRectangle(oPin1, oRect1)
                e.Graphics.FillRectangle(oPin2, oRect2)
            End If
            If oPinO IsNot Nothing Then
                e.Graphics.FillRectangle(oPinO, oObj)
            End If
            e.PaintContent(oCelda)
            e.Paint(oCelda, DataGridViewPaintParts.Border)
            e.Handled = True
        Catch ex As Exception
            Debug.Print(ex.Message)
        Finally
            If oPin1 IsNot Nothing Then
                oPin1.Dispose()
                oPin1 = Nothing
            End If
            If oPin2 IsNot Nothing Then
                oPin2.Dispose()
                oPin2 = Nothing
            End If
            If oPinO IsNot Nothing Then
                oPinO.Dispose()
                oPinO = Nothing
            End If
        End Try
    End Sub
End Class
Este método está listo para ser llamado desde el evento CellPainting del DataGridView. He aquí un ejemplo:
Private Sub ctlLista_CellPainting(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellPaintingEventArgs) Handles ctlLista.CellPainting
        Try
            If e.ColumnIndex < 0 OrElse e.RowIndex < 0 Then Exit Sub
            e.Handled = True
            Dim oRow As Objetos.CapturaPesos.Registro = Nothing
            Select Case DirectCast(e.ColumnIndex, Columnas)
                Case Columnas.Peso
                    oRow = DirectCast(ctlLista.Rows(e.RowIndex).DataBoundItem, Equin.ApplicationFramework.ObjectView(Of Objetos.CapturaPesos.Registro)).Object
                    If oRow.Peso >= oRow.PesoMinimo AndAlso oRow.Peso <= oRow.PesoMaximo Then
                        Barras.PintaDegradado(Color.LightGreen, e)
                    Else
                        Barras.PintaDegradado(Color.Red, e)
                    End If
                Case Else
                    e.Paint(e.CellBounds, DataGridViewPaintParts.All)
            End Select

        Catch ex As Exception
            Debug.Print(ex.Message)
        End Try
    End Sub
Y eso es todo. Espero que disfrutéis con el truco.

jueves, 27 de mayo de 2010

Enlace a datos con Visual Studio .Net

Uno de los aspectos más importantes (por no decir el más importante) a la hora de ensamblar una aplicación de gestión es la técnica utilizada para enlazar dicha aplicación con la base de datos.

En el pasado el tipo de base de datos usado para guardar los datos persistentes de nuestra aplicación era crucial y debía ser cuidadosamente elegido antes de comenzar a transformar el proyecto en código. Desde que Visual Studio .Net apareció dicha elección ya no es tan crucial; los adaptadores de datos y la posibilidad de manejar tablas "desconectadas" nos ha permitido separar muy fácilmente la capa de datos de la lógica de la aplicación, lo que facilita a su vez la migración de un tipo de base de datos a otro, manteniendo intacta casi toda la codificación de nuestro proyecto.

Si bien el tipo de base de datos usado ya no es tan determinante, las últimas versiones de Visual Studio nos ofrecen un amplio abanico de opciones (DataSets, LINQ to SQL, Entity Framework, etc.) a la hora de conectar nuestra aplicación con dicha base de datos, lo cual nos genera un sinfín de dudas acerca de cuál de ellos es el mejor y el que debemos usar. Pues bien, en este artículo intentaré arrojar algo de luz para despejar dichas dudas, pero siempre desde mi experiencia como desarrollador, tratando de ofrecer, no una guía, sino una ayuda.

¿Cuál es la mejor técnica de enlace a datos (DataBinding)?: No hay una respuesta directa para esta pregunta. Todo depende del contexto en que se vaya a usar ese enlace y de las funciones que tendrá en cada caso (sólo lectura o con edición).

Empezaremos por describir las principales técnicas de enlace y sus cualidades e inconvenientes:
  • DataAdapter y DataSet. Nacen con Visual Studio .Net y suponen un gran salto desde el ADO o DAO que integraba Visual Studio 6.0. Proporcionan la posibilidad de manipular los datos de forma desconectada (cargados en memoria) y son fáciles de implementar. Actualmente se encuentran en desuso puesto que con la versión 2.0 de .Net Framework se introduce el siguiente de nuestros protagonistas:
  • Typed DataSet. Es una combinación de los dos anteriores en una sóla entidad, con la posibilidad de extender su funcionalidad con nuestro propio código. Facilita mucho más la implementación y resulta mucho más potente.
  • DataReader. Sólo permite una lectura secuencial de registros desde la base de datos. Por si mismo no es un método de enlace a datos, pero lo facilita. De hecho es la técnica que usan los TableAdapter para llenar un DataSet con datos. No obstante lo expongo como un método de enlace porque en el caso de extracción de informes (datos de sólo lectura) puede ser una opción muy a tener en cuenta.
  • LINQ to SQL. Basado en el lenguaje de consulta a datos introducido en la versión 3.5 de .Net Framework y clases como contenedoras de datos, permite un enlace bidireccional de datos de forma desconectada (igual que los DataSet). La eficacia e inteligencia con la que gestiona las lecturas y escrituras a la base de datos consiguen un rendimiento espectacular. A la vez, la potencia de las consultas integradas en el lenguaje (LINQ) permiten la extracción de datos de forma muy flexible y cómoda para el programador. Como limitación diremos que sólo permite la conexión con SQL Server, eso sí, en cualquiera de sus ediciones.
  • LINQ to Entities. Combina las consultas integradas en el lenguaje (LINQ) con Entity Framework. Básicamente es lo mismo que LINQ to SQL pero nos permite conectar con cualquier otro tipo de base de datos a través de OLEDB.
Desde mi experiencia, no es recomendable usar la misma técnica para todos los casos.

La más apropiada en cada caso dependerá de diversos factores. Entre ellos está la finalidad del enlace a realizar (mantenimiento de datos, ejecución de procesos, extracción de informes y gráficos, etc.), la velocidad de la conexión con la base de datos, etc...

La técnica que ofrece la mejor velocidad en lectura de datos es el uso de DataReaders. Leen secuencialmente los registros uno a uno y simplemente devuelven los datos leídos a medida que los vamos pidiendo. Por contra, requiere más líneas de código para procesarlos, dado que debemos indizar los campos devueltos (con un Enum, por ejemplo) para tener acceso a sus valores. También tenemos que encargarnos de la conversión del tipo de dato obtenido si es necesario. En resumen: alta velocidad de procesamiento pero más líneas de código a escribir. Sería la mejor elección para realizar lecturas de una gran cantidad de datos (decenas de miles de registros), con conexiones pobres en velocidad (bases de datos que están en Internet) o dentro de procesos muy largos donde se repiten amenudo las lecturas. Obviamente si necesitamos actualizar los datos leídos en la base de datos tendremos que usar objetos Command.

Los DataSets nos permiten un enlace completo (con inserciones, actualizaciones y eliminaciones). Se basan en un comando SELECT de SQL y el diseñador de Visual Studio nos genera automáticamente las instrucciones INSERT, UPDATE y DELETE. Cargan en memoria los datos leídos, permiten su modificación en memoria y la escritura de dichas modificaciones en el momento en que lo decidamos, manteniendo la integridad con los cambios de otros usuarios en la base de datos. Nos ahorran muchas líneas de código (que genera automáticamente el diseñador), y tienen un rendimiento muy aceptable. Es una elección óptima en el caso de mantenimientos de tablas de datos y casi la única si nuestro ensamblado sólo corre bajo la versión 2.0 de .Net Framework. En mi experiencia, cuando un DataSet contiene demasiadas tablas el diseñador resulta demasiado lento, lo que nos hace perder un poco la paciencia.

LINQ (to SQL y to Entities) para mí es la opción que sustituye a los DataSet (si podemos usar la versión 3.5 o superior de .Net Framework) por estas razones:
  • Ofrecen la misma funcionalidad (lectura, manipulación desconectada y escritura). Bueno, casi toda la funcionalidad. pues nativamente no integran el proveedor de errores (esos signos de admiración rojos que salen en los DataGrid cuando hay algún dato erróneo). No obstante, se puede implementar fácilmente mediante herencia.
  • Rendimiento similar o superior. En algunos casos muy concretos (queries con uno o dos campos en el resultado) ha resultado más rápido el DataSet, pero en general es al contrario. Además, LINQ nos permite compilar las consultas, lo que acelera su ejecución.
  • Baja dependencia del tipo de base de datos. No es necesario escribir ni una sóla consulta SQL contra la base de datos, pues LINQ actúa sobre tablas, no sobre consultas. Las consultas las realizamos después con LINQ sobre los datos leídos de las tablas. Esto tiene la ventaja de que no necesitamos ser expertos programadores de SQL Server u Oracle. A la vez descargamos de trabajo al servidor de la base de datos, pues no le encargamos complicadas consultas.
  • Facilísima implementación. Basta con arrastrar las tablas al diseñador del archivo .dbml o .edmx y ya está creada la clase con todos los campos lista para ser consultada. Después podemos crear asociaciones entre tablas y ya tenemos relaciones de uno a varios o de uno a uno disponibles en las propiedades de las clases.
  • Muy fácil de extender. Mediante Partial Class podemos dotar de nuestros propios métodos o propiedades a una tabla. A diferencia de los DataSet, cuando creamos una propiedad mediante código (que no se corresponde con un campo de la tabla), esta es enlazable directamente a un control de Windows Forms.
En general recomiendo usar esta técnica tanto para mantenimiento de datos como para informes y gráficos, con buenos resultados. No obstante, en casos donde el rendimiento es muy crítico, es mejor recurrir a DataReaders. Hay que tener en cuenta que las clases de LINQ to SQL son colecciones enlazables y, por lo tanto, requieren de unos recursos y necesitan una estructura interna que afecta a la velocidad de su llenado.

Conclusión:
  • DataReader: Usar sólo en situaciones de sólo lectura donde el rendimiento es crítico.
  • DataSet: Usar sólo si nuestra aplicación no corre con .Net Framework 3.5 o posterior o vamos a realizar un enlace con archivos XML.
  • LINQ: Usar en el resto de los casos.