miércoles, 2 de diciembre de 2015

Windows Forms. DataGridView. Columna para números con decimales (y II)

Artículos anteriores:
Windows Forms. DataGridView. Columna para números con decimales (I)

Una vez definida la clase para la definición de la columna aún nos quedaría implementar la lógica de la clase NumericGridCell que se encargará de controlar los caracteres introducidos por el usuario y el formato en el que se mostrarán los números en la celda.


En primer lugar voy a crear el constructor, que simplemente llamará al constructor de la clase base, y una propiedad privada de sólo lectura que devolverá el formato de número definido en la columna.

    public class NumericGridCell: DataGridViewTextBoxCell
    {
        public NumericGridCell() : base() { }

        private NumberFormatInfo NumberFormat
        {
            get
            {
                return ((NumericGridColumn)OwningColumn).NumberFormat;
            }
        }

    }
Public Class NumericGridCell
    Inherits DataGridViewTextBoxCell

    Public Sub New()
        MyBase.New()
    End Sub

    Private ReadOnly Property NumberFormat As NumberFormatInfo
        Get
            Return CType(OwningColumn, NumericGridColumn).NumberFormat
        End Get
    End Property

End Class


En la clase NumericGridCell voy a sobreescribir la propiedad ValueType para especificar que el tipo de los valores de la celda es decimal? (el que sea un tipo de datos que acepte nulos nos permitirá diferenciar entre un valor 0 y un valor vacío), y el método InitializeEditingControl para añadir un controlador de evento al evento KeyPress del control de edición de la celda.

El controlador de este evento (el método NumericCell_KeyPress) va a ser el responsable de controlar que los caracteres introducidos son válidos. Si se trata de un carácter numérico se comprueba que el número de decimales no exceda del indicado en la propiedad DecimalDigits de la columna. Si se trata del carácter definido como separador decimal en la propiedad DecimalSeparator de la columna, se comprueba que no se haya introducido anteriormente.  En caso contrario establece la propiedad Handled del objeto KeyPressEventArgs a true para anular el tratamiento de la tecla pulsada.

        public override Type ValueType
        {
            get { return typeof(decimal?); }
        }

        public override void InitializeEditingControl(int rowIndex, object initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
        {
            base.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle);
            Control ctl = DataGridView.EditingControl;
            ctl.KeyPress += new KeyPressEventHandler(NumericCell_KeyPress);
        }

        private void NumericCell_KeyPress(object sender, KeyPressEventArgs e)
        {
            DataGridViewCell currentCell = ((IDataGridViewEditingControl)sender).EditingControlDataGridView.CurrentCell;
            if (!(currentCell is NumericGridCell)) return;

            DataGridViewTextBoxEditingControl ctl = (DataGridViewTextBoxEditingControl)sender;
            if (char.IsDigit(e.KeyChar))
            {
                int separatorPosition = ctl.Text.IndexOf(NumberFormat.NumberDecimalSeparator);
                if (separatorPosition >= 0 && ctl.SelectionStart>separatorPosition)
                {
                    int currentDecimals = ctl.Text.Length - separatorPosition 
                        - NumberFormat.NumberDecimalSeparator.Length - ctl.SelectionLength;
                    if (currentDecimals >= NumberFormat.NumberDecimalDigits)
                        e.Handled = true;
                }
            }
            else if (e.KeyChar.ToString().Equals(NumberFormat.NumberDecimalSeparator))
            {
                e.Handled = ctl.Text.IndexOf(NumberFormat.NumberDecimalSeparator) >= 0;                
            }
            else
            {
                e.Handled = !char.IsControl(e.KeyChar);
            }
        }
    Public Overrides ReadOnly Property ValueType As Type
        Get
            Return GetType(Decimal?)
        End Get
    End Property

    Public Overrides Sub InitializeEditingControl(rowIndex As Integer, initialFormattedValue As Object, dataGridViewCellStyle As DataGridViewCellStyle)
        MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)
        Dim ctl As Control = DataGridView.EditingControl
        AddHandler ctl.KeyPress, AddressOf NumericCell_KeyPress
    End Sub

    Private Sub NumericCell_KeyPress(sender As Object, e As KeyPressEventArgs)
        Dim currentCell As DataGridViewCell =
            CType(sender, IDataGridViewEditingControl).EditingControlDataGridView.CurrentCell
        If TypeOf currentCell IsNot NumericGridCell Then Return

        Dim ctl As DataGridViewTextBoxEditingControl = CType(sender, DataGridViewTextBoxEditingControl)
        If Char.IsDigit(e.KeyChar) Then
            Dim separatorPosition As Integer = ctl.Text.IndexOf(NumberFormat.NumberDecimalSeparator)
            If separatorPosition >= 0 AndAlso ctl.SelectionStart > separatorPosition Then
                Dim currentDecimals = ctl.Text.Length - separatorPosition _
                    - NumberFormat.NumberDecimalSeparator.Length - ctl.SelectionLength
                If currentDecimals >= NumberFormat.NumberDecimalDigits Then
                    e.Handled = True
                End If
            End If
        ElseIf e.KeyChar.ToString().Equals(NumberFormat.NumberDecimalSeparator) Then
            e.Handled = ctl.Text.IndexOf(NumberFormat.NumberDecimalSeparator) >= 0
        Else
            e.Handled = Not Char.IsControl(e.KeyChar)
        End If
    End Sub

A continuación sobrescribiré la propiedad DefaultNewRowValue para asegurarme de que el valor devuelto es un valor válido. La propiedad devolverá null si no se ha definido un valor decimal válido como valor por defecto.

        public override object DefaultNewRowValue
        {
            get
            {
                object defaultValue = base.DefaultNewRowValue;
                if (defaultValue is decimal)
                    return defaultValue;
                else
                    return null;
            }
        }

    Public Overrides ReadOnly Property DefaultNewRowValue As Object
        Get
            Dim defaultValue As Object = MyBase.DefaultNewRowValue
            If TypeOf defaultValue Is Decimal Then
                Return defaultValue
            Else
                Return Nothing
            End If
        End Get
    End Property




Por último, y para que el valor de la celda se represente de acuerdo al formato indicado en las propiedades de la columna, sobrescribiré GetInheritedStyle para establecer la propiedad FormatProvider del objeto DataGridViewCellSytle devuelto al formato definido en la propiedad privada NumberFormat. Y el método GetFormattedValue para que devuelva el valor de la celda correctamente formateado:

        public override DataGridViewCellStyle GetInheritedStyle(DataGridViewCellStyle inheritedCellStyle, int rowIndex, bool includeColors)
        {
            DataGridViewCellStyle style= base.GetInheritedStyle(inheritedCellStyle, rowIndex, includeColors);
            style.FormatProvider = NumberFormat;
            return style;
        }

        protected override object GetFormattedValue(object value, int rowIndex, ref DataGridViewCellStyle cellStyle, TypeConverter valueTypeConverter, TypeConverter formattedValueTypeConverter, DataGridViewDataErrorContexts context)
        {
            if (value == null)
                return null;
            else
                return ((decimal)value).ToString(cellStyle.FormatProvider);
        }
    Public Overrides Function GetInheritedStyle(inheritedCellStyle As DataGridViewCellStyle, rowIndex As Integer, includeColors As Boolean) As DataGridViewCellStyle
        Dim style As DataGridViewCellStyle = MyBase.GetInheritedStyle(inheritedCellStyle, rowIndex, includeColors)
        style.FormatProvider = NumberFormat
        Return style
    End Function

    Protected Overrides Function GetFormattedValue(value As Object, rowIndex As Integer, ByRef cellStyle As DataGridViewCellStyle, valueTypeConverter As TypeConverter, formattedValueTypeConverter As TypeConverter, context As DataGridViewDataErrorContexts) As Object
        If value Is Nothing Then
            Return Nothing
        Else
            Return CType(value, Decimal).ToString(cellStyle.FormatProvider)
        End If
    End Function

Para probar la nueva columna simplemente deberemos añadir una nueva columna a un control DataGridView seleccionando NumericGridColumn como tipo de columna.

Dentro de las propiedades de la columna encontraremos las dos nuevas propiedades:


Al ejecutar comprobaremos que la columna nos permite introducir valores decimales con el separador decimal y el número de decimales indicados:



El código completo de este artículo, tanto en C# como en Visual Basic, está disponible en:

Códigos de muestra - Ejemplos MSDN

4 comentarios:

  1. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  2. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  3. Hola Assier, soy yo de nuevo despues de mucho tiempo, este ejemplo era lo que estaba esperando de hace mucho tiempo, se ve realmente bueno, lo único que no me sirvió, me muestra el grid, pro al introducir los números, no me pone el punto decimal, estoy en VB 2015, proyecto de inicio es Grid test.

    Me puedes ayudar ?

    Gracias

    ResponderEliminar
  4. excelente aporte, tenia tiempo tratando de resolver una situacion con estas caracteristicas.

    ResponderEliminar