Cómo detectar la tecla pulsada en una celda del control DataGridView
Por Enrique Martínez Montejo
Última revisión: 05/09/2007
 

Este es uno de los temas estrella del grupo de noticias en español de Visual Basic .net, y su razón de ser tiene, porque en principio, todos creemos que mediante los clásicos eventos KeyDown, KeyPress y KeyUp del control DataGridView, podemos tener un cierto control sobre lo que se escribe en sus celdas. Pero cuando observamos que los eventos KeyDown y KeyUp sólo responden a un cierto tipo de teclas que son obvias (modificadoras, de desplazamiento, de función, escape, intro, etc.), y que ninguno de los tres eventos se desencadena al pulsar las teclas correspondientes a letras, números o símbolos, nos entra la desesperación y un tanto de desilusión.

Es cierto que existen otras formas de controlar de una manera general, la pulsación de las teclas que se efectúan en el control DataGridView. Por ejemplo, podemos utilizar los tres eventos de teclado del formulario que contiene al control DataGridView, o podemos también reeemplazar la función ProcessCmdKey. Pero mientras los primeros requieren que la propiedad KeyPreview del formulario tenga el valor True (para que se desencadenen los eventos del formulario antes que los mismos eventos correspondientes a los controles incluidos en aquel), el reemplazar la función ProcessCmdKey requiere trabajar con mensajes de Windows e identificadores de ventanas (los famosos valores hWnd o Handled de la API de Windows).

Aparte, tanto los eventos del formulario, como la función ProcessCmdKey, se desencadenarían, no sólo para los eventos de teclado del control DataGridView, sino para todos los eventos de teclado de los restantes controles existentes en el formulario, con lo cual tendríamos que estar comprobando qué control tiene el foco, o el identificador de ventana (hWnd) del control que ha provocado el evento. En mi opinión personal, hay otra solución más eficaz, que es tratar por todos los medios que los propios eventos de teclado del control DataGridView, sobre todo el evento KeyPress, respondan a las pulsaciones de las teclas presionadas. Pero antes de entrar en detalle sobre los eventos de teclado, vamos a conocer un poco más sobre lo que se esconde por dentro del control DataGridView, que no es, ni más ni menos, que una serie de controles que los programadores de Visual Basic suelen conocer bastante bien.

La clase DataGridViewCell

Esta clase representa una celda individual existente en un control DataGridView, que podemos referenciar fácilmente efectuando una simple consulta a la propiedad CurrentCell del control DataGridView:

' Referenciamos la celda actual
'

Dim dgvCell As DataGridViewCell = Me.DataGridView1.CurrentCell

De esta manera ya estamos en condiciones de obtener el valor de las restantes propiedades de la celda, como el índice de la columna (ColumnIndex), el índice de la fila (RowIndex), o el valor de la celda (Value). Pero también podemos conocer igualmente, el tipo de control de edición que se aloja en la celda mediante la propiedad EditType del control DataGridView, que dependiendo del tipo asignado a la columna, en principio puede corresponderse con alguna de las siguientes clases:

  Tipos de columnas Tipos de objetos existentes en las celdas  
  DataGridViewButtonColumn DataGridViewButtonCell  
  DataGridViewCheckBoxColumn DataGridViewCheckBoxCell  
  DataGridViewComboBoxColumn DataGridViewComboBoxCell  
  DataGridViewImageColumn DataGridViewImageCell  
  DataGridViewLinkColumn DataGridViewLinkCell  
  DataGridViewTextBoxColumn DataGridViewTextBoxCell  

Parece ser que el nombre de cada clase denota bien el tipo de control que representan las columnas y celdas del control DataGridView.

Cuando referenciamos una celda cualquiera del control DataGridView, automáticamente obtendremos el tipo de control alojado en ella, que se corresponderá con el tipo de control asignado a la columna a la que pertenece la celda. Continuando con el ejemplo anterior, si la celda actual se encuentra en una columna tipo DataGridViewCheckBoxColumn, la variable objeto dgvCell referenciará a un objeto DataGridViewCheckBoxCell, y si la columna es del tipo DataGridViewTextBoxColumn, el tipo de objeto de la celda será DataGridViewTextBoxCell.

La clase DataGridViewTextBoxCell

Esta clase es la encargada de mostrar texto modificable en una celda del control DataGridView, y es la clase que por defecto tendrán todas las celdas existententes en dicho control, salvo que expresamente le hayamos asignado a la columna otro tipo de clase DataGridViewXXXColumn de las mencionadas en el apartado anterior, en cuyo caso, las celdas de esa columna serán del tipo de objeto especializado.

La clase DataGridViewTextBoxCell hereda directamente de la clase base DataGridViewCell, de la que toma la inmensa mayoría de sus propiedades y métodos.

Si nuestra intención es referenciar el objeto DataGridViewTextBoxCell alojado en la celda actual del control DataGridView, ejecutaríamos algo parecido a lo siguiente:

' Referenciamos el control DataGridViewTextBoxCell actual
'

Dim
textBoxCell As DataGridViewTextBoxCell = _
    TryCast(Me.DataGridView1.CurrentCell, DataGridViewTextBoxCell)

La importancia de esta clase radica en que la celda actualmente seleccionada, aloja a su vez un control de la clase DataGridViewTextBoxEditingControl, que es el control que nos permite editar el valor de la celda, siempre y cuando el valor de la propiedad ReadOnly del control DataGridViewTextBoxCell sea False.

El control DataGridViewTextBoxEditingControl

Por fin hemos llegado al objeto más recóndito de la celda; al objeto DataGridViewTextBoxEditingControl alojado en el objeto DataGridViewTextBoxCell existente a su vez en el objeto DataGridViewCell, representado éste por la celda actual del control DataGridView.

Desde luego, no me extraña que con tantos nombres parecidos, y de gran longitud, más de uno se haga un pequeño lío. Pero cuando se hayan peleado un par de veces con las celdas del control DataGridView, les aseguro que verán las cosas de otra manera.

Como bien nos da a entender el nombre del control DataGridViewTextBoxEditingControl, éste hereda directamente del clásico control de edición, el control TextBox del espacio de nombres System.Windows.Forms, por tanto, implementa todas las propiedades, métodos y eventos de éste último control, permitiéndonos editar el contenido de la celda.

Cuando una celda cualquiera del control DataGridView pasa a modo de edición, se desencadena el evento EditingControlShowing, donde el segundo parámetro de su firma es un objeto de la clase DataGridViewEditingControlShowingEventArgs (otro nombre más largo aún para añadir a la lista). Esta clase dispone únicamente de dos propiedades públicas: CellStyle (un objeto DataGridViewCellStyle que referencia a la celda editada), y Control (que es el control proporcionado para editar el valor de la celda). Será de la propiedad Control donde obtendremos el control DataGridViewTextBoxEditingControl escondido, tal y como muestra el siguiente ejemplo:

Private Sub DataGridView1_EditingControlShowing( _
    ByVal sender As Object, _
    ByVal
e As DataGridViewEditingControlShowingEventArgs) _
    Handles
DataGridView1.EditingControlShowing

    ' Referenciamos el control TextBox subyacente en la celda actual.
    '

    Dim
cellTextBox As DataGridViewTextBoxEditingControl = _
        TryCast
(e.Control, DataGridViewTextBoxEditingControl)

    ' Obtenemos el valor actual de la celda.
    '
   
MessageBox.Show(cellTextBox.Text)

    ' Obtenemos el estilo de la celda actual
    '

    Dim style As DataGridViewCellStyle = e.CellStyle

    ' Mientras se edita la celda, aumentaremos la fuente
    ' y rellenaremos el color de fondo de la celda actual.
    '

    With style
        .Font = New Font(style.Font.FontFamily, 10, FontStyle.Bold)
        .BackColor = Color.Beige
    End With

End Sub

El resultado será el que se muestra en la imagen, donde se observa que la celda cambia de color de fondo y aumenta el tamaño de la fuente mientras se está editando.

 

Una vez que hemos sido capaces de referenciar el objeto derivado de la clase TextBox existente en una celda, ¿por qué no vamos a ser capaces de controlar las pulsaciones de teclas que se efectúan en el mismo, si en definitiva se trata de un simple control TextBox? La solución se encuentra en instalar los correspondientes controladores para los tres eventos del teclado del control TextBox: KeyDown, KeyPress y KeyUp.

En el ejemplo anterior, la variable cellTextBox está declarada a nivel del propio procedimiento que la contiene, por lo su ámbito es local. Es cierto que en el mismo procedimiento podemos instalar los correspondientes controladores de evento mediante la instrucción AddHandler, pero cada vez que se edite una celda, se estará instalando un controlador, y si se editan 20.000 celdas, se instalarán 20.000 controladores, que desencadenarán 20.000 eventos, que en el caso de estar instalados para los tres eventos de teclado, producirán 60.000 eventos de teclado. Para evitar que se instale un controlador cada vez que se edita una celda, habría que eliminar primero el controlador con la instrucción RemoveHandler, por ejemplo, en el evento CellEndEdit del control DataGridView, que se produce cuando finalice la edición de la celda.

Pero, ¡claro! Esto último tiene un gran inconveniente, porque si eliminamos el controlador de evento en otro procedimiento distinto (CellEndEdit) a aquel donde se ha instalado (EditingControlShowing), necesitaremos una variable objeto con un ámbito superior al local para poder eliminar el controlador de evento. La solución pasa por declarar a nivel del propio formulario que contiene el control DataGridView, una variable objeto que será la que sucesivamente estará referenciando al control DataGridViewTextBoxEditingControl de la celda que actualmente se encuentre en modo de edición, y declararemos la variable objeto con la palabra clave WithEvents, para no tener que añadir y eliminar manualmente los controladores para los tres eventos de teclado, tal y como muestro a continuación:

Private WithEvents cellTextBox As DataGridViewTextBoxEditingControl

Private Sub cellTextBox_KeyDown( _
    ByVal sender As Object, _
    ByVal e As System.Windows.Forms.KeyEventArgs) Handles cellTextBox.KeyDown

End Sub

Private Sub cellTextBox_KeyPress( _
    ByVal sender As Object, _
    ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles cellTextBox.KeyPress

End Sub

Private Sub cellTextBox_KeyUp( _
    ByVal sender As Object, _
    ByVal e As System.Windows.Forms.KeyEventArgs) Handles cellTextBox.KeyUp

End Sub

Private
Sub DataGridView1_EditingControlShowing( _
                ByVal sender As Object, _
                ByVal e As DataGridViewEditingControlShowingEventArgs) _
                Handles DataGridView1.EditingControlShowing

    ' Este evento se producirá cuando la celda pasa a modo de edición.

    ' Referenciamos el control DataGridViewTextBoxEditingControl actual.
    '

    cellTextBox = TryCast(e.Control, DataGridViewTextBoxEditingControl)

    ' Obtenemos el estilo de la celda actual
    '

    Dim style As DataGridViewCellStyle = e.CellStyle

    ' Mientras se edita la celda, aumentaremos la fuente
    ' y rellenaremos el color de fondo de la celda actual.
    '

    With style
        .Font = New Font(style.Font.FontFamily, 10, FontStyle.Bold)
        .BackColor = Color.Beige
    End With

End Sub

 

Cómo declarar un objeto DataGridViewTextBoxEditingControl para ser utilizado por los eventos del teclado

Bueno. Ya está en condiciones de detectar la tecla pulsada en cualquier celda del control DataGridView que se encuentre actualmente en modo de edición.

Los eventos correspondientes al control de edición de la celda, unidos a los tres eventos del teclado del propio control DataGridView, en teoría nos debe de dar toda la flexibilidad posible para detectar las teclas que el usuario ha presionado.

A continuación paso a exponer algunos ejemplos para trabajar con los eventos del teclado descritos, dependiendo de ciertas acciones en concreto. Con total seguridad, en la Red de redes existirán miles de ejemplos mejores que los que encontrará aquí, pero lo que sólo pretendo es mostrarle simplemente una manera más de realizar una operación concreta, que no quiere decir que sea la mejor.

Permitir sólo números, el carácter separador decimal, y el carácter negativo, en las celdas de un control DataGridView.

Si en nuestro control DataGridView tenemos una columna donde nos interesaría que sólo se introdujeran datos numéricos (enteros o decimales), controlaríamos la pulsación de teclas en el evento KeyPress del objeto DataGridViewTextBoxEditingControl declarado a nivel del formulario.

Por supuesto que el ejemplo también serviría para permitir los citados caracteres en un simple control TextBox, que como se ha explicado en el artículo, es el control que existe en las celdas de un control DataGridView.

Private Sub cellTextBox_KeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs) Handles cellTextBox.KeyPress

    ' Referenciamos el control TextBox subyacente.
    '

    Dim tb As TextBox = TryCast(sender, TextBox)

    ' Si la conversión ha fallado, abandonamos el procedimiento.
    '

    If (tb Is Nothing) Then
        e.Handled = True
        Return
    End If

    Dim isDecimal, isSign, isValidChar As Boolean
    Dim decimalSeparator As String = Nothing

    Select Case e.KeyChar
        Case "."c, ","c
            ' Obtenemos el carácter separador decimal existente
            ' actualmente en la configuración regional de Windows.
            '

            decimalSeparator = Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator

            ' Hacemos que el carácter tecleado coincida con el
            ' carácter separador existentente en la configuración
            ' regional.
            '

            e.KeyChar = decimalSeparator.Chars(0)

            ' Si el primer carácter que se teclea es el separador decimal,
            ' o si bien, existe un signo en el primer carácter, envío la
            ' combinación '0,'.
            '

            If (((tb.TextLength = 0) OrElse (tb.SelectionLength = tb.TextLength)) OrElse _
                ((tb.TextLength = 1) AndAlso ((tb.Text.Contains("-")) OrElse _
                (Text.Contains("+"))))) Then

                ' NOTA: Envío la combinación "0," mediante el método Send,
                ' para que en el código cliente se desencadenen los
                ' eventos de teclado.
                '

                SendKeys.Send("{0}")
                SendKeys.Send("{" & decimalSeparator & "}")
                e.Handled = True
                Return
            End If

            ' Es un carácter válido.
            '
            isDecimal = True
            isValidChar = True

        Case "-"c, "+"c    ' Signos negativo y positivo
            ' Es un carácter válido.
            '

            isSign = True
            isValidChar = True

        Case Else
            ' Sólo se admitirán números y la tecla de retroceso.
            '
           
Dim isDigit As Boolean = Char.IsDigit(e.KeyChar)
            Dim isControl As Boolean = Char.IsControl(e.KeyChar)

            If ((isDigit) OrElse (isControl)) Then
                isValidChar = True

            Else
                e.Handled = True
                Return

            End If

    End Select

    ' Si es un carácter válido, y el texto del control
    ' se encuentra totalmente seleccionado, elimino
    ' el valor actual del control.
    '

    If ((isValidChar) And (tb.SelectionLength = tb.TextLength)) Then
        tb.Text = String.Empty
    End If

    If (isSign) Then
       
' Admitimos los caracteres positivo y negativo, siempre y cuando
        ' sea el primer carácter del texto, y no exista ya ningún otro
        ' signo escrito en el control.
        '

        If ((tb.SelectionStart <> 0) OrElse _
            (tb.Text.IndexOf("-") >= 0) OrElse _
            (tb.Text.IndexOf("+") >= 0)) Then
            e.Handled = True
            Return
        End If
    End If

    If (isDecimal) Then
        ' Si en el control hay ya escrito un separador decimal,
        ' deshechamos insertar otro separador más.
        '

        If (tb.Text.IndexOf(decimalSeparator) >= 0) Then
            e.Handled = True
        End If
    End If

End Sub

Como bien podrá comprobar, el ejemplo tiene en cuenta el carácter separador decimal existente en la configuración regional del usuario, que en algunos casos será el punto (.), y en otros la coma (,).

Asimismo, también tiene en cuenta si se desea introducir un numéro negativo, que sólo se permitirá si el signo menos (-) es el primer carácter existente en la celda o en el control TextBox.

 

Otros enlaces de interés:

Índice de la colección de ejemplos de las clases del marco de trabajo de .NET


Enrique Martínez Montejo - 2007

NOTA: La información contenida en este artículo, así como el código fuente incluido en el mismo, se proporciona COMO ESTÁ, sin garantías de ninguna clase, y no otorga derecho alguno. Usted asume cualquier riesgo al poner en práctica, utilizar o ejecutar lo explicado, recomendado o sugerido en el presente artículo.

NOTE: The information contained in this article and source code included therein, is provided AS IS without warranty of any kind, and confers no rights. You assume any risk to implement, use or run it explained, recommended or suggested in this article.