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.

3 comentarios:

  1. Tenéis disponible una versión que he publicado de este artículo con un proyecto de ejemplo en http://www.codeproject.com/KB/grid/barchartdatagridview.aspx.

    Está en inglés.

    ResponderEliminar
  2. Me encantaria aplicar las barras de progreso a un datagrid que uso como pianoroll midi, el cual dispara notas segun valor de cada cell.
    Quisiera saber si estarias interesado en adaptarlo a mi app, bien remunerado por supuesto. Gracias circex@telefonica.net

    ResponderEliminar